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
This commit is contained in:
350
tests/compliance/test_missing_unified_schema.py
Normal file
350
tests/compliance/test_missing_unified_schema.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user