- 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
287 lines
9.1 KiB
Python
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)
|