From e2b6706dd2a007f545d2bdcb4800dc4583eda6c7 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:18:14 -0700 Subject: [PATCH 1/2] chore(scripts): remove seed_chaos_data.py fake-data seeder Delete scripts/seed_chaos_data.py, its guard tests in tests/scripts/test_prod_default_guards.py, and its .secrets.baseline entry. No other callers, docs, or CI references remain. Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- .secrets.baseline | 9 - scripts/seed_chaos_data.py | 511 ---------------------- tests/scripts/test_prod_default_guards.py | 57 --- 3 files changed, 577 deletions(-) delete mode 100644 scripts/seed_chaos_data.py diff --git a/.secrets.baseline b/.secrets.baseline index dc26a198f..3ba070a96 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -2895,15 +2895,6 @@ "line_number": 163 } ], - "scripts/seed_chaos_data.py": [ - { - "type": "Basic Auth Credentials", - "filename": "scripts/seed_chaos_data.py", - "hashed_secret": "635bf77ef2cb61d6f2a92280ecc2fa9f227551bc", - "is_verified": false, - "line_number": 20 - } - ], "scripts/utilities/db_test.py": [ { "type": "Basic Auth Credentials", diff --git a/scripts/seed_chaos_data.py b/scripts/seed_chaos_data.py deleted file mode 100644 index 8d6cdbe60..000000000 --- a/scripts/seed_chaos_data.py +++ /dev/null @@ -1,511 +0,0 @@ -""" -RegEngine Chaos Test Data Seeder -================================ -Seeds known quantities of test data into Neo4j and PostgreSQL -before chaos engineering tests run so data integrity checks are -meaningful (not just 0 -> 0). - - -Usage: - python seed_chaos_data.py # Seed all stores - python seed_chaos_data.py --neo4j-only # Seed Neo4j only - python seed_chaos_data.py --postgres-only # Seed Postgres only - python seed_chaos_data.py --cleanup # Remove seeded data - - -Environment variables: - NEO4J_URI (default: bolt://localhost:7687) - NEO4J_USER (default: neo4j) - NEO4J_PASSWORD (default: neo4j) - POSTGRES_URL (default: postgresql://regengine:regengine@localhost:5432/regengine) -""" - -import argparse -import json -import logging -import os -import sys -from datetime import datetime, timezone -from uuid import uuid4 - -# APScheduler imports moved to seed_scheduler_jobs() - - -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") -logger = logging.getLogger("chaos-seeder") - - -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- -NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687") -NEO4J_USER = os.getenv("NEO4J_USER", "neo4j") -NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "neo4j") -# Default matches the current dev stack โ€” the legacy regengine_admin DB on -# 5433 no longer holds any tables (see .env.example). -POSTGRES_URL = os.getenv("POSTGRES_URL", "postgresql://regengine:regengine@localhost:5432/regengine") - -# Localhost is the default target. Seeding a hosted or non-localhost store -# requires the explicit --allow-production flag. -PRODUCTION_DB_HINTS = ( - "supabase.co", - "supabase.com", - "neon.tech", - "railway.app", - "amazonaws.com", - "ondigitalocean.com", - "render.com", -) - - -def _host_of(url: str) -> str: - from urllib.parse import urlsplit - - try: - return (urlsplit(url).hostname or "").lower() - except ValueError: - return "" - - -def _effective_hosts(url: str) -> list[str]: - """Every host a driver may connect to for this URL. - - The authority host plus any ``host`` / ``hostaddr`` query values, which - Postgres drivers accept as overrides. A URL that looks like localhost - but carries a remote query host is not local. - """ - from urllib.parse import parse_qs, urlsplit - - hosts: list[str] = [] - try: - parts = urlsplit(url) - except ValueError: - return [""] - if parts.hostname: - hosts.append(parts.hostname.lower()) - try: - query = parse_qs(parts.query) - except ValueError: - return hosts or [""] - for key in ("host", "hostaddr"): - for value in query.get(key, []): - # Multi-host values are comma-separated; each names a target. - hosts.extend(h.strip().lower() for h in value.split(",") if h.strip()) - return hosts or [""] - - -LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1"} -# Compose service names from .env.example (local dev stack only). -DEV_COMPOSE_HOSTS = {"postgres", "neo4j", "redis"} -LOCAL_HOSTS = LOOPBACK_HOSTS | DEV_COMPOSE_HOSTS - - -def is_production_target(url: str) -> bool: - """True when a store URL points anywhere other than a local host.""" - lowered = url.lower() - if any(hint in lowered for hint in PRODUCTION_DB_HINTS): - return True - hosts = _effective_hosts(url) - return not (hosts and all(h in LOCAL_HOSTS for h in hosts)) - - -def ensure_local_targets(postgres_url: str, neo4j_uri: str, allow_production: bool) -> None: - """Refuse hosted or non-localhost stores without explicit opt-in.""" - env = os.getenv("REGENGINE_ENV", "").strip().lower() - if (is_production_target(postgres_url) or is_production_target(neo4j_uri) or env == "production") and not allow_production: - logger.error( - "Refusing to seed a non-localhost store. " - "Use localhost URLs or pass --allow-production only if you are certain." - ) - sys.exit(2) - - -# Seed tag so we can identify and clean up test data -CHAOS_SEED_TAG = "chaos_test_seed" - - -# How much data to seed -NEO4J_REGULATION_COUNT = 25 -NEO4J_SECTION_COUNT = 50 # 2 sections per regulation -NEO4J_REQUIREMENT_COUNT = 100 # 2 requirements per section -POSTGRES_API_KEY_COUNT = 10 -POSTGRES_SCHEDULER_JOB_COUNT = 15 - - -# --------------------------------------------------------------------------- -# Neo4j Seeder -# --------------------------------------------------------------------------- -def seed_neo4j(): - """Seed Neo4j with a realistic regulatory graph structure.""" - try: - from neo4j import GraphDatabase - except ImportError: - logger.error("โŒ neo4j package not installed. Run: pip install neo4j") - return False - - logger.info(f"๐Ÿ”Œ Connecting to Neo4j at {NEO4J_URI}") - - # Retry logic for Neo4j connection - import time - - max_retries = 30 - for attempt in range(max_retries): - try: - driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD)) - driver.verify_connectivity() - logger.info("โœ… Neo4j connection verified") - break - except Exception as e: - if attempt < max_retries - 1: - logger.warning( - f"โš ๏ธ Neo4j connection failed (attempt {attempt + 1}/{max_retries}): {e}. Retrying in 2s..." - ) - time.sleep(2) - else: - logger.error(f"โŒ Neo4j connection failed after {max_retries} attempts: {e}") - return False - - with driver.session() as session: - # Clean up any previous seed data first - session.run("MATCH (n {_chaos_seed: $tag}) DETACH DELETE n", tag=CHAOS_SEED_TAG) - logger.info("๐Ÿงน Cleaned up previous seed data") - - # Create Regulation nodes - regulations = [] - for i in range(NEO4J_REGULATION_COUNT): - reg_id = f"REG-CHAOS-{i:04d}" - regulations.append(reg_id) - session.run( - """ - CREATE (r:Regulation { - id: $id, - title: $title, - agency: $agency, - effective_date: $effective_date, - status: 'active', - _chaos_seed: $tag - }) - """, - id=reg_id, - title=f"Test Regulation {i} - FSMA 204 Subsection", - agency="FDA", - effective_date="2026-01-01", - tag=CHAOS_SEED_TAG, - ) - - logger.info(f" ๐Ÿ“„ Created {NEO4J_REGULATION_COUNT} Regulation nodes") - - # Create Section nodes linked to Regulations - sections = [] - for i in range(NEO4J_SECTION_COUNT): - sec_id = f"SEC-CHAOS-{i:04d}" - reg_id = regulations[i % NEO4J_REGULATION_COUNT] - sections.append(sec_id) - session.run( - """ - MATCH (r:Regulation {id: $reg_id}) - CREATE (s:Section { - id: $sec_id, - title: $title, - section_number: $section_number, - _chaos_seed: $tag - }) - CREATE (r)-[:HAS_SECTION]->(s) - """, - reg_id=reg_id, - sec_id=sec_id, - title=f"Section {i} - Traceability Requirements", - section_number=f"{(i % 10) + 1}.{i}", - tag=CHAOS_SEED_TAG, - ) - - logger.info(f" ๐Ÿ“‘ Created {NEO4J_SECTION_COUNT} Section nodes with HAS_SECTION relationships") - - # Create Requirement nodes linked to Sections - for i in range(NEO4J_REQUIREMENT_COUNT): - req_id = f"REQ-CHAOS-{i:04d}" - sec_id = sections[i % NEO4J_SECTION_COUNT] - session.run( - """ - MATCH (s:Section {id: $sec_id}) - CREATE (req:Requirement { - id: $req_id, - text: $text, - severity: $severity, - _chaos_seed: $tag - }) - CREATE (s)-[:HAS_REQUIREMENT]->(req) - """, - sec_id=sec_id, - req_id=req_id, - text=f"Requirement {i}: Maintain records of KDEs for CTEs in the food supply chain", - severity=["critical", "major", "minor"][i % 3], - tag=CHAOS_SEED_TAG, - ) - - logger.info(f" โœ… Created {NEO4J_REQUIREMENT_COUNT} Requirement nodes with HAS_REQUIREMENT relationships") - - # Verify counts - result = session.run("MATCH (n {_chaos_seed: $tag}) RETURN count(n) AS node_count", tag=CHAOS_SEED_TAG).single() - total_nodes = result["node_count"] - - result = session.run( - """ - MATCH (a {_chaos_seed: $tag})-[r]->(b {_chaos_seed: $tag}) - RETURN count(r) AS rel_count - """, - tag=CHAOS_SEED_TAG, - ).single() - total_rels = result["rel_count"] - - logger.info(f" ๐Ÿ“Š Neo4j totals: {total_nodes} nodes, {total_rels} relationships") - - driver.close() - - # Write manifest for the chaos test to reference - manifest = { - "neo4j_nodes": total_nodes, - "neo4j_relationships": total_rels, - "neo4j_regulations": NEO4J_REGULATION_COUNT, - "neo4j_sections": NEO4J_SECTION_COUNT, - "neo4j_requirements": NEO4J_REQUIREMENT_COUNT, - } - return manifest - - -# --------------------------------------------------------------------------- -# PostgreSQL Seeder -# --------------------------------------------------------------------------- -def seed_scheduler_jobs(): - """Seed persistent scheduler jobs using APScheduler.""" - logger.info(" ๐Ÿ“… Seeding persistent scheduler jobs...") - - try: - from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore - from apscheduler.schedulers.background import BackgroundScheduler - from apscheduler.triggers.interval import IntervalTrigger - - # Create a temporary scheduler to insert jobs into the DB - jobstores = {"default": SQLAlchemyJobStore(url=POSTGRES_URL)} - scheduler = BackgroundScheduler(jobstores=jobstores) - # We don't need to start it to add jobs, but adding a jobstore requires it? - # Actually add_jobstore works on running or stopped scheduler. - # But allow tables to be created if not exist. - - # We start it in paused mode to avoid running jobs here - scheduler.start(paused=True) - - for i in range(POSTGRES_SCHEDULER_JOB_COUNT): - job_id = f"chaos-job-{i:04d}" - # Use 'print' as it is safe and available everywhere - scheduler.add_job( - print, - args=[f"Chaos Job {i} execution"], - trigger=IntervalTrigger(minutes=60), - id=job_id, - name=f"Chaos Job {i}", - replace_existing=True, - coalesce=True, - ) - - scheduler.shutdown() - logger.info(f" ๐Ÿ“… Created {POSTGRES_SCHEDULER_JOB_COUNT} persistent scheduler jobs") - - return {"postgres_scheduler_jobs": POSTGRES_SCHEDULER_JOB_COUNT} - - except Exception as e: - logger.error(f" โŒ Failed to seed scheduler jobs: {e}") - return None - - -def seed_postgres(): - """Seed PostgreSQL with API keys and Scheduler Jobs.""" - logger.info("๐Ÿ”Œ Connecting to PostgreSQL") - - # Retry logic for PostgreSQL connection - import time - - max_retries = 30 - engine = None - - for attempt in range(max_retries): - try: - from sqlalchemy import create_engine, make_url, text - - engine = create_engine( - make_url(POSTGRES_URL).set(drivername="postgresql+psycopg") - if POSTGRES_URL.startswith("postgres") - else POSTGRES_URL - ) - - with engine.connect() as conn: - conn.execute(text("SELECT 1")) - logger.info("โœ… PostgreSQL connection verified") - break - except Exception as e: - if attempt < max_retries - 1: - logger.warning( - f"โš ๏ธ PostgreSQL connection failed (attempt {attempt + 1}/{max_retries}): {e}. Retrying in 2s..." - ) - time.sleep(2) - else: - logger.error(f"โŒ PostgreSQL connection failed after {max_retries} attempts: {e}") - return False - - # 1. Seed API Keys (Raw SQL) - api_manifest = {} - try: - with engine.connect() as conn: - # Clean up API keys - conn.execute(text("DELETE FROM api_keys WHERE name LIKE 'Chaos Test Key %'")) - conn.commit() - logger.info("๐Ÿงน Cleaned up previous API key seed data") - - # Seed API keys - for i in range(POSTGRES_API_KEY_COUNT): - conn.execute( - text(""" - INSERT INTO api_keys (id, tenant_id, key_hash, name, enabled, created_at) - VALUES (:id, :tenant_id, :key_hash, :name, true, :created_at) - ON CONFLICT (id) DO NOTHING - """), - { - "id": str(uuid4()), - "tenant_id": str(uuid4()), - "key_hash": f"hash_{i}", # In real app this would be hashed - "name": f"Chaos Test Key {i:04d}", - "created_at": datetime.now(timezone.utc).isoformat(), - }, - ) - conn.commit() - - # Count API keys - api_count = conn.execute(text("SELECT count(*) FROM api_keys WHERE name LIKE 'Chaos Test Key %'")).scalar() - api_manifest["postgres_api_keys"] = api_count - logger.info(f" ๐Ÿ”‘ Created {api_count} API keys") - - except Exception as e: - logger.error(f" โŒ Failed to seed API keys: {e}") - return None - - # 2. Seed Scheduler Jobs (APScheduler) - job_manifest = seed_scheduler_jobs() - - if job_manifest: - api_manifest.update(job_manifest) - - engine.dispose() - - # Verify Total State - # (Optional: check DB for jobs table) - - return api_manifest - - -# --------------------------------------------------------------------------- -# Cleanup -# --------------------------------------------------------------------------- -def cleanup_all(): - """Remove all chaos seed data from both stores.""" - logger.info("๐Ÿงน Cleaning up all chaos seed data...") - - try: - from neo4j import GraphDatabase - - driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD)) - with driver.session() as session: - result = session.run( - "MATCH (n {_chaos_seed: $tag}) DETACH DELETE n RETURN count(n) AS deleted", tag=CHAOS_SEED_TAG - ).single() - logger.info(f" Neo4j: deleted {result['deleted']} nodes") - driver.close() - except Exception as e: - logger.warning(f" Neo4j cleanup skipped: {e}") - - try: - from sqlalchemy import create_engine, make_url, text - - engine = create_engine( - make_url(POSTGRES_URL).set(drivername="postgresql+psycopg") - if POSTGRES_URL.startswith("postgres") - else POSTGRES_URL - ) - with engine.connect() as conn: - r1 = conn.execute(text("DELETE FROM api_keys WHERE name LIKE 'Chaos Test Key %'")) - # Cleanup persistent jobs - # Table name is 'apscheduler_jobs' by default for SQLAlchemyJobStore - try: - r2 = conn.execute(text("DELETE FROM apscheduler_jobs WHERE id LIKE 'chaos-job-%'")) - jobs_deleted = r2.rowcount - except Exception: - jobs_deleted = 0 # Table might not exist if seeding failed - - conn.commit() - logger.info(f" PostgreSQL: deleted {r1.rowcount} API keys, {jobs_deleted} scheduler jobs") - engine.dispose() - except Exception as e: - logger.warning(f" PostgreSQL cleanup skipped: {e}") - - logger.info("โœ… Cleanup complete") - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- -def main(): - parser = argparse.ArgumentParser(description="Seed test data for chaos engineering tests") - parser.add_argument("--neo4j-only", action="store_true", help="Seed Neo4j only") - parser.add_argument("--postgres-only", action="store_true", help="Seed PostgreSQL only") - parser.add_argument("--cleanup", action="store_true", help="Remove all seed data") - parser.add_argument( - "--manifest", - type=str, - default="chaos_seed_manifest.json", - help="Path to write seed manifest (default: chaos_seed_manifest.json)", - ) - parser.add_argument( - "--allow-production", - action="store_true", - help="Allow seeding a non-localhost store. Almost never what you want.", - ) - args = parser.parse_args() - - ensure_local_targets(POSTGRES_URL, NEO4J_URI, args.allow_production) - - if args.cleanup: - cleanup_all() - return - - logger.info("๐ŸŒฑ Starting chaos test data seeding...") - manifest = {"seeded_at": datetime.now(timezone.utc).isoformat(), "tag": CHAOS_SEED_TAG} - - seed_neo = not args.postgres_only - seed_pg = not args.neo4j_only - - if seed_neo: - neo4j_manifest = seed_neo4j() - if neo4j_manifest: - manifest.update(neo4j_manifest) - else: - logger.error("โŒ Neo4j seeding failed") - sys.exit(1) - - if seed_pg: - pg_manifest = seed_postgres() - if pg_manifest: - manifest.update(pg_manifest) - else: - logger.error("โŒ PostgreSQL seeding failed") - sys.exit(1) - - # Write manifest file for the chaos test runner to consume - with open(args.manifest, "w") as f: - json.dump(manifest, f, indent=2) - - logger.info(f"๐Ÿ“‹ Seed manifest written to {args.manifest}") - logger.info(f"๐Ÿ“Š Manifest: {json.dumps(manifest, indent=2)}") - logger.info("โœ… Seeding complete โ€” ready for chaos!") - - -if __name__ == "__main__": - main() diff --git a/tests/scripts/test_prod_default_guards.py b/tests/scripts/test_prod_default_guards.py index 85ad2e0b0..562fbcbc7 100644 --- a/tests/scripts/test_prod_default_guards.py +++ b/tests/scripts/test_prod_default_guards.py @@ -165,9 +165,6 @@ def test_compose_hostnames_count_as_local(monkeypatch): diag = _load("diag_db_compose", SCRIPTS / "utilities" / "diag_db.py") monkeypatch.delenv("REGENGINE_ENV", raising=False) diag.ensure_local_database("postgresql://regengine:regengine@postgres:5432/regengine", False) - chaos = _load("seed_chaos_data_compose", SCRIPTS / "seed_chaos_data.py") - assert not chaos.is_production_target("postgresql://regengine:regengine@postgres:5432/regengine") - assert not chaos.is_production_target("bolt://neo4j:7687") inflow = _load("verify_inflow_e2e_compose", SCRIPTS / "verify_inflow_to_regengine_e2e.py") inflow.ensure_local_database("postgresql://regengine:regengine@postgres:5432/regengine", False) @@ -232,19 +229,6 @@ def test_diag_db_refuses_production_env_on_localhost(monkeypatch): module.ensure_local_database("postgresql://regengine@localhost:5432/regengine", False) -def test_chaos_seeder_refuses_query_host_override(monkeypatch): - module = _load("seed_chaos_data_query_host", SCRIPTS / "seed_chaos_data.py") - monkeypatch.delenv("REGENGINE_ENV", raising=False) - assert module.is_production_target("postgresql://regengine@localhost:5432/regengine?host=10.0.0.5") - assert not module.is_production_target("postgresql://regengine@localhost:5432/regengine?sslmode=require") - with pytest.raises(SystemExit): - module.ensure_local_targets( - "postgresql://regengine@localhost:5432/regengine?hostaddr=10.0.0.5", - "bolt://localhost:7687", - False, - ) - - def test_inflow_verifier_refuses_query_host_override(monkeypatch): module = _load("verify_inflow_e2e_query_host", SCRIPTS / "verify_inflow_to_regengine_e2e.py") monkeypatch.delenv("REGENGINE_ENV", raising=False) @@ -258,29 +242,6 @@ def test_inflow_verifier_refuses_query_host_override(monkeypatch): # --- entrypoint wiring: the guard must run inside main(), not just as a helper --- -def test_chaos_main_refuses_remote_store_before_seeding(monkeypatch): - module = _load("seed_chaos_data_main", SCRIPTS / "seed_chaos_data.py") - monkeypatch.delenv("REGENGINE_ENV", raising=False) - monkeypatch.setattr(sys, "argv", ["seed_chaos_data.py"]) - monkeypatch.setattr(module, "POSTGRES_URL", "postgresql://user@db.supabase.co:5432/postgres") - monkeypatch.setattr(module, "NEO4J_URI", "bolt://localhost:7687") - # Code 2 is the guard refusal; a seeding failure exits 1, success exits 0. - with pytest.raises(SystemExit) as excinfo: - module.main() - assert excinfo.value.code == 2 - - -def test_chaos_cleanup_still_guarded(monkeypatch): - module = _load("seed_chaos_data_cleanup", SCRIPTS / "seed_chaos_data.py") - monkeypatch.delenv("REGENGINE_ENV", raising=False) - monkeypatch.setattr(sys, "argv", ["seed_chaos_data.py", "--cleanup"]) - monkeypatch.setattr(module, "POSTGRES_URL", "postgresql://user@db.supabase.co:5432/postgres") - monkeypatch.setattr(module, "NEO4J_URI", "bolt://localhost:7687") - with pytest.raises(SystemExit) as excinfo: - module.main() - assert excinfo.value.code == 2 - - def test_diag_main_refuses_remote_database(monkeypatch): module = _load("diag_db_main", SCRIPTS / "utilities" / "diag_db.py") monkeypatch.delenv("REGENGINE_ENV", raising=False) @@ -330,24 +291,6 @@ def test_diag_db_refuses_hosted_database(monkeypatch): assert module._parse_args(["--allow-production"]).allow_production is True -# --- seed_chaos_data.py: store guard --- - - -def test_chaos_seeder_refuses_remote_stores(monkeypatch): - module = _load("seed_chaos_data", SCRIPTS / "seed_chaos_data.py") - monkeypatch.delenv("REGENGINE_ENV", raising=False) - assert module.is_production_target("postgresql://user@db.supabase.co:5432/postgres") - assert not module.is_production_target("postgresql://regengine@localhost:5432/regengine") - assert not module.is_production_target("bolt://localhost:7687") - with pytest.raises(SystemExit): - module.ensure_local_targets("postgresql://user@db.supabase.co:5432/postgres", "bolt://localhost:7687", False) - module.ensure_local_targets( - "postgresql://regengine@localhost:5432/regengine", - "bolt://localhost:7687", - False, - ) - - # --- regctl: masked keys in list output, 0600 secret file --- From 5ea2da39a3517ddce640c0dcfce0c4893e336ff5 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:18:46 -0700 Subject: [PATCH 2/2] chore(ci): retrigger checks Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>