- 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
253 lines
8.9 KiB
Python
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
|