From 51f71ccc15024f52cfb8602f6d30e46e243baa3c Mon Sep 17 00:00:00 2001 From: malteos Date: Wed, 15 Oct 2025 17:33:13 +0200 Subject: [PATCH 01/10] feat: Adds Github action to check CC's CDX index server every week --- .github/workflows/weekly-cc-server-check.yaml | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/weekly-cc-server-check.yaml diff --git a/.github/workflows/weekly-cc-server-check.yaml b/.github/workflows/weekly-cc-server-check.yaml new file mode 100644 index 0000000..90a8944 --- /dev/null +++ b/.github/workflows/weekly-cc-server-check.yaml @@ -0,0 +1,38 @@ +# This tests if the fail2ban filtering on the CC cdx index server is too strict. + +name: Weekly CC server check + +on: + schedule: + # Every Monday at 9:00 AM UTC + - cron: '0 9 * * 1' + workflow_dispatch: # Allows manual triggering + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Get Runner IP + run: | + echo "Runner IP: $(curl -s https://ipinfo.io/ip)" + + - name: Install setuptools on python 3.12+ + run: | + pip install setuptools + + - name: Install cdx_toolkit + run: pip install .[test] + + - name: Run example + run: | + python examples/iter-and-warc.py + From 4ac40bbac4c8575c719caa97378b223832620d0e Mon Sep 17 00:00:00 2001 From: malteos Date: Thu, 16 Oct 2025 15:57:30 +0200 Subject: [PATCH 02/10] Enable manual run also for PRs --- .github/workflows/weekly-cc-server-check.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/weekly-cc-server-check.yaml b/.github/workflows/weekly-cc-server-check.yaml index 90a8944..8402fc0 100644 --- a/.github/workflows/weekly-cc-server-check.yaml +++ b/.github/workflows/weekly-cc-server-check.yaml @@ -7,6 +7,7 @@ on: # Every Monday at 9:00 AM UTC - cron: '0 9 * * 1' workflow_dispatch: # Allows manual triggering + pull_request: # Allows manual triggering on PRs jobs: check: @@ -30,7 +31,7 @@ jobs: pip install setuptools - name: Install cdx_toolkit - run: pip install .[test] + run: pip install ".[test]" - name: Run example run: | From 798bfbe1c0d67c40c2082fac7cb313d5b5cb2091 Mon Sep 17 00:00:00 2001 From: malteos Date: Thu, 16 Oct 2025 16:02:27 +0200 Subject: [PATCH 03/10] Disable run for PRs --- .github/workflows/weekly-cc-server-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/weekly-cc-server-check.yaml b/.github/workflows/weekly-cc-server-check.yaml index 8402fc0..0f9d2e4 100644 --- a/.github/workflows/weekly-cc-server-check.yaml +++ b/.github/workflows/weekly-cc-server-check.yaml @@ -7,7 +7,7 @@ on: # Every Monday at 9:00 AM UTC - cron: '0 9 * * 1' workflow_dispatch: # Allows manual triggering - pull_request: # Allows manual triggering on PRs + # pull_request: # Run automatically for PRs jobs: check: From 199f4aaf618a9268f6a3d08578adc388d6697c96 Mon Sep 17 00:00:00 2001 From: malteos Date: Wed, 19 Nov 2025 11:37:19 +0100 Subject: [PATCH 04/10] Added server check with custom test script --- ...server-check.yaml => cc-server-check.yaml} | 26 +- tests/test_fail2ban.py | 336 ++++++++++++++++++ 2 files changed, 352 insertions(+), 10 deletions(-) rename .github/workflows/{weekly-cc-server-check.yaml => cc-server-check.yaml} (53%) create mode 100644 tests/test_fail2ban.py diff --git a/.github/workflows/weekly-cc-server-check.yaml b/.github/workflows/cc-server-check.yaml similarity index 53% rename from .github/workflows/weekly-cc-server-check.yaml rename to .github/workflows/cc-server-check.yaml index 0f9d2e4..950ab3c 100644 --- a/.github/workflows/weekly-cc-server-check.yaml +++ b/.github/workflows/cc-server-check.yaml @@ -1,10 +1,10 @@ # This tests if the fail2ban filtering on the CC cdx index server is too strict. -name: Weekly CC server check +name: CC server check (weekly) on: schedule: - # Every Monday at 9:00 AM UTC + # Weekly schedule: every Monday at 9:00 AM UTC - cron: '0 9 * * 1' workflow_dispatch: # Allows manual triggering # pull_request: # Run automatically for PRs @@ -26,14 +26,20 @@ jobs: run: | echo "Runner IP: $(curl -s https://ipinfo.io/ip)" - - name: Install setuptools on python 3.12+ + - name: Install dependencies run: | - pip install setuptools - - - name: Install cdx_toolkit - run: pip install ".[test]" - - - name: Run example + pip install requests + + - name: Run external API tests + id: api_test run: | - python examples/iter-and-warc.py + python tests/test_fail2ban.py + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: fail2ban-test-results-${{ github.run_number }} + path: fail2ban_test_results.json + retention-days: 14 diff --git a/tests/test_fail2ban.py b/tests/test_fail2ban.py new file mode 100644 index 0000000..2c561cd --- /dev/null +++ b/tests/test_fail2ban.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +""" +Test CDX API endpoints from external source (e.g., GitHub action ) to detect if fail2ban is working as expected. + +Usage: + +```bash +python tests/test_fail2ban.py +``` + +""" + +import requests +import time +import sys +import json +from datetime import datetime +from typing import List, Dict + +API_BASE = 'https://index.commoncrawl.org' # Update with your actual domain +USER_AGENT = 'pypi_cdx_toolkit/fail2ban-monitor' +CRAWL_ID = 'CC-MAIN-2025-43' +DEFAULT_LIMIT = 1 + +DOMAINS = [ + 'blogspot.com', + 'wikipedia.org', + 'wordpress.org', + 'ebay.com', + 'europa.eu', + 'app.link', + 'google.com', + 'wiktionary.org', + 'ning.com', +] # taken from https://commoncrawl.github.io/cc-crawl-statistics/plots/domains + +# Test scenarios that simulate legitimate usage patterns +test_scenarios = [ + { + 'name': 'Single user normal browsing', + 'description': 'Simulates a user making occasional requests', + 'requests': [ + { + 'url': f'{API_BASE}/{CRAWL_ID}-index', + 'params': {'url': 'example.com/*', 'output': 'json', 'limit': DEFAULT_LIMIT}, + }, + { + 'url': f'{API_BASE}/{CRAWL_ID}-index', + 'params': {'url': 'example.org/*', 'output': 'json', 'limit': DEFAULT_LIMIT}, + }, + { + 'url': f'{API_BASE}/{CRAWL_ID}-index', + 'params': {'url': 'example.net/*', 'output': 'json', 'limit': DEFAULT_LIMIT}, + }, + ], + 'delay_between': 2.0, # seconds + 'should_succeed': True, + }, + { + 'name': 'Moderate API usage', + 'description': 'Simulates a script making regular requests', + 'requests': [ + { + 'url': f'{API_BASE}/{CRAWL_ID}-index', + 'params': {'url': f'{DOMAINS[i]}/*', 'output': 'json', 'limit': DEFAULT_LIMIT}, + } + for i in range(8) + ], + 'delay_between': 8.0, # 8 requests over ~64 seconds (within cdx limit of 10/60s) + 'should_succeed': True, + }, + { + 'name': 'Collection info check', + 'description': 'Checking collection info (stricter limits)', + 'requests': [ + {'url': f'{API_BASE}/collinfo.json', 'params': {}}, + {'url': f'{API_BASE}/collinfo.json', 'params': {}}, + ], + 'delay_between': 6.0, # 2 requests over 6+ seconds (within limit of 3/10s) + 'should_succeed': True, + }, + { + 'name': 'Edge case - near limit', + 'description': 'Tests behavior near the rate limit threshold', + 'requests': [ + {'url': f'{API_BASE}/cc-index', 'params': {'url': DOMAINS[i], 'output': 'json', 'limit': DEFAULT_LIMIT}} + for i in range(9) + ], + 'delay_between': 7.0, # 9 requests in ~63 seconds (just under 10/60s limit) + 'should_succeed': True, + }, + # { + # 'name': 'Burst detection - collinfo', + # 'description': 'Tests if legitimate burst triggers ban on collinfo endpoint', + # 'requests': [{'url': f'{API_BASE}/collinfo.json', 'params': {}} for _ in range(3)], + # 'delay_between': 4.0, # 3 requests over 8 seconds (WILL trigger ban at 3/10s) + # 'should_succeed': False, # This SHOULD get banned + # }, +] + + +def is_connection_blocked(error_msg: str) -> bool: + """Determine if an error indicates IP blocking.""" + blocking_indicators = [ + 'Connection refused', + '[Errno 61]', # macOS/BSD connection refused + '[Errno 111]', # Linux connection refused + 'Max retries exceeded', + 'NewConnectionError', + ] + return any(indicator in str(error_msg) for indicator in blocking_indicators) + + +def make_request(url: str, params: Dict, request_num: int) -> Dict: + """Make a single request and return results.""" + result = { + 'request_num': request_num, + 'timestamp': datetime.now().isoformat(), + 'url': url, + 'params': params, + 'success': False, + 'status_code': None, + 'blocked': False, + 'error': None, + 'response_time': None, + } + + start_time = time.time() + try: + response = requests.get(url, params=params, timeout=15, headers={'User-Agent': 'fail2ban-monitor/1.0'}) + result['response_time'] = time.time() - start_time + result['status_code'] = response.status_code + + if response.status_code == 200: + result['success'] = True + elif response.status_code in [403, 429, 503]: + result['blocked'] = True + result['error'] = f'HTTP blocked: {response.status_code}' + else: + result['error'] = f'Unexpected status: {response.status_code}' + + except requests.exceptions.Timeout: + result['error'] = 'Request timeout' + result['response_time'] = time.time() - start_time + # Timeout could indicate blocking, but not conclusive + except requests.exceptions.ConnectionError as e: + result['response_time'] = time.time() - start_time + error_str = str(e) + result['error'] = f'Connection error: {error_str}' + # Connection refused is a strong indicator of IP blocking + if is_connection_blocked(error_str): + result['blocked'] = True + except requests.exceptions.RequestException as e: + result['error'] = f'Request error: {str(e)}' + + return result + + +def run_scenario(scenario: Dict) -> Dict: + """Run a complete test scenario.""" + print(f'\n{"=" * 70}') + print(f'Scenario: {scenario["name"]}') + print(f'Description: {scenario["description"]}') + print(f'Total requests: {len(scenario["requests"])}') + print(f'{"=" * 70}') + + results = [] + blocked_at = None + + for i, request in enumerate(scenario['requests'], 1): + print(f'\n Request {i}/{len(scenario["requests"])}') + result = make_request(request['url'], request['params'], i) + results.append(result) + + if result['success']: + print(f' ✓ Success (200) - {result["response_time"]:.2f}s') + elif result['blocked']: + print(f' ✗ BLOCKED - {result["error"]}') + if result['status_code']: + print(f' Status code: {result["status_code"]}') + blocked_at = i + # Don't break immediately - log that we're blocked but continue to see pattern + # Actually, we should break because we can't make more requests + break + else: + print(f' ⚠ Failed - {result["error"]}') + + # Wait before next request (except after last one or if blocked) + if i < len(scenario['requests']) and not result['blocked']: + print(f' ⏱ Waiting {scenario["delay_between"]}s...') + time.sleep(scenario['delay_between']) + + # Analyze results + successful = sum(1 for r in results if r['success']) + blocked = sum(1 for r in results if r['blocked']) + failed = len(results) - successful - blocked + + summary = { + 'name': scenario['name'], + 'description': scenario['description'], + 'should_succeed': scenario['should_succeed'], + 'total_requests': len(scenario['requests']), + 'completed_requests': len(results), + 'successful': successful, + 'blocked': blocked, + 'failed': failed, + 'blocked_at_request': blocked_at, + 'unexpected_block': blocked > 0 and scenario['should_succeed'], + 'results': results, + } + + return summary + + +def print_summary(all_summaries: List[Dict]): + """Print overall test summary.""" + print(f'\n\n{"=" * 70}') + print('OVERALL SUMMARY') + print(f'{"=" * 70}\n') + + total_scenarios = len(all_summaries) + problematic_scenarios = [] + + for summary in all_summaries: + # Problematic if: (1) should succeed but got blocked, OR (2) should fail but didn't get blocked + is_problematic = False + + if summary['should_succeed'] and summary['blocked'] > 0: + # Should have worked but got blocked = TOO STRICT + is_problematic = True + status = '❌ TOO STRICT' + elif not summary['should_succeed'] and summary['blocked'] == 0: + # Should have been blocked but wasn't = TOO LENIENT + is_problematic = True + status = '❌ TOO LENIENT' + elif not summary['should_succeed'] and summary['blocked'] > 0: + # Correctly blocked as expected + status = '✅ BLOCKED (expected)' + else: + # Correctly succeeded + status = '✅ OK' + + if is_problematic: + problematic_scenarios.append(summary) + + print(f'{status} {summary["name"]}') + print(f' {summary["successful"]}/{summary["completed_requests"]} successful', end='') + + if summary['blocked'] > 0: + print(f', {summary["blocked"]} blocked at request #{summary["blocked_at_request"]}') + else: + print() + + if summary['failed'] > 0: + print(f' ⚠ {summary["failed"]} requests failed (non-block errors)') + + print(f'\n{"=" * 70}') + print(f'Total scenarios: {total_scenarios}') + print(f'Problematic: {len(problematic_scenarios)}') + print(f'{"=" * 70}\n') + + if problematic_scenarios: + print('⚠️ FAIL2BAN CONFIGURATION ISSUES DETECTED ⚠️\n') + + too_strict = [s for s in problematic_scenarios if s['should_succeed'] and s['blocked'] > 0] + too_lenient = [s for s in problematic_scenarios if not s['should_succeed'] and s['blocked'] == 0] + + if too_strict: + print('🔒 TOO STRICT - Legitimate usage patterns are being blocked:\n') + for scenario in too_strict: + print(f' • {scenario["name"]}') + print(f' Blocked at request {scenario["blocked_at_request"]}/{scenario["total_requests"]}') + print() + + if too_lenient: + print('🔓 TOO LENIENT - Abuse patterns are NOT being blocked:\n') + for scenario in too_lenient: + print(f' • {scenario["name"]}') + print(f' Completed {scenario["completed_requests"]}/{scenario["total_requests"]} without ban') + print() + + print('📋 Recommendations:') + if too_strict: + print(' - Increase maxretry values') + print(' - Increase findtime windows') + print(' - Review filter patterns for false positives') + if too_lenient: + print(' - Decrease maxretry values') + print(' - Decrease findtime windows') + print(' - Verify fail2ban is running and filters are active') + return False + else: + print('✅ All test scenarios behaved as expected') + print(' fail2ban rules appear correctly configured') + return True + + +def main(): + print(f'Starting fail2ban external monitoring at {datetime.now()}') + print(f'Target: {API_BASE}\n') + + all_summaries = [] + + for scenario in test_scenarios: + summary = run_scenario(scenario) + all_summaries.append(summary) + + # If we got blocked, warn and wait longer before next scenario + if summary['blocked'] > 0: + print(f'\n⚠️ IP may be blocked - waiting 60s for potential unban...') + time.sleep(60) + else: + print(f'\n⏱ Waiting 30s before next scenario...') + time.sleep(30) + + # Print summary and determine exit code + success = print_summary(all_summaries) + + # Save detailed results to file for GitHub Actions artifact + with open('fail2ban_test_results.json', 'w') as f: + json.dump( + { + 'timestamp': datetime.now().isoformat(), + 'api_base': API_BASE, + 'overall_success': success, + 'summaries': all_summaries, + }, + f, + indent=2, + ) + + sys.exit(0 if success else 1) + + +if __name__ == '__main__': + main() From 4613f0b13267313aebd4f45b33afe991c81398a7 Mon Sep 17 00:00:00 2001 From: malteos Date: Wed, 19 Nov 2025 11:39:25 +0100 Subject: [PATCH 05/10] renamed file --- tests/{test_fail2ban.py => cc_server_check.py} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename tests/{test_fail2ban.py => cc_server_check.py} (98%) diff --git a/tests/test_fail2ban.py b/tests/cc_server_check.py similarity index 98% rename from tests/test_fail2ban.py rename to tests/cc_server_check.py index 2c561cd..ad695bc 100644 --- a/tests/test_fail2ban.py +++ b/tests/cc_server_check.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -""" -Test CDX API endpoints from external source (e.g., GitHub action ) to detect if fail2ban is working as expected. +"""Check CDX API endpoints from external source (e.g., GitHub action) to detect if fail2ban is working as expected. + +NOTE: This is a dedicated script and NOT a unit test. Usage: ```bash -python tests/test_fail2ban.py +python tests/cc_server_check.py ``` """ From a674b51d6716e9d9656da8bc55142e2f18be8637 Mon Sep 17 00:00:00 2001 From: malteos Date: Wed, 19 Nov 2025 11:45:49 +0100 Subject: [PATCH 06/10] Fixed lint --- tests/cc_server_check.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cc_server_check.py b/tests/cc_server_check.py index ad695bc..c381ff9 100644 --- a/tests/cc_server_check.py +++ b/tests/cc_server_check.py @@ -308,11 +308,11 @@ def main(): # If we got blocked, warn and wait longer before next scenario if summary['blocked'] > 0: - print(f'\n⚠️ IP may be blocked - waiting 60s for potential unban...') + print('\n⚠️ IP may be blocked - waiting 60s for potential unban...') time.sleep(60) else: - print(f'\n⏱ Waiting 30s before next scenario...') - time.sleep(30) + print('\n⏱ Waiting 60s before next scenario...') + time.sleep(60) # Print summary and determine exit code success = print_summary(all_summaries) From d0bcb78cb4b24b81683469252e05054d728f1211 Mon Sep 17 00:00:00 2001 From: malteos Date: Wed, 19 Nov 2025 11:48:07 +0100 Subject: [PATCH 07/10] Run workflow on push --- .github/workflows/cc-server-check.yaml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cc-server-check.yaml b/.github/workflows/cc-server-check.yaml index 950ab3c..72f7a06 100644 --- a/.github/workflows/cc-server-check.yaml +++ b/.github/workflows/cc-server-check.yaml @@ -8,10 +8,16 @@ on: - cron: '0 9 * * 1' workflow_dispatch: # Allows manual triggering # pull_request: # Run automatically for PRs + push: + branches: + - 'feat/**' # Trigger on feature branches + paths: + - 'tests/cc_server_check.py' + - '.github/workflows/cc-server-check.yaml' jobs: check: - runs-on: ubuntu-latest + runs-on: ubuntu-latest steps: - name: checkout @@ -20,7 +26,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.12' - name: Get Runner IP run: | @@ -29,12 +35,12 @@ jobs: - name: Install dependencies run: | pip install requests - + - name: Run external API tests id: api_test run: | - python tests/test_fail2ban.py - + python tests/cc_server_check.py + - name: Upload test results if: always() uses: actions/upload-artifact@v4 @@ -42,4 +48,3 @@ jobs: name: fail2ban-test-results-${{ github.run_number }} path: fail2ban_test_results.json retention-days: 14 - From e28852b63578f320c3015064246d3fe98961c425 Mon Sep 17 00:00:00 2001 From: malteos Date: Thu, 20 Nov 2025 17:17:46 +0100 Subject: [PATCH 08/10] enabled failing scenario --- tests/cc_server_check.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/cc_server_check.py b/tests/cc_server_check.py index c381ff9..3be44c0 100644 --- a/tests/cc_server_check.py +++ b/tests/cc_server_check.py @@ -90,13 +90,13 @@ 'delay_between': 7.0, # 9 requests in ~63 seconds (just under 10/60s limit) 'should_succeed': True, }, - # { - # 'name': 'Burst detection - collinfo', - # 'description': 'Tests if legitimate burst triggers ban on collinfo endpoint', - # 'requests': [{'url': f'{API_BASE}/collinfo.json', 'params': {}} for _ in range(3)], - # 'delay_between': 4.0, # 3 requests over 8 seconds (WILL trigger ban at 3/10s) - # 'should_succeed': False, # This SHOULD get banned - # }, + { + 'name': 'Burst detection - collinfo', + 'description': 'Tests if legitimate burst triggers ban on collinfo endpoint', + 'requests': [{'url': f'{API_BASE}/collinfo.json', 'params': {}} for _ in range(3)], + 'delay_between': 4.0, # 3 requests over 8 seconds (WILL trigger ban at 3/10s) + 'should_succeed': False, # This SHOULD get banned + }, ] From 2639c6ff9276bb721174bcbfa68f6c874eafd740 Mon Sep 17 00:00:00 2001 From: malteos Date: Fri, 21 Nov 2025 12:46:16 +0100 Subject: [PATCH 09/10] fixed failing scenario --- tests/cc_server_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cc_server_check.py b/tests/cc_server_check.py index 3be44c0..6a45cb5 100644 --- a/tests/cc_server_check.py +++ b/tests/cc_server_check.py @@ -93,8 +93,8 @@ { 'name': 'Burst detection - collinfo', 'description': 'Tests if legitimate burst triggers ban on collinfo endpoint', - 'requests': [{'url': f'{API_BASE}/collinfo.json', 'params': {}} for _ in range(3)], - 'delay_between': 4.0, # 3 requests over 8 seconds (WILL trigger ban at 3/10s) + 'requests': [{'url': f'{API_BASE}/collinfo.json', 'params': {}} for _ in range(4)], + 'delay_between': 4.0, # 4 requests over 8 seconds (WILL trigger ban at 3/10s) 'should_succeed': False, # This SHOULD get banned }, ] From 57d84c8c38c95f33be7faae6624b9e871b63aeb6 Mon Sep 17 00:00:00 2001 From: malteos Date: Fri, 21 Nov 2025 13:29:05 +0100 Subject: [PATCH 10/10] fixed bad urls --- tests/cc_server_check.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/cc_server_check.py b/tests/cc_server_check.py index 6a45cb5..d93f023 100644 --- a/tests/cc_server_check.py +++ b/tests/cc_server_check.py @@ -84,7 +84,14 @@ 'name': 'Edge case - near limit', 'description': 'Tests behavior near the rate limit threshold', 'requests': [ - {'url': f'{API_BASE}/cc-index', 'params': {'url': DOMAINS[i], 'output': 'json', 'limit': DEFAULT_LIMIT}} + { + 'url': f'{API_BASE}/cc-index', + 'params': { + 'url': f'{DOMAINS[i]}/*', + 'output': 'json', + 'limit': DEFAULT_LIMIT + } + } for i in range(9) ], 'delay_between': 7.0, # 9 requests in ~63 seconds (just under 10/60s limit) @@ -143,7 +150,7 @@ def make_request(url: str, params: Dict, request_num: int) -> Dict: except requests.exceptions.Timeout: result['error'] = 'Request timeout' result['response_time'] = time.time() - start_time - # Timeout could indicate blocking, but not conclusive + result['blocked'] = True except requests.exceptions.ConnectionError as e: result['response_time'] = time.time() - start_time error_str = str(e) @@ -280,15 +287,15 @@ def print_summary(all_summaries: List[Dict]): print(f' Completed {scenario["completed_requests"]}/{scenario["total_requests"]} without ban') print() - print('📋 Recommendations:') - if too_strict: - print(' - Increase maxretry values') - print(' - Increase findtime windows') - print(' - Review filter patterns for false positives') - if too_lenient: - print(' - Decrease maxretry values') - print(' - Decrease findtime windows') - print(' - Verify fail2ban is running and filters are active') + # print('📋 Recommendations:') + # if too_strict: + # print(' - Increase maxretry values') + # print(' - Increase findtime windows') + # print(' - Review filter patterns for false positives') + # if too_lenient: + # print(' - Decrease maxretry values') + # print(' - Decrease findtime windows') + # print(' - Verify fail2ban is running and filters are active') return False else: print('✅ All test scenarios behaved as expected')