Files
compliance-scan/tests/reporter/test_summary_ssh_duplicates.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

253 lines
8.9 KiB
Python

"""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