mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
448 lines
17 KiB
Python
Executable File
448 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
NetAlertX SQL Injection Fix - Integration Testing
|
|
Validates the complete implementation as requested by maintainer jokob-sk
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import sqlite3
|
|
import json
|
|
import unittest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
import tempfile
|
|
import subprocess
|
|
|
|
# Add server paths
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'server'))
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'server', 'db'))
|
|
|
|
# Import our modules
|
|
from db.sql_safe_builder import SafeConditionBuilder, create_safe_condition_builder
|
|
from messaging.reporting import get_notifications
|
|
|
|
class NetAlertXIntegrationTest(unittest.TestCase):
|
|
"""
|
|
Comprehensive integration tests to validate:
|
|
1. Fresh install compatibility
|
|
2. Existing DB/config compatibility
|
|
3. Notification system integration
|
|
4. Settings persistence
|
|
5. Device operations
|
|
6. Plugin functionality
|
|
7. Error handling
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment"""
|
|
self.test_db_path = tempfile.mktemp(suffix='.db')
|
|
self.builder = SafeConditionBuilder()
|
|
self.create_test_database()
|
|
|
|
def tearDown(self):
|
|
"""Clean up test environment"""
|
|
if os.path.exists(self.test_db_path):
|
|
os.remove(self.test_db_path)
|
|
|
|
def create_test_database(self):
|
|
"""Create test database with NetAlertX schema"""
|
|
conn = sqlite3.connect(self.test_db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# Create minimal schema for testing
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS Events_Devices (
|
|
eve_MAC TEXT,
|
|
eve_DateTime TEXT,
|
|
devLastIP TEXT,
|
|
eve_EventType TEXT,
|
|
devName TEXT,
|
|
devComments TEXT,
|
|
eve_PendingAlertEmail INTEGER
|
|
)
|
|
''')
|
|
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS Devices (
|
|
devMac TEXT PRIMARY KEY,
|
|
devName TEXT,
|
|
devComments TEXT,
|
|
devAlertEvents INTEGER DEFAULT 1,
|
|
devAlertDown INTEGER DEFAULT 1
|
|
)
|
|
''')
|
|
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS Events (
|
|
eve_MAC TEXT,
|
|
eve_DateTime TEXT,
|
|
eve_EventType TEXT,
|
|
eve_PendingAlertEmail INTEGER
|
|
)
|
|
''')
|
|
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS Plugins_Events (
|
|
Plugin TEXT,
|
|
Object_PrimaryId TEXT,
|
|
Object_SecondaryId TEXT,
|
|
DateTimeChanged TEXT,
|
|
Watched_Value1 TEXT,
|
|
Watched_Value2 TEXT,
|
|
Watched_Value3 TEXT,
|
|
Watched_Value4 TEXT,
|
|
Status TEXT
|
|
)
|
|
''')
|
|
|
|
# Insert test data
|
|
test_data = [
|
|
('aa:bb:cc:dd:ee:ff', '2024-01-01 12:00:00', '192.168.1.100', 'New Device', 'Test Device', 'Test Comment', 1),
|
|
('11:22:33:44:55:66', '2024-01-01 12:01:00', '192.168.1.101', 'Connected', 'Test Device 2', 'Another Comment', 1),
|
|
('77:88:99:aa:bb:cc', '2024-01-01 12:02:00', '192.168.1.102', 'Disconnected', 'Test Device 3', 'Third Comment', 1),
|
|
]
|
|
|
|
cursor.executemany('''
|
|
INSERT INTO Events_Devices (eve_MAC, eve_DateTime, devLastIP, eve_EventType, devName, devComments, eve_PendingAlertEmail)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', test_data)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def test_1_fresh_install_compatibility(self):
|
|
"""Test 1: Fresh install (no DB/config)"""
|
|
print("\n=== TEST 1: Fresh Install Compatibility ===")
|
|
|
|
# Test SafeConditionBuilder initialization
|
|
builder = create_safe_condition_builder()
|
|
self.assertIsInstance(builder, SafeConditionBuilder)
|
|
|
|
# Test empty condition handling
|
|
condition, params = builder.get_safe_condition_legacy("")
|
|
self.assertEqual(condition, "")
|
|
self.assertEqual(params, {})
|
|
|
|
# Test basic valid condition
|
|
condition, params = builder.get_safe_condition_legacy("AND devName = 'TestDevice'")
|
|
self.assertIn("devName = :", condition)
|
|
self.assertIn('TestDevice', list(params.values()))
|
|
|
|
print("✅ Fresh install compatibility: PASSED")
|
|
|
|
def test_2_existing_db_compatibility(self):
|
|
"""Test 2: Existing DB/config compatibility"""
|
|
print("\n=== TEST 2: Existing DB/Config Compatibility ===")
|
|
|
|
# Mock database connection
|
|
mock_db = Mock()
|
|
mock_sql = Mock()
|
|
mock_db.sql = mock_sql
|
|
mock_db.get_table_as_json = Mock()
|
|
|
|
# Mock return value for get_table_as_json
|
|
mock_result = Mock()
|
|
mock_result.columnNames = ['MAC', 'Datetime', 'IP', 'Event Type', 'Device name', 'Comments']
|
|
mock_result.json = {'data': []}
|
|
mock_db.get_table_as_json.return_value = mock_result
|
|
|
|
# Mock settings
|
|
with patch('messaging.reporting.get_setting_value') as mock_settings:
|
|
mock_settings.side_effect = lambda key: {
|
|
'NTFPRCS_INCLUDED_SECTIONS': ['new_devices', 'events'],
|
|
'NTFPRCS_new_dev_condition': "AND devName = 'TestDevice'",
|
|
'NTFPRCS_event_condition': "AND devComments LIKE '%test%'",
|
|
'NTFPRCS_alert_down_time': '60'
|
|
}.get(key, '')
|
|
|
|
with patch('messaging.reporting.get_timezone_offset', return_value='+00:00'):
|
|
# Test get_notifications function
|
|
result = get_notifications(mock_db)
|
|
|
|
# Verify structure
|
|
self.assertIn('new_devices', result)
|
|
self.assertIn('events', result)
|
|
self.assertIn('new_devices_meta', result)
|
|
self.assertIn('events_meta', result)
|
|
|
|
# Verify parameterized queries were called
|
|
self.assertTrue(mock_db.get_table_as_json.called)
|
|
|
|
# Check that calls used parameters (not direct concatenation)
|
|
calls = mock_db.get_table_as_json.call_args_list
|
|
for call in calls:
|
|
args, kwargs = call
|
|
if len(args) > 1: # Has parameters
|
|
self.assertIsInstance(args[1], dict) # Parameters should be dict
|
|
|
|
print("✅ Existing DB/config compatibility: PASSED")
|
|
|
|
def test_3_notification_system_integration(self):
|
|
"""Test 3: Notification testing integration"""
|
|
print("\n=== TEST 3: Notification System Integration ===")
|
|
|
|
# Test that SafeConditionBuilder integrates with notification queries
|
|
builder = create_safe_condition_builder()
|
|
|
|
# Test email notification conditions
|
|
email_condition = "AND devName = 'EmailTestDevice'"
|
|
condition, params = builder.get_safe_condition_legacy(email_condition)
|
|
self.assertIn("devName = :", condition)
|
|
self.assertIn('EmailTestDevice', list(params.values()))
|
|
|
|
# Test Apprise notification conditions
|
|
apprise_condition = "AND eve_EventType = 'Connected'"
|
|
condition, params = builder.get_safe_condition_legacy(apprise_condition)
|
|
self.assertIn("eve_EventType = :", condition)
|
|
self.assertIn('Connected', list(params.values()))
|
|
|
|
# Test webhook notification conditions
|
|
webhook_condition = "AND devComments LIKE '%webhook%'"
|
|
condition, params = builder.get_safe_condition_legacy(webhook_condition)
|
|
self.assertIn("devComments LIKE :", condition)
|
|
self.assertIn('%webhook%', list(params.values()))
|
|
|
|
# Test MQTT notification conditions
|
|
mqtt_condition = "AND eve_MAC = 'aa:bb:cc:dd:ee:ff'"
|
|
condition, params = builder.get_safe_condition_legacy(mqtt_condition)
|
|
self.assertIn("eve_MAC = :", condition)
|
|
self.assertIn('aa:bb:cc:dd:ee:ff', list(params.values()))
|
|
|
|
print("✅ Notification system integration: PASSED")
|
|
|
|
def test_4_settings_persistence(self):
|
|
"""Test 4: Settings persistence"""
|
|
print("\n=== TEST 4: Settings Persistence ===")
|
|
|
|
# Test various setting formats that should be supported
|
|
test_settings = [
|
|
"AND devName = 'Persistent Device'",
|
|
"AND devComments = {s-quote}Legacy Quote{s-quote}",
|
|
"AND eve_EventType IN ('Connected', 'Disconnected')",
|
|
"AND devLastIP = '192.168.1.1'",
|
|
"" # Empty setting should work
|
|
]
|
|
|
|
builder = create_safe_condition_builder()
|
|
|
|
for setting in test_settings:
|
|
try:
|
|
condition, params = builder.get_safe_condition_legacy(setting)
|
|
# Should not raise exception
|
|
self.assertIsInstance(condition, str)
|
|
self.assertIsInstance(params, dict)
|
|
except Exception as e:
|
|
if setting != "": # Empty is allowed to "fail" gracefully
|
|
self.fail(f"Setting '{setting}' failed: {e}")
|
|
|
|
print("✅ Settings persistence: PASSED")
|
|
|
|
def test_5_device_operations(self):
|
|
"""Test 5: Device operations"""
|
|
print("\n=== TEST 5: Device Operations ===")
|
|
|
|
# Test device-related conditions
|
|
builder = create_safe_condition_builder()
|
|
|
|
device_conditions = [
|
|
"AND devName = 'Updated Device'",
|
|
"AND devMac = 'aa:bb:cc:dd:ee:ff'",
|
|
"AND devComments = 'Device updated successfully'",
|
|
"AND devLastIP = '192.168.1.200'"
|
|
]
|
|
|
|
for condition in device_conditions:
|
|
safe_condition, params = builder.get_safe_condition_legacy(condition)
|
|
self.assertTrue(len(params) > 0 or safe_condition == "")
|
|
# Ensure no direct string concatenation in output
|
|
self.assertNotIn("'", safe_condition) # No literal quotes in SQL
|
|
|
|
print("✅ Device operations: PASSED")
|
|
|
|
def test_6_plugin_functionality(self):
|
|
"""Test 6: Plugin functionality"""
|
|
print("\n=== TEST 6: Plugin Functionality ===")
|
|
|
|
# Test plugin-related conditions that might be used
|
|
builder = create_safe_condition_builder()
|
|
|
|
plugin_conditions = [
|
|
"AND Plugin = 'TestPlugin'",
|
|
"AND Object_PrimaryId = 'primary123'",
|
|
"AND Status = 'Active'"
|
|
]
|
|
|
|
for condition in plugin_conditions:
|
|
safe_condition, params = builder.get_safe_condition_legacy(condition)
|
|
if safe_condition: # If condition was accepted
|
|
self.assertIn(":", safe_condition) # Should have parameter placeholder
|
|
self.assertTrue(len(params) > 0) # Should have parameters
|
|
|
|
# Test that plugin data structure is preserved
|
|
mock_db = Mock()
|
|
mock_db.sql = Mock()
|
|
mock_result = Mock()
|
|
mock_result.columnNames = ['Plugin', 'Object_PrimaryId', 'Status']
|
|
mock_result.json = {'data': []}
|
|
mock_db.get_table_as_json.return_value = mock_result
|
|
|
|
with patch('messaging.reporting.get_setting_value') as mock_settings:
|
|
mock_settings.side_effect = lambda key: {
|
|
'NTFPRCS_INCLUDED_SECTIONS': ['plugins']
|
|
}.get(key, '')
|
|
|
|
result = get_notifications(mock_db)
|
|
self.assertIn('plugins', result)
|
|
self.assertIn('plugins_meta', result)
|
|
|
|
print("✅ Plugin functionality: PASSED")
|
|
|
|
def test_7_sql_injection_prevention(self):
|
|
"""Test 7: SQL injection prevention (critical security test)"""
|
|
print("\n=== TEST 7: SQL Injection Prevention ===")
|
|
|
|
# Test malicious inputs are properly blocked
|
|
malicious_inputs = [
|
|
"'; DROP TABLE Events_Devices; --",
|
|
"' OR '1'='1",
|
|
"1' UNION SELECT * FROM Devices --",
|
|
"'; INSERT INTO Events VALUES ('hacked'); --",
|
|
"' AND (SELECT COUNT(*) FROM sqlite_master) > 0 --"
|
|
]
|
|
|
|
builder = create_safe_condition_builder()
|
|
|
|
for malicious_input in malicious_inputs:
|
|
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
|
# All malicious inputs should result in empty/safe condition
|
|
self.assertEqual(condition, "", f"Malicious input not blocked: {malicious_input}")
|
|
self.assertEqual(params, {}, f"Parameters returned for malicious input: {malicious_input}")
|
|
|
|
print("✅ SQL injection prevention: PASSED")
|
|
|
|
def test_8_error_log_inspection(self):
|
|
"""Test 8: Error handling and logging"""
|
|
print("\n=== TEST 8: Error Handling and Logging ===")
|
|
|
|
# Test that invalid inputs are logged properly
|
|
builder = create_safe_condition_builder()
|
|
|
|
# This should log an error but not crash
|
|
invalid_condition = "INVALID SQL SYNTAX HERE"
|
|
condition, params = builder.get_safe_condition_legacy(invalid_condition)
|
|
|
|
# Should return empty/safe values
|
|
self.assertEqual(condition, "")
|
|
self.assertEqual(params, {})
|
|
|
|
# Test edge cases
|
|
edge_cases = [
|
|
None, # This would cause TypeError in unpatched version
|
|
"",
|
|
" ",
|
|
"\n\t",
|
|
"AND column_not_in_whitelist = 'value'"
|
|
]
|
|
|
|
for case in edge_cases:
|
|
try:
|
|
if case is not None:
|
|
condition, params = builder.get_safe_condition_legacy(case)
|
|
self.assertIsInstance(condition, str)
|
|
self.assertIsInstance(params, dict)
|
|
except Exception as e:
|
|
# Should not crash on any input
|
|
self.fail(f"Unexpected exception for input {case}: {e}")
|
|
|
|
print("✅ Error handling and logging: PASSED")
|
|
|
|
def test_9_backward_compatibility(self):
|
|
"""Test 9: Backward compatibility with legacy settings"""
|
|
print("\n=== TEST 9: Backward Compatibility ===")
|
|
|
|
# Test legacy {s-quote} placeholder support
|
|
builder = create_safe_condition_builder()
|
|
|
|
legacy_conditions = [
|
|
"AND devName = {s-quote}Legacy Device{s-quote}",
|
|
"AND devComments = {s-quote}Old Style Quote{s-quote}",
|
|
"AND devName = 'Normal Quote'" # Modern style should still work
|
|
]
|
|
|
|
for legacy_condition in legacy_conditions:
|
|
condition, params = builder.get_safe_condition_legacy(legacy_condition)
|
|
if condition: # If accepted as valid
|
|
# Should not contain the {s-quote} placeholder in output
|
|
self.assertNotIn("{s-quote}", condition)
|
|
# Should have proper parameter binding
|
|
self.assertIn(":", condition)
|
|
self.assertTrue(len(params) > 0)
|
|
|
|
print("✅ Backward compatibility: PASSED")
|
|
|
|
def test_10_performance_impact(self):
|
|
"""Test 10: Performance impact measurement"""
|
|
print("\n=== TEST 10: Performance Impact ===")
|
|
|
|
import time
|
|
|
|
builder = create_safe_condition_builder()
|
|
|
|
# Test performance of condition building
|
|
test_condition = "AND devName = 'Performance Test Device'"
|
|
|
|
start_time = time.time()
|
|
for _ in range(1000): # Run 1000 times
|
|
condition, params = builder.get_safe_condition_legacy(test_condition)
|
|
end_time = time.time()
|
|
|
|
total_time = end_time - start_time
|
|
avg_time_ms = (total_time / 1000) * 1000
|
|
|
|
print(f"Average condition building time: {avg_time_ms:.3f}ms")
|
|
|
|
# Should be under 1ms per condition
|
|
self.assertLess(avg_time_ms, 1.0, "Performance regression detected")
|
|
|
|
print("✅ Performance impact: PASSED")
|
|
|
|
def run_integration_tests():
|
|
"""Run all integration tests and generate report"""
|
|
print("=" * 70)
|
|
print("NetAlertX SQL Injection Fix - Integration Test Suite")
|
|
print("Validating PR #1182 as requested by maintainer jokob-sk")
|
|
print("=" * 70)
|
|
|
|
# Run tests
|
|
suite = unittest.TestLoader().loadTestsFromTestCase(NetAlertXIntegrationTest)
|
|
runner = unittest.TextTestRunner(verbosity=2)
|
|
result = runner.run(suite)
|
|
|
|
# Generate summary
|
|
print("\n" + "=" * 70)
|
|
print("INTEGRATION TEST SUMMARY")
|
|
print("=" * 70)
|
|
|
|
total_tests = result.testsRun
|
|
failures = len(result.failures)
|
|
errors = len(result.errors)
|
|
passed = total_tests - failures - errors
|
|
|
|
print(f"Total Tests: {total_tests}")
|
|
print(f"Passed: {passed}")
|
|
print(f"Failed: {failures}")
|
|
print(f"Errors: {errors}")
|
|
print(f"Success Rate: {(passed/total_tests)*100:.1f}%")
|
|
|
|
if failures == 0 and errors == 0:
|
|
print("\n🎉 ALL INTEGRATION TESTS PASSED!")
|
|
print("✅ Ready for maintainer approval")
|
|
return True
|
|
else:
|
|
print("\n❌ INTEGRATION TESTS FAILED")
|
|
print("🚫 Requires fixes before approval")
|
|
return False
|
|
|
|
if __name__ == "__main__":
|
|
success = run_integration_tests()
|
|
sys.exit(0 if success else 1) |