- 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
351 lines
10 KiB
Python
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
|