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:
252
tests/reporter/test_summary_ssh_duplicates.py
Normal file
252
tests/reporter/test_summary_ssh_duplicates.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user