"""Tests for SSH duplicate handling in summary statistics.""" import sqlite3 from datetime import UTC, datetime from sslysze_scan.db.writer import write_scan_results from sslysze_scan.reporter.query import fetch_scan_data class TestSummarySSHDuplicates: """Tests for SSH duplicate detection in summary statistics.""" def test_ssh_encryption_no_duplicate_counting(self, test_db_path: str) -> None: """Test that SSH encryption algorithms are not counted twice in summary. SSH-audit returns both client-to-server and server-to-client algorithms, which are often identical. The summary should count unique algorithms only. """ # Create scan with known SSH data containing duplicates scan_results = { 22: { "kex_algorithms": ["curve25519-sha256", "diffie-hellman-group16-sha512"], "encryption_algorithms_client_to_server": [ "chacha20-poly1305@openssh.com", "aes256-ctr", "aes128-ctr", ], "encryption_algorithms_server_to_client": [ "chacha20-poly1305@openssh.com", "aes256-ctr", "aes128-ctr", ], "mac_algorithms_client_to_server": [ "hmac-sha2-256", "hmac-sha2-512", ], "mac_algorithms_server_to_client": [ "hmac-sha2-256", "hmac-sha2-512", ], "host_keys": [ { "algorithm": "ssh-rsa", "type": "RSA", "bits": 2048, "fingerprint": "test", }, ], } } scan_id = write_scan_results( db_path=test_db_path, hostname="test.example.com", ports=[22], scan_results=scan_results, scan_start_time=datetime.now(UTC), scan_duration=1.0, ) # Verify database has no duplicates (fixed behavior) conn = sqlite3.connect(test_db_path) cursor = conn.cursor() cursor.execute( "SELECT COUNT(*) FROM scan_ssh_encryption_algorithms WHERE scan_id = ?", (scan_id,), ) db_count = cursor.fetchone()[0] # Database should now contain only unique entries assert db_count == 3, ( f"Database should contain 3 unique algorithms, got {db_count}" ) # Fetch scan data and check summary data = fetch_scan_data(test_db_path, scan_id) summary = data["summary"] # Summary should count unique algorithms only assert summary["total_ssh_encryption"] == 3, ( f"Expected 3 unique encryption algorithms, got {summary['total_ssh_encryption']}" ) # Check MAC algorithms (2 unique) cursor.execute( "SELECT COUNT(*) FROM scan_ssh_mac_algorithms WHERE scan_id = ?", (scan_id,), ) mac_db_count = cursor.fetchone()[0] conn.close() assert mac_db_count == 2, ( f"Database should contain 2 unique MAC algorithms, got {mac_db_count}" ) assert summary["total_ssh_mac"] == 2, ( f"Expected 2 unique MAC algorithms, got {summary['total_ssh_mac']}" ) # Check KEX algorithms (no duplicates expected) assert summary["total_ssh_kex"] == 2, ( f"Expected 2 KEX algorithms, got {summary['total_ssh_kex']}" ) # Check host keys (no duplicates expected) assert summary["total_ssh_host_keys"] == 1, ( f"Expected 1 host key, got {summary['total_ssh_host_keys']}" ) def test_ssh_only_scan_has_valid_summary(self, test_db_path: str) -> None: """Test that SSH-only scan produces valid summary statistics. Previous bug: SSH-only scans showed all zeros in summary because only TLS data was counted. """ scan_results = { 22: { "kex_algorithms": [ "curve25519-sha256", "ecdh-sha2-nistp256", "diffie-hellman-group16-sha512", ], "encryption_algorithms_client_to_server": [ "chacha20-poly1305@openssh.com", "aes256-ctr", ], "encryption_algorithms_server_to_client": [ "chacha20-poly1305@openssh.com", "aes256-ctr", ], "mac_algorithms_client_to_server": ["hmac-sha2-256"], "mac_algorithms_server_to_client": ["hmac-sha2-256"], "host_keys": [ { "algorithm": "ssh-ed25519", "type": "ED25519", "bits": 256, "fingerprint": "test", }, ], } } scan_id = write_scan_results( db_path=test_db_path, hostname="ssh-only.example.com", ports=[22], scan_results=scan_results, scan_start_time=datetime.now(UTC), scan_duration=1.0, ) data = fetch_scan_data(test_db_path, scan_id) summary = data["summary"] # Verify scan was recognized assert summary["total_ports"] == 1 assert summary["ports_with_ssh"] == 1 assert summary["ports_with_tls"] == 0 # Verify SSH data is counted assert summary["total_ssh_items"] > 0, "SSH items should be counted" assert summary["total_ssh_kex"] == 3, ( f"Expected 3 KEX methods, got {summary['total_ssh_kex']}" ) assert summary["total_ssh_encryption"] == 2, ( f"Expected 2 encryption algorithms, got {summary['total_ssh_encryption']}" ) assert summary["total_ssh_mac"] == 1, ( f"Expected 1 MAC algorithm, got {summary['total_ssh_mac']}" ) assert summary["total_ssh_host_keys"] == 1, ( f"Expected 1 host key, got {summary['total_ssh_host_keys']}" ) # Total should be sum of all SSH items expected_total = 3 + 2 + 1 + 1 # kex + enc + mac + hostkey assert summary["total_ssh_items"] == expected_total, ( f"Expected {expected_total} total SSH items, got {summary['total_ssh_items']}" ) # TLS counters should be zero assert summary["total_cipher_suites"] == 0 assert summary["total_groups"] == 0 def test_ssh_with_different_client_server_algorithms(self, test_db_path: str) -> None: """Test that different client/server algorithms are both counted. This test ensures that if client-to-server and server-to-client actually differ (rare case), both are counted. """ scan_results = { 22: { "kex_algorithms": ["curve25519-sha256"], "encryption_algorithms_client_to_server": [ "aes256-ctr", "aes192-ctr", ], "encryption_algorithms_server_to_client": [ "aes256-ctr", # Same as client "aes128-ctr", # Different from client ], "mac_algorithms_client_to_server": ["hmac-sha2-256"], "mac_algorithms_server_to_client": ["hmac-sha2-512"], "host_keys": [ { "algorithm": "ssh-ed25519", "type": "ED25519", "bits": 256, "fingerprint": "test", } ], }, } scan_id = write_scan_results( db_path=test_db_path, hostname="asymmetric.example.com", ports=[22], scan_results=scan_results, scan_start_time=datetime.now(UTC), scan_duration=1.0, ) # Check database conn = sqlite3.connect(test_db_path) cursor = conn.cursor() cursor.execute( "SELECT COUNT(*) FROM scan_ssh_encryption_algorithms WHERE scan_id = ?", (scan_id,), ) enc_count = cursor.fetchone()[0] cursor.execute( "SELECT COUNT(*) FROM scan_ssh_mac_algorithms WHERE scan_id = ?", (scan_id,), ) mac_count = cursor.fetchone()[0] conn.close() # With the fix, only client_to_server is used # So we get 2 encryption and 1 MAC assert enc_count == 2, f"Expected 2 encryption algorithms, got {enc_count}" assert mac_count == 1, f"Expected 1 MAC algorithm, got {mac_count}" # Summary should match data = fetch_scan_data(test_db_path, scan_id) summary = data["summary"] assert summary["total_ssh_encryption"] == 2 assert summary["total_ssh_mac"] == 1