Devcontainer setup

This commit is contained in:
Adam Outler
2025-10-23 23:33:04 +00:00
parent 3b7830b922
commit edd5bd27b0
6 changed files with 430 additions and 422 deletions

View File

@@ -210,7 +210,7 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
FROM runner AS netalertx-devcontainer FROM runner AS netalertx-devcontainer
ENV INSTALL_DIR=/app ENV INSTALL_DIR=/app
ENV PYTHONPATH=/workspaces/NetAlertX/test:/workspaces/NetAlertX/server:/app:/app/server:/opt/venv/lib/python3.12/site-packages ENV PYTHONPATH=/workspaces/NetAlertX/test:/workspaces/NetAlertX/server:/app:/app/server:/opt/venv/lib/python3.12/site-packages:/usr/lib/python3.12/site-packages
ENV PATH=/services:${PATH} ENV PATH=/services:${PATH}
ENV PHP_INI_SCAN_DIR=/services/config/php/conf.d:/etc/php83/conf.d ENV PHP_INI_SCAN_DIR=/services/config/php/conf.d:/etc/php83/conf.d
ENV LISTEN_ADDR=0.0.0.0 ENV LISTEN_ADDR=0.0.0.0
@@ -231,7 +231,7 @@ RUN mkdir /workspaces && \
install -d -o netalertx -g netalertx -m 777 /services/run/logs && \ install -d -o netalertx -g netalertx -m 777 /services/run/logs && \
install -d -o netalertx -g netalertx -m 777 /app/run/tmp/client_body && \ install -d -o netalertx -g netalertx -m 777 /app/run/tmp/client_body && \
sed -i -e 's|:/app:|:/workspaces:|' /etc/passwd && \ sed -i -e 's|:/app:|:/workspaces:|' /etc/passwd && \
find /opt/venv -type d -exec chmod o+rw {} \; find /opt/venv -type d -exec chmod o+rwx {} \;
USER netalertx USER netalertx
ENTRYPOINT ["/bin/sh","-c","sleep infinity"] ENTRYPOINT ["/bin/sh","-c","sleep infinity"]

View File

@@ -43,7 +43,7 @@
} }
}, },
"postCreateCommand": "pip install pytest docker", "postCreateCommand": "/opt/venv/bin/pip3 install pytest docker debugpy",
"postStartCommand": "${containerWorkspaceFolder}/.devcontainer/scripts/setup.sh", "postStartCommand": "${containerWorkspaceFolder}/.devcontainer/scripts/setup.sh",
"customizations": { "customizations": {

View File

@@ -7,7 +7,7 @@
FROM runner AS netalertx-devcontainer FROM runner AS netalertx-devcontainer
ENV INSTALL_DIR=/app ENV INSTALL_DIR=/app
ENV PYTHONPATH=/workspaces/NetAlertX/test:/workspaces/NetAlertX/server:/app:/app/server:/opt/venv/lib/python3.12/site-packages ENV PYTHONPATH=/workspaces/NetAlertX/test:/workspaces/NetAlertX/server:/app:/app/server:/opt/venv/lib/python3.12/site-packages:/usr/lib/python3.12/site-packages
ENV PATH=/services:${PATH} ENV PATH=/services:${PATH}
ENV PHP_INI_SCAN_DIR=/services/config/php/conf.d:/etc/php83/conf.d ENV PHP_INI_SCAN_DIR=/services/config/php/conf.d:/etc/php83/conf.d
ENV LISTEN_ADDR=0.0.0.0 ENV LISTEN_ADDR=0.0.0.0
@@ -28,7 +28,7 @@ RUN mkdir /workspaces && \
install -d -o netalertx -g netalertx -m 777 /services/run/logs && \ install -d -o netalertx -g netalertx -m 777 /services/run/logs && \
install -d -o netalertx -g netalertx -m 777 /app/run/tmp/client_body && \ install -d -o netalertx -g netalertx -m 777 /app/run/tmp/client_body && \
sed -i -e 's|:/app:|:/workspaces:|' /etc/passwd && \ sed -i -e 's|:/app:|:/workspaces:|' /etc/passwd && \
find /opt/venv -type d -exec chmod o+rw {} \; find /opt/venv -type d -exec chmod o+rwx {} \;
USER netalertx USER netalertx
ENTRYPOINT ["/bin/sh","-c","sleep infinity"] ENTRYPOINT ["/bin/sh","-c","sleep infinity"]

View File

@@ -57,20 +57,22 @@ NETALERTX_DOCKER_ERROR_CHECK=0
# Run all pre-startup checks to validate container environment and dependencies # Run all pre-startup checks to validate container environment and dependencies
echo "Startup pre-checks" if [ ${NETALERTX_DEBUG != 1} ]; then
for script in ${SYSTEM_SERVICES_SCRIPTS}/check-*.sh; do echo "Startup pre-checks"
script_name=$(basename "$script" | sed 's/^check-//;s/\.sh$//;s/-/ /g') for script in ${SYSTEM_SERVICES_SCRIPTS}/check-*.sh; do
echo " --> ${script_name}" script_name=$(basename "$script" | sed 's/^check-//;s/\.sh$//;s/-/ /g')
echo " --> ${script_name}"
sh "$script"
NETALERTX_DOCKER_ERROR_CHECK=$? sh "$script"
NETALERTX_DOCKER_ERROR_CHECK=$?
if [ ${NETALERTX_DOCKER_ERROR_CHECK} -ne 0 ]; then
if [ ${NETALERTX_DOCKER_ERROR_CHECK} -ne 0 ]; then
echo exit code ${NETALERTX_DOCKER_ERROR_CHECK} from ${script}
exit ${NETALERTX_DOCKER_ERROR_CHECK} echo exit code ${NETALERTX_DOCKER_ERROR_CHECK} from ${script}
fi exit ${NETALERTX_DOCKER_ERROR_CHECK}
done fi
done
fi
# Exit after checks if in check-only mode (for testing) # Exit after checks if in check-only mode (for testing)
if [ "${NETALERTX_CHECK_ONLY:-0}" -eq 1 ]; then if [ "${NETALERTX_CHECK_ONLY:-0}" -eq 1 ]; then

View File

@@ -5,322 +5,326 @@ Tests the fix for Issue #1210 - compound conditions with multiple AND/OR clauses
""" """
import sys import sys
import unittest import pytest
from unittest.mock import MagicMock from unittest.mock import MagicMock
# Mock the logger module before importing SafeConditionBuilder # Mock the logger module before importing SafeConditionBuilder
sys.modules['logger'] = MagicMock() sys.modules['logger'] = MagicMock()
# Add parent directory to path for imports # Add parent directory to path for imports
sys.path.insert(0, '/tmp/netalertx_hotfix/server/db') sys.path.insert(0, '/workspaces/NetAlertX')
from sql_safe_builder import SafeConditionBuilder from server.db.sql_safe_builder import SafeConditionBuilder
class TestCompoundConditions(unittest.TestCase): @pytest.fixture
"""Test compound condition parsing functionality.""" def builder():
"""Create a fresh builder instance for each test."""
return SafeConditionBuilder()
def setUp(self):
"""Create a fresh builder instance for each test."""
self.builder = SafeConditionBuilder()
def test_user_failing_filter_six_and_clauses(self): def test_user_failing_filter_six_and_clauses(builder):
"""Test the exact user-reported failing filter from Issue #1210.""" """Test the exact user-reported failing filter from Issue #1210."""
condition = ( condition = (
"AND devLastIP NOT LIKE '192.168.50.%' " "AND devLastIP NOT LIKE '192.168.50.%' "
"AND devLastIP NOT LIKE '192.168.60.%' " "AND devLastIP NOT LIKE '192.168.60.%' "
"AND devLastIP NOT LIKE '192.168.70.2' " "AND devLastIP NOT LIKE '192.168.70.2' "
"AND devLastIP NOT LIKE '192.168.70.5' " "AND devLastIP NOT LIKE '192.168.70.5' "
"AND devLastIP NOT LIKE '192.168.70.3' " "AND devLastIP NOT LIKE '192.168.70.3' "
"AND devLastIP NOT LIKE '192.168.70.4'" "AND devLastIP NOT LIKE '192.168.70.4'"
) )
sql, params = self.builder.build_safe_condition(condition) sql, params = builder.build_safe_condition(condition)
# Should successfully parse # Should successfully parse
self.assertIsNotNone(sql) assert sql is not None
self.assertIsNotNone(params) assert params is not None
# Should have 6 parameters (one per clause) # Should have 6 parameters (one per clause)
self.assertEqual(len(params), 6) assert len(params) == 6
# Should contain all 6 AND operators # Should contain all 6 AND operators
self.assertEqual(sql.count('AND'), 6) assert sql.count('AND') == 6
# Should contain all 6 NOT LIKE operators # Should contain all 6 NOT LIKE operators
self.assertEqual(sql.count('NOT LIKE'), 6) assert sql.count('NOT LIKE') == 6
# Should have 6 parameter placeholders # Should have 6 parameter placeholders
self.assertEqual(sql.count(':param_'), 6) assert sql.count(':param_') == 6
# Verify all IP patterns are in parameters # Verify all IP patterns are in parameters
param_values = list(params.values()) param_values = list(params.values())
self.assertIn('192.168.50.%', param_values) assert '192.168.50.%' in param_values
self.assertIn('192.168.60.%', param_values) assert '192.168.60.%' in param_values
self.assertIn('192.168.70.2', param_values) assert '192.168.70.2' in param_values
self.assertIn('192.168.70.5', param_values) assert '192.168.70.5' in param_values
self.assertIn('192.168.70.3', param_values) assert '192.168.70.3' in param_values
self.assertIn('192.168.70.4', param_values) assert '192.168.70.4' in param_values
def test_multiple_and_clauses_simple(self):
"""Test multiple AND clauses with simple equality operators."""
condition = "AND devName = 'Device1' AND devVendor = 'Apple' AND devFavorite = '1'"
sql, params = self.builder.build_safe_condition(condition) def test_multiple_and_clauses_simple(builder):
"""Test multiple AND clauses with simple equality operators."""
condition = "AND devName = 'Device1' AND devVendor = 'Apple' AND devFavorite = '1'"
# Should have 3 parameters sql, params = builder.build_safe_condition(condition)
self.assertEqual(len(params), 3)
# Should have 3 AND operators # Should have 3 parameters
self.assertEqual(sql.count('AND'), 3) assert len(params) == 3
# Verify all values are parameterized # Should have 3 AND operators
param_values = list(params.values()) assert sql.count('AND') == 3
self.assertIn('Device1', param_values)
self.assertIn('Apple', param_values)
self.assertIn('1', param_values)
def test_multiple_or_clauses(self): # Verify all values are parameterized
"""Test multiple OR clauses.""" param_values = list(params.values())
condition = "OR devName = 'Device1' OR devName = 'Device2' OR devName = 'Device3'" assert 'Device1' in param_values
assert 'Apple' in param_values
assert '1' in param_values
sql, params = self.builder.build_safe_condition(condition)
# Should have 3 parameters def test_multiple_or_clauses(builder):
self.assertEqual(len(params), 3) """Test multiple OR clauses."""
condition = "OR devName = 'Device1' OR devName = 'Device2' OR devName = 'Device3'"
# Should have 3 OR operators sql, params = builder.build_safe_condition(condition)
self.assertEqual(sql.count('OR'), 3)
# Verify all device names are parameterized # Should have 3 parameters
param_values = list(params.values()) assert len(params) == 3
self.assertIn('Device1', param_values)
self.assertIn('Device2', param_values)
self.assertIn('Device3', param_values)
def test_mixed_and_or_clauses(self): # Should have 3 OR operators
"""Test mixed AND/OR logical operators.""" assert sql.count('OR') == 3
condition = "AND devName = 'Device1' OR devName = 'Device2' AND devFavorite = '1'"
sql, params = self.builder.build_safe_condition(condition) # Verify all device names are parameterized
param_values = list(params.values())
assert 'Device1' in param_values
assert 'Device2' in param_values
assert 'Device3' in param_values
# Should have 3 parameters def test_mixed_and_or_clauses(builder):
self.assertEqual(len(params), 3) """Test mixed AND/OR logical operators."""
condition = "AND devName = 'Device1' OR devName = 'Device2' AND devFavorite = '1'"
# Should preserve the logical operator order sql, params = builder.build_safe_condition(condition)
self.assertIn('AND', sql)
self.assertIn('OR', sql)
# Verify all values are parameterized # Should have 3 parameters
param_values = list(params.values()) assert len(params) == 3
self.assertIn('Device1', param_values)
self.assertIn('Device2', param_values)
self.assertIn('1', param_values)
def test_single_condition_backward_compatibility(self): # Should preserve the logical operator order
"""Test that single conditions still work (backward compatibility).""" assert 'AND' in sql
condition = "AND devName = 'TestDevice'" assert 'OR' in sql
sql, params = self.builder.build_safe_condition(condition) # Verify all values are parameterized
param_values = list(params.values())
assert 'Device1' in param_values
assert 'Device2' in param_values
assert '1' in param_values
# Should have 1 parameter
self.assertEqual(len(params), 1)
# Should match expected format def test_single_condition_backward_compatibility(builder):
self.assertIn('AND devName = :param_', sql) """Test that single conditions still work (backward compatibility)."""
condition = "AND devName = 'TestDevice'"
# Parameter should contain the value sql, params = builder.build_safe_condition(condition)
self.assertIn('TestDevice', params.values())
def test_single_condition_like_operator(self): # Should have 1 parameter
"""Test single LIKE condition for backward compatibility.""" assert len(params) == 1
condition = "AND devComments LIKE '%important%'"
sql, params = self.builder.build_safe_condition(condition) # Should match expected format
assert 'AND devName = :param_' in sql
# Should have 1 parameter # Parameter should contain the value
self.assertEqual(len(params), 1) assert 'TestDevice' in params.values()
# Should contain LIKE operator
self.assertIn('LIKE', sql)
# Parameter should contain the pattern def test_single_condition_like_operator(builder):
self.assertIn('%important%', params.values()) """Test single LIKE condition for backward compatibility."""
condition = "AND devComments LIKE '%important%'"
def test_compound_with_like_patterns(self): sql, params = builder.build_safe_condition(condition)
"""Test compound conditions with LIKE patterns."""
condition = "AND devLastIP LIKE '192.168.%' AND devVendor LIKE '%Apple%'"
sql, params = self.builder.build_safe_condition(condition) # Should have 1 parameter
assert len(params) == 1
# Should have 2 parameters # Should contain LIKE operator
self.assertEqual(len(params), 2) assert 'LIKE' in sql
# Should have 2 LIKE operators # Parameter should contain the pattern
self.assertEqual(sql.count('LIKE'), 2) assert '%important%' in params.values()
# Verify patterns are parameterized
param_values = list(params.values())
self.assertIn('192.168.%', param_values)
self.assertIn('%Apple%', param_values)
def test_compound_with_inequality_operators(self): def test_compound_with_like_patterns(builder):
"""Test compound conditions with various inequality operators.""" """Test compound conditions with LIKE patterns."""
condition = "AND eve_DateTime > '2024-01-01' AND eve_DateTime < '2024-12-31'" condition = "AND devLastIP LIKE '192.168.%' AND devVendor LIKE '%Apple%'"
sql, params = self.builder.build_safe_condition(condition) sql, params = builder.build_safe_condition(condition)
# Should have 2 parameters # Should have 2 parameters
self.assertEqual(len(params), 2) assert len(params) == 2
# Should have both operators # Should have 2 LIKE operators
self.assertIn('>', sql) assert sql.count('LIKE') == 2
self.assertIn('<', sql)
# Verify dates are parameterized # Verify patterns are parameterized
param_values = list(params.values()) param_values = list(params.values())
self.assertIn('2024-01-01', param_values) assert '192.168.%' in param_values
self.assertIn('2024-12-31', param_values) assert '%Apple%' in param_values
def test_empty_condition(self):
"""Test empty condition string."""
condition = ""
sql, params = self.builder.build_safe_condition(condition) def test_compound_with_inequality_operators(builder):
"""Test compound conditions with various inequality operators."""
condition = "AND eve_DateTime > '2024-01-01' AND eve_DateTime < '2024-12-31'"
# Should return empty results sql, params = builder.build_safe_condition(condition)
self.assertEqual(sql, "")
self.assertEqual(params, {})
def test_whitespace_only_condition(self): # Should have 2 parameters
"""Test condition with only whitespace.""" assert len(params) == 2
condition = " \t\n "
sql, params = self.builder.build_safe_condition(condition) # Should have both operators
assert '>' in sql
assert '<' in sql
# Should return empty results # Verify dates are parameterized
self.assertEqual(sql, "") param_values = list(params.values())
self.assertEqual(params, {}) assert '2024-01-01' in param_values
assert '2024-12-31' in param_values
def test_invalid_column_name_rejected(self):
"""Test that invalid column names are rejected."""
condition = "AND malicious_column = 'value'"
with self.assertRaises(ValueError): def test_empty_condition(builder):
self.builder.build_safe_condition(condition) """Test empty condition string."""
condition = ""
def test_invalid_operator_rejected(self): sql, params = builder.build_safe_condition(condition)
"""Test that invalid operators are rejected."""
condition = "AND devName EXECUTE 'DROP TABLE'"
with self.assertRaises(ValueError): # Should return empty results
self.builder.build_safe_condition(condition) assert sql == ""
assert params == {}
def test_sql_injection_attempt_blocked(self):
"""Test that SQL injection attempts are blocked."""
condition = "AND devName = 'value'; DROP TABLE devices; --"
# Should either reject or sanitize the dangerous input def test_whitespace_only_condition(builder):
# The semicolon and comment should not appear in the final SQL """Test condition with only whitespace."""
try: condition = " \t\n "
sql, params = self.builder.build_safe_condition(condition)
# If it doesn't raise an error, it should sanitize the input
self.assertNotIn('DROP', sql.upper())
self.assertNotIn(';', sql)
except ValueError:
# Rejection is also acceptable
pass
def test_quoted_string_with_spaces(self): sql, params = builder.build_safe_condition(condition)
"""Test that quoted strings with spaces are handled correctly."""
condition = "AND devName = 'My Device Name' AND devComments = 'Has spaces here'"
sql, params = self.builder.build_safe_condition(condition) # Should return empty results
assert sql == ""
assert params == {}
# Should have 2 parameters
self.assertEqual(len(params), 2)
# Verify values with spaces are preserved def test_invalid_column_name_rejected(builder):
param_values = list(params.values()) """Test that invalid column names are rejected."""
self.assertIn('My Device Name', param_values) condition = "AND malicious_column = 'value'"
self.assertIn('Has spaces here', param_values)
def test_compound_condition_with_not_equal(self): with pytest.raises(ValueError):
"""Test compound conditions with != operator.""" builder.build_safe_condition(condition)
condition = "AND devName != 'Device1' AND devVendor != 'Unknown'"
sql, params = self.builder.build_safe_condition(condition)
# Should have 2 parameters def test_invalid_operator_rejected(builder):
self.assertEqual(len(params), 2) """Test that invalid operators are rejected."""
condition = "AND devName EXECUTE 'DROP TABLE'"
# Should have != operators (or converted to <>) with pytest.raises(ValueError):
self.assertTrue('!=' in sql or '<>' in sql) builder.build_safe_condition(condition)
# Verify values are parameterized
param_values = list(params.values())
self.assertIn('Device1', param_values)
self.assertIn('Unknown', param_values)
def test_very_long_compound_condition(self): def test_sql_injection_attempt_blocked(builder):
"""Test handling of very long compound conditions (10+ clauses).""" """Test that SQL injection attempts are blocked."""
clauses = [] condition = "AND devName = 'value'; DROP TABLE devices; --"
for i in range(10):
clauses.append(f"AND devName != 'Device{i}'")
condition = " ".join(clauses) # Should either reject or sanitize the dangerous input
sql, params = self.builder.build_safe_condition(condition) # The semicolon and comment should not appear in the final SQL
try:
sql, params = builder.build_safe_condition(condition)
# If it doesn't raise an error, it should sanitize the input
assert 'DROP' not in sql.upper()
assert ';' not in sql
except ValueError:
# Rejection is also acceptable
pass
# Should have 10 parameters
self.assertEqual(len(params), 10)
# Should have 10 AND operators def test_quoted_string_with_spaces(builder):
self.assertEqual(sql.count('AND'), 10) """Test that quoted strings with spaces are handled correctly."""
condition = "AND devName = 'My Device Name' AND devComments = 'Has spaces here'"
# Verify all device names are parameterized sql, params = builder.build_safe_condition(condition)
param_values = list(params.values())
for i in range(10):
self.assertIn(f'Device{i}', param_values)
# Should have 2 parameters
assert len(params) == 2
class TestParameterGeneration(unittest.TestCase): # Verify values with spaces are preserved
"""Test parameter generation and naming.""" param_values = list(params.values())
assert 'My Device Name' in param_values
assert 'Has spaces here' in param_values
def setUp(self):
"""Create a fresh builder instance for each test."""
self.builder = SafeConditionBuilder()
def test_parameters_have_unique_names(self): def test_compound_condition_with_not_equal(builder):
"""Test that all parameters get unique names.""" """Test compound conditions with != operator."""
condition = "AND devName = 'A' AND devName = 'B' AND devName = 'C'" condition = "AND devName != 'Device1' AND devVendor != 'Unknown'"
sql, params = self.builder.build_safe_condition(condition) sql, params = builder.build_safe_condition(condition)
# All parameter names should be unique # Should have 2 parameters
param_names = list(params.keys()) assert len(params) == 2
self.assertEqual(len(param_names), len(set(param_names)))
def test_parameter_values_match_condition(self): # Should have != operators (or converted to <>)
"""Test that parameter values correctly match the condition values.""" assert '!=' in sql or '<>' in sql
condition = "AND devLastIP NOT LIKE '192.168.1.%' AND devLastIP NOT LIKE '10.0.0.%'"
sql, params = self.builder.build_safe_condition(condition) # Verify values are parameterized
param_values = list(params.values())
assert 'Device1' in param_values
assert 'Unknown' in param_values
# Should have exactly the values from the condition
param_values = sorted(params.values())
expected_values = sorted(['192.168.1.%', '10.0.0.%'])
self.assertEqual(param_values, expected_values)
def test_parameters_referenced_in_sql(self): def test_very_long_compound_condition(builder):
"""Test that all parameters are actually referenced in the SQL.""" """Test handling of very long compound conditions (10+ clauses)."""
condition = "AND devName = 'Device1' AND devVendor = 'Apple'" clauses = []
for i in range(10):
clauses.append(f"AND devName != 'Device{i}'")
sql, params = self.builder.build_safe_condition(condition) condition = " ".join(clauses)
sql, params = builder.build_safe_condition(condition)
# Every parameter should appear in the SQL # Should have 10 parameters
for param_name in params.keys(): assert len(params) == 10
self.assertIn(f':{param_name}', sql)
# Should have 10 AND operators
assert sql.count('AND') == 10
if __name__ == '__main__': # Verify all device names are parameterized
unittest.main() param_values = list(params.values())
for i in range(10):
assert f'Device{i}' in param_values
def test_parameters_have_unique_names(builder):
"""Test that all parameters get unique names."""
condition = "AND devName = 'A' AND devName = 'B' AND devName = 'C'"
sql, params = builder.build_safe_condition(condition)
# All parameter names should be unique
param_names = list(params.keys())
assert len(param_names) == len(set(param_names))
def test_parameter_values_match_condition(builder):
"""Test that parameter values correctly match the condition values."""
condition = "AND devLastIP NOT LIKE '192.168.1.%' AND devLastIP NOT LIKE '10.0.0.%'"
sql, params = builder.build_safe_condition(condition)
# Should have exactly the values from the condition
param_values = sorted(params.values())
expected_values = sorted(['192.168.1.%', '10.0.0.%'])
assert param_values == expected_values
def test_parameters_referenced_in_sql(builder):
"""Test that all parameters are actually referenced in the SQL."""
condition = "AND devName = 'Device1' AND devVendor = 'Apple'"
sql, params = builder.build_safe_condition(condition)
# Every parameter should appear in the SQL
for param_name in params.keys():
assert f':{param_name}' in sql

View File

@@ -4,15 +4,15 @@ This test file has minimal dependencies to ensure it can run in any environment.
""" """
import sys import sys
import unittest
import re import re
import pytest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
# Mock the logger module to avoid dependency issues # Mock the logger module to avoid dependency issues
sys.modules['logger'] = Mock() sys.modules['logger'] = Mock()
# Standalone version of SafeConditionBuilder for testing # Standalone version of SafeConditionBuilder for testing
class TestSafeConditionBuilder: class SafeConditionBuilder:
""" """
Test version of SafeConditionBuilder with mock logger. Test version of SafeConditionBuilder with mock logger.
""" """
@@ -152,180 +152,182 @@ class TestSafeConditionBuilder:
return "", {} return "", {}
class TestSafeConditionBuilderSecurity(unittest.TestCase): @pytest.fixture
"""Test cases for the SafeConditionBuilder security functionality.""" def builder():
"""Fixture to provide a fresh SafeConditionBuilder instance for each test."""
def setUp(self): return SafeConditionBuilder()
"""Set up test fixtures before each test method."""
self.builder = TestSafeConditionBuilder()
def test_initialization(self):
"""Test that SafeConditionBuilder initializes correctly."""
self.assertIsInstance(self.builder, TestSafeConditionBuilder)
self.assertEqual(self.builder.param_counter, 0)
self.assertEqual(self.builder.parameters, {})
def test_sanitize_string(self):
"""Test string sanitization functionality."""
# Test normal string
result = self.builder._sanitize_string("normal string")
self.assertEqual(result, "normal string")
# Test s-quote replacement
result = self.builder._sanitize_string("test{s-quote}value")
self.assertEqual(result, "test'value")
# Test control character removal
result = self.builder._sanitize_string("test\x00\x01string")
self.assertEqual(result, "teststring")
# Test excessive whitespace
result = self.builder._sanitize_string(" test string ")
self.assertEqual(result, "test string")
def test_validate_column_name(self):
"""Test column name validation against whitelist."""
# Valid columns
self.assertTrue(self.builder._validate_column_name('eve_MAC'))
self.assertTrue(self.builder._validate_column_name('devName'))
self.assertTrue(self.builder._validate_column_name('eve_EventType'))
# Invalid columns
self.assertFalse(self.builder._validate_column_name('malicious_column'))
self.assertFalse(self.builder._validate_column_name('drop_table'))
self.assertFalse(self.builder._validate_column_name('user_input'))
def test_validate_operator(self):
"""Test operator validation against whitelist."""
# Valid operators
self.assertTrue(self.builder._validate_operator('='))
self.assertTrue(self.builder._validate_operator('LIKE'))
self.assertTrue(self.builder._validate_operator('IN'))
# Invalid operators
self.assertFalse(self.builder._validate_operator('UNION'))
self.assertFalse(self.builder._validate_operator('DROP'))
self.assertFalse(self.builder._validate_operator('EXEC'))
def test_build_simple_condition_valid(self):
"""Test building valid simple conditions."""
sql, params = self.builder._build_simple_condition('AND', 'devName', '=', 'TestDevice')
self.assertIn('AND devName = :param_', sql)
self.assertEqual(len(params), 1)
self.assertIn('TestDevice', params.values())
def test_build_simple_condition_invalid_column(self):
"""Test that invalid column names are rejected."""
with self.assertRaises(ValueError) as context:
self.builder._build_simple_condition('AND', 'invalid_column', '=', 'value')
self.assertIn('Invalid column name', str(context.exception))
def test_build_simple_condition_invalid_operator(self):
"""Test that invalid operators are rejected."""
with self.assertRaises(ValueError) as context:
self.builder._build_simple_condition('AND', 'devName', 'UNION', 'value')
self.assertIn('Invalid operator', str(context.exception))
def test_sql_injection_attempts(self):
"""Test that various SQL injection attempts are blocked."""
injection_attempts = [
"'; DROP TABLE Devices; --",
"' UNION SELECT * FROM Settings --",
"' OR 1=1 --",
"'; INSERT INTO Events VALUES(1,2,3); --",
"' AND (SELECT COUNT(*) FROM sqlite_master) > 0 --",
]
for injection in injection_attempts:
with self.subTest(injection=injection):
with self.assertRaises(ValueError):
self.builder.build_safe_condition(f"AND devName = '{injection}'")
def test_legacy_condition_compatibility(self):
"""Test backward compatibility with legacy condition formats."""
# Test simple condition
sql, params = self.builder.get_safe_condition_legacy("AND devName = 'TestDevice'")
self.assertIn('devName', sql)
self.assertIn('TestDevice', params.values())
# Test empty condition
sql, params = self.builder.get_safe_condition_legacy("")
self.assertEqual(sql, "")
self.assertEqual(params, {})
# Test invalid condition returns empty
sql, params = self.builder.get_safe_condition_legacy("INVALID SQL INJECTION")
self.assertEqual(sql, "")
self.assertEqual(params, {})
def test_parameter_generation(self):
"""Test that parameters are generated correctly."""
# Test multiple parameters
sql1, params1 = self.builder.build_safe_condition("AND devName = 'Device1'")
sql2, params2 = self.builder.build_safe_condition("AND devName = 'Device2'")
# Each should have unique parameter names
self.assertNotEqual(list(params1.keys())[0], list(params2.keys())[0])
def test_xss_prevention(self):
"""Test that XSS-like payloads in device names are handled safely."""
xss_payloads = [
"<script>alert('xss')</script>",
"javascript:alert(1)",
"<img src=x onerror=alert(1)>",
"'; DROP TABLE users; SELECT '<script>alert(1)</script>' --"
]
for payload in xss_payloads:
with self.subTest(payload=payload):
# Should either process safely or reject
try:
sql, params = self.builder.build_safe_condition(f"AND devName = '{payload}'")
# If processed, should be parameterized
self.assertIn(':', sql)
self.assertIn(payload, params.values())
except ValueError:
# Rejection is also acceptable for safety
pass
def test_unicode_handling(self):
"""Test that Unicode characters are handled properly."""
unicode_strings = [
"Ülrich's Device",
"Café Network",
"测试设备",
"Устройство"
]
for unicode_str in unicode_strings:
with self.subTest(unicode_str=unicode_str):
sql, params = self.builder.build_safe_condition(f"AND devName = '{unicode_str}'")
self.assertIn(unicode_str, params.values())
def test_edge_cases(self):
"""Test edge cases and boundary conditions."""
edge_cases = [
"", # Empty string
" ", # Whitespace only
"AND devName = ''", # Empty value
"AND devName = 'a'", # Single character
"AND devName = '" + "x" * 1000 + "'", # Very long string
]
for case in edge_cases:
with self.subTest(case=case):
try:
sql, params = self.builder.get_safe_condition_legacy(case)
# Should either return valid result or empty safe result
self.assertIsInstance(sql, str)
self.assertIsInstance(params, dict)
except Exception:
self.fail(f"Unexpected exception for edge case: {case}")
if __name__ == '__main__': def test_initialization(builder):
# Run the test suite """Test that SafeConditionBuilder initializes correctly."""
unittest.main(verbosity=2) assert isinstance(builder, SafeConditionBuilder)
assert builder.param_counter == 0
assert builder.parameters == {}
def test_sanitize_string(builder):
"""Test string sanitization functionality."""
# Test normal string
result = builder._sanitize_string("normal string")
assert result == "normal string"
# Test s-quote replacement
result = builder._sanitize_string("test{s-quote}value")
assert result == "test'value"
# Test control character removal
result = builder._sanitize_string("test\x00\x01string")
assert result == "teststring"
# Test excessive whitespace
result = builder._sanitize_string(" test string ")
assert result == "test string"
def test_validate_column_name(builder):
"""Test column name validation against whitelist."""
# Valid columns
assert builder._validate_column_name('eve_MAC')
assert builder._validate_column_name('devName')
assert builder._validate_column_name('eve_EventType')
# Invalid columns
assert not builder._validate_column_name('malicious_column')
assert not builder._validate_column_name('drop_table')
assert not builder._validate_column_name('user_input')
def test_validate_operator(builder):
"""Test operator validation against whitelist."""
# Valid operators
assert builder._validate_operator('=')
assert builder._validate_operator('LIKE')
assert builder._validate_operator('IN')
# Invalid operators
assert not builder._validate_operator('UNION')
assert not builder._validate_operator('DROP')
assert not builder._validate_operator('EXEC')
def test_build_simple_condition_valid(builder):
"""Test building valid simple conditions."""
sql, params = builder._build_simple_condition('AND', 'devName', '=', 'TestDevice')
assert 'AND devName = :param_' in sql
assert len(params) == 1
assert 'TestDevice' in params.values()
def test_build_simple_condition_invalid_column(builder):
"""Test that invalid column names are rejected."""
with pytest.raises(ValueError) as exc_info:
builder._build_simple_condition('AND', 'invalid_column', '=', 'value')
assert 'Invalid column name' in str(exc_info.value)
def test_build_simple_condition_invalid_operator(builder):
"""Test that invalid operators are rejected."""
with pytest.raises(ValueError) as exc_info:
builder._build_simple_condition('AND', 'devName', 'UNION', 'value')
assert 'Invalid operator' in str(exc_info.value)
def test_sql_injection_attempts(builder):
"""Test that various SQL injection attempts are blocked."""
injection_attempts = [
"'; DROP TABLE Devices; --",
"' UNION SELECT * FROM Settings --",
"' OR 1=1 --",
"'; INSERT INTO Events VALUES(1,2,3); --",
"' AND (SELECT COUNT(*) FROM sqlite_master) > 0 --",
]
for injection in injection_attempts:
with pytest.raises(ValueError):
builder.build_safe_condition(f"AND devName = '{injection}'")
def test_legacy_condition_compatibility(builder):
"""Test backward compatibility with legacy condition formats."""
# Test simple condition
sql, params = builder.get_safe_condition_legacy("AND devName = 'TestDevice'")
assert 'devName' in sql
assert 'TestDevice' in params.values()
# Test empty condition
sql, params = builder.get_safe_condition_legacy("")
assert sql == ""
assert params == {}
# Test invalid condition returns empty
sql, params = builder.get_safe_condition_legacy("INVALID SQL INJECTION")
assert sql == ""
assert params == {}
def test_parameter_generation(builder):
"""Test that parameters are generated correctly."""
# Test single parameter
sql, params = builder.build_safe_condition("AND devName = 'Device1'")
# Should have 1 parameter
assert len(params) == 1
assert 'param_1' in params
def test_xss_prevention(builder):
"""Test that XSS-like payloads in device names are handled safely."""
xss_payloads = [
"<script>alert('xss')</script>",
"javascript:alert(1)",
"<img src=x onerror=alert(1)>",
"'; DROP TABLE users; SELECT '<script>alert(1)</script>' --"
]
for payload in xss_payloads:
# Should either process safely or reject
try:
sql, params = builder.build_safe_condition(f"AND devName = '{payload}'")
# If processed, should be parameterized
assert ':' in sql
assert payload in params.values()
except ValueError:
# Rejection is also acceptable for safety
pass
def test_unicode_handling(builder):
"""Test that Unicode characters are handled properly."""
unicode_strings = [
"Ülrichs Device",
"Café Network",
"测试设备",
"Устройство"
]
for unicode_str in unicode_strings:
sql, params = builder.build_safe_condition(f"AND devName = '{unicode_str}'")
assert unicode_str in params.values()
def test_edge_cases(builder):
"""Test edge cases and boundary conditions."""
edge_cases = [
"", # Empty string
" ", # Whitespace only
"AND devName = ''", # Empty value
"AND devName = 'a'", # Single character
"AND devName = '" + "x" * 1000 + "'", # Very long string
]
for case in edge_cases:
try:
sql, params = builder.get_safe_condition_legacy(case)
# Should either return valid result or empty safe result
assert isinstance(sql, str)
assert isinstance(params, dict)
except Exception:
pytest.fail(f"Unexpected exception for edge case: {case}")