A Python script that analyzes Cisco Firewall Management Center (FMC) access control rules based on hit counts and automatically disables unused rules that meet specific criteria.
Clone this repository:
git clone https://github.com/apinto/fmc_rule_cleanup.git
cd fmc_rule_cleanupInstall required dependencies:
pip install -r requirements.txt
For development (optional):
pip install -r requirements-dev.txt
The example.env file provides a template for creating environment-specific configurations that set shell variables for easier script execution:
Copy and customize the template:
cp example.env my-production.env
Edit the file with your actual values:
# my-production.env export FMC_HOST="192.168.1.100" export FMC_USERNAME="your_username" export FMC_PASSWORD="your_password" export DEVICE_NAME="firewall01.example.local" export MAX_RULES="1000" export LOG_FILE="production.log" export EXCLUDE_ZONES="TRUSTED CRITICAL MANAGEMENT" # Space-separated list
Source the environment file and run the script:
# Load environment variables source my-production.env # Run the script using the variables python3 fmc_rule_cleanup.py \ --host "$FMC_HOST" \ --username "$FMC_USERNAME" \ --password "$FMC_PASSWORD" \ --device "$DEVICE_NAME" \ --max-rules "$MAX_RULES" \ --log-file "$LOG_FILE" \ --exclude-zones $EXCLUDE_ZONES \ --dry-run
Create an executable wrapper script:
# create-production-script.sh #!/bin/bash # Load environment configuration source my-production.env # Run FMC script with environment variables python3 fmc_rule_cleanup.py \ --host "$FMC_HOST" \ --username "$FMC_USERNAME" \ --password "$FMC_PASSWORD" \ --device "$DEVICE_NAME" \ --max-rules "$MAX_RULES" \ --log-file "$LOG_FILE" \ --exclude-zones $EXCLUDE_ZONES \ "$@" # Pass any additional arguments
Make it executable and use:
chmod +x create-production-script.sh ./create-production-script.sh --dry-run ./create-production-script.sh --max-rules 500
# Set variables directly in your session export FMC_HOST="192.168.1.100" export FMC_USERNAME="your_username" export FMC_PASSWORD="your_password" export DEVICE_NAME="firewall01.example.local" # Use them in the script python3 fmc_rule_cleanup.py \ --host "$FMC_HOST" \ --username "$FMC_USERNAME" \ --password "$FMC_PASSWORD" \ --device "$DEVICE_NAME" \ --dry-run
Security Note: Keep your .env files secure and exclude them from version control. They contain sensitive credentials.
⚠️ Always start with a dry run to understand what the script will do:
python3 fmc_rule_cleanup.py \ --host YOUR_FMC_IP \ --username YOUR_USERNAME \ --password YOUR_PASSWORD \ --device YOUR_DEVICE_NAME \ --dry-run
Note: By default, no security zones are excluded. To exclude specific zones (e.g., TRUSTED, DMZ), use the --exclude-zones flag:
python3 fmc_rule_cleanup.py \ --host YOUR_FMC_IP \ --username YOUR_USERNAME \ --password YOUR_PASSWORD \ --device YOUR_DEVICE_NAME \ --exclude-zones TRUSTED DMZ \ --dry-run
Note: To exclude rules based on IP address ranges (e.g., protecting internal networks), use the --exclude-prefixes flag with CIDR notation:
python3 fmc_rule_cleanup.py \ --host YOUR_FMC_IP \ --username YOUR_USERNAME \ --password YOUR_PASSWORD \ --device YOUR_DEVICE_NAME \ --exclude-prefixes 10.0.0.0/8 192.168.0.0/16 \ --dry-run
IMPORTANT NOTE:
Hit count information in Cisco FMC is not synchronized in real time. To avoid the risk of disabling rules that have recent hits, it is strongly recommended to manually synchronize the hit counts in FMC before running this script.In FMC, go to your Access Control Policy (ACP), select Analyse → Hit Counts → See Last Fetched, and click Refresh to update the hit count data.
python3 fmc_rule_cleanup.py --host 192.168.1.100 --username apiuser --password secret --device firewall01.example.local
python3 fmc_rule_cleanup.py --host 192.168.1.100 --username apiuser --password secret --device firewall01.example.local --dry-run
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --max-rules 500 \ --exclude-zones TRUSTED DMZ \ --log-file fmc_analysis.log \ --debug \ --dry-run
Zone Exclusion Behavior:
--exclude-zones to specify one or more zones (space-separated) to protect from rule changes--exclude-zones TRUSTED CRITICAL MANAGEMENT will skip any rule that involves any of these zonesIP Prefix Exclusion:
--exclude-prefixes to specify one or more IP prefixes (space-separated)10.0.0.0/8, 192.168.1.0/2410.1.1.5, 192.168.1.10010.1.1.5-10.1.1.50 (FMC range notation)--exclude-prefixes 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12 protects RFC1918 private networksPrefix Match Modes:
overlap mode (default): Excludes rules with ANY network overlap
10.0.0.0/8 → excluded when using --exclude-prefixes 10.2.0.0/16 (superset)10.2.5.0/24 → excluded when using --exclude-prefixes 10.2.0.0/16 (subset)"any" → excluded (encompasses all IPs)subnet mode: Only excludes rules with networks that are subsets of excluded prefixes
10.0.0.0/8 → NOT excluded when using --exclude-prefixes 10.2.0.0/16 (superset)10.2.5.0/24 → excluded when using --exclude-prefixes 10.2.0.0/16 (subset)"any" → NOT excluded--prefix-match-mode subnetTarget older rules (created before 2020):
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --year-threshold 2020 \ --dry-run
Target BLOCK rules instead of ALLOW rules:
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --rule-actions BLOCK \ --dry-run
Target both ALLOW and BLOCK rules:
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --rule-actions ALLOW BLOCK \ --dry-run
Combined advanced criteria:
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --year-threshold 2022 \ --rule-actions ALLOW BLOCK \ --max-rules 200 \ --exclude-zones TRUSTED CRITICAL \ --exclude-prefixes 10.0.0.0/8 192.168.0.0/16 \ --log-file cleanup.log \ --dry-run
Protect RFC1918 private networks:
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --exclude-prefixes 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 \ --dry-run
Use strict subnet matching (only exclude rules with exact subnets):
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --exclude-prefixes 10.2.0.0/16 \ --prefix-match-mode subnet \ --dry-run
Generate Excel report with analysis results:
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --excel-report analysis_report.xlsx \ --dry-run
--host: FMC host IP address--username: FMC username with API access--password: FMC password--device: Target device name (cluster name)--autodeploy: Enable automatic deployment (default: False)--page-limit: API page limit for queries (default: 500)--debug: Enable debug logging (default: False)--timeout: API timeout in seconds (default: 10). Increase this value (e.g., 30-60) if you experience frequent timeout errors with large rulesets--log-file: Log file name (default: console only). Logs are directed to the file only, keeping console output clean--max-rules: Maximum rules to disable per run (default: 1000)--dry-run: Simulate without making changes (default: False)--exclude-zones: Space-separated list of zones to exclude from processing (no default - must be explicitly specified if needed)--exclude-prefixes: Space-separated list of IP prefixes in CIDR notation to exclude from processing (e.g., 10.0.0.0/8 192.168.0.0/16)--prefix-match-mode: Mode for matching excluded prefixes - overlap (default, excludes any overlap) or subnet (only excludes subsets)--excel-report: Generate an Excel report file with three tabs: Operation Summary, Disabled Rules, and Ignored Rules (e.g., report.xlsx). Requires openpyxl package.--year-threshold: Consider rules created before this year for disabling (default: current year - 1)--rule-actions: Rule actions to consider for disabling - ALLOW, BLOCK, or both (default: ALLOW)Note: The year threshold automatically defaults to the previous year (e.g., in 2025 it defaults to 2024). This provides a sensible balance: protecting recent rules while targeting older unused rules for cleanup. The threshold adjusts automatically each year without requiring code changes.
A rule will be disabled if it meets ALL of the following criteria:
--rule-actions (default: "ALLOW")--year-threshold (default: current year - 1), OR--exclude-zones (if provided)--exclude-prefixes (if provided)
The script provides detailed output including:
Example console output:
Logging to file: firewall01.log
Successfully connected to FMC at 192.168.1.100
DRY RUN MODE - No changes will be made to FMC
Found 450 rules with zero hit counts
Processing 450 rules...
Progress: |█████████████████████████████████████████████████| 100.0% Complete (120 rules disabled)
============================================================
ANALYSIS COMPLETE - Summary:
- Total rules analyzed: 1250
- Rules with zero hits: 450
- Rules disabled: 120
- Rules skipped: 330
- Connection failures: 0
DRY RUN COMPLETED - No changes were made to FMC.
============================================================
Example output during connection retry:
Progress: |████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░| 45.2% (25 rules) | Retry #2/4 - 43s remaining (consecutive: 3/10)
--timeout 30 or --timeout 60 when analyzing devices with thousands of rulesThe script includes several features to handle large deployments and edge cases:
--timeout flag based on your environment (default: 10 seconds)The script handles various IP address formats used in FMC rules:
10.0.0.0/8, 192.168.1.0/24 - Standard network format10.1.1.5 - Treated as /32 (IPv4) or /128 (IPv6)10.1.1.5-10.1.1.50 - FMC's range notation
For large environments (1000+ rules with zero hits):
python3 fmc_rule_cleanup.py \ --host 192.168.1.100 \ --username apiuser \ --password secret \ --device firewall01.example.local \ --timeout 60 \ --max-rules 100 \ --log-file firewall01.log \ --dry-run
The script will show a clean progress bar with retry information when needed:
Progress: |████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░| 45.2% (25 rules disabled)
If connection issues occur, the retry information is integrated into the progress display:
Progress: |████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░| 45.2% (25 rules) | Retry #2/4 - 43s remaining (consecutive: 3/10)
The script follows Python best practices:
# Check code style flake8 fmc_rule_cleanup.py # Format code black fmc_rule_cleanup.py # Type checking mypy fmc_rule_cleanup.py # Comprehensive linting pylint fmc_rule_cleanup.py
Use --debug flag for detailed troubleshooting information:
python3 fmc_rule_cleanup.py --host 192.168.1.100 --username apiuser --password secret --device firewall01.example.local --debug --dry-run
Artur Pinto (arturj.pinto@gmail.com)
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature')git push origin feature/AmazingFeature)This project is licensed under the MIT License - see the LICENSE file for details.
This software is provided as-is for educational and operational purposes. Use at your own risk and ensure proper testing before production use.
Owner
Contributors
Categories
Products
Secure FirewallProgramming Languages
PythonLicense
Code Exchange Community
Get help, share code, and collaborate with other developers in the Code Exchange community.View Community