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

317 lines
12 KiB
Python

"""Tests for SSH-specific CSV export functionality."""
from unittest.mock import Mock, patch
from src.sslysze_scan.reporter.csv_export import (
_export_ssh_encryption_algorithms,
_export_ssh_host_keys,
_export_ssh_kex_methods,
_export_ssh_mac_algorithms,
)
class TestSshCsvExport:
"""Tests for SSH CSV export functions."""
def test_export_ssh_kex_methods(self) -> None:
"""Test SSH key exchange methods export."""
# Create mock exporter
with patch(
"src.sslysze_scan.reporter.csv_export.CSVExporter"
) as mock_exporter_class:
mock_exporter = Mock()
mock_exporter_class.return_value = mock_exporter
mock_exporter.write_csv.return_value = "/tmp/test.csv"
# Test data
port = 22
ssh_kex_methods = [
{
"name": "curve25519-sha256",
"accepted": True,
"iana_recommended": "Y",
"bsi_approved": True,
"bsi_valid_until": 2031,
"compliant": True,
},
{
"name": "diffie-hellman-group14-sha256",
"accepted": True,
"iana_recommended": "N",
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
]
# Call the function
result = _export_ssh_kex_methods(mock_exporter, port, ssh_kex_methods)
# Verify the call
assert len(result) == 1
mock_exporter.write_csv.assert_called_once()
args, kwargs = mock_exporter.write_csv.call_args
assert args[0] == "22_ssh_kex_methods.csv"
assert args[1] == "ssh_kex_methods"
assert len(args[2]) == 2 # Two rows of data plus header
def test_export_ssh_encryption_algorithms(self) -> None:
"""Test SSH encryption algorithms export."""
# Create mock exporter
with patch(
"src.sslysze_scan.reporter.csv_export.CSVExporter"
) as mock_exporter_class:
mock_exporter = Mock()
mock_exporter_class.return_value = mock_exporter
mock_exporter.write_csv.return_value = "/tmp/test.csv"
# Test data
port = 22
ssh_encryption_algorithms = [
{
"name": "chacha20-poly1305@openssh.com",
"accepted": True,
"iana_recommended": "Y",
"bsi_approved": True,
"bsi_valid_until": 2031,
"compliant": True,
},
{
"name": "aes128-ctr",
"accepted": True,
"iana_recommended": "N",
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
]
# Call the function
result = _export_ssh_encryption_algorithms(
mock_exporter, port, ssh_encryption_algorithms
)
# Verify the call
assert len(result) == 1
mock_exporter.write_csv.assert_called_once()
args, kwargs = mock_exporter.write_csv.call_args
assert args[0] == "22_ssh_encryption_algorithms.csv"
assert args[1] == "ssh_encryption_algorithms"
assert len(args[2]) == 2 # Two rows of data plus header
def test_export_ssh_mac_algorithms(self) -> None:
"""Test SSH MAC algorithms export."""
# Create mock exporter
with patch(
"src.sslysze_scan.reporter.csv_export.CSVExporter"
) as mock_exporter_class:
mock_exporter = Mock()
mock_exporter_class.return_value = mock_exporter
mock_exporter.write_csv.return_value = "/tmp/test.csv"
# Test data
port = 22
ssh_mac_algorithms = [
{
"name": "hmac-sha2-256",
"accepted": True,
"iana_recommended": "Y",
"bsi_approved": True,
"bsi_valid_until": 2031,
"compliant": True,
},
{
"name": "umac-64-etm@openssh.com",
"accepted": True,
"iana_recommended": "N",
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
]
# Call the function
result = _export_ssh_mac_algorithms(mock_exporter, port, ssh_mac_algorithms)
# Verify the call
assert len(result) == 1
mock_exporter.write_csv.assert_called_once()
args, kwargs = mock_exporter.write_csv.call_args
assert args[0] == "22_ssh_mac_algorithms.csv"
assert args[1] == "ssh_mac_algorithms"
assert len(args[2]) == 2 # Two rows of data plus header
def test_export_ssh_host_keys(self) -> None:
"""Test SSH host keys export."""
# Create mock exporter
with patch(
"src.sslysze_scan.reporter.csv_export.CSVExporter"
) as mock_exporter_class:
mock_exporter = Mock()
mock_exporter_class.return_value = mock_exporter
mock_exporter.write_csv.return_value = "/tmp/test.csv"
# Test data
port = 22
ssh_host_keys = [
{
"algorithm": "ssh-ed25519",
"type": "ed25519",
"bits": 256,
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
{
"algorithm": "rsa-sha2-512",
"type": "rsa",
"bits": 3072,
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
{
"algorithm": "ecdsa-sha2-nistp256",
"type": "ecdsa",
"bits": None, # Test the derivation logic
"bsi_approved": True,
"bsi_valid_until": 2031,
"compliant": True,
},
{
"algorithm": "rsa-sha2-256",
"type": "rsa",
"bits": "-", # Test the derivation logic
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
]
# Call the function
result = _export_ssh_host_keys(mock_exporter, port, ssh_host_keys)
# Verify the call
assert len(result) == 1
mock_exporter.write_csv.assert_called_once()
args, kwargs = mock_exporter.write_csv.call_args
assert args[0] == "22_ssh_host_keys.csv"
assert args[1] == "ssh_host_keys"
assert len(args[2]) == 4 # Four rows of data plus header
# Verify that each row has 6 columns (algorithm, type, bits, bsi_approved, bsi_valid_until, compliant)
for row in args[2]:
assert (
len(row) == 6
) # 6 columns: Algorithm, Type, Bits, BSI Approved, BSI Valid Until, Compliant
# Verify that the bits are derived correctly when not provided
# Row 2 should have bits = 256 for nistp256
assert (
args[2][2][2] == 256
) # Third row (index 2), third column (index 2) should be 256
# Row 3 should have bits = 2048 for rsa-sha2-256
assert (
args[2][3][2] == 2048
) # Fourth row (index 3), third column (index 2) should be 2048
def test_export_ssh_host_keys_derived_bits(self) -> None:
"""Test that SSH host keys export properly derives bits from algorithm names."""
# Create mock exporter
with patch(
"src.sslysze_scan.reporter.csv_export.CSVExporter"
) as mock_exporter_class:
mock_exporter = Mock()
mock_exporter_class.return_value = mock_exporter
mock_exporter.write_csv.return_value = "/tmp/test.csv"
# Test data with missing bits to test derivation logic
port = 22
ssh_host_keys = [
{
"algorithm": "ecdsa-sha2-nistp521",
"type": "ecdsa",
"bits": None,
"bsi_approved": True,
"bsi_valid_until": 2031,
"compliant": True,
},
{
"algorithm": "ecdsa-sha2-nistp384",
"type": "ecdsa",
"bits": None,
"bsi_approved": True,
"bsi_valid_until": 2031,
"compliant": True,
},
{
"algorithm": "ecdsa-sha2-nistp256",
"type": "ecdsa",
"bits": None,
"bsi_approved": True,
"bsi_valid_until": 2031,
"compliant": True,
},
{
"algorithm": "ssh-ed25519",
"type": "ed25519",
"bits": None,
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
{
"algorithm": "rsa-sha2-256",
"type": "rsa",
"bits": None,
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
{
"algorithm": "rsa-sha2-512",
"type": "rsa",
"bits": None, # Should derive 4096 from algorithm name
"fingerprint": "SHA256:test6",
"iana_recommended": None,
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
{
"algorithm": "unknown-algorithm",
"type": "unknown",
"bits": None, # Should remain as "-" for unknown algorithm
"fingerprint": "SHA256:test7",
"iana_recommended": None,
"bsi_approved": False,
"bsi_valid_until": None,
"compliant": False,
},
]
# Call the function
result = _export_ssh_host_keys(mock_exporter, port, ssh_host_keys)
# Verify the call
assert len(result) == 1
mock_exporter.write_csv.assert_called_once()
args, kwargs = mock_exporter.write_csv.call_args
# Verify that each row has 7 columns (algorithm, type, bits, iana_recommended, bsi_approved, bsi_valid_until, compliant)
# Verify that each row has 6 columns
for row in args[2]:
assert (
len(row) == 6
) # 6 columns: Algorithm, Type, Bits, BSI Approved, BSI Valid Until, Compliant
# Verify that bits are derived correctly from algorithm names
assert args[2][0][2] == 521 # nistp521 -> 521
assert args[2][1][2] == 384 # nistp384 -> 384
assert args[2][2][2] == 256 # nistp256 -> 256
assert args[2][3][2] == 255 # ed25519 -> 255
assert args[2][4][2] == 2048 # rsa-sha2-256 -> 2048
assert (
args[2][6][2] == "-"
) # unknown algorithm -> "-" (since bits is None and no derivation rule)