|
| 1 | +r"""extend findings.source CHECK with ICS protocol catalog sources (Rule #52 instance #3 Phase 2) |
| 2 | +
|
| 3 | +Revision ID: 2cba3d4e5f6a |
| 4 | +Revises: 1c52a4b5c6d7 |
| 5 | +Create Date: 2026-05-22 10:00:00.000000 |
| 6 | +
|
| 7 | +Rule #25 Shape-1 cross-stack alignment commit. Mirrors the bare_metal |
| 8 | +``fd6e7f8a9b0c`` precedent — extends ``ck_findings_source`` with 5 NEW |
| 9 | +finding sources matching the closed ``IcsProtocolFamily`` Literal in |
| 10 | +``app/schemas/ics_protocol.py``. Per W2-β §SC5-NEW-ICS-S2-η mitigation, |
| 11 | +ALL family values get a corresponding finding source — adding a new |
| 12 | +protocol YAML (Phase 5: DNP3, S7Comm; future: opc_ua, ethercat) without |
| 13 | +a matching source value would silently reject finding emits at runtime. |
| 14 | +
|
| 15 | +Cross-stack contract surfaces (Rule #48 5-part): |
| 16 | + - DB CHECK: this migration extends ``ck_findings_source`` |
| 17 | + - Pydantic Literal: ``IcsProtocolFindingSource`` in app/schemas/finding.py |
| 18 | + - Frontend mirror: ``FindingSource`` union in types/index.ts + |
| 19 | + ``FINDING_SOURCE_CONFIG`` keys in statusConfig.ts |
| 20 | + - Alignment test: ``test_finding_source_alignment`` auto-discovers |
| 21 | + this migration via ``_latest_source_check_migration_path()`` walking |
| 22 | + the alembic chain from HEAD; pulls ``_NEW_SOURCE_VALUES`` tuple as |
| 23 | + the authoritative DB-side allowlist |
| 24 | +
|
| 25 | +NEW SOURCES: |
| 26 | +
|
| 27 | +- ``ics_modbus_tcp_detected`` — emitted when the walker matches at |
| 28 | + least one binary against a Modbus/TCP manifest (Session 1's |
| 29 | + ``_system/modbus_tcp.yaml`` is the production reference; Phase 5 |
| 30 | + will not extend with sibling Modbus/RTU YAMLs). |
| 31 | +- ``ics_modbus_rtu_detected`` — Modbus RTU over serial (RS-485); |
| 32 | + forward-prepared for future serial-protocol YAML extensions. |
| 33 | +- ``ics_dnp3_detected`` — emitted when the walker matches at least one |
| 34 | + binary against a DNP3 manifest (Phase 5 ``_system/dnp3.yaml`` will |
| 35 | + exercise this source). |
| 36 | +- ``ics_s7comm_detected`` — emitted when the walker matches at least |
| 37 | + one binary against a Siemens S7Comm/S7Comm-Plus manifest (Phase 5 |
| 38 | + ``_system/s7comm.yaml`` will exercise this source). |
| 39 | +- ``ics_unknown_ics_detected`` — emitted when a plugin (Phase 4) flags |
| 40 | + an ICS-like stack the closed-grammar catalog cannot identify; |
| 41 | + forward-prepared for the W2-β §SC5-NEW-ICS-7 hot-reload mitigation |
| 42 | + envelope (plugin escape hatch). |
| 43 | +
|
| 44 | +Live audit at migration-authoring time (2026-05-22): |
| 45 | +
|
| 46 | + SELECT source, COUNT(*) FROM findings GROUP BY source ORDER BY 2 DESC; |
| 47 | + -- 0 rows for any ics_* source (Phase 2 first consumer). |
| 48 | +
|
| 49 | +Zero existing rows ⇒ "extend CHECK" path; no defensive backfill needed. |
| 50 | +
|
| 51 | +Mirrors ``fd6e7f8a9b0c_extend_findings_source_bare_metal.py``. Same |
| 52 | +drop-and-recreate shape with extended ``_NEW_SOURCE_VALUES`` tuple. |
| 53 | +Tuple is exposed under that exact name so |
| 54 | +``test_finding_source_alignment._load_db_source_values`` can import + |
| 55 | +assert agreement. |
| 56 | +
|
| 57 | +Chains from ICS Phase 1.A DDL ``1c52a4b5c6d7``. |
| 58 | +""" |
| 59 | +from __future__ import annotations |
| 60 | + |
| 61 | +from alembic import op |
| 62 | + |
| 63 | + |
| 64 | +revision: str = "2cba3d4e5f6a" |
| 65 | +down_revision: str | None = "1c52a4b5c6d7" |
| 66 | +branch_labels: str | None = None |
| 67 | +depends_on: str | None = None |
| 68 | + |
| 69 | + |
| 70 | +_NEW_SOURCE_VALUES: tuple[str, ...] = ( |
| 71 | + # ── canonical core ───────────────────────────────────────────────── |
| 72 | + "manual", "security_audit", "yara_scan", "attack_surface", "sbom_scan", |
| 73 | + "hardware_firmware_graph", "apk-manifest-scan", "apk-bytecode-scan", |
| 74 | + "apk-mobsfscan", "cwe_checker", "uefi_scan", "clamav_scan", "vt_scan", |
| 75 | + "abusech_scan", "fuzzing", "unpack_audit", "security_review", "ai_discovered", |
| 76 | + # ── windows family ──────────────────────────────────────────────── |
| 77 | + "windows_authenticode", "windows_dbx_revoked", |
| 78 | + "windows_registry_persistence", "windows_inf", "windows_driver_imports", |
| 79 | + "windows_r2r_stomp", "windows_il_capa", |
| 80 | + "windows_sysmon_proc_create", "windows_logon_success", "windows_logon_failure", |
| 81 | + "windows_amcache_install", "windows_prefetch_execution", |
| 82 | + "windows_srum_network_activity", "windows_srum_application_runtime", |
| 83 | + "windows_powershell_script_block", "windows_scheduled_task_persistence", |
| 84 | + "windows_lnk_abnormal_target", |
| 85 | + "windows_mft_ads_hidden_content", "windows_mft_timestomping", |
| 86 | + "windows_byovd_driver", |
| 87 | + "windows_bcd_suspicious_path", "windows_bcd_testsigning_enabled", |
| 88 | + "windows_wmi_persistence", |
| 89 | + "windows_esp_unsigned", "windows_esp_dbx_revoked", |
| 90 | + "windows_mbr_bootkit", "windows_vbr_anomaly", |
| 91 | + "windows_sdb_inject_dll", "windows_sdb_redirect_exe", "windows_sdb_custom_shim", |
| 92 | + "windows_etl_kernel_proc_after_clear", "windows_etl_provider_disabled", |
| 93 | + "windows_etl_unusual_provider", "windows_etl_non_microsoft_in_diagtrack", |
| 94 | + "windows_efs_orphaned_drf", "windows_efs_unusual_recovery_agent", |
| 95 | + "windows_efs_domain_admin_in_ddf", "windows_efs_large_drf", |
| 96 | + "windows_appcompat_suspicious_path", "windows_appcompat_temp_execution", |
| 97 | + "windows_appcompat_recent_baseline", |
| 98 | + "windows_dpapi_orphaned_masterkey", "windows_dpapi_admin_creator_sid", |
| 99 | + "windows_dpapi_large_masterkey", |
| 100 | + "windows_usnjrnl_file_deletion", "windows_usnjrnl_temp_create_delete_pair", |
| 101 | + "windows_usnjrnl_renamed_executable", |
| 102 | + # ── linux family ────────────────────────────────────────────────── |
| 103 | + "linux_journald_priority_critical", "linux_journald_oom_killer", |
| 104 | + "linux_journald_suspicious_unit", "linux_journald_log_clear", |
| 105 | + "linux_journald_selinux_denied", |
| 106 | + "linux_systemd_suspicious_path", "linux_systemd_obfuscated_exec", |
| 107 | + "linux_systemd_socket_unusual_port", "linux_systemd_root_minimal_deps", |
| 108 | + "linux_systemd_enabled_outside_standard", |
| 109 | + "linux_container_privileged_mode", "linux_container_dangerous_capability", |
| 110 | + "linux_container_unsafe_host_mount", "linux_container_unconfined_security", |
| 111 | + "linux_container_unknown_registry_image", |
| 112 | + "linux_bash_history_clear", "linux_cron_suspicious_command", |
| 113 | + "linux_ld_preload_hijack", |
| 114 | + # ── Rule #52 instance #1 — bare-metal MCU/DSP family ───────────── |
| 115 | + "c28x_unsecure_csm", |
| 116 | + "c28x_csm_perma_lock", |
| 117 | + "bare_metal_chip_unknown_with_hints", |
| 118 | + "bare_metal_encrypted_region_skipped", |
| 119 | + # ── Rule #52 instance #3 — ICS protocol catalog family (THIS) ──── |
| 120 | + # All 5 IcsProtocolFamily Literal values get an ics_*_detected |
| 121 | + # source per W2-β §SC5-NEW-ICS-S2-η mitigation — forward-prepares |
| 122 | + # future protocol YAMLs (opc_ua, ethercat) without requiring a |
| 123 | + # second cross-stack alignment commit each time. |
| 124 | + "ics_modbus_tcp_detected", |
| 125 | + "ics_modbus_rtu_detected", |
| 126 | + "ics_dnp3_detected", |
| 127 | + "ics_s7comm_detected", |
| 128 | + "ics_unknown_ics_detected", |
| 129 | +) |
| 130 | + |
| 131 | + |
| 132 | +_REMOVED_IN_THIS_REVISION: tuple[str, ...] = ( |
| 133 | + "ics_modbus_tcp_detected", |
| 134 | + "ics_modbus_rtu_detected", |
| 135 | + "ics_dnp3_detected", |
| 136 | + "ics_s7comm_detected", |
| 137 | + "ics_unknown_ics_detected", |
| 138 | +) |
| 139 | + |
| 140 | + |
| 141 | +def _in_list_sql(column: str, values: tuple[str, ...]) -> str: |
| 142 | + quoted = ", ".join(f"'{v}'" for v in values) |
| 143 | + return f"{column} IN ({quoted})" |
| 144 | + |
| 145 | + |
| 146 | +def upgrade() -> None: |
| 147 | + op.drop_constraint("ck_findings_source", "findings", type_="check") |
| 148 | + op.create_check_constraint( |
| 149 | + "ck_findings_source", |
| 150 | + "findings", |
| 151 | + _in_list_sql("source", _NEW_SOURCE_VALUES), |
| 152 | + ) |
| 153 | + |
| 154 | + |
| 155 | +def downgrade() -> None: |
| 156 | + op.execute( |
| 157 | + "UPDATE findings SET source = 'manual' " |
| 158 | + "WHERE source IN (" |
| 159 | + + ", ".join(f"'{v}'" for v in _REMOVED_IN_THIS_REVISION) |
| 160 | + + ")" |
| 161 | + ) |
| 162 | + |
| 163 | + op.drop_constraint("ck_findings_source", "findings", type_="check") |
| 164 | + prior = tuple(v for v in _NEW_SOURCE_VALUES if v not in _REMOVED_IN_THIS_REVISION) |
| 165 | + op.create_check_constraint( |
| 166 | + "ck_findings_source", |
| 167 | + "findings", |
| 168 | + _in_list_sql("source", prior), |
| 169 | + ) |
0 commit comments