- 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
233 lines
7.6 KiB
Python
233 lines
7.6 KiB
Python
"""Tests for IANA data validators."""
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from sslysze_scan.iana_validator import (
|
|
ValidationError,
|
|
get_min_rows,
|
|
normalize_header,
|
|
validate_cipher_suite_row,
|
|
validate_headers,
|
|
validate_ikev2_row,
|
|
validate_registry_data,
|
|
validate_signature_schemes_row,
|
|
validate_supported_groups_row,
|
|
)
|
|
|
|
|
|
class TestNormalizeHeader:
|
|
"""Tests for header normalization."""
|
|
|
|
def test_normalize_combined(self) -> None:
|
|
"""Test combined normalization."""
|
|
assert normalize_header("RFC/Draft") == "rfc_draft"
|
|
assert normalize_header("Recommended") == "recommended"
|
|
|
|
|
|
class TestValidateHeaders:
|
|
"""Tests for header validation against database schema."""
|
|
|
|
def test_validate_headers_matching(self, test_db_path: str) -> None:
|
|
"""Test that correct headers pass validation."""
|
|
conn = sqlite3.connect(test_db_path)
|
|
|
|
headers = ["Value", "Description", "DTLS", "Recommended", "RFC/Draft"]
|
|
validate_headers("iana_tls_cipher_suites", headers, conn)
|
|
|
|
conn.close()
|
|
|
|
def test_validate_headers_mismatch_count(self, test_db_path: str) -> None:
|
|
"""Test that wrong number of columns raises error."""
|
|
conn = sqlite3.connect(test_db_path)
|
|
|
|
headers = ["Value", "Description"]
|
|
with pytest.raises(ValidationError, match="Column count mismatch"):
|
|
validate_headers("iana_tls_cipher_suites", headers, conn)
|
|
|
|
conn.close()
|
|
|
|
def test_validate_headers_mismatch_name(self, test_db_path: str) -> None:
|
|
"""Test that wrong column name raises error."""
|
|
conn = sqlite3.connect(test_db_path)
|
|
|
|
headers = ["Value", "Name", "DTLS", "Recommended", "RFC/Draft"]
|
|
with pytest.raises(ValidationError, match="Column .* mismatch"):
|
|
validate_headers("iana_tls_cipher_suites", headers, conn)
|
|
|
|
conn.close()
|
|
|
|
|
|
class TestCipherSuiteValidation:
|
|
"""Tests for cipher suite data validation."""
|
|
|
|
def test_valid_cipher_suite(self) -> None:
|
|
"""Test that valid cipher suite passes."""
|
|
row = {
|
|
"value": "0x13,0x01",
|
|
"description": "TLS_AES_128_GCM_SHA256",
|
|
"dtls": "Y",
|
|
"recommended": "Y",
|
|
"rfc_draft": "rfc: rfc8446",
|
|
}
|
|
validate_cipher_suite_row(row)
|
|
|
|
def test_missing_required_field(self) -> None:
|
|
"""Test that missing value field raises error."""
|
|
row = {"description": "TLS_AES_128_GCM_SHA256"}
|
|
with pytest.raises(ValidationError, match="Missing required field"):
|
|
validate_cipher_suite_row(row)
|
|
|
|
def test_invalid_value_format(self) -> None:
|
|
"""Test that invalid value format raises error."""
|
|
row = {
|
|
"value": "1301",
|
|
"description": "TLS_AES_128_GCM_SHA256",
|
|
}
|
|
with pytest.raises(ValidationError, match="Invalid value format"):
|
|
validate_cipher_suite_row(row)
|
|
|
|
def test_invalid_recommended_value(self) -> None:
|
|
"""Test that invalid Recommended value raises error."""
|
|
row = {
|
|
"value": "0x13,0x01",
|
|
"description": "TLS_AES_128_GCM_SHA256",
|
|
"recommended": "X",
|
|
}
|
|
with pytest.raises(ValidationError, match="Invalid Recommended value"):
|
|
validate_cipher_suite_row(row)
|
|
|
|
def test_empty_recommended_valid(self) -> None:
|
|
"""Test that empty Recommended field is valid."""
|
|
row = {
|
|
"value": "0x13,0x01",
|
|
"description": "TLS_AES_128_GCM_SHA256",
|
|
"recommended": "",
|
|
}
|
|
validate_cipher_suite_row(row)
|
|
|
|
|
|
class TestSupportedGroupsValidation:
|
|
"""Tests for supported groups data validation."""
|
|
|
|
def test_valid_supported_group(self) -> None:
|
|
"""Test that valid supported group passes."""
|
|
row = {
|
|
"value": "23",
|
|
"description": "secp256r1",
|
|
"recommended": "Y",
|
|
}
|
|
validate_supported_groups_row(row)
|
|
|
|
def test_invalid_value_non_numeric(self) -> None:
|
|
"""Test that non-numeric value raises error."""
|
|
row = {"value": "0x17", "description": "secp256r1"}
|
|
with pytest.raises(ValidationError, match="Value must be numeric"):
|
|
validate_supported_groups_row(row)
|
|
|
|
|
|
class TestSignatureSchemesValidation:
|
|
"""Tests for signature schemes data validation."""
|
|
|
|
def test_valid_signature_scheme(self) -> None:
|
|
"""Test that valid signature scheme passes."""
|
|
row = {
|
|
"value": "0x0403",
|
|
"description": "ecdsa_secp256r1_sha256",
|
|
"recommended": "Y",
|
|
}
|
|
validate_signature_schemes_row(row)
|
|
|
|
def test_invalid_value_format(self) -> None:
|
|
"""Test that invalid value format raises error."""
|
|
row = {"value": "0403", "description": "ecdsa_secp256r1_sha256"}
|
|
with pytest.raises(ValidationError, match="Invalid value format"):
|
|
validate_signature_schemes_row(row)
|
|
|
|
|
|
class TestIKEv2Validation:
|
|
"""Tests for IKEv2 data validation."""
|
|
|
|
def test_valid_ikev2_row(self) -> None:
|
|
"""Test that valid IKEv2 row passes."""
|
|
row = {
|
|
"value": "12",
|
|
"description": "ENCR_AES_CBC",
|
|
"esp": "Y",
|
|
"ikev2": "Y",
|
|
}
|
|
validate_ikev2_row(row)
|
|
|
|
def test_invalid_value_non_numeric(self) -> None:
|
|
"""Test that non-numeric value raises error."""
|
|
row = {"value": "0x0C", "description": "ENCR_AES_CBC"}
|
|
with pytest.raises(ValidationError, match="Value must be numeric"):
|
|
validate_ikev2_row(row)
|
|
|
|
|
|
class TestGetMinRows:
|
|
"""Tests for minimum row count lookup."""
|
|
|
|
def test_get_min_rows_unknown_table(self) -> None:
|
|
"""Test that unknown tables return default minimum."""
|
|
assert get_min_rows("iana_unknown_table") == 5
|
|
|
|
|
|
class TestValidateRegistryData:
|
|
"""Tests for complete registry data validation."""
|
|
|
|
def test_validate_registry_sufficient_rows(self) -> None:
|
|
"""Test that sufficient rows pass validation."""
|
|
rows = [
|
|
{
|
|
"value": f"0x13,0x{i:02x}",
|
|
"description": f"Cipher_{i}",
|
|
"dtls": "Y",
|
|
"recommended": "Y",
|
|
"rfc_draft": "rfc: rfc8446",
|
|
}
|
|
for i in range(60)
|
|
]
|
|
|
|
validate_registry_data("iana_tls_cipher_suites", rows)
|
|
|
|
def test_validate_registry_insufficient_rows(self) -> None:
|
|
"""Test that insufficient rows raise error."""
|
|
rows = [
|
|
{
|
|
"value": "0x13,0x01",
|
|
"description": "Cipher_1",
|
|
"dtls": "Y",
|
|
"recommended": "Y",
|
|
"rfc_draft": "rfc: rfc8446",
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError, match="Insufficient data"):
|
|
validate_registry_data("iana_tls_cipher_suites", rows)
|
|
|
|
def test_validate_registry_invalid_row(self) -> None:
|
|
"""Test that invalid row in dataset raises error."""
|
|
rows = [
|
|
{
|
|
"value": f"0x13,0x{i:02x}",
|
|
"description": f"Cipher_{i}",
|
|
"dtls": "Y",
|
|
"recommended": "Y",
|
|
"rfc_draft": "rfc: rfc8446",
|
|
}
|
|
for i in range(60)
|
|
]
|
|
|
|
rows[30]["value"] = "invalid"
|
|
|
|
with pytest.raises(ValidationError, match="Row 31"):
|
|
validate_registry_data("iana_tls_cipher_suites", rows)
|
|
|
|
def test_validate_registry_no_validator(self) -> None:
|
|
"""Test that tables without validator pass basic validation."""
|
|
rows = [{"value": "1", "description": f"Item_{i}"} for i in range(10)]
|
|
|
|
validate_registry_data("iana_tls_alerts", rows)
|