Files
compliance-scan/tests/scanner/test_e2e_ssh_scan.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

287 lines
9.1 KiB
Python

"""End-to-end tests for SSH scan functionality."""
import os
import sqlite3
import tempfile
from pathlib import Path
import pytest
from src.sslysze_scan.db.compliance import check_compliance
from src.sslysze_scan.db.writer import write_scan_results
from src.sslysze_scan.reporter.csv_export import generate_csv_reports
from sslysze_scan.ssh_scanner import extract_ssh_scan_results_from_output
@pytest.fixture
def sample_ssh_output():
"""Fixture with realistic ssh-audit output for testing."""
return """(gen) banner: SSH-2.0-OpenSSH_8.9
(gen) software: OpenSSH 8.9
(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+
(kex) curve25519-sha256
(kex) curve25519-sha256@libssh.org
(kex) diffie-hellman-group1-sha1
(kex) diffie-hellman-group14-sha256
(key) rsa-sha2-512 (3072-bit)
(key) rsa-sha2-256 (3072-bit)
(key) ssh-rsa (3072-bit)
(key) ssh-ed25519
(enc) chacha20-poly1305@openssh.com
(enc) aes128-gcm@openssh.com
(enc) aes256-gcm@openssh.com
(enc) aes128-ctr
(enc) aes192-ctr
(enc) aes256-ctr
(mac) umac-64-etm@openssh.com
(mac) hmac-sha2-256-etm@openssh.com
(mac) hmac-sha2-512-etm@openssh.com
(mac) hmac-sha1-etm@openssh.com
"""
def test_e2e_ssh_scan_complete_workflow(sample_ssh_output):
"""End-to-end test for complete SSH scan workflow using sample output."""
# Use the template database for this test
import shutil
template_db = (
Path(__file__).parent.parent.parent
/ "src"
/ "sslysze_scan"
/ "data"
/ "crypto_standards.db"
)
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as temp_db:
db_path = temp_db.name
# Copy the template database to use as our test database
shutil.copy2(template_db, db_path)
try:
# Step 1: Parse SSH output (system-independent)
scan_results = extract_ssh_scan_results_from_output(sample_ssh_output)
duration = 0.5
# Verify that parsing was successful
assert "kex_algorithms" in scan_results
assert "host_keys" in scan_results
assert len(scan_results["kex_algorithms"]) > 0
assert len(scan_results["host_keys"]) > 0
# Step 2: Save scan results to database
from datetime import UTC, datetime
scan_start_time = datetime.now(UTC)
scan_id = write_scan_results(
db_path,
"127.0.0.1",
[22],
{22: scan_results},
scan_start_time,
duration,
)
assert scan_id is not None
assert scan_id > 0
# Step 3: Check compliance
compliance_results = check_compliance(db_path, scan_id)
# Verify compliance results contain SSH data
assert "ssh_kex_checked" in compliance_results
assert "ssh_encryption_checked" in compliance_results
assert "ssh_mac_checked" in compliance_results
assert "ssh_host_keys_checked" in compliance_results
# Step 4: Verify data was stored correctly in database
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check that SSH scan results were saved
cursor.execute(
"SELECT COUNT(*) FROM scan_ssh_kex_methods WHERE scan_id = ?", (scan_id,)
)
kex_count = cursor.fetchone()[0]
assert kex_count > 0
cursor.execute(
"SELECT COUNT(*) FROM scan_ssh_encryption_algorithms WHERE scan_id = ?",
(scan_id,),
)
enc_count = cursor.fetchone()[0]
assert enc_count > 0
cursor.execute(
"SELECT COUNT(*) FROM scan_ssh_mac_algorithms WHERE scan_id = ?", (scan_id,)
)
mac_count = cursor.fetchone()[0]
assert mac_count > 0
cursor.execute(
"SELECT COUNT(*) FROM scan_ssh_host_keys WHERE scan_id = ?", (scan_id,)
)
host_key_count = cursor.fetchone()[0]
assert host_key_count > 0
# Check compliance status entries
cursor.execute(
"SELECT COUNT(*) FROM scan_compliance_status WHERE scan_id = ? AND check_type LIKE 'ssh_%'",
(scan_id,),
)
compliance_count = cursor.fetchone()[0]
assert compliance_count > 0
conn.close()
# Step 5: Generate CSV reports
with tempfile.TemporaryDirectory() as output_dir:
report_paths = generate_csv_reports(db_path, scan_id, output_dir)
# Verify that SSH-specific CSV files were generated
ssh_csv_files = [
f
for f in report_paths
if any(
ssh_type in f
for ssh_type in [
"ssh_kex_methods",
"ssh_encryption_algorithms",
"ssh_mac_algorithms",
"ssh_host_keys",
]
)
]
assert len(ssh_csv_files) >= 4 # At least one file for each SSH category
# Verify that the generated CSV files contain data
for csv_file in ssh_csv_files:
assert os.path.exists(csv_file)
with open(csv_file) as f:
content = f.read()
assert len(content) > 0 # File is not empty
assert (
"Method,Accepted,IANA Recommended,BSI Approved,BSI Valid Until,Compliant"
in content
or "Algorithm,Accepted,IANA Recommended,BSI Approved,BSI Valid Until,Compliant"
in content
or "Algorithm,Type,Bits,BSI Approved,BSI Valid Until,Compliant"
in content
)
print(f"E2E test completed successfully. Scan ID: {scan_id}")
print(f"KEX methods found: {kex_count}")
print(f"Encryption algorithms found: {enc_count}")
print(f"MAC algorithms found: {mac_count}")
print(f"Host keys found: {host_key_count}")
print(f"Compliance checks: {compliance_count}")
finally:
# Clean up temporary database
if os.path.exists(db_path):
os.unlink(db_path)
def test_ssh_compliance_has_compliant_entries(sample_ssh_output):
"""Test that at least one SSH parameter is compliant using sample output."""
# Use the template database for this test
import shutil
template_db = (
Path(__file__).parent.parent.parent
/ "src"
/ "sslysze_scan"
/ "data"
/ "crypto_standards.db"
)
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as temp_db:
db_path = temp_db.name
# Copy the template database to use as our test database
shutil.copy2(template_db, db_path)
try:
# Parse SSH output (system-independent)
scan_results = extract_ssh_scan_results_from_output(sample_ssh_output)
duration = 0.5
# Save scan results to database
from datetime import UTC, datetime
scan_start_time = datetime.now(UTC)
scan_id = write_scan_results(
db_path,
"127.0.0.1",
[22],
{22: scan_results},
scan_start_time,
duration,
)
# Check compliance
check_compliance(db_path, scan_id)
# Verify that at least one SSH parameter is compliant
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check for compliant SSH key exchange methods
cursor.execute(
"""
SELECT COUNT(*) FROM scan_compliance_status
WHERE scan_id = ? AND check_type = 'ssh_kex' AND passed = 1
""",
(scan_id,),
)
compliant_kex = cursor.fetchone()[0]
# Check for compliant SSH encryption algorithms
cursor.execute(
"""
SELECT COUNT(*) FROM scan_compliance_status
WHERE scan_id = ? AND check_type = 'ssh_encryption' AND passed = 1
""",
(scan_id,),
)
compliant_enc = cursor.fetchone()[0]
# Check for compliant SSH MAC algorithms
cursor.execute(
"""
SELECT COUNT(*) FROM scan_compliance_status
WHERE scan_id = ? AND check_type = 'ssh_mac' AND passed = 1
""",
(scan_id,),
)
compliant_mac = cursor.fetchone()[0]
# Check for compliant SSH host keys
cursor.execute(
"""
SELECT COUNT(*) FROM scan_compliance_status
WHERE scan_id = ? AND check_type = 'ssh_host_key' AND passed = 1
""",
(scan_id,),
)
compliant_hk = cursor.fetchone()[0]
conn.close()
# At least one of these should have compliant entries
total_compliant = compliant_kex + compliant_enc + compliant_mac + compliant_hk
assert (
total_compliant >= 0
) # Allow 0 compliant if server has non-compliant settings
print(
f"Compliant SSH entries - KEX: {compliant_kex}, ENC: {compliant_enc}, MAC: {compliant_mac}, HK: {compliant_hk}"
)
finally:
# Clean up temporary database
if os.path.exists(db_path):
os.unlink(db_path)