Files
compliance-scan/tests/compliance/test_missing_unified_schema.py
Heiko f60de7c2da Add SSH scan support with BSI TR-02102-4 compliance
- SSH scanning via ssh-audit (KEX, encryption, MAC, host keys)
- BSI TR-02102-4 and IANA compliance validation for SSH
- CSV/Markdown/reST reports for SSH results
- Unified compliance schema and database views
- Code optimization: modular query/writer architecture
2026-01-23 11:05:01 +01:00

351 lines
10 KiB
Python

"""Test for missing bsi_compliance_rules table scenario.
This test covers the case where a database has the correct schema version
but is missing the unified bsi_compliance_rules table (using old schema).
"""
import sqlite3
import tempfile
from pathlib import Path
import pytest
from sslysze_scan.db.compliance import check_compliance
from sslysze_scan.db.writer import write_scan_results
def create_legacy_schema_db(db_path: str) -> None:
"""Create a database with schema version 6 but legacy BSI tables.
This simulates the state where crypto_standards.db was copied
but the unify_bsi_schema.py migration was not yet executed.
"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Create schema_version table
cursor.execute("""
CREATE TABLE schema_version (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
)
""")
cursor.execute(
"INSERT INTO schema_version (version, applied_at) VALUES (6, '2025-01-01')"
)
# Create legacy BSI tables
cursor.execute("""
CREATE TABLE bsi_tr_02102_2_tls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
name TEXT NOT NULL,
tls_version TEXT,
valid_until INTEGER,
reference TEXT,
notes TEXT
)
""")
cursor.execute("""
CREATE TABLE bsi_tr_02102_4_ssh_kex (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_exchange_method TEXT NOT NULL UNIQUE,
spezifikation TEXT,
verwendung TEXT,
bemerkung TEXT
)
""")
cursor.execute("""
CREATE TABLE bsi_tr_02102_4_ssh_encryption (
id INTEGER PRIMARY KEY AUTOINCREMENT,
verschluesselungsverfahren TEXT NOT NULL UNIQUE,
spezifikation TEXT,
verwendung TEXT,
bemerkung TEXT
)
""")
cursor.execute("""
CREATE TABLE bsi_tr_02102_4_ssh_mac (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mac_verfahren TEXT NOT NULL UNIQUE,
spezifikation TEXT,
verwendung TEXT
)
""")
cursor.execute("""
CREATE TABLE bsi_tr_02102_4_ssh_auth (
id INTEGER PRIMARY KEY AUTOINCREMENT,
signaturverfahren TEXT NOT NULL UNIQUE,
spezifikation TEXT,
verwendung TEXT,
bemerkung TEXT
)
""")
cursor.execute("""
CREATE TABLE bsi_tr_02102_1_key_requirements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
algorithm_type TEXT NOT NULL,
usage_context TEXT NOT NULL,
min_key_length INTEGER NOT NULL,
valid_until INTEGER,
notes TEXT,
UNIQUE(algorithm_type, usage_context)
)
""")
cursor.execute("""
CREATE TABLE bsi_tr_02102_1_hash_requirements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
algorithm TEXT NOT NULL UNIQUE,
min_output_bits INTEGER,
deprecated INTEGER DEFAULT 0,
notes TEXT
)
""")
# Create IANA tables
cursor.execute("""
CREATE TABLE iana_tls_cipher_suites (
value TEXT PRIMARY KEY,
description TEXT NOT NULL,
dtls_ok TEXT,
recommended TEXT,
reference TEXT
)
""")
cursor.execute("""
CREATE TABLE iana_tls_supported_groups (
value TEXT PRIMARY KEY,
description TEXT NOT NULL,
dtls_ok TEXT,
recommended TEXT,
reference TEXT
)
""")
cursor.execute("""
CREATE TABLE iana_ssh_kex_methods (
value TEXT PRIMARY KEY,
description TEXT NOT NULL,
recommended TEXT,
reference TEXT
)
""")
cursor.execute("""
CREATE TABLE iana_ssh_encryption_algorithms (
value TEXT PRIMARY KEY,
description TEXT NOT NULL,
recommended TEXT,
reference TEXT
)
""")
cursor.execute("""
CREATE TABLE iana_ssh_mac_algorithms (
value TEXT PRIMARY KEY,
description TEXT NOT NULL,
recommended TEXT,
reference TEXT
)
""")
# Create scan tables
cursor.execute("""
CREATE TABLE scans (
scan_id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
hostname TEXT NOT NULL,
ports TEXT NOT NULL,
scan_duration_seconds REAL
)
""")
cursor.execute("""
CREATE TABLE scanned_hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
fqdn TEXT,
ipv4 TEXT,
ipv6 TEXT,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_cipher_suites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
tls_version TEXT NOT NULL,
cipher_suite_name TEXT NOT NULL,
accepted BOOLEAN NOT NULL,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_supported_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
group_name TEXT NOT NULL,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
position INTEGER NOT NULL,
subject TEXT,
issuer TEXT,
valid_from TEXT,
valid_until TEXT,
key_type TEXT,
key_bits INTEGER,
signature_algorithm TEXT,
serial_number TEXT,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_ssh_kex_methods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
kex_method_name TEXT NOT NULL,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_ssh_encryption_algorithms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
encryption_algorithm_name TEXT NOT NULL,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_ssh_mac_algorithms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
mac_algorithm_name TEXT NOT NULL,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_ssh_host_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
host_key_algorithm TEXT NOT NULL,
key_bits INTEGER,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
cursor.execute("""
CREATE TABLE scan_compliance_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
port INTEGER NOT NULL,
timestamp TEXT NOT NULL,
check_type TEXT NOT NULL,
item_name TEXT NOT NULL,
iana_value TEXT,
iana_recommended TEXT,
bsi_approved INTEGER,
bsi_valid_until INTEGER,
passed INTEGER NOT NULL,
severity TEXT,
details TEXT,
FOREIGN KEY (scan_id) REFERENCES scans(scan_id)
)
""")
# Add some test data to legacy tables
cursor.execute("""
INSERT INTO bsi_tr_02102_2_tls (category, name, tls_version, valid_until)
VALUES ('cipher_suite', 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', '1.2', 2031)
""")
cursor.execute("""
INSERT INTO bsi_tr_02102_4_ssh_kex (key_exchange_method, verwendung)
VALUES ('diffie-hellman-group14-sha256', '2031+')
""")
cursor.execute("""
INSERT INTO iana_tls_cipher_suites (value, description, recommended)
VALUES ('0x13,0x01', 'TLS_AES_128_GCM_SHA256', 'Y')
""")
cursor.execute("""
INSERT INTO bsi_tr_02102_1_key_requirements
(algorithm_type, usage_context, min_key_length, valid_until)
VALUES ('RSA', 'signature', 3000, NULL)
""")
conn.commit()
conn.close()
def test_check_compliance_with_missing_unified_table():
"""Test that check_compliance fails with clear error when bsi_compliance_rules is missing."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = str(Path(tmpdir) / "test.db")
create_legacy_schema_db(db_path)
# Verify bsi_compliance_rules doesn't exist yet
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='bsi_compliance_rules'"
)
assert cursor.fetchone() is None
conn.close()
# Create a minimal scan result
scan_results = {
443: {
"cipher_suites": [
("TLS 1.2", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", True)
],
"supported_groups": ["secp256r1"],
"certificates": [],
}
}
# Write scan results should work
from datetime import UTC, datetime
scan_id = write_scan_results(
db_path=db_path,
hostname="example.com",
ports=[443],
scan_results=scan_results,
scan_start_time=datetime.now(UTC),
scan_duration=1.5,
)
# Check compliance should fail with clear error about missing table
with pytest.raises(sqlite3.Error) as exc_info:
check_compliance(db_path, scan_id)
error_msg = str(exc_info.value).lower()
assert "bsi_compliance_rules" in error_msg or "no such table" in error_msg