- 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
317 lines
12 KiB
Python
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)
|