- 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
325 lines
10 KiB
Python
325 lines
10 KiB
Python
"""Tests for IANA update functionality."""
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from sslysze_scan.commands.update_iana import (
|
|
calculate_diff,
|
|
process_registry_with_validation,
|
|
)
|
|
from sslysze_scan.iana_validator import ValidationError
|
|
|
|
|
|
class TestCalculateDiff:
|
|
"""Tests for diff calculation between old and new data."""
|
|
|
|
def test_calculate_diff_no_changes(self) -> None:
|
|
"""Test diff calculation when data is unchanged."""
|
|
rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"),
|
|
]
|
|
|
|
diff = calculate_diff(rows, rows)
|
|
|
|
assert len(diff["added"]) == 0
|
|
assert len(diff["deleted"]) == 0
|
|
assert len(diff["modified"]) == 0
|
|
|
|
def test_calculate_diff_added_rows(self) -> None:
|
|
"""Test diff calculation with added rows."""
|
|
old_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
]
|
|
new_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"),
|
|
]
|
|
|
|
diff = calculate_diff(old_rows, new_rows)
|
|
|
|
assert len(diff["added"]) == 1
|
|
assert "0x13,0x02" in diff["added"]
|
|
assert len(diff["deleted"]) == 0
|
|
assert len(diff["modified"]) == 0
|
|
|
|
def test_calculate_diff_deleted_rows(self) -> None:
|
|
"""Test diff calculation with deleted rows."""
|
|
old_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"),
|
|
]
|
|
new_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
]
|
|
|
|
diff = calculate_diff(old_rows, new_rows)
|
|
|
|
assert len(diff["added"]) == 0
|
|
assert len(diff["deleted"]) == 1
|
|
assert "0x13,0x02" in diff["deleted"]
|
|
assert len(diff["modified"]) == 0
|
|
|
|
def test_calculate_diff_modified_rows(self) -> None:
|
|
"""Test diff calculation with modified rows."""
|
|
old_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
]
|
|
new_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "N", "rfc8446"),
|
|
]
|
|
|
|
diff = calculate_diff(old_rows, new_rows)
|
|
|
|
assert len(diff["added"]) == 0
|
|
assert len(diff["deleted"]) == 0
|
|
assert len(diff["modified"]) == 1
|
|
assert "0x13,0x01" in diff["modified"]
|
|
|
|
def test_calculate_diff_mixed_changes(self) -> None:
|
|
"""Test diff calculation with mixed changes."""
|
|
old_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"),
|
|
("0x00,0x9C", "TLS_RSA_WITH_AES_128_GCM_SHA256", "Y", "N", "rfc5288"),
|
|
]
|
|
new_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "N", "rfc8446"),
|
|
("0x13,0x03", "TLS_CHACHA20_POLY1305_SHA256", "Y", "Y", "rfc8446"),
|
|
("0x00,0x9C", "TLS_RSA_WITH_AES_128_GCM_SHA256", "Y", "N", "rfc5288"),
|
|
]
|
|
|
|
diff = calculate_diff(old_rows, new_rows)
|
|
|
|
assert len(diff["added"]) == 1
|
|
assert "0x13,0x03" in diff["added"]
|
|
assert len(diff["deleted"]) == 1
|
|
assert "0x13,0x02" in diff["deleted"]
|
|
assert len(diff["modified"]) == 1
|
|
assert "0x13,0x01" in diff["modified"]
|
|
|
|
|
|
class TestProcessRegistryWithValidation:
|
|
"""Tests for registry processing with validation."""
|
|
|
|
def test_process_valid_registry(self, test_db_path: str) -> None:
|
|
"""Test processing valid registry data."""
|
|
xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml"
|
|
|
|
with open(xml_path, encoding="utf-8") as f:
|
|
xml_content = f.read()
|
|
|
|
conn = sqlite3.connect(test_db_path)
|
|
|
|
headers = ["Value", "Description", "DTLS", "Recommended", "RFC/Draft"]
|
|
|
|
row_count, diff = process_registry_with_validation(
|
|
xml_content,
|
|
"tls-parameters-4",
|
|
"iana_tls_cipher_suites",
|
|
headers,
|
|
conn,
|
|
skip_min_rows_check=True,
|
|
)
|
|
|
|
assert row_count == 5
|
|
assert isinstance(diff, dict)
|
|
assert "added" in diff
|
|
assert "deleted" in diff
|
|
assert "modified" in diff
|
|
|
|
conn.close()
|
|
|
|
def test_process_registry_invalid_headers(self, test_db_path: str) -> None:
|
|
"""Test that invalid headers raise ValidationError."""
|
|
xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml"
|
|
|
|
with open(xml_path, encoding="utf-8") as f:
|
|
xml_content = f.read()
|
|
|
|
conn = sqlite3.connect(test_db_path)
|
|
|
|
headers = ["Value", "Name"]
|
|
|
|
with pytest.raises(ValidationError, match="Column count mismatch"):
|
|
process_registry_with_validation(
|
|
xml_content,
|
|
"tls-parameters-4",
|
|
"iana_tls_cipher_suites",
|
|
headers,
|
|
conn,
|
|
skip_min_rows_check=True,
|
|
)
|
|
|
|
conn.close()
|
|
|
|
def test_process_registry_nonexistent_registry_id(self, test_db_path: str) -> None:
|
|
"""Test that nonexistent registry ID raises ValueError."""
|
|
xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml"
|
|
|
|
with open(xml_path, encoding="utf-8") as f:
|
|
xml_content = f.read()
|
|
|
|
conn = sqlite3.connect(test_db_path)
|
|
|
|
headers = ["Value", "Description", "DTLS", "Recommended", "RFC/Draft"]
|
|
|
|
with pytest.raises(ValueError, match="Registry .* not found"):
|
|
process_registry_with_validation(
|
|
xml_content,
|
|
"nonexistent-registry",
|
|
"iana_tls_cipher_suites",
|
|
headers,
|
|
conn,
|
|
skip_min_rows_check=True,
|
|
)
|
|
|
|
conn.close()
|
|
|
|
|
|
class TestTransactionHandling:
|
|
"""Tests for database transaction handling."""
|
|
|
|
def test_transaction_rollback_preserves_data(self, test_db_path: str) -> None:
|
|
"""Test that rollback preserves original data."""
|
|
conn = sqlite3.connect(test_db_path)
|
|
cursor = conn.cursor()
|
|
|
|
original_count = cursor.execute(
|
|
"SELECT COUNT(*) FROM iana_tls_cipher_suites"
|
|
).fetchone()[0]
|
|
|
|
conn.execute("BEGIN TRANSACTION")
|
|
cursor.execute("DELETE FROM iana_tls_cipher_suites")
|
|
|
|
after_delete = cursor.execute(
|
|
"SELECT COUNT(*) FROM iana_tls_cipher_suites"
|
|
).fetchone()[0]
|
|
assert after_delete == 0
|
|
|
|
conn.rollback()
|
|
|
|
after_rollback = cursor.execute(
|
|
"SELECT COUNT(*) FROM iana_tls_cipher_suites"
|
|
).fetchone()[0]
|
|
assert after_rollback == original_count
|
|
|
|
conn.close()
|
|
|
|
def test_transaction_commit_persists_changes(self, test_db_path: str) -> None:
|
|
"""Test that commit persists changes."""
|
|
conn = sqlite3.connect(test_db_path)
|
|
cursor = conn.cursor()
|
|
|
|
conn.execute("BEGIN TRANSACTION")
|
|
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO iana_tls_cipher_suites
|
|
VALUES ('0xFF,0xFF', 'TEST_CIPHER', 'Y', 'N', 'test')
|
|
"""
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
result = cursor.execute(
|
|
"""
|
|
SELECT COUNT(*) FROM iana_tls_cipher_suites
|
|
WHERE value = '0xFF,0xFF'
|
|
"""
|
|
).fetchone()[0]
|
|
|
|
assert result == 1
|
|
|
|
cursor.execute("DELETE FROM iana_tls_cipher_suites WHERE value = '0xFF,0xFF'")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
class TestDiffCalculationEdgeCases:
|
|
"""Tests for edge cases in diff calculation."""
|
|
|
|
def test_calculate_diff_empty_old_rows(self) -> None:
|
|
"""Test diff with empty old rows (initial import)."""
|
|
old_rows = []
|
|
new_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"),
|
|
]
|
|
|
|
diff = calculate_diff(old_rows, new_rows)
|
|
|
|
assert len(diff["added"]) == 2
|
|
assert len(diff["deleted"]) == 0
|
|
assert len(diff["modified"]) == 0
|
|
|
|
def test_calculate_diff_empty_new_rows(self) -> None:
|
|
"""Test diff with empty new rows (complete deletion)."""
|
|
old_rows = [
|
|
("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"),
|
|
]
|
|
new_rows = []
|
|
|
|
diff = calculate_diff(old_rows, new_rows)
|
|
|
|
assert len(diff["added"]) == 0
|
|
assert len(diff["deleted"]) == 1
|
|
assert len(diff["modified"]) == 0
|
|
|
|
def test_calculate_diff_different_pk_index(self) -> None:
|
|
"""Test diff calculation with different primary key index."""
|
|
old_rows = [
|
|
("desc1", "0x01", "Y"),
|
|
("desc2", "0x02", "Y"),
|
|
]
|
|
new_rows = [
|
|
("desc1", "0x01", "N"),
|
|
("desc3", "0x03", "Y"),
|
|
]
|
|
|
|
diff = calculate_diff(old_rows, new_rows, pk_index=1)
|
|
|
|
assert "0x03" in diff["added"]
|
|
assert "0x02" in diff["deleted"]
|
|
assert "0x01" in diff["modified"]
|
|
|
|
|
|
class TestConsecutiveUpdates:
|
|
"""Tests for consecutive IANA updates."""
|
|
|
|
def test_consecutive_updates_show_no_changes(self, test_db_path: str) -> None:
|
|
"""Test that second update with same data shows no changes."""
|
|
xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml"
|
|
|
|
with open(xml_path, encoding="utf-8") as f:
|
|
xml_content = f.read()
|
|
|
|
conn = sqlite3.connect(test_db_path)
|
|
headers = ["Value", "Description", "DTLS", "Recommended", "RFC/Draft"]
|
|
|
|
row_count_1, diff_1 = process_registry_with_validation(
|
|
xml_content,
|
|
"tls-parameters-4",
|
|
"iana_tls_cipher_suites",
|
|
headers,
|
|
conn,
|
|
skip_min_rows_check=True,
|
|
)
|
|
|
|
row_count_2, diff_2 = process_registry_with_validation(
|
|
xml_content,
|
|
"tls-parameters-4",
|
|
"iana_tls_cipher_suites",
|
|
headers,
|
|
conn,
|
|
skip_min_rows_check=True,
|
|
)
|
|
|
|
assert row_count_1 == row_count_2
|
|
assert len(diff_2["added"]) == 0
|
|
assert len(diff_2["deleted"]) == 0
|
|
assert len(diff_2["modified"]) == 0
|
|
|
|
conn.close()
|