mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-06 19:54:12 +00:00
232 lines
6.8 KiB
Python
232 lines
6.8 KiB
Python
"""Backwards-compatible facade over the ingest and sink modules.
|
|
|
|
Historically this module owned a single Neo4j driver used for both the
|
|
cartography temp database and the per-tenant sink database. The port to AWS
|
|
Neptune split those roles: the cartography ingest (temp) database is always
|
|
Neo4j and lives in `api.attack_paths.ingest`; the sink is configurable
|
|
(Neo4j or Neptune) and lives in `api.attack_paths.sink`. This shim preserves
|
|
the public API that `tasks/` and `api/v1/views.py` already depend on, and
|
|
dispatches to the right module by database-name prefix.
|
|
|
|
A database name starting with `db-tmp-scan-` is a cartography temp DB and
|
|
routes to ingest. Everything else routes to the configured sink.
|
|
"""
|
|
|
|
from contextlib import AbstractContextManager
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
import neo4j # noqa: F401 - kept for tests that patch api.attack_paths.database.neo4j
|
|
from api.attack_paths import ingest
|
|
from api.attack_paths import sink as sink_module
|
|
from config.env import env
|
|
from django.conf import (
|
|
settings, # noqa: F401 - kept for tests that patch ...database.settings
|
|
)
|
|
|
|
MAX_CUSTOM_QUERY_NODES = env.int("ATTACK_PATHS_MAX_CUSTOM_QUERY_NODES", default=250)
|
|
|
|
TEMP_DB_PREFIX = "db-tmp-scan-"
|
|
DATABASE_NOT_FOUND_CODE = "Neo.ClientError.Database.DatabaseNotFound"
|
|
|
|
|
|
# Exceptions
|
|
|
|
|
|
class GraphDatabaseQueryException(Exception):
|
|
def __init__(self, message: str, code: str | None = None) -> None:
|
|
super().__init__(message)
|
|
self.message = message
|
|
self.code = code
|
|
|
|
def __str__(self) -> str:
|
|
if self.code:
|
|
return f"{self.code}: {self.message}"
|
|
return self.message
|
|
|
|
|
|
class NeptuneWriteRetryExhaustedException(GraphDatabaseQueryException):
|
|
pass
|
|
|
|
|
|
class WriteQueryNotAllowedException(GraphDatabaseQueryException):
|
|
pass
|
|
|
|
|
|
class ClientStatementException(GraphDatabaseQueryException):
|
|
pass
|
|
|
|
|
|
# Routing
|
|
|
|
|
|
def _is_ingest_database(database: str | None) -> bool:
|
|
return bool(database) and database.startswith(TEMP_DB_PREFIX)
|
|
|
|
|
|
# Driver lifecycle
|
|
|
|
|
|
def init_driver() -> Any:
|
|
"""Initialize the configured sink backend.
|
|
|
|
The ingest driver (Neo4j for cartography temp DBs) stays lazy: it is
|
|
only initialized when a temp-DB operation actually runs, which never
|
|
happens on API pods.
|
|
"""
|
|
return sink_module.init()
|
|
|
|
|
|
def close_driver() -> None:
|
|
"""Close every driver held by this process."""
|
|
sink_module.close()
|
|
ingest.close_driver()
|
|
|
|
|
|
def get_driver() -> neo4j.Driver:
|
|
"""Return the sink backend's underlying driver.
|
|
|
|
Only meaningful for the Neo4j sink (where the backend has a single Neo4j
|
|
driver). On Neptune this returns the writer driver. Kept for tests and
|
|
legacy call-sites; prefer `get_session` for new code.
|
|
"""
|
|
backend = sink_module.get_backend()
|
|
|
|
# Neo4jSink exposes get_driver(); NeptuneSink exposes get_writer()
|
|
if hasattr(backend, "get_driver"):
|
|
return backend.get_driver()
|
|
|
|
if hasattr(backend, "get_writer"):
|
|
return backend.get_writer()
|
|
|
|
raise RuntimeError("Active sink backend does not expose a driver handle")
|
|
|
|
|
|
def verify_connectivity() -> None:
|
|
"""Raise if the configured graph database is unreachable on the API read path.
|
|
|
|
Backend-agnostic entry point for the readiness probe: Neo4j verifies its
|
|
driver, Neptune verifies the reader endpoint.
|
|
"""
|
|
sink_module.get_backend().verify_connectivity()
|
|
|
|
|
|
def verify_scan_databases_available() -> None:
|
|
"""Raise if either graph database needed by an Attack Paths scan is unavailable."""
|
|
errors: list[str] = []
|
|
first_error: Exception | None = None
|
|
|
|
try:
|
|
ingest.get_driver().verify_connectivity()
|
|
except Exception as exc:
|
|
errors.append(f"ingest Neo4j: {exc}")
|
|
first_error = exc
|
|
|
|
try:
|
|
get_driver().verify_connectivity()
|
|
except Exception as exc:
|
|
errors.append(f"sink {settings.ATTACK_PATHS_SINK_DATABASE}: {exc}")
|
|
if first_error is None:
|
|
first_error = exc
|
|
|
|
if errors:
|
|
raise RuntimeError(
|
|
"Attack Paths graph database unavailable before scan start: "
|
|
+ "; ".join(errors)
|
|
) from first_error
|
|
|
|
|
|
def get_uri() -> str:
|
|
"""Return the sink URI. Retained for backwards compatibility."""
|
|
if settings.ATTACK_PATHS_SINK_DATABASE == "neptune":
|
|
cfg = settings.DATABASES["neptune"]
|
|
return f"bolt+s://{cfg['WRITER_ENDPOINT']}:{cfg['PORT']}"
|
|
|
|
cfg = settings.DATABASES["neo4j"]
|
|
return f"bolt://{cfg['HOST']}:{cfg['PORT']}"
|
|
|
|
|
|
def get_ingest_uri() -> str:
|
|
"""Neo4j URI for the cartography temp (ingest) database, which is always
|
|
Neo4j regardless of the configured sink."""
|
|
return ingest.get_uri()
|
|
|
|
|
|
# Session API
|
|
|
|
|
|
def get_session(
|
|
database: str | None = None,
|
|
default_access_mode: str | None = None,
|
|
) -> AbstractContextManager:
|
|
"""Return a session against the right backend.
|
|
|
|
- `database` names starting with `db-tmp-scan-` always go to ingest.
|
|
- No database name → ingest (used for CREATE / DROP DATABASE admin ops).
|
|
- Any other name → sink.
|
|
"""
|
|
if _is_ingest_database(database) or database is None:
|
|
return ingest.get_session(
|
|
database=database, default_access_mode=default_access_mode
|
|
)
|
|
|
|
return sink_module.get_backend().get_session(
|
|
database=database, default_access_mode=default_access_mode
|
|
)
|
|
|
|
|
|
def execute_read_query(
|
|
database: str,
|
|
cypher: str,
|
|
parameters: dict[str, Any] | None = None,
|
|
) -> neo4j.graph.Graph:
|
|
"""Read-only query against the sink."""
|
|
return sink_module.get_backend().execute_read_query(database, cypher, parameters)
|
|
|
|
|
|
def create_database(database: str) -> None:
|
|
"""Create a database. Temp DBs always land on ingest (Neo4j).
|
|
|
|
On the Neo4j sink, tenant DBs also route to ingest because both drivers
|
|
connect to the same Neo4j cluster. On the Neptune sink, tenant DB creates
|
|
are no-ops.
|
|
"""
|
|
if _is_ingest_database(database):
|
|
ingest.create_database(database)
|
|
return
|
|
|
|
sink_module.get_backend().create_database(database)
|
|
|
|
|
|
def drop_database(database: str) -> None:
|
|
"""Drop a database. Mirrors `create_database` routing."""
|
|
if _is_ingest_database(database):
|
|
ingest.drop_database(database)
|
|
return
|
|
|
|
sink_module.get_backend().drop_database(database)
|
|
|
|
|
|
def drop_subgraph(database: str, provider_id: str) -> int:
|
|
return sink_module.get_backend().drop_subgraph(database, provider_id)
|
|
|
|
|
|
def has_provider_data(database: str, provider_id: str) -> bool:
|
|
return sink_module.get_backend().has_provider_data(database, provider_id)
|
|
|
|
|
|
def clear_cache(database: str) -> None:
|
|
if _is_ingest_database(database):
|
|
ingest.clear_cache(database)
|
|
return
|
|
|
|
sink_module.get_backend().clear_cache(database)
|
|
|
|
|
|
# Name helper
|
|
|
|
|
|
def get_database_name(entity_id: str | UUID, temporary: bool = False) -> str:
|
|
prefix = "tmp-scan" if temporary else "tenant"
|
|
return f"db-{prefix}-{str(entity_id).lower()}"
|