662 lines
29 KiB
Python
662 lines
29 KiB
Python
"""Run the narrowly scoped, read-only Mission 00 Firebird probe."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ctypes
|
|
import importlib.metadata
|
|
import json
|
|
import os
|
|
import platform
|
|
import re
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
|
|
from gti_discovery.firebird_connection import (
|
|
AUTHORIZED_DATABASE_PATH,
|
|
_FirebirdAdapter,
|
|
_ADAPTER_CREATION_TOKEN,
|
|
_create_firebird_adapter,
|
|
connect_authorized_read_only,
|
|
get_database_identity,
|
|
validate_authorized_database_path,
|
|
validate_authorized_database_identity,
|
|
)
|
|
from gti_discovery.collector import _safe_sample_rows, collect_metadata
|
|
from gti_discovery.fingerprint import (
|
|
SCHEMA_CHANGED,
|
|
SCHEMA_SUPPORTED,
|
|
SCHEMA_UNKNOWN,
|
|
build_fingerprint,
|
|
classify_schema,
|
|
)
|
|
from gti_discovery.mapping import build_schema_mapping, identify_product_candidates
|
|
from gti_discovery.provenance import (
|
|
ProvenanceError,
|
|
validate_fdb_provenance,
|
|
validate_imported_fdb_provenance,
|
|
)
|
|
|
|
|
|
DEFAULT_TARGET = AUTHORIZED_DATABASE_PATH
|
|
CLIENT_LIBRARY = Path(r"C:\Program Files\Firebird\Firebird_2_5\bin\fbclient.dll")
|
|
_BLOCKER_CATEGORIES = {
|
|
ValueError: "target_rejected",
|
|
ImportError: "driver_unavailable",
|
|
FileNotFoundError: "client_library_unavailable",
|
|
PermissionError: "connection_failed",
|
|
RuntimeError: "connection_failed",
|
|
}
|
|
|
|
|
|
class _FdbDriver:
|
|
version = "unknown"
|
|
|
|
def __init__(self, fdb_module: Any):
|
|
self._fdb = fdb_module
|
|
self.version = str(getattr(fdb_module, "__version__", "unknown"))
|
|
self._last_connection = None
|
|
|
|
def connect(self, **options):
|
|
if set(options) - {"database", "read_only", "client_library"}:
|
|
raise RuntimeError("unexpected connection settings")
|
|
if options.pop("read_only", None) is not True:
|
|
raise RuntimeError("read-only mode is required")
|
|
client_library = options.pop("client_library", None)
|
|
user = os.environ.get("FB_USER")
|
|
password = os.environ.get("FB_PASSWORD")
|
|
if not user or not password:
|
|
raise RuntimeError("Firebird credentials are unavailable in FB_USER/FB_PASSWORD")
|
|
isolation_level = _read_only_isolation_level(self._fdb)
|
|
raw = self._fdb.connect(
|
|
database=options["database"],
|
|
user=user,
|
|
password=password,
|
|
fb_library_name=client_library,
|
|
isolation_level=isolation_level,
|
|
)
|
|
self._last_connection = raw
|
|
observed = getattr(raw, "default_tpb", None)
|
|
if observed != isolation_level:
|
|
close = getattr(raw, "close", None)
|
|
if callable(close):
|
|
close()
|
|
raise RuntimeError("driver did not establish the fdb read-only TPB")
|
|
evidence = {
|
|
"accepted": True,
|
|
"driver_api": "fdb.Connection.default_tpb",
|
|
"configured_tpb": bytes(isolation_level),
|
|
"observed_default_tpb": bytes(observed),
|
|
}
|
|
return _create_firebird_adapter(raw, evidence, _ADAPTER_CREATION_TOKEN)
|
|
|
|
def facts(self) -> dict[str, Any]:
|
|
return {name: _fact(self._last_connection, name) for name in (
|
|
"engine_version", "database_sql_dialect", "ods_version",
|
|
"ods_minor_version", "charset", "page_size",
|
|
)}
|
|
|
|
def compatibility_evidence(self, client_version: str) -> dict[str, Any]:
|
|
isolation_level = _read_only_isolation_level(self._fdb)
|
|
return {
|
|
"fdb_version": self.version,
|
|
"client_version": client_version,
|
|
"firebird_version": ".".join(str(client_version).split(".")[:3]),
|
|
"read_only_isolation": _read_only_isolation_level(self._fdb) is not None,
|
|
"read_only_tpb": bytes(isolation_level),
|
|
"read_only_api": "fdb.Connection.default_tpb",
|
|
}
|
|
|
|
|
|
def _read_only_isolation_level(fdb: Any) -> Any:
|
|
"""Use fdb 2.0.4's actual read-only TPB constant (including its spelling)."""
|
|
value = getattr(fdb, "ISOLATION_LEVEL_READ_COMMITED_RO", None)
|
|
if isinstance(value, bytes) and value == b"\x03\x08\x06\x0f\x11":
|
|
return value
|
|
raise RuntimeError("fdb read-only isolation API is unavailable")
|
|
|
|
|
|
def _fact(connection: Any, name: str) -> Any:
|
|
try:
|
|
value = getattr(connection, name)
|
|
except Exception:
|
|
return None
|
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
return value
|
|
return str(value)
|
|
|
|
|
|
def _read_file_version(path: Path) -> str | None:
|
|
"""Read Windows file-version metadata without loading the client DLL."""
|
|
if sys.platform != "win32":
|
|
return None
|
|
try:
|
|
version = ctypes.windll.version
|
|
size = version.GetFileVersionInfoSizeW(str(path), None)
|
|
if not size:
|
|
return None
|
|
buffer = ctypes.create_string_buffer(size)
|
|
if not version.GetFileVersionInfoW(str(path), 0, size, buffer):
|
|
return None
|
|
pointer = ctypes.c_void_p()
|
|
length = ctypes.c_uint()
|
|
if not version.VerQueryValueW(buffer, "\\", ctypes.byref(pointer), ctypes.byref(length)):
|
|
return None
|
|
class FixedFileInfo(ctypes.Structure):
|
|
_fields_ = [("signature", ctypes.c_uint32), ("struct_version", ctypes.c_uint32),
|
|
("file_version_ms", ctypes.c_uint32), ("file_version_ls", ctypes.c_uint32)]
|
|
info = ctypes.cast(pointer, ctypes.POINTER(FixedFileInfo)).contents
|
|
return ".".join((str(info.file_version_ms >> 16), str(info.file_version_ms & 0xffff),
|
|
str(info.file_version_ls >> 16), str(info.file_version_ls & 0xffff)))
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _client_metadata(path: Path) -> dict[str, Any]:
|
|
exists = path.is_file()
|
|
return {"path": str(path), "exists": exists, "version": _read_file_version(path) if exists else None}
|
|
|
|
|
|
def _read_client_architecture(path: Path) -> str:
|
|
"""Read PE architecture metadata without loading or executing the DLL."""
|
|
try:
|
|
with path.open("rb") as handle:
|
|
if handle.read(2) != b"MZ":
|
|
return "UNKNOWN"
|
|
handle.seek(0x3C)
|
|
offset = struct.unpack("<I", handle.read(4))[0]
|
|
handle.seek(offset)
|
|
if handle.read(4) != b"PE\x00\x00":
|
|
return "UNKNOWN"
|
|
machine = struct.unpack("<H", handle.read(2))[0]
|
|
except (OSError, struct.error):
|
|
return "UNKNOWN"
|
|
return {0x014C: "x86", 0x8664: "x64", 0xAA64: "arm64"}.get(machine, "UNKNOWN")
|
|
|
|
|
|
def _python_architecture() -> str:
|
|
return "x64" if struct.calcsize("P") == 8 else "x86" if struct.calcsize("P") == 4 else "UNKNOWN"
|
|
|
|
|
|
def validate_firebird_runtime(*, fdb_module: Any = None) -> dict[str, Any]:
|
|
"""Return deterministic dependency evidence; never infer unknown facts."""
|
|
try:
|
|
provenance = validate_fdb_provenance()
|
|
except ProvenanceError as error:
|
|
return {"status": "UNKNOWN", "blocker": "compatibility_unproven", "provenance": error.evidence}
|
|
client = _client_metadata(CLIENT_LIBRARY)
|
|
client = dict(client)
|
|
client["architecture"] = client.get("architecture", _read_client_architecture(CLIENT_LIBRARY))
|
|
distribution_version = None
|
|
try:
|
|
distribution_version = importlib.metadata.version("fdb")
|
|
except (importlib.metadata.PackageNotFoundError, ImportError):
|
|
pass
|
|
fdb_version = getattr(fdb_module, "__version__", distribution_version)
|
|
python_architecture = _python_architecture()
|
|
read_only_api = (
|
|
fdb_module is not None
|
|
and isinstance(getattr(fdb_module, "ISOLATION_LEVEL_READ_COMMITED_RO", None), bytes)
|
|
and getattr(fdb_module, "ISOLATION_LEVEL_READ_COMMITED_RO") == b"\x03\x08\x06\x0f\x11"
|
|
)
|
|
version_match = fdb_version == "2.0.4" and distribution_version == "2.0.4"
|
|
architecture_match = client["architecture"] in {"x86", "x64", "arm64"} and (
|
|
(python_architecture == "x64" and client["architecture"] == "x64")
|
|
or (python_architecture == "x86" and client["architecture"] == "x86")
|
|
)
|
|
valid = (
|
|
provenance.get("status") == "VALID"
|
|
and client["exists"] is True
|
|
and client["version"] == "2.5.9.27139"
|
|
and client["architecture"] != "UNKNOWN"
|
|
and architecture_match
|
|
and version_match
|
|
and fdb_module is not None
|
|
and read_only_api
|
|
)
|
|
compatibility = {
|
|
"status": "VALID" if valid else "UNKNOWN",
|
|
"python_client_architecture_match": architecture_match,
|
|
"fdb_client_version_match": version_match,
|
|
"read_only_api": read_only_api,
|
|
}
|
|
return {
|
|
"status": "VALID" if valid else "UNKNOWN",
|
|
"python_runtime": {"version": platform.python_version(), "architecture": python_architecture},
|
|
"fdb": {"version": fdb_version, "distribution_version": distribution_version},
|
|
"client": client,
|
|
"provenance": provenance,
|
|
"compatibility": compatibility,
|
|
**({} if valid else {"blocker": "compatibility_unproven"}),
|
|
}
|
|
|
|
|
|
def _blocker_category(error: Exception) -> str:
|
|
if type(error).__name__ == "DatabaseError" and (
|
|
"-902" in str(error) or "user name and password are not defined" in str(error).lower()
|
|
):
|
|
return "credentials_rejected"
|
|
if isinstance(error, ValueError) and "read-only" in str(error).lower():
|
|
return "read_only_unproven"
|
|
if isinstance(error, RuntimeError) and "credentials" in str(error).lower():
|
|
return "credentials_unavailable"
|
|
if isinstance(error, RuntimeError) and "compatib" in str(error).lower():
|
|
return "compatibility_unproven"
|
|
if isinstance(error, RuntimeError) and "provenance" in str(error).lower():
|
|
return "compatibility_unproven"
|
|
return next((category for error_type, category in _BLOCKER_CATEGORIES.items()
|
|
if isinstance(error, error_type)), "unexpected_failure")
|
|
|
|
|
|
def _firebird_259(version: Any) -> bool:
|
|
match = re.match(r"^(\d+)\.(\d+)\.(\d+)", str(version or ""))
|
|
return bool(match and tuple(int(item) for item in match.groups()) == (2, 5, 9))
|
|
|
|
|
|
def _validate_runtime_compatibility(driver: Any) -> dict[str, Any]:
|
|
"""Prove the installed 2.5.9 client and usable driver before connect."""
|
|
client = _client_metadata(CLIENT_LIBRARY)
|
|
if not client["exists"] or not _firebird_259(client["version"]):
|
|
raise RuntimeError("Firebird 2.5.9 client compatibility is unproven")
|
|
if type(driver) is not _FdbDriver:
|
|
raise RuntimeError("Firebird driver/client compatibility is unproven")
|
|
try:
|
|
import fdb as installed_fdb
|
|
installed_version = importlib.metadata.version("fdb")
|
|
except (ImportError, importlib.metadata.PackageNotFoundError) as error:
|
|
raise RuntimeError("Firebird driver/client compatibility is unproven") from error
|
|
if (
|
|
driver._fdb is not installed_fdb
|
|
or installed_version != "2.0.4"
|
|
or getattr(installed_fdb, "__version__", None) != "2.0.4"
|
|
or _read_only_isolation_level(installed_fdb) != b"\x03\x08\x06\x0f\x11"
|
|
):
|
|
raise RuntimeError("Firebird driver/client compatibility is unproven")
|
|
return client
|
|
|
|
|
|
def _validate_preimport_compatibility() -> dict[str, Any]:
|
|
provenance = validate_fdb_provenance()
|
|
client = _client_metadata(CLIENT_LIBRARY)
|
|
client = dict(client)
|
|
client["architecture"] = client.get("architecture", _read_client_architecture(CLIENT_LIBRARY))
|
|
try:
|
|
fdb_version = importlib.metadata.version("fdb")
|
|
except importlib.metadata.PackageNotFoundError as error:
|
|
raise RuntimeError("fdb 2.0.4 compatibility is unproven") from error
|
|
if (
|
|
not client["exists"]
|
|
or not _firebird_259(client["version"])
|
|
or client["architecture"] == "UNKNOWN"
|
|
or client["architecture"] != _python_architecture()
|
|
or fdb_version != "2.0.4"
|
|
):
|
|
raise RuntimeError("Firebird 2.5.9/fdb 2.0.4 compatibility is unproven")
|
|
if not _preimport_read_only_api_proven():
|
|
raise RuntimeError("Firebird read-only isolation API compatibility is unproven")
|
|
return {"client": client, "provenance": provenance}
|
|
|
|
|
|
def _preimport_read_only_api_proven() -> bool:
|
|
"""Prove the pinned driver's read-only API from installed source, without import."""
|
|
try:
|
|
spec = __import__("importlib.util", fromlist=["find_spec"]).find_spec("fdb")
|
|
locations = list(getattr(spec, "submodule_search_locations", ()) or ())
|
|
if not locations:
|
|
return False
|
|
package = Path(locations[0])
|
|
init_source = (package / "__init__.py").read_text(encoding="utf-8")
|
|
core_source = (package / "fbcore.py").read_text(encoding="utf-8")
|
|
except (OSError, UnicodeError, ImportError, AttributeError, TypeError):
|
|
return False
|
|
return (
|
|
"ISOLATION_LEVEL_READ_COMMITED_RO" in init_source
|
|
and "ISOLATION_LEVEL_READ_COMMITED_RO = bs([" in core_source
|
|
and all(token in core_source for token in (
|
|
"isc_tpb_version3", "isc_tpb_read", "isc_tpb_wait",
|
|
"isc_tpb_read_committed", "isc_tpb_rec_version",
|
|
))
|
|
)
|
|
|
|
|
|
def _security_gate(path: os.PathLike[str] | str, driver: Any) -> dict[str, Any]:
|
|
validate_authorized_database_path(path)
|
|
if not os.environ.get("FB_USER") or not os.environ.get("FB_PASSWORD"):
|
|
raise RuntimeError("Firebird credentials are unavailable in FB_USER/FB_PASSWORD")
|
|
evidence = validate_firebird_runtime(fdb_module=getattr(driver, "_fdb", None))
|
|
if evidence["status"] != "VALID":
|
|
raise RuntimeError("Firebird driver/client compatibility is unproven")
|
|
return {"client": evidence["client"], "compatibility_evidence": evidence}
|
|
|
|
|
|
_SAMPLE_DENYLIST = (
|
|
"CUSTOMER", "CLIENT", "EMAIL", "PHONE", "MOBILE", "CONTACT", "ADDRESS",
|
|
"PRICE", "COST", "AMOUNT", "TOTAL", "TAX", "CRED", "PASSWORD",
|
|
"SECRET", "TOKEN", "BLOB", "SOURCE", "DEFAULT", "SALARY", "MARGIN",
|
|
"DISCOUNT", "PROFIT", "IMAGE", "PHOTO", "FILE", "PATH", "URL",
|
|
)
|
|
_SAMPLE_ALLOWLIST = (
|
|
"ID", "KEY", "CODE", "CODIGO", "SKU", "ITEM", "PRODUCT", "DESCRIPTION",
|
|
"DESCRICAO", "NAME", "NOME", "BARCODE", "EAN", "GTIN", "REFERENCE",
|
|
"REFERENCIA", "REF", "UNIT", "UNIDADE", "CATEGORY", "GROUP", "GRUPO",
|
|
"BRAND", "MARCA", "PRODUTO", "ESTOQUE", "STATUS", "ACTIVE", "ATIVO",
|
|
)
|
|
|
|
|
|
def _permitted_sample_plan(metadata: dict[str, Any]) -> list[tuple[str, list[str]]]:
|
|
fields_by_relation: dict[str, list[dict[str, Any]]] = {}
|
|
for field in metadata.get("fields", []):
|
|
if not isinstance(field, dict):
|
|
continue
|
|
relation = str(field.get("RELATION_NAME", "")).strip()
|
|
if relation:
|
|
fields_by_relation.setdefault(relation.upper(), []).append(field)
|
|
candidates = identify_product_candidates(metadata, {})
|
|
plan = []
|
|
for candidate in candidates:
|
|
relation = str(candidate.get("relation", "")).strip()
|
|
if not relation or candidate.get("classification") == "UNKNOWN":
|
|
continue
|
|
columns = []
|
|
for field in fields_by_relation.get(relation.upper(), []):
|
|
name = str(field.get("FIELD_NAME", "")).strip()
|
|
type_name = str(field.get("TYPE_NAME", field.get("FIELD_TYPE", ""))).upper()
|
|
upper_name = name.upper()
|
|
if not name or any(word in upper_name for word in _SAMPLE_DENYLIST):
|
|
continue
|
|
if "BLOB" in type_name or not any(word in upper_name for word in _SAMPLE_ALLOWLIST):
|
|
continue
|
|
columns.append(name)
|
|
if columns:
|
|
plan.append((relation, columns[:8]))
|
|
return plan
|
|
|
|
|
|
def _collect_samples(reader: Any, metadata: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
|
|
samples = {}
|
|
plan = _permitted_sample_plan(metadata)
|
|
if not isinstance(plan, list) or any(
|
|
not isinstance(item, tuple)
|
|
or len(item) != 2
|
|
or not isinstance(item[0], str)
|
|
or not isinstance(item[1], list)
|
|
or not item[1]
|
|
or any(
|
|
not isinstance(column, str)
|
|
or any(word in column.upper() for word in _SAMPLE_DENYLIST)
|
|
or not any(word in column.upper() for word in _SAMPLE_ALLOWLIST)
|
|
for column in item[1]
|
|
)
|
|
for item in plan
|
|
):
|
|
raise RuntimeError("sample plan is not permitted")
|
|
for relation, columns in plan:
|
|
rows = reader.sample_relation_rows(relation, columns, 5)
|
|
if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
|
|
raise RuntimeError("sample evidence is malformed")
|
|
samples[relation] = _safe_sample_rows(rows)
|
|
return samples
|
|
|
|
|
|
def _blocked(
|
|
blocker: str,
|
|
*,
|
|
connection_attempts: int = 0,
|
|
other_databases_accessed: int | str = 0,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"status": "MISSION_00_BLOCKED",
|
|
"blocker": blocker,
|
|
"application_writes": 0,
|
|
"other_databases_accessed": other_databases_accessed,
|
|
"connection_attempts": connection_attempts,
|
|
}
|
|
|
|
|
|
def _valid_metadata(metadata: Any) -> bool:
|
|
if not isinstance(metadata, dict):
|
|
return False
|
|
if not all(isinstance(metadata.get(name), list) for name in ("relations", "fields", "errors", "unavailable")):
|
|
return False
|
|
return (
|
|
all(isinstance(row, dict) for name in ("relations", "fields", "errors") for row in metadata[name])
|
|
and all(isinstance(name, str) and bool(name) for name in metadata["unavailable"])
|
|
)
|
|
|
|
|
|
def _valid_mapping(mapping: Any) -> bool:
|
|
required = {"product", "fields", "fiscal", "stock", "media", "incremental", "triggers"}
|
|
if type(mapping) is not dict or set(mapping) != required:
|
|
return False
|
|
allowed = {"CONFIRMED", "INFERRED", "UNKNOWN"}
|
|
|
|
def strings(value: Any) -> bool:
|
|
return type(value) is list and all(type(item) is str and bool(item) for item in value)
|
|
|
|
def classified(value: Any, keys: set[str]) -> bool:
|
|
return type(value) is dict and set(value) == keys and value["classification"] in allowed
|
|
|
|
def field_record(value: Any) -> bool:
|
|
if type(value) is not dict or not {"classification", "sample_present", "type"} <= set(value):
|
|
return False
|
|
if set(value) - {"classification", "sample_present", "type", "subtype", "value"}:
|
|
return False
|
|
return value["classification"] in allowed and type(value["sample_present"]) is bool and type(value["type"]) is str and bool(value["type"]) and all(
|
|
type(value[key]) in (str, int, float, bool) for key in ("subtype", "value") if key in value
|
|
)
|
|
|
|
def field_map(value: Any) -> bool:
|
|
return type(value) is dict and all(type(key) is str and bool(key) and field_record(item) for key, item in value.items())
|
|
|
|
def relation_record(value: Any) -> bool:
|
|
if type(value) is not dict or not {"classification", "fields", "relationships"} <= set(value):
|
|
return False
|
|
if set(value) - {"classification", "fields", "relationships", "evidence"}:
|
|
return False
|
|
relationships = value["relationships"]
|
|
return value["classification"] in allowed and field_map(value["fields"]) and type(relationships) is list and all(
|
|
type(item) is dict and set(item) == {"child_relation", "child_field", "referenced_relation", "referenced_field"}
|
|
and all(type(item[key]) is str and bool(item[key]) for key in item)
|
|
for item in relationships
|
|
) and ("evidence" not in value or type(value["evidence"]) is str)
|
|
|
|
def relation_map(value: Any) -> bool:
|
|
return type(value) is dict and all(type(key) is str and bool(key) and relation_record(item) for key, item in value.items())
|
|
|
|
product = mapping["product"]
|
|
product_keys = {"classification", "relation", "evidence", "candidates"}
|
|
candidate_keys = {"classification", "relation", "evidence"}
|
|
evidence_keys = {"primary_key", "field_count", "semantic_fields", "sample_fields", "semantic_sample_fields", "approximate_count"}
|
|
if not classified(product, product_keys) or (product["relation"] is not None and (type(product["relation"]) is not str or not product["relation"])) or type(product["evidence"]) is not dict or set(product["evidence"]) - evidence_keys:
|
|
return False
|
|
candidates = product.get("candidates")
|
|
if not isinstance(candidates, list) or not all(
|
|
classified(item, candidate_keys) and type(item["relation"]) is str and bool(item["relation"])
|
|
and type(item["evidence"]) is dict and set(item["evidence"]) - evidence_keys == set()
|
|
and strings(item["evidence"].get("primary_key"))
|
|
and type(item["evidence"].get("field_count")) is int
|
|
and strings(item["evidence"].get("semantic_fields")) for item in candidates
|
|
):
|
|
return False
|
|
for evidence in (product["evidence"], *(item["evidence"] for item in candidates)):
|
|
if not evidence:
|
|
continue
|
|
if not strings(evidence.get("primary_key")) or type(evidence.get("field_count")) is not int or evidence["field_count"] < 0 or not strings(evidence.get("semantic_fields")):
|
|
return False
|
|
if "sample_fields" in evidence and not strings(evidence["sample_fields"]):
|
|
return False
|
|
if "semantic_sample_fields" in evidence and not strings(evidence["semantic_sample_fields"]):
|
|
return False
|
|
if "approximate_count" in evidence and type(evidence["approximate_count"]) not in (str, int, float, bool, type(None)):
|
|
return False
|
|
if not field_map(mapping["fields"]):
|
|
return False
|
|
for section in ("fiscal", "media"):
|
|
value = mapping[section]
|
|
if not classified(value, {"classification", "fields", "relations"}) or not field_map(value["fields"]) or not relation_map(value["relations"]):
|
|
return False
|
|
if not classified(mapping["stock"], {"classification", "relations"}) or not relation_map(mapping["stock"]["relations"]):
|
|
return False
|
|
incremental = mapping["incremental"]
|
|
evidence = incremental.get("evidence")
|
|
if not classified(incremental, {"classification", "evidence"}) or type(evidence) is not dict or set(evidence) != {"timestamps", "triggers", "generators"}:
|
|
return False
|
|
if not strings(evidence["timestamps"]) or type(evidence["triggers"]) is not int or evidence["triggers"] < 0 or type(evidence["generators"]) is not int or evidence["generators"] < 0:
|
|
return False
|
|
triggers = mapping["triggers"]
|
|
return type(triggers) is list and all(
|
|
classified(trigger, {"classification", "relation", "trigger", "timing", "event", "trigger_type", "dependencies", "source"})
|
|
and all(type(trigger[key]) is str and bool(trigger[key]) for key in ("relation", "trigger", "timing", "event", "source"))
|
|
and type(trigger["trigger_type"]) in (str, int) and bool(str(trigger["trigger_type"]))
|
|
and strings(trigger.get("dependencies"))
|
|
for trigger in triggers
|
|
)
|
|
|
|
|
|
def _valid_fingerprint(value: Any) -> bool:
|
|
return isinstance(value, str) and bool(re.fullmatch(r"[0-9a-f]{64}", value))
|
|
|
|
|
|
def _public_compatibility(evidence: Any) -> dict[str, str]:
|
|
"""Expose compatibility categories only; never serialize internal evidence."""
|
|
if not isinstance(evidence, dict):
|
|
return {"status": "UNKNOWN"}
|
|
return {
|
|
"status": evidence.get("status", "UNKNOWN") if evidence.get("status") in {"VALID", "UNKNOWN"} else "UNKNOWN",
|
|
"architecture": "MATCH" if evidence.get("compatibility", {}).get("python_client_architecture_match") is True else "UNKNOWN",
|
|
"fdb_client": "MATCH" if evidence.get("compatibility", {}).get("fdb_client_version_match") is True else "UNKNOWN",
|
|
"read_only_api": "PROVEN" if evidence.get("compatibility", {}).get("read_only_api") is True else "UNKNOWN",
|
|
}
|
|
|
|
|
|
def _public_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
|
"""Remove database identity rows, which may contain an absolute FDB path."""
|
|
return {key: value for key, value in metadata.items() if key != "database"}
|
|
|
|
|
|
def run_discovery(path: os.PathLike[str] | str, *, driver: Any) -> dict[str, Any]:
|
|
"""Run the gated, structured, sanitized discovery workflow."""
|
|
try:
|
|
security = _security_gate(path, driver)
|
|
client = security.get("client", security)
|
|
except RuntimeError as error:
|
|
return _blocked(_blocker_category(error))
|
|
try:
|
|
connection = connect_authorized_read_only(
|
|
path, driver, client_library=str(CLIENT_LIBRARY)
|
|
)
|
|
except Exception as error:
|
|
return _blocked(_blocker_category(error), connection_attempts=1)
|
|
try:
|
|
try:
|
|
database_name = get_database_identity(connection)
|
|
validate_authorized_database_identity(database_name, path)
|
|
except Exception:
|
|
return _blocked(
|
|
"identity_mismatch",
|
|
connection_attempts=1,
|
|
other_databases_accessed="UNKNOWN",
|
|
)
|
|
try:
|
|
facts = driver.facts() if callable(getattr(driver, "facts", None)) else {}
|
|
metadata = collect_metadata(connection)
|
|
if not _valid_metadata(metadata):
|
|
return _blocked("discovery_invalid", connection_attempts=1)
|
|
samples = _collect_samples(connection, metadata)
|
|
mapping = build_schema_mapping(metadata, samples)
|
|
if not _valid_mapping(mapping):
|
|
return _blocked("discovery_invalid", connection_attempts=1)
|
|
unavailable = set(metadata.get("unavailable", []))
|
|
optional_catalog_gaps = {"events", "packages", "procedure_parameters"}
|
|
blocking_errors = [
|
|
error for error in metadata.get("errors", [])
|
|
if not (
|
|
error.get("kind") == "optional_unsupported"
|
|
or error.get("query") == "monitoring"
|
|
)
|
|
]
|
|
complete = (
|
|
not blocking_errors
|
|
and unavailable <= optional_catalog_gaps
|
|
and (not metadata.get("incomplete") or bool(unavailable) and unavailable <= optional_catalog_gaps)
|
|
and bool(metadata.get("relations"))
|
|
and bool(samples)
|
|
)
|
|
fingerprint = None
|
|
classification = SCHEMA_UNKNOWN
|
|
if complete:
|
|
evidence = {"metadata": metadata, "samples": samples, "mapping": mapping}
|
|
fingerprint = build_fingerprint(evidence)
|
|
if not _valid_fingerprint(fingerprint):
|
|
return _blocked("discovery_invalid", connection_attempts=1)
|
|
expected = getattr(driver, "expected_fingerprints", ())
|
|
classification = classify_schema(
|
|
metadata,
|
|
expected,
|
|
mapping_ambiguous=mapping.get("product", {}).get("classification") == "UNKNOWN",
|
|
)
|
|
if classification not in {SCHEMA_SUPPORTED, SCHEMA_CHANGED, SCHEMA_UNKNOWN}:
|
|
return _blocked("discovery_invalid", connection_attempts=1)
|
|
return {
|
|
"status": "CONNECTED" if complete else SCHEMA_UNKNOWN,
|
|
"driver_version": str(getattr(driver, "version", "unknown")),
|
|
"architecture": platform.architecture()[0],
|
|
"engine_version": facts.get("engine_version"),
|
|
"dialect": facts.get("database_sql_dialect"),
|
|
"ods": {
|
|
"major": facts.get("ods_version"),
|
|
"minor": facts.get("ods_minor_version"),
|
|
},
|
|
"charset": facts.get("charset"),
|
|
"page_size": facts.get("page_size"),
|
|
"metadata": _public_metadata(metadata),
|
|
"samples": samples,
|
|
"schema_mapping": mapping,
|
|
"fingerprint": fingerprint,
|
|
"schema_classification": classification,
|
|
"compatibility": _public_compatibility(security.get("compatibility_evidence")),
|
|
"application_writes": 0,
|
|
"other_databases_accessed": 0,
|
|
"connection_attempts": 1,
|
|
}
|
|
except Exception:
|
|
return _blocked("discovery_failed", connection_attempts=1)
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.parse_args(argv)
|
|
try:
|
|
if not os.environ.get("FB_USER") or not os.environ.get("FB_PASSWORD"):
|
|
raise RuntimeError("credentials unavailable")
|
|
compatibility = _validate_preimport_compatibility()
|
|
import fdb
|
|
|
|
provenance = validate_imported_fdb_provenance(fdb)
|
|
runtime_compatibility = validate_firebird_runtime(fdb_module=fdb)
|
|
if runtime_compatibility["status"] != "VALID":
|
|
raise RuntimeError("Firebird driver/client compatibility is unproven")
|
|
result = run_discovery(DEFAULT_TARGET, driver=_FdbDriver(fdb))
|
|
result["compatibility"] = _public_compatibility(runtime_compatibility)
|
|
except Exception as error:
|
|
result = {
|
|
"status": "MISSION_00_BLOCKED",
|
|
"blocker": _blocker_category(error),
|
|
"application_writes": 0,
|
|
"other_databases_accessed": 0,
|
|
"connection_attempts": 0,
|
|
}
|
|
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
|
|
return 0 if result["status"] == "CONNECTED" else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|