{
    "schema": "https://saferpage.de/schemas/security-feed-storage-readiness.v1",
    "generated_at": "2026-08-03T06:12:03+00:00",
    "available": true,
    "ready_for_write": false,
    "migration_required": false,
    "admin_dsn_required": false,
    "summary": "Security-Feed-Storage ist noch nicht schreibbereit: 0 Datenbank-Artefakt(e) fehlen oder die Serverfreigabe ist nicht aktiv.",
    "metrics": {
        "required_table_count": 3,
        "ready_table_count": 3,
        "required_index_count": 6,
        "ready_index_count": 6,
        "required_trigger_count": 2,
        "ready_trigger_count": 2,
        "missing_database_artifact_count": 0,
        "preflight_check_count": 11,
        "preflight_ready_artifact_count": 0,
        "preflight_missing_required_artifact_count": 11,
        "migration_signoff_item_count": 6,
        "rollback_rehearsal_step_count": 5,
        "post_migration_canary_step_count": 6,
        "feed_source_quality_count": 6,
        "observation_review_workflow_step_count": 6,
        "false_positive_playbook_step_count": 5,
        "public_claim_policy_count": 5,
        "retention_dpia_control_count": 5,
        "feed_escalation_slo_count": 4,
        "feed_owner_handoff_step_count": 6,
        "feed_owner_handoff_private_input_count": 6,
        "feed_owner_handoff_public_output_count": 7,
        "feed_owner_handoff_forbidden_output_count": 8,
        "feed_owner_handoff_acceptance_count": 7,
        "feed_owner_handoff_evidence_url_count": 7,
        "feed_owner_handoff_validation_query_count": 5,
        "feed_owner_handoff_safe_command_count": 5
    },
    "database": {
        "available": true,
        "error": "",
        "database_name": "saferpage",
        "db_user_hash": "d3234487bb1bc80ee9da6915667ce1f2",
        "public_schema_create_privilege": false,
        "tables_ready": true,
        "indexes_ready": true,
        "triggers_ready": true
    },
    "storage_preflight": {
        "admin_dsn_required": false,
        "available": true,
        "checks": [
            {
                "id": "security_feed_observations_table",
                "label": "Feed-Observations-Tabelle",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Private Malware-/Blacklist-/DAST-Observations mit Hash, Verdict, Review- und Publish-Status.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_alert_links_table",
                "label": "Alert-Link-Tabelle",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Verknuepft private Feed-Observations mit Alerts/Nachweispositionen ohne Rohpayload-Export.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_audit_log_table",
                "label": "Feed-Auditlog",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Append-only Review-, Publish-, Suppress- und Delete-Entscheidungen.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_observations_domain_checked_idx",
                "label": "Domain/Checked-Index",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Schneller Zugriff auf letzte Feed-Checks pro Domain.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_observations_source_verdict_idx",
                "label": "Source/Verdict-Index",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Auswertung nach Feed-Quelle und Verdict.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_observations_expires_idx",
                "label": "Retention-Index",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Lösch- und Ablaufjobs finden fällige Observations.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_observations_result_meta_gin_idx",
                "label": "Result-Meta-GIN-Index",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Sanitisierte Metadaten bleiben filterbar.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_alert_links_dedupe_idx",
                "label": "Alert-Dedupe-Index",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Verhindert doppelte Alert-Verknuepfungen.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_audit_log_dedupe_idx",
                "label": "Audit-Dedupe-Index",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Audit-Events können pro Dedupe-Key nachvollzogen werden.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_observations_set_updated_at_trigger",
                "label": "Observations-updated_at-Trigger",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Änderungen setzen updated_at automatisch.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            },
            {
                "id": "security_feed_alert_links_set_updated_at_trigger",
                "label": "Alert-Link-updated_at-Trigger",
                "operator_action": "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung aus sicherer Shell anwenden.",
                "purpose": "Änderungen setzen updated_at automatisch.",
                "ready": false,
                "required_for_storage": true,
                "status": "missing"
            }
        ],
        "database_reachable": false,
        "error_class": "connection_failed",
        "generated_at": "2026-07-14T20:54:01.571485Z",
        "migration_path": "infra/postgres/migrations/security-feed-storage.sql",
        "migration_required": false,
        "migration_sha256": "bcaa177b61f0c9ea578e80abcdb8fa2af9c0615fd197f153b7367161603e9453",
        "missing_artifact_count": 11,
        "missing_required_artifacts": [
            "security_feed_observations_table",
            "security_feed_alert_links_table",
            "security_feed_audit_log_table",
            "security_feed_observations_domain_checked_idx",
            "security_feed_observations_source_verdict_idx",
            "security_feed_observations_expires_idx",
            "security_feed_observations_result_meta_gin_idx",
            "security_feed_alert_links_dedupe_idx",
            "security_feed_audit_log_dedupe_idx",
            "security_feed_observations_set_updated_at_trigger",
            "security_feed_alert_links_set_updated_at_trigger"
        ],
        "ok": false,
        "operator_runbook": [
            "Kurzlebigen Admin-DSN nur in einer sicheren Shell setzen; nicht in History, Projektdateien, systemd-Units oder Public-State speichern.",
            "scripts/run-security-feed-storage-preflight.sh ausführen und missing_required_artifacts prüfen.",
            "infra/postgres/migrations/security-feed-storage.sql mit DB-Admin-Verantwortung anwenden oder scripts/run-storage-migrations.sh nutzen.",
            "Preflight erneut ausführen, danach Retention/Review freigeben und erst dann SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED setzen.",
            "Storage-Canary ausführen, bevor echte externe Feed-Treffer gespeichert werden."
        ],
        "ready_artifact_count": 0,
        "required_artifact_count": 11,
        "safe_apply_command": "psql -d saferpage -v ON_ERROR_STOP=1 -f infra/postgres/migrations/security-feed-storage.sql",
        "safe_combined_apply_command": "SAFERPAGE_MIGRATION_DATABASE_URL='<admin-dsn-from-secure-shell>' scripts/run-storage-migrations.sh",
        "safe_next_action": "Restore database connectivity and rerun this preflight before applying the migration.",
        "safe_preflight_command": "scripts/run-security-feed-storage-preflight.sh",
        "schema": "https://saferpage.de/schemas/security-feed-storage-preflight.v1",
        "secret_policy": "No DSN, password, raw database user, host, port, feed credential, webhook URL or raw feed payload is exported.",
        "preflight_evidence_url": "https://saferpage.de/evidence/security-feed-storage-preflight.json"
    },
    "approval": {
        "storage_approved": false,
        "required_env": "SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED=yes",
        "reason": "Ohne Serverfreigabe bleibt Speicherung deaktiviert, selbst wenn Tabellen vorhanden sind."
    },
    "tables": [
        {
            "id": "security_feed_observations",
            "exists": true,
            "status": "ready",
            "row_count": 0,
            "purpose": "Normalisierte Momentaufnahmen aus externen Malware-/Blacklist-/DAST-Feeds.",
            "required_fields": [
                "domain",
                "source_id",
                "checked_at",
                "verdict",
                "severity",
                "matched_url_hash",
                "reference_id",
                "dedupe_key",
                "evidence_url",
                "expires_at",
                "created_by_job_id",
                "review_status",
                "publish_decision",
                "result_meta"
            ]
        },
        {
            "id": "security_feed_alert_links",
            "exists": true,
            "status": "ready",
            "row_count": 0,
            "purpose": "Verknuepfung gespeicherter Feed-Beobachtungen mit Alert, Nachweisposition, Verantwortlichkeit, Zielzeit und Reviewstatus.",
            "required_fields": [
                "dedupe_key",
                "alert_id",
                "ticket_id",
                "owner_role",
                "sla",
                "publish_decision",
                "review_status"
            ]
        },
        {
            "id": "security_feed_audit_log",
            "exists": true,
            "status": "ready",
            "row_count": 0,
            "purpose": "Auditlog für Import, Review, Freigabe, Löschung und Re-Scan.",
            "required_fields": [
                "dedupe_key",
                "event_type",
                "actor_role",
                "before_hash",
                "after_hash",
                "reason",
                "meta"
            ]
        }
    ],
    "indexes": [
        {
            "id": "security_feed_observations_domain_checked_idx",
            "exists": true,
            "status": "ready"
        },
        {
            "id": "security_feed_observations_source_verdict_idx",
            "exists": true,
            "status": "ready"
        },
        {
            "id": "security_feed_observations_expires_idx",
            "exists": true,
            "status": "ready"
        },
        {
            "id": "security_feed_observations_result_meta_gin_idx",
            "exists": true,
            "status": "ready"
        },
        {
            "id": "security_feed_alert_links_dedupe_idx",
            "exists": true,
            "status": "ready"
        },
        {
            "id": "security_feed_audit_log_dedupe_idx",
            "exists": true,
            "status": "ready"
        }
    ],
    "triggers": [
        {
            "id": "security_feed_observations_set_updated_at",
            "table": "security_feed_observations",
            "exists": true,
            "status": "ready"
        },
        {
            "id": "security_feed_alert_links_set_updated_at",
            "table": "security_feed_alert_links",
            "exists": true,
            "status": "ready"
        }
    ],
    "write_contract": {
        "allowed_fields": [
            "domain",
            "source_id",
            "checked_at",
            "verdict",
            "severity",
            "matched_url_hash",
            "reference_id",
            "dedupe_key",
            "evidence_url",
            "expires_at",
            "created_by_job_id",
            "review_status",
            "publish_decision",
            "result_meta"
        ],
        "forbidden_fields": [
            "raw_malware_sample",
            "page_content_dump",
            "secret_value",
            "visitor_ip",
            "personal_log_line",
            "full_feed_payload"
        ],
        "retention": "Clean-Momentaufnahmen 30 Tage, Treffer mit Review/Audit 365 Tage, inconclusive 14 Tage.",
        "publish_policy": "Öffentliche Anzeige erst nach Betreiber-Review; Runner speichert private Evidence, keine öffentlichen Treffer."
    },
    "runbook": [
        "Dedizierte Migration mit autorisierter DB-Verantwortung anwenden: psql -d saferpage -v ON_ERROR_STOP=1 -f infra/postgres/migrations/security-feed-storage.sql",
        "Danach Readiness erneut prüfen und Tabellen/Indizes/Trigger als ready bestätigen.",
        "SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED=yes erst nach Betreiberfreigabe, Retention-Review und Alert-Routing setzen.",
        "Optionalen Storage-Canary mit --storage-canary ausführen, um eine synthetische private Observation ohne externe Feed-Abfrage zu testen.",
        "Separaten systemd-Canary-Timer prüfen: systemctl list-timers saferpage-security-feed-canary.timer --no-pager.",
        "Ersten Runner-Lauf kontrollieren: stored_observation_count muss zu den aktivierungsbereiten Domains passen."
    ],
    "migration_package": {
        "schema_version": "security-feed-storage-2026-06-09",
        "requires_role": "PostgreSQL-Rolle mit Besitz- oder Adminrechten sowie CREATE TABLE, CREATE INDEX, CREATE TRIGGER im Schema public.",
        "does_not_require_secret_values": true,
        "sql_url": "https://saferpage.de/sicherheit/feed-storage-migration.sql",
        "preflight_evidence_url": "https://saferpage.de/evidence/security-feed-storage-preflight.json",
        "migration_required": false,
        "admin_dsn_required": false,
        "safe_next_action": "Storage-Artefakte sind vorhanden; als nächstes Retention-Freigabe, Storage-Approval und Canary prüfen.",
        "sql_sha256": "bcaa177b61f0c9ea578e80abcdb8fa2af9c0615fd197f153b7367161603e9453",
        "sql_bytes": 3885,
        "preflight_commands": [
            "psql -d saferpage -Atq -c \"select current_database(), current_user;\"",
            "psql -d saferpage -Atq -c \"select coalesce(to_regclass('public.security_feed_observations')::text,'missing');\""
        ],
        "migration_command": "psql -d saferpage -v ON_ERROR_STOP=1 -f infra/postgres/migrations/security-feed-storage.sql",
        "download_and_apply_command": "curl -fsS https://saferpage.de/sicherheit/feed-storage-migration.sql -o /tmp/saferpage-security-feed-storage.sql && psql -d saferpage -v ON_ERROR_STOP=1 -f /tmp/saferpage-security-feed-storage.sql",
        "smoke_test_commands": [
            "psql -d saferpage -Atq -c \"select to_regclass('public.security_feed_observations') is not null, to_regclass('public.security_feed_alert_links') is not null, to_regclass('public.security_feed_audit_log') is not null;\"",
            "psql -d saferpage -Atq -c \"select count(*) from pg_trigger where tgname in ('security_feed_observations_set_updated_at','security_feed_alert_links_set_updated_at');\"",
            "curl -fsS https://saferpage.de/sicherheit/feed-storage-readiness-json | python3 -m json.tool"
        ],
        "activation_order": [
            "DDL-Migration anwenden.",
            "Readiness-Endpunkt prüfen: Tabellen, Indizes und Trigger müssen ready sein.",
            "Feed- und Delivery-Secrets serverseitig setzen und PHP/Runner neu laden.",
            "Canary-Timer installieren/aktivieren und zuerst ohne Storage-Freigabe blockiertes Verhalten prüfen.",
            "SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED=yes erst nach Betreiberfreigabe setzen.",
            "Canary-Service manuell starten und storage_canary_stored_observation_count=1 prüfen.",
            "Runner einmal starten und stored_observation_count kontrollieren."
        ],
        "rollback_or_pause": [
            "Sofortige Pause ohne DDL-Rollback: SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED entfernen und Runner neu laden.",
            "Bei fehlerhaften Observations: review_status=suppressed setzen statt Rohdaten zu löschen, sofern Auditpflicht besteht.",
            "DDL-Drop nur nach Retention-/Audit-Entscheidung und Backupfreigabe ausführen."
        ],
        "acceptance_criteria": [
            "Readiness zeigt required_table_count=3 und ready_table_count=3.",
            "Readiness zeigt required_index_count=6 und ready_index_count=6.",
            "Readiness zeigt required_trigger_count=2 und ready_trigger_count=2.",
            "Storage-Freigabe ist nur nach dokumentierter Betreiberentscheidung aktiv.",
            "Runner-State zeigt keine Secret-Werte und keine Feed-Rohpayloads."
        ]
    },
    "migration_signoff_pack": [
        {
            "id": "admin_role_confirmed",
            "label": "DB-Admin-Verantwortung bestätigt",
            "owner": "Platform/DBA",
            "evidence": "Preflight zeigt current_database/current_user nur redigiert; Admin-DSN bleibt in sicherer Shell.",
            "status": "ready_or_not_required"
        },
        {
            "id": "ddl_hash_pinned",
            "label": "DDL-Hash festgehalten",
            "owner": "Platform/DBA",
            "evidence": "bcaa177b61f0c9ea578e80abcdb8fa2af9c0615fd197f153b7367161603e9453",
            "status": "ready"
        },
        {
            "id": "preflight_before_after",
            "label": "Preflight vor und nach Migration",
            "owner": "Platform/Compliance",
            "evidence": "https://saferpage.de/evidence/security-feed-storage-preflight.json",
            "status": "ready"
        },
        {
            "id": "retention_review",
            "label": "Retention- und Löschregel freigegeben",
            "owner": "Datenschutz/Legal",
            "evidence": "Clean 30 Tage, Treffer/Audit 365 Tage, inconclusive 14 Tage; keine Rohpayloads.",
            "status": "required_before_approval"
        },
        {
            "id": "storage_approval",
            "label": "Storage-Approval bewusst gesetzt",
            "owner": "Betreiber/Trust Ops",
            "evidence": "SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED=yes erst nach DDL, Retention, Canary und Routing.",
            "status": "blocked_expected"
        },
        {
            "id": "canary_receipt",
            "label": "Synthetischer Canary-Receipt erzeugt",
            "owner": "Security Ops",
            "evidence": "https://saferpage.de/sicherheit/feed-storage-canary-json",
            "status": "blocked_until_storage_ready"
        }
    ],
    "rollback_rehearsal": [
        {
            "step": 1,
            "label": "Approval entfernen",
            "action": "SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED entfernen und PHP/Runner neu laden.",
            "expected": "Neue Storage-Schreibvorgänge blockieren ohne DDL zu ändern."
        },
        {
            "step": 2,
            "label": "Timer pausieren",
            "action": "Feed-Runner- und Canary-Timer stoppen, bevor weitere Observations entstehen.",
            "expected": "Keine neuen Feed-Jobs starten."
        },
        {
            "step": 3,
            "label": "Audit bewahren",
            "action": "Bestehende security_feed_audit_log-Zeilen nicht löschen; Entscheidungen weiter nachvollziehbar halten.",
            "expected": "Review- und Suppress-Entscheidungen bleiben prüfbar."
        },
        {
            "step": 4,
            "label": "Suppress statt Hard Delete",
            "action": "Fehlerhafte Observations zunächst review_status=suppressed setzen.",
            "expected": "Öffentliche Anzeige bleibt aus, Audit bleibt erhalten."
        },
        {
            "step": 5,
            "label": "DDL-Drop nur mit Freigabe",
            "action": "Tabellen nur nach Backup-, Retention- und Legal-Freigabe entfernen.",
            "expected": "Keine unbeabsichtigte Beweisvernichtung."
        }
    ],
    "post_migration_canary_sequence": [
        {
            "step": 1,
            "label": "Preflight grün",
            "command": "scripts/run-security-feed-storage-preflight.sh",
            "expected": "missing_artifact_count=0."
        },
        {
            "step": 2,
            "label": "Readiness prüfen",
            "command": "curl -fsS https://saferpage.de/sicherheit/feed-storage-readiness-json",
            "expected": "ready_for_write bleibt ohne Approval false."
        },
        {
            "step": 3,
            "label": "Approval bewusst setzen",
            "command": "SAFERPAGE_SECURITY_FEED_STORAGE_APPROVED=yes nur serverseitig setzen",
            "expected": "Public Export zeigt nur Status, keinen Secret-Wert."
        },
        {
            "step": 4,
            "label": "Canary blockiert testen",
            "command": "python3 scripts/run-security-feed-schedule.py anrufer.info --storage-canary --dry-run",
            "expected": "Keine externe Feed-Abfrage, keine öffentliche Trefferanzeige."
        },
        {
            "step": 5,
            "label": "Canary schreiben",
            "command": "python3 scripts/run-security-feed-schedule.py anrufer.info --storage-canary --execute",
            "expected": "Genau eine synthetische private Observation und Audit-Zeile."
        },
        {
            "step": 6,
            "label": "Runner nachziehen",
            "command": "curl -fsS https://saferpage.de/sicherheit/feed-runner-json",
            "expected": "stored_observation_count plausibel, keine Rohpayloads oder Secrets."
        }
    ],
    "feed_owner_handoff_packet": {
        "status": "waiting_for_db_owner_and_operator_approval",
        "summary": "Security-Feed-Handoff für DB-Verantwortung und Security Ops: Storage-Migration, Credential-Referenzen, Canary und Produktclaims werden no-secret getrennt.",
        "handoff_steps": [
            {
                "id": "verify_storage_sql_hash",
                "label": "Storage-SQL-Hash prüfen",
                "owner": "DB-Verantwortung",
                "action": "SHA-256 der öffentlichen Migration mit der lokal angewendeten Datei vergleichen.",
                "acceptance": "Hash stimmt und SQL enthält keine Credential-, Webhook- oder Rohpayload-Werte.",
                "evidence_url": "https://saferpage.de/sicherheit/feed-storage-readiness-json",
                "boundary": "Hash beweist Dateigleichheit, nicht erfolgreiche Speicherung."
            },
            {
                "id": "apply_storage_with_db_owner",
                "label": "Storage-Artefakte mit DB-Verantwortung anwenden",
                "owner": "DB-Verantwortung/Platform",
                "action": "Tabellen, Indizes und Trigger aus sicherer Shell anwenden und Preflight danach neu erzeugen.",
                "acceptance": "Tabellen 3/3, Indizes 6/6, Trigger 2/2 sind ready.",
                "evidence_url": "https://saferpage.de/evidence/security-feed-storage-preflight.json",
                "boundary": "Admin-DSN und DB-Host bleiben privat."
            },
            {
                "id": "approve_retention_and_dpia",
                "label": "Retention/DSFA freigeben",
                "owner": "Datenschutz/Legal",
                "action": "Zweck, Datenminimierung, TTLs, Audit und Löschbarkeit vor Storage-Approval dokumentieren.",
                "acceptance": "Clean/Inconclusive/Hit-Retention und DPIA-Review sind freigegeben.",
                "evidence_url": "https://saferpage.de/sicherheit/feed-storage-readiness-md",
                "boundary": "Öffentlich erscheinen nur Policy-Zusammenfassung und Status, keine internen Datenschutzakten."
            },
            {
                "id": "run_private_storage_canary",
                "label": "Synthetischen Storage-Canary ausführen",
                "owner": "Security Ops",
                "action": "Erst blockierten Canary testen, danach mit Storage-Approval genau eine private Observation schreiben.",
                "acceptance": "Canary speichert publish_decision=not_public und löst keinen Alert/Badge/öffentlichen Treffer aus.",
                "evidence_url": "https://saferpage.de/sicherheit/feed-storage-canary-json",
                "boundary": "Canary ersetzt keine echten Feed-Credentials und ist kein Malwaretreffer."
            },
            {
                "id": "set_feed_credentials_privately",
                "label": "Feed-Credential-Referenzen privat setzen",
                "owner": "Security Ops",
                "action": "URLhaus- und Safe-Browsing-Keys nur als Server-Env oder Secret-Manager-Refs setzen; Rotation dokumentieren.",
                "acceptance": "Credential-Preflight zeigt Present-Status ohne Werte; Live-Probe bleibt gated.",
                "evidence_url": "https://saferpage.de/sicherheit/feed-credential-preflight-json",
                "boundary": "Public Evidence zeigt nie Feed-Key, Token, Quota-Header oder Rohantwort."
            },
            {
                "id": "activate_external_runs_after_go_live",
                "label": "Externe Runs erst nach Go-live aktivieren",
                "owner": "Operator/Security",
                "action": "Execute-ready, Storage-Approval, Alert-Routing und Stop-Bedingungen im Go-live-Center abnehmen.",
                "acceptance": "Security-Feed-Smoke bleibt grün; offene Produktiv-Gates werden nicht als live behauptet.",
                "evidence_url": "https://saferpage.de/betreiber/go-live-json",
                "boundary": "Ohne Credentials/Freigabe keine Behauptung laufender täglicher Feeds."
            }
        ],
        "private_inputs_required": [
            {
                "id": "admin_dsn",
                "label": "Kurzlebiger Admin-DSN",
                "reason": "DDL für Feed-Storage braucht DB-Verantwortungsrechte.",
                "public_handling": "Nie öffentlich ausgeben; nur Status und SQL-Hash zeigen."
            },
            {
                "id": "urlhaus_auth_key_ref",
                "label": "URLhaus-Key-Referenz",
                "reason": "Live-Connector braucht Credential und Quota-Verantwortung.",
                "public_handling": "Nur present/missing und Rotationsstatus exportieren."
            },
            {
                "id": "google_safe_browsing_key_ref",
                "label": "Google-Safe-Browsing-Key-Referenz",
                "reason": "Threat-Match-Abfragen brauchen serverseitigen API-Key.",
                "public_handling": "Keine Key-Werte, Quota-Header oder API-Rohantworten exportieren."
            },
            {
                "id": "storage_approval_decision",
                "label": "Storage-Approval-Entscheidung",
                "reason": "Observations dürfen erst nach Retention-/DPIA-Review gespeichert werden.",
                "public_handling": "Nur Freigabestatus und Entscheidungsklasse zeigen."
            },
            {
                "id": "alert_routing_targets",
                "label": "Alert-Routing-Zielsysteme",
                "reason": "Feed-Hits können Betreiberalarme auslösen.",
                "public_handling": "Keine Webhook-URLs, Empfänger oder Kundenzielsysteme exportieren."
            },
            {
                "id": "rollback_owner",
                "label": "Rollback-/Pause-Verantwortlichkeit",
                "reason": "Runner, Storage und Alerts müssen bei Fehlverhalten sofort pausierbar sein.",
                "public_handling": "Nur Rollenname und Stop-Bedingungen öffentlich zeigen."
            }
        ],
        "public_outputs_allowed": [
            {
                "id": "migration_sha256",
                "label": "Migration-SHA-256",
                "reason": "DB-Verantwortung kann die Datei vor Apply prüfen."
            },
            {
                "id": "artifact_counts",
                "label": "Tabellen-/Index-/Trigger-Zähler",
                "reason": "Storage-Fortschritt ist ohne Dateninhalt sichtbar."
            },
            {
                "id": "credential_present_flags",
                "label": "Credential-Present/Missing-Flags",
                "reason": "Go-live-Status bleibt prüfbar, ohne Secrets zu zeigen."
            },
            {
                "id": "canary_receipt_status",
                "label": "Canary-Receipt-Status",
                "reason": "Synthetischer privater Schreibtest ist nachvollziehbar."
            },
            {
                "id": "review_and_retention_policy",
                "label": "Review-/Retention-Policy",
                "reason": "Betreiber verstehen Grenzen und Löschregeln."
            },
            {
                "id": "smoke_status_counts",
                "label": "Smoke-Status und Zähler",
                "reason": "Produktreife ist maschinenlesbar, ohne private Details zu leaken."
            },
            {
                "id": "saferpage_evidence_urls",
                "label": "SaferPage-Evidence-URLs",
                "reason": "Abnahme kann verlinkt werden."
            }
        ],
        "forbidden_outputs": [
            "Admin-DSN, DB-Host, Port, Passwort oder interner DB-Username",
            "URLhaus-, Safe-Browsing- oder sonstige Feed-Keys",
            "Feed-Rohantworten, Malware-Samples, Page Dumps oder DOM-Snapshots aus externen Feeds",
            "Webhook-, Slack-, Teams-, Jira- oder E-Mail-Zieladressen",
            "Authorization-Header, Bearer-Token, HMAC-Secrets oder echte Signaturen",
            "Besucherlogs, IP-Adressen, personenbezogene Requestdetails oder Kundendokumente",
            "Private Betreiberentscheidung mit Personennamen oder internen Ticket-URLs",
            "Öffentliche Behauptung eines echten Feed-Treffers ohne Reviewstatus und Betreiberkontext"
        ],
        "validation_queries": [
            "select coalesce(to_regclass('public.security_feed_observations')::text, 'missing');",
            "select coalesce(to_regclass('public.security_feed_alert_links')::text, 'missing');",
            "select coalesce(to_regclass('public.security_feed_audit_log')::text, 'missing');",
            "select count(*) from pg_indexes where schemaname = 'public' and indexname like 'security_feed_%';",
            "select tgname from pg_trigger where tgname in ('security_feed_observations_set_updated_at', 'security_feed_alert_links_set_updated_at');"
        ],
        "safe_commands": [
            "scripts/run-security-feed-storage-preflight.sh",
            "scripts/run-security-feed-readiness-smoke.sh",
            "python3 scripts/run-security-feed-schedule.py anrufer.info --base-url http://127.0.0.1 --max 1 --timeout 15",
            "curl -fsS https://saferpage.de/sicherheit/feed-storage-canary-json",
            "curl -fsS https://saferpage.de/sicherheit/feed-credential-preflight-json"
        ],
        "acceptance_criteria": [
            "Storage-Artefakte sind mit DB-Verantwortung angewendet und Preflight zeigt keine fehlenden Pflichtartefakte.",
            "Storage-Approval ist erst nach Retention/DPIA und Canary-Freigabe aktiv.",
            "Credential-Preflight zeigt nur Referenzstatus, keine Feed-Key-Werte.",
            "Synthetischer Canary bleibt privat und erzeugt keine öffentliche Warnung.",
            "Dry-run ruft keine externen Feeds auf und speichert keine echten Observations.",
            "Go-live-Center nennt Stop-Bedingungen, Rollback-Verantwortlichkeit und Nachlaufbeobachtung.",
            "Öffentliche Claims sagen vorbereitet/no-secret belegbar, aber nicht produktiv live ohne Credentials und Freigabe."
        ],
        "evidence_urls": [
            {
                "id": "storage_readiness",
                "label": "Storage-Readiness",
                "url": "https://saferpage.de/sicherheit/feed-storage-readiness-json"
            },
            {
                "id": "storage_preflight",
                "label": "Storage-Preflight",
                "url": "https://saferpage.de/evidence/security-feed-storage-preflight.json"
            },
            {
                "id": "storage_canary",
                "label": "Storage-Canary",
                "url": "https://saferpage.de/sicherheit/feed-storage-canary-json"
            },
            {
                "id": "credential_preflight",
                "label": "Credential-Preflight",
                "url": "https://saferpage.de/sicherheit/feed-credential-preflight-json"
            },
            {
                "id": "runner_state",
                "label": "Runner-State",
                "url": "https://saferpage.de/sicherheit/feed-runner-json"
            },
            {
                "id": "readiness_smoke",
                "label": "Security-Feed-Smoke",
                "url": "https://saferpage.de/evidence/security-feed-readiness-smoke.json"
            },
            {
                "id": "operator_go_live",
                "label": "Operator-Go-live",
                "url": "https://saferpage.de/betreiber/go-live-json"
            }
        ]
    },
    "feed_source_quality": [
        {
            "source": "URLhaus",
            "category": "Malware URL",
            "quality_gate": "Credential present, HTTP 200, schema parseable, timestamp fresh.",
            "publish_rule": "Nur nach Review als Risiko-Signal, nie als Rechts- oder Sperrlisten-Zertifikat."
        },
        {
            "source": "Google Safe Browsing",
            "category": "Threat Match",
            "quality_gate": "API-Key present, match response normalized, quota/backoff tracked.",
            "publish_rule": "Nur Threat-Kategorie und Reviewstatus, keine API-Rohantwort."
        },
        {
            "source": "Security Headers",
            "category": "Webschutz",
            "quality_gate": "Header-Evidence aus passivem Scan und Retry bei Transportfehlern.",
            "publish_rule": "Als technischer Befund mit Fix-Guide, nicht als Kompromittierungsbeweis."
        },
        {
            "source": "TLS/HSTS",
            "category": "Transport Security",
            "quality_gate": "Zertifikat, HSTS und Redirect-Ziel getrennt prüfen.",
            "publish_rule": "Nur sichtbare Konfiguration, keine interne Infrastruktur behaupten."
        },
        {
            "source": "DAST Passive",
            "category": "Passive Browser Evidence",
            "quality_gate": "Nur passive Requests; keine Angriffs- oder Login-Tests.",
            "publish_rule": "Als Prüfhinweis mit Betreiber-Review und klarer Scan-Grenze."
        },
        {
            "source": "Synthetic Canary",
            "category": "Storage Control",
            "quality_gate": "source_id=security_feed_storage_canary, publish_decision=not_public.",
            "publish_rule": "Nie als echter Treffer veröffentlichen."
        }
    ],
    "observation_review_workflow": [
        {
            "step": 1,
            "label": "Observation normalisieren",
            "owner": "Security Ops",
            "decision": "dedupe_key, source_id, severity, reference_id und publish_decision setzen."
        },
        {
            "step": 2,
            "label": "Treffer kontextualisieren",
            "owner": "Security Analyst",
            "decision": "Domain, Ziel-URL-Hash, Zeitpunkt, Quelle und Wiederholung prüfen."
        },
        {
            "step": 3,
            "label": "False Positive prüfen",
            "owner": "Security Analyst/Betreiber",
            "decision": "Quelle, Redirect, CDN, historische URL und Betreiberfeedback abgleichen."
        },
        {
            "step": 4,
            "label": "Betreiberaktion ableiten",
            "owner": "Security Ops",
            "decision": "Fix-Guide, Re-Scan, Monitoring oder Suppression wählen."
        },
        {
            "step": 5,
            "label": "Publish-Grenze entscheiden",
            "owner": "Trust/Legal",
            "decision": "Öffentlich nur Reviewstatus, Schweregrad und sanitisierte Evidence zeigen."
        },
        {
            "step": 6,
            "label": "Audit und Retention setzen",
            "owner": "Compliance",
            "decision": "after_hash, reason, expires_at und review_status auditieren."
        }
    ],
    "false_positive_playbook": [
        {
            "id": "source_recheck",
            "label": "Quelle erneut prüfen",
            "trigger": "Betreiber widerspricht oder Feed liefert alten Treffer.",
            "action": "Re-Check mit derselben Quelle und zweiter passiver Evidence durchführen."
        },
        {
            "id": "redirect_context",
            "label": "Redirect-/CDN-Kontext prüfen",
            "trigger": "Matched URL zeigt auf CDN, Shortener oder Weiterleitung.",
            "action": "Final URL, Host, Pfad-Hash und Zeitpunkt getrennt dokumentieren."
        },
        {
            "id": "temporary_suppression",
            "label": "Temporär unterdrücken",
            "trigger": "Treffer ist plausibel falsch, aber noch nicht belegt.",
            "action": "review_status=suppressed_pending_review, öffentlich keine Warnung anzeigen."
        },
        {
            "id": "operator_receipt",
            "label": "Betreiber-Receipt erzeugen",
            "trigger": "Betreiber liefert Entlastungsnachweis.",
            "action": "Receipt mit Request-ID, Quelle, Zeitpunkt und Entscheidung speichern."
        },
        {
            "id": "reopen_condition",
            "label": "Reopen-Bedingung setzen",
            "trigger": "Quelle meldet erneut oder andere Quelle bestätigt.",
            "action": "Suppression aufheben und Review erneut starten."
        }
    ],
    "public_claim_policy": [
        {
            "id": "no_clean_certificate",
            "claim": "Kein Zertifikat \"malwarefrei\"",
            "rule": "Clean bedeutet nur: im passiven Prüfumfang kein aktiver Feed-Treffer."
        },
        {
            "id": "no_legal_certainty",
            "claim": "Keine Rechts- oder Compliance-Freigabe",
            "rule": "Security-Feeds liefern technische Signale, keine abschließende rechtliche Bewertung."
        },
        {
            "id": "review_before_badge_warning",
            "claim": "Warnungen erst nach Review",
            "rule": "Öffentliche Badges/Reports zeigen kritische Feed-Treffer erst nach Betreiber- oder Security-Review."
        },
        {
            "id": "source_attribution",
            "claim": "Quelle und Zeitpunkt nennen",
            "rule": "Jeder veröffentlichte Feed-Befund braucht source_id, checked_at und Reviewstatus."
        },
        {
            "id": "privacy_minimization",
            "claim": "Keine Rohpayloads",
            "rule": "Keine Feed-Rohantworten, IPs, Secret-Werte, Besucherlogs oder personenbezogene Daten veröffentlichen."
        }
    ],
    "retention_dpia_controls": [
        {
            "id": "clean_ttl",
            "label": "Clean TTL",
            "policy": "Clean-Momentaufnahmen maximal 30 Tage speichern."
        },
        {
            "id": "inconclusive_ttl",
            "label": "Inconclusive TTL",
            "policy": "Unklare oder synthetische Canary-Observations nach 14 Tagen erneuern oder löschen."
        },
        {
            "id": "hit_audit_ttl",
            "label": "Treffer-Audit",
            "policy": "Review- und Entscheidungsnachweise bis 365 Tage halten, danach Retention-Review."
        },
        {
            "id": "data_minimization",
            "label": "Datenminimierung",
            "policy": "Nur Hashes, Referenz-IDs und sanitisierte Metadaten speichern."
        },
        {
            "id": "dpia_review",
            "label": "DPIA/DSFA-Review",
            "policy": "Neue Feed-Quellen vor produktiver Speicherung auf Zweck, Datenarten, Drittland und Löschbarkeit prüfen."
        }
    ],
    "feed_escalation_slo": [
        {
            "severity": "critical_confirmed_feed_hit",
            "owner": "Security Ops",
            "response": "30 Minuten",
            "action": "Betreiber informieren, öffentliche Claim-Grenze prüfen, Re-Scan planen."
        },
        {
            "severity": "high_unreviewed_feed_hit",
            "owner": "Security Analyst",
            "response": "4 Stunden",
            "action": "Quelle, Referenz und False-Positive-Risiko prüfen."
        },
        {
            "severity": "medium_storage_or_parse_issue",
            "owner": "Platform",
            "response": "1 Arbeitstag",
            "action": "Parser, Storage, Backoff und Runner-State prüfen."
        },
        {
            "severity": "low_canary_or_freshness_issue",
            "owner": "Security Ops",
            "response": "2 Arbeitstage",
            "action": "Canary, Timer und Quellenfrische nachziehen."
        }
    ],
    "links": {
        "html": "https://saferpage.de/sicherheit/feed-storage-readiness",
        "json": "https://saferpage.de/sicherheit/feed-storage-readiness-json",
        "csv": "https://saferpage.de/sicherheit/feed-storage-readiness-csv",
        "markdown": "https://saferpage.de/sicherheit/feed-storage-readiness-md",
        "runner": "https://saferpage.de/sicherheit/feed-runner-json",
        "storage_example": "https://saferpage.de/sicherheit/anrufer.info/feed-storage-json",
        "activation_example": "https://saferpage.de/sicherheit/anrufer.info/feed-activation-json",
        "secrets_example": "https://saferpage.de/sicherheit/anrufer.info/feed-secrets-json",
        "schema_registry": "https://saferpage.de/schemas/security-feed-storage-readiness.v1",
        "migration_sql": "https://saferpage.de/sicherheit/feed-storage-migration.sql",
        "storage_canary_html": "https://saferpage.de/sicherheit/feed-storage-canary",
        "storage_canary_json": "https://saferpage.de/sicherheit/feed-storage-canary-json",
        "storage_canary_csv": "https://saferpage.de/sicherheit/feed-storage-canary-csv",
        "storage_canary_markdown": "https://saferpage.de/sicherheit/feed-storage-canary-md"
    },
    "disclaimer": "Readiness zeigt technische Schreibbereitschaft und Betreiberfreigabe. Es veröffentlicht keine Secrets, keine Feed-Rohdaten und keine personenbezogenen Logs."
}
