]> Andreas Glashauser | Gitweb - reputationchecker.git/commitdiff
Initial commit main
authorAndreas Glashauser <ag@andreasglashauser.com>
Sat, 29 Mar 2025 22:59:45 +0000 (23:59 +0100)
committerAndreas Glashauser <ag@andreasglashauser.com>
Sat, 29 Mar 2025 22:59:45 +0000 (23:59 +0100)
19 files changed:
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
config/dnsbl_config.py [new file with mode: 0644]
handlers/barracuda.py [new file with mode: 0644]
handlers/base.py [new file with mode: 0644]
handlers/blocklist_de.py [new file with mode: 0644]
handlers/cinsscore.py [new file with mode: 0644]
handlers/dronebl.py [new file with mode: 0644]
handlers/hostkarma.py [new file with mode: 0644]
handlers/mailspike.py [new file with mode: 0644]
handlers/spamcop.py [new file with mode: 0644]
handlers/spamhaus.py [new file with mode: 0644]
handlers/spamrats.py [new file with mode: 0644]
models/dnsbl.py [new file with mode: 0644]
reputationchecker.py [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
utils/ip.py [new file with mode: 0644]
utils/validators.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..5b478e6
--- /dev/null
@@ -0,0 +1,2 @@
+__pycache__/
+*.log
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..b45608a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,9 @@
+# Released under MIT License
+
+Copyright (c) 2025 Andreas Glashauser.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..254799a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+reputationchecker is a Python tool to check if an IP address or domain is listed in various DNS-based blacklists, whitelists, and abuse lists.
+
+### Setup
+
+```bash
+git clone https://github.com/yourusername/reputationcheck.git
+cd reputationcheck
+pip install -r requirements.txt
+
+```
+
+### Usage
+
+Check a ip:
+```bash
+python reputation_checker.py 1.2.3.4
+```
+
+Check a domain:
+```bash
+python reputation_checker.py example.com
+```
+
+Filter results by category:
+```bash
+python reputation_checker.py 1.2.3.4 --category botnet
+```
+
+The tool checks against services in the following categories:
+
+- Spam/Abuse Lists
+- Botnet Detection
+- Botnet Command & Control Servers
+- Phishing & Fraud Lists
+- Threat Intelligence Aggregators
+- Tor & Anonymization Networks
+- Scanner/Probe Detection
+- Brute Force Detection
diff --git a/config/dnsbl_config.py b/config/dnsbl_config.py
new file mode 100644 (file)
index 0000000..54dc2be
--- /dev/null
@@ -0,0 +1,115 @@
+from models.dnsbl import DNSBLService
+
+DNSBL_SERVICES = {
+    'spamhaus': DNSBLService(
+        name='spamhaus',
+        dnsbl='zen.spamhaus.org',
+        description='Spamhaus ZEN (includes SBL, XBL, and PBL)',
+        category='spam',
+        special=True
+    ),
+    'barracuda': DNSBLService(
+        name='barracuda',
+        dnsbl='b.barracudacentral.org',
+        description='Barracuda Reputation Block List',
+        category='spam',
+        special=True
+    ),
+    'spamcop': DNSBLService(
+        name='spamcop',
+        dnsbl='bl.spamcop.net',
+        description='SpamCop Blocking List',
+        category='spam',
+        special=True
+    ),
+    'dronebl': DNSBLService(
+        name='dronebl',
+        dnsbl='dnsbl.dronebl.org',
+        description='DroneBL (Botnet Detection)',
+        category='botnet',
+        special=True
+    ),
+    'tor': DNSBLService(
+        name='tor',
+        dnsbl='tor.dan.me.uk',
+        description='Tor Exit Node List',
+        category='anonymization'
+    ),    
+    'blocklist_de': DNSBLService(
+        name='blocklist_de',
+        dnsbl='bl.blocklist.de',
+        description='Blocklist.de (Scanner/Probe Detection)',
+        category='scanner',
+        special=True
+    ),
+    'cinsscore': DNSBLService(
+        name='cinsscore',
+        dnsbl='cinsscore.com',
+        description='CINSscore (Bad IPs)',
+        category='badips',
+        special=True
+    ),
+    'swinog (dnsrbl)': DNSBLService(
+        name='swinog (dnsrbl)',
+        dnsbl='dnsrbl.swinog.ch',
+        description='Realtime blacklist assembled by spamtraps',
+        category='spam',
+    ),
+    'swinog (spamrbl)': DNSBLService(
+        name='swinog (spamrbl)',
+        dnsbl='spamrbl.swinog.ch',
+        description='IP-adresses from catched spammails',
+        category='spam',
+    ),  
+    'swinog (uribl)': DNSBLService(
+        name='swinog (uribl)',
+        dnsbl='uribl.swinog.ch',
+        description='Realtime blacklist built from spamtrap sources',
+        category='spam',
+    ),  
+    'lashback': DNSBLService(
+        name='lashback',
+        dnsbl='blacklist.lashback.com',
+        description='world\'s largest unsubscribe intelligence database',
+        category='spam',
+    ),  
+    'spamrats': DNSBLService(
+        name='spamrats',
+        dnsbl='all.spamrats.com',
+        description='Spamrats ALL',
+        category='badips',
+        special=True
+    ),
+    'mailspike': DNSBLService(
+        name='mailspike',
+        dnsbl='bl.mailspike.net',
+        description='Mailspike Reputation Service',
+        category='reputation',
+        special=True
+    ),
+    'sem-backscatter': DNSBLService(
+        name='sem-backscatter',
+        dnsbl='backscatter.spameatingmonkey.net',
+        description='SpamEatingMonkey Backscatter',
+        category='spam',
+    ),
+    'sem-black': DNSBLService(
+        name='sem-black',
+        dnsbl='bl.spameatingmonkey.net',
+        description='SpamEatingMonkey Black',
+        category='spam',
+    ),
+    'psbl-surriel': DNSBLService(
+        name='psbl-surriel',
+        dnsbl='psbl.surriel.com',
+        description='Passive Spam Block List',
+        category='spam',
+    ),
+    'hostkarma': DNSBLService(
+        name='hostkarma',
+        dnsbl='hostkarma.junkemailfilter.com',
+        description='Hostkarma (Junk Email Filter)',
+        category='reputation',
+        special=True
+    ),
+} 
\ No newline at end of file
diff --git a/handlers/barracuda.py b/handlers/barracuda.py
new file mode 100644 (file)
index 0000000..67e1cfe
--- /dev/null
@@ -0,0 +1,30 @@
+from typing import Tuple
+from .base import DNSBLHandler
+
+class BarracudaHandler(DNSBLHandler):
+    """Handler for Barracuda DNSBL service."""
+    
+    RETURN_CODES = {
+        '127.0.0.2': 'General spam source'
+    }
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in Barracuda.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        lookup = self._get_lookup_name(target)
+        return_ip = self._get_a_record(lookup)
+        
+        if not return_ip:
+            return False, "Not listed"
+            
+        if return_ip in self.RETURN_CODES:
+            return True, f"{self.RETURN_CODES[return_ip]} (Return IP: {return_ip})"
+            
+        return True, f"Listed (Return IP: {return_ip})"
\ No newline at end of file
diff --git a/handlers/base.py b/handlers/base.py
new file mode 100644 (file)
index 0000000..5266bae
--- /dev/null
@@ -0,0 +1,119 @@
+from abc import ABC, abstractmethod
+from typing import Tuple, Optional, Dict, List
+import dns.resolver
+from models.dnsbl import DNSBLService, DNSBLResult
+from utils.ip import is_valid_ip
+
+class DNSBLHandler(ABC):
+    """Base class for DNSBL handlers."""
+    
+    RETURN_CODES: Dict[str, str] = {}
+    RATE_LIMIT_CODES: Dict[str, str] = {}
+    COLOR_LOGIC: Dict[str, List[str]] = {}
+    
+    def __init__(self, service: DNSBLService):
+        """
+        Initialize the handler with a DNSBL service configuration.
+        
+        Args:
+            service: DNSBLService configuration object
+        """
+        self.service = service
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in the DNSBL service.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        lookup = self._get_lookup_name(target)
+        return_ip = self._get_a_record(lookup)
+        
+        if not return_ip:
+            return False, "Not listed"
+            
+        if return_ip in self.RATE_LIMIT_CODES:
+            return False, self.RATE_LIMIT_CODES[return_ip]
+            
+        if return_ip in self.RETURN_CODES:
+            return True, f"{self.RETURN_CODES[return_ip]} (Return IP: {return_ip})"
+            
+        return True, f"Listed (Return IP: {return_ip})"
+    
+    def create_result(self, is_listed: bool, details: str) -> DNSBLResult:
+        """
+        Create a DNSBLResult object from the check result.
+        
+        Args:
+            is_listed: Whether the target is listed
+            details: Details about the listing
+            
+        Returns:
+            DNSBLResult: The result object
+        """
+        return_ip = None
+        if "(Return IP: " in details:
+            return_ip = details.split("(Return IP: ")[1].rstrip(")")
+        
+        status = "Listed" if is_listed else "Not Listed"
+        if return_ip and self.COLOR_LOGIC:
+            for color, codes in self.COLOR_LOGIC.items():
+                if return_ip in codes:
+                    status = color.capitalize()
+                    break
+        
+        return DNSBLResult(
+            list_name=self.service.name,
+            description=self.service.description,
+            category=self.service.category,
+            status=status,
+            details=details
+        )
+    
+    def _reverse_ip(self, ip: str) -> str:
+        """
+        Reverse an IP address for DNSBL lookup.
+        
+        Args:
+            ip: The IP address to reverse
+            
+        Returns:
+            str: The reversed IP address
+        """
+        return '.'.join(reversed(ip.split('.')))
+    
+    def _get_lookup_name(self, target: str) -> str:
+        """
+        Get the DNS lookup name for a target.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            str: The DNS lookup name
+        """
+        if is_valid_ip(target):
+            return f"{self._reverse_ip(target)}.{self.service.dnsbl}"
+        return f"{target}.{self.service.dnsbl}"
+    
+    def _get_a_record(self, lookup: str) -> Optional[str]:
+        """
+        Get the A record for a DNS lookup.
+        
+        Args:
+            lookup: The DNS lookup name
+            
+        Returns:
+            Optional[str]: The A record if found, None otherwise
+        """
+        try:
+            answers = dns.resolver.resolve(lookup, 'A')
+            return str(answers[0])
+        except dns.resolver.NXDOMAIN:
+            return None
+        except Exception as e:
+            raise Exception(f"DNS lookup error: {str(e)}") 
\ No newline at end of file
diff --git a/handlers/blocklist_de.py b/handlers/blocklist_de.py
new file mode 100644 (file)
index 0000000..30d341e
--- /dev/null
@@ -0,0 +1,50 @@
+from typing import Tuple
+from .base import DNSBLHandler
+
+class BlocklistDEHandler(DNSBLHandler):
+    """Handler for Blocklist.de DNSBL service."""
+    
+    RETURN_CODES = {
+        '127.0.0.2': 'amavis',
+        '127.0.0.3': 'apacheddos',
+        '127.0.0.4': 'asterisk',
+        '127.0.0.5': 'badbot',
+        '127.0.0.6': 'ftp',
+        '127.0.0.7': 'imap',
+        '127.0.0.8': 'ircbot',
+        '127.0.0.9': 'mail',
+        '127.0.0.10': 'pop3',
+        '127.0.0.11': 'regbot',
+        '127.0.0.12': 'rfi-attack',
+        '127.0.0.13': 'sasl',
+        '127.0.0.14': 'ssh',
+        '127.0.0.15': 'w00tw00t',
+        '127.0.0.16': 'portflood',
+        '127.0.0.17': 'sql-injection',
+        '127.0.0.18': 'webmin',
+        '127.0.0.19': 'trigger-spam',
+        '127.0.0.20': 'manuall',
+        '127.0.0.21': 'bruteforcelogin',
+        '127.0.0.22': 'mysql'
+    }
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in Blocklist.de.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        lookup = self._get_lookup_name(target)
+        return_ip = self._get_a_record(lookup)
+        
+        if not return_ip:
+            return False, "Not listed"
+            
+        if return_ip in self.RETURN_CODES:
+            return True, f"{self.RETURN_CODES[return_ip]} (Return IP: {return_ip})"
+            
+        return True, f"Listed (Return IP: {return_ip})"
\ No newline at end of file
diff --git a/handlers/cinsscore.py b/handlers/cinsscore.py
new file mode 100644 (file)
index 0000000..939f5c6
--- /dev/null
@@ -0,0 +1,116 @@
+from typing import Tuple
+import os
+import platform
+import requests
+import logging
+from datetime import datetime, timedelta
+from .base import DNSBLHandler
+
+class CINSScoreHandler(DNSBLHandler):
+    """Handler for CINSscore IP reputation list."""
+    
+    def __init__(self, service):
+        super().__init__(service)
+        self.logger = logging.getLogger('reputation_checker.cinsscore')
+        self.cache_dir = self._get_cache_dir()
+        self.cache_file = os.path.join(self.cache_dir, 'cinsscore_badguys.txt')
+        self.cache_duration = timedelta(hours=24) 
+        self.logger.debug(f"Initialized CINSscore handler with cache file: {self.cache_file}")
+        
+    def _get_cache_dir(self) -> str:
+        """Get the appropriate cache directory for the current OS."""
+        system = platform.system().lower()
+        
+        if system == 'linux':
+            xdg_cache = os.environ.get('XDG_CACHE_HOME')
+            if xdg_cache:
+                self.logger.debug(f"Using XDG cache directory: {xdg_cache}")
+                return xdg_cache
+            cache_dir = os.path.expanduser('~/.cache')
+            self.logger.debug(f"Using fallback cache directory: {cache_dir}")
+            return cache_dir
+        elif system == 'windows':
+            cache_dir = os.path.expandvars('%LOCALAPPDATA%')
+            self.logger.debug(f"Using Windows cache directory: {cache_dir}")
+            return cache_dir
+        else:
+            cache_dir = os.path.expanduser('~/.cache')
+            self.logger.debug(f"Using default cache directory: {cache_dir}")
+            return cache_dir
+    
+    def _download_list(self) -> bool:
+        """Download the CINSscore list and save it to cache."""
+        self.logger.info("Downloading CINSscore bad guys list")
+        try:
+            response = requests.get('https://cinsscore.com/list/ci-badguys.txt')
+            response.raise_for_status()
+            
+            os.makedirs(self.cache_dir, exist_ok=True)
+            self.logger.debug(f"Created cache directory: {self.cache_dir}")
+            
+            with open(self.cache_file, 'w') as f:
+                f.write(response.text)
+            
+            self.logger.info(f"Successfully downloaded and cached {len(response.text.splitlines())} IPs")
+            return True
+        except requests.exceptions.RequestException as e:
+            self.logger.error(f"Failed to download CINSscore list: {str(e)}")
+            return False
+        except Exception as e:
+            self.logger.error(f"Unexpected error while downloading list: {str(e)}", exc_info=True)
+            return False
+    
+    def _is_cache_valid(self) -> bool:
+        """Check if the cached list is still valid."""
+        if not os.path.exists(self.cache_file):
+            self.logger.debug("Cache file does not exist")
+            return False
+            
+        mtime = datetime.fromtimestamp(os.path.getmtime(self.cache_file))
+        age = datetime.now() - mtime
+        is_valid = age < self.cache_duration
+        
+        if is_valid:
+            self.logger.debug(f"Cache is valid (age: {age})")
+        else:
+            self.logger.debug(f"Cache is expired (age: {age})")
+            
+        return is_valid
+    
+    def _load_cached_list(self) -> set:
+        """Load the IP list from cache."""
+        self.logger.debug("Loading cached IP list")
+        try:
+            with open(self.cache_file, 'r') as f:
+                ips = set(line.strip() for line in f if line.strip())
+            self.logger.debug(f"Loaded {len(ips)} IPs from cache")
+            return ips
+        except Exception as e:
+            self.logger.error(f"Error reading cached list: {str(e)}", exc_info=True)
+            return set()
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in CINSscore.
+        
+        Args:
+            target: The target to check (IP address)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        self.logger.info(f"Checking target: {target}")
+        
+        if not self._is_cache_valid():
+            self.logger.info("Cache invalid or missing, downloading new list")
+            if not self._download_list():
+                return False, "Error: Could not download CINSscore list"
+        
+        bad_ips = self._load_cached_list()
+        
+        if target in bad_ips:
+            self.logger.info(f"Target {target} found in CINSscore list")
+            return True, "Listed in CINSscore bad guys list"
+            
+        self.logger.info(f"Target {target} not found in CINSscore list")
+        return False, "Not listed" 
\ No newline at end of file
diff --git a/handlers/dronebl.py b/handlers/dronebl.py
new file mode 100644 (file)
index 0000000..477e8b8
--- /dev/null
@@ -0,0 +1,51 @@
+from typing import Tuple
+from .base import DNSBLHandler
+
+class DroneBLHandler(DNSBLHandler):
+    """Handler for DroneBL DNSBL service."""
+    
+    RETURN_CODES = {
+        '2': 'Sample',
+        '3': 'IRC Drone',
+        '5': 'Bottler',
+        '6': 'Unknown spambot or drone',
+        '7': 'DDOS Drone',
+        '8': 'SOCKS Proxy',
+        '9': 'HTTP Proxy',
+        '10': 'ProxyChain',
+        '11': 'Web Page Proxy',
+        '12': 'Open DNS Resolver',
+        '13': 'Brute force attackers',
+        '14': 'Open Wingate Proxy',
+        '15': 'Compromised router / gateway',
+        '16': 'Autorooting worms',
+        '17': 'Automatically determined botnet IPs (experimental)',
+        '18': 'DNS/MX type hostname detected on IRC',
+        '255': 'Unknown'
+    }
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in DroneBL.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        try:
+            lookup = self._get_lookup_name(target)
+            return_ip = self._get_a_record(lookup)
+            
+            if not return_ip:
+                return False, "Not Listed"
+            
+            code = return_ip.split('.')[-1]
+            
+            threat_type = self.RETURN_CODES.get(code, "Unknown Threat Type")
+            
+            return True, f"{threat_type} (Code: {code}, Return IP: {return_ip})"
+            
+        except Exception as e:
+            return False, f"Error: {str(e)}" 
\ No newline at end of file
diff --git a/handlers/hostkarma.py b/handlers/hostkarma.py
new file mode 100644 (file)
index 0000000..4753562
--- /dev/null
@@ -0,0 +1,59 @@
+from typing import Tuple, Dict, List
+import logging
+from .base import DNSBLHandler
+
+class HostkarmaHandler(DNSBLHandler):
+    """Handler for Hostkarma DNSBL service."""
+    
+    RETURN_CODES: Dict[str, str] = {
+        '127.0.0.1': 'Whitelist - Trusted nonspam',
+        '127.0.0.2': 'Blacklist - Block spam',
+        '127.0.0.3': 'Yellowlist - Mix of spam and nonspam',
+        '127.0.0.4': 'Brownlist - All spam, but not yet enough to blacklist',
+        '127.0.0.5': 'NOBL - IP is not a spam only source'
+    }
+    
+    COLOR_LOGIC: Dict[str, List[str]] = {
+        'red': ['127.0.0.2', '127.0.0.4'],
+        'yellow': ['127.0.0.3'],
+        'green': ['127.0.0.1', '127.0.0.5']
+    }
+    
+    def __init__(self, service):
+        super().__init__(service)
+        self.logger = logging.getLogger('reputation_checker.hostkarma')
+        self.logger.debug("Initialized Hostkarma handler")
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in Hostkarma.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        self.logger.info(f"Checking target: {target}")
+        
+        try:
+            lookup = self._get_lookup_name(target)
+            self.logger.debug(f"DNS lookup: {lookup}")
+            
+            return_ip = self._get_a_record(lookup)
+            
+            if not return_ip:
+                self.logger.info(f"Target {target} not listed in Hostkarma")
+                return False, "Not listed"
+            
+            if return_ip in self.RETURN_CODES:
+                listing_type = self.RETURN_CODES[return_ip]
+                self.logger.info(f"Target {target} listed in Hostkarma as {listing_type}")
+                return True, f"{listing_type} (Return IP: {return_ip})"
+            
+            self.logger.warning(f"Target {target} returned unknown code: {return_ip}")
+            return True, f"Listed (Unknown return code: {return_ip})"
+            
+        except Exception as e:
+            self.logger.error(f"Error checking Hostkarma: {str(e)}", exc_info=True)
+            return False, f"Error: {str(e)}" 
\ No newline at end of file
diff --git a/handlers/mailspike.py b/handlers/mailspike.py
new file mode 100644 (file)
index 0000000..689de80
--- /dev/null
@@ -0,0 +1,40 @@
+from typing import Tuple
+from .base import DNSBLHandler
+
+class MailspikeHandler(DNSBLHandler):
+    """Handler for Mailspike DNSBL service."""
+    
+    RETURN_CODES = {
+        '127.0.0.10': 'L5 - Worst possible reputation',
+        '127.0.0.11': 'L4 - Very bad reputation',
+        '127.0.0.12': 'L3 - Bad reputation',
+        '127.0.0.13': 'L2 - Suspicious behavior reputation',
+        '127.0.0.14': 'L1 - Neutral - Probably spam',
+        '127.0.0.15': 'LHO - Neutral',
+        '127.0.0.16': 'H1 - Neutral - Probably legit',
+        '127.0.0.17': 'H2 - Possible legit sender',
+        '127.0.0.18': 'H3 - Good Reputation',
+        '127.0.0.19': 'H4 - Very Good Reputation',
+        '127.0.0.20': 'H5 - Excellent Reputation'
+    }
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in Mailspike.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        lookup = self._get_lookup_name(target)
+        return_ip = self._get_a_record(lookup)
+        
+        if not return_ip:
+            return False, "Not listed"
+            
+        if return_ip in self.RETURN_CODES:
+            return True, f"{self.RETURN_CODES[return_ip]} (Return IP: {return_ip})"
+            
+        return True, f"Listed (Return IP: {return_ip})"
\ No newline at end of file
diff --git a/handlers/spamcop.py b/handlers/spamcop.py
new file mode 100644 (file)
index 0000000..aea62e7
--- /dev/null
@@ -0,0 +1,30 @@
+from typing import Tuple, Dict
+from .base import DNSBLHandler
+
+class SpamCopHandler(DNSBLHandler):
+    """Handler for SpamCop DNSBL service."""
+    
+    RETURN_CODES: Dict[str, str] = {
+        '127.0.0.2': 'General spam source'
+    }
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in SpamCop.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        lookup = self._get_lookup_name(target)
+        return_ip = self._get_a_record(lookup)
+        
+        if not return_ip:
+            return False, "Not listed"
+            
+        if return_ip in self.RETURN_CODES:
+            return True, f"{self.RETURN_CODES[return_ip]} (Return IP: {return_ip})"
+            
+        return True, f"Listed (Return IP: {return_ip})" 
\ No newline at end of file
diff --git a/handlers/spamhaus.py b/handlers/spamhaus.py
new file mode 100644 (file)
index 0000000..21cfcac
--- /dev/null
@@ -0,0 +1,44 @@
+from typing import Tuple, Dict
+from .base import DNSBLHandler
+
+class SpamhausHandler(DNSBLHandler):
+    """Handler for Spamhaus DNSBL service."""
+    
+    RETURN_CODES: Dict[str, str] = {
+        '127.0.0.2': 'SBL (General spam source)',
+        '127.0.0.3': 'PBL (Policy Block List)',
+        '127.0.0.4': 'XBL (Compromised or infected machine)',
+        '127.0.0.5': 'PBL (Policy Block List)',
+        '127.0.0.6': 'SBL and XBL',
+        '127.0.0.7': 'SBL, XBL, and PBL',
+        '127.0.0.9': 'SBL and PBL',
+        '127.0.0.10': 'XBL (Other exploit activities)',
+    }
+    
+    RATE_LIMIT_CODES: Dict[str, str] = {
+        '127.255.255.254': 'Query blocked or rate-limited'
+    }
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in Spamhaus.
+        
+        Args:
+            target: The target to check (IP or domain)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        lookup = self._get_lookup_name(target)
+        return_ip = self._get_a_record(lookup)
+        
+        if not return_ip:
+            return False, "Not listed"
+            
+        if return_ip == "127.255.255.254":
+            return False, "Query blocked or rate-limited"
+            
+        if return_ip in self.RETURN_CODES:
+            return True, f"{self.RETURN_CODES[return_ip]} (Return IP: {return_ip})"
+            
+        return True, f"Listed (Return IP: {return_ip})" 
\ No newline at end of file
diff --git a/handlers/spamrats.py b/handlers/spamrats.py
new file mode 100644 (file)
index 0000000..c89388f
--- /dev/null
@@ -0,0 +1,52 @@
+from typing import Tuple
+import logging
+from .base import DNSBLHandler
+
+class SpamRATSHandler(DNSBLHandler):
+    """Handler for SpamRATS reputation lists."""
+    
+    RETURN_CODES = {
+        '127.0.0.36': 'RATS-Dyna',
+        '127.0.0.37': 'RATS-NoPtr',
+        '127.0.0.38': 'RATS-Spam',
+        '127.0.0.43': 'RATS-Auth'
+    }
+    
+    def __init__(self, service):
+        super().__init__(service)
+        self.logger = logging.getLogger('reputation_checker.spamrats')
+        self.logger.debug("Initialized SpamRATS handler")
+    
+    def check(self, target: str) -> Tuple[bool, str]:
+        """
+        Check if a target is listed in SpamRATS.
+        
+        Args:
+            target: The target to check (IP address)
+            
+        Returns:
+            Tuple[bool, str]: (is_listed, details)
+        """
+        self.logger.info(f"Checking target: {target}")
+        
+        try:
+            lookup = self._get_lookup_name(target)
+            self.logger.debug(f"DNS lookup: {lookup}")
+            
+            return_ip = self._get_a_record(lookup)
+            
+            if not return_ip:
+                self.logger.info(f"Target {target} not listed in SpamRATS")
+                return False, "Not listed"
+            
+            if return_ip in self.RETURN_CODES:
+                listing_type = self.RETURN_CODES[return_ip]
+                self.logger.info(f"Target {target} listed in SpamRATS as {listing_type}")
+                return True, f"Listed in {listing_type} (Return IP: {return_ip})"
+            
+            self.logger.warning(f"Target {target} returned unknown code: {return_ip}")
+            return True, f"Listed (Unknown return code: {return_ip})"
+            
+        except Exception as e:
+            self.logger.error(f"Error checking SpamRATS: {str(e)}", exc_info=True)
+            return False, f"Error: {str(e)}" 
\ No newline at end of file
diff --git a/models/dnsbl.py b/models/dnsbl.py
new file mode 100644 (file)
index 0000000..133ba4a
--- /dev/null
@@ -0,0 +1,22 @@
+from dataclasses import dataclass
+from typing import Optional
+
+@dataclass
+class DNSBLResult:
+    """Represents the result of a DNSBL check."""
+    list_name: str
+    description: str
+    category: str
+    status: str
+    details: str
+
+@dataclass
+class DNSBLService:
+    """Represents a DNSBL service configuration."""
+    name: str
+    dnsbl: str
+    description: str
+    category: str
+    special: bool = False
+    return_codes: Optional[dict] = None 
+    color_logic: Optional[dict] = None
\ No newline at end of file
diff --git a/reputationchecker.py b/reputationchecker.py
new file mode 100644 (file)
index 0000000..070e3dc
--- /dev/null
@@ -0,0 +1,258 @@
+import dns.resolver
+import click
+import logging
+from rich.console import Console
+from rich.table import Table
+from rich.panel import Panel
+from rich.text import Text
+from typing import List, Dict, Optional, Tuple
+from datetime import datetime
+
+from models.dnsbl import DNSBLResult
+from config.dnsbl_config import DNSBL_SERVICES
+from utils.ip import is_valid_ip, is_valid_domain, reverse_ip, validate_target
+from handlers.spamhaus import SpamhausHandler
+from handlers.barracuda import BarracudaHandler
+from handlers.spamcop import SpamCopHandler
+from handlers.dronebl import DroneBLHandler
+from handlers.blocklist_de import BlocklistDEHandler
+from handlers.cinsscore import CINSScoreHandler
+from handlers.spamrats import SpamRATSHandler
+from handlers.hostkarma import HostkarmaHandler
+from handlers.mailspike import MailspikeHandler
+
+console = Console()
+
+def setup_logging(verbosity: int):
+    """Configure logging based on verbosity level."""
+    log_levels = {
+        0: logging.WARNING,
+        1: logging.INFO,
+        2: logging.DEBUG,
+        3: logging.DEBUG
+    }
+    
+    logging.basicConfig(
+        level=log_levels.get(verbosity, logging.WARNING),
+        format='%(asctime)s - %(levelname)s - %(message)s',
+        datefmt='%Y-%m-%d %H:%M:%S'
+    )
+    
+    logger = logging.getLogger('reputationchecker')
+    
+    if verbosity >= 2:
+        fh = logging.FileHandler('reputationchecker.log')
+        fh.setLevel(logging.DEBUG)
+        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+        fh.setFormatter(formatter)
+        logger.addHandler(fh)
+    
+    return logger
+
+HANDLERS = {
+    'spamhaus': SpamhausHandler(DNSBL_SERVICES['spamhaus']),
+    'barracuda': BarracudaHandler(DNSBL_SERVICES['barracuda']),
+    'spamcop': SpamCopHandler(DNSBL_SERVICES['spamcop']),
+    'dronebl': DroneBLHandler(DNSBL_SERVICES['dronebl']),
+    'blocklist_de': BlocklistDEHandler(DNSBL_SERVICES['blocklist_de']),
+    'cinsscore': CINSScoreHandler(DNSBL_SERVICES['cinsscore']),
+    'spamrats': SpamRATSHandler(DNSBL_SERVICES['spamrats']),
+    'hostkarma': HostkarmaHandler(DNSBL_SERVICES['hostkarma']),
+    'mailspike': MailspikeHandler(DNSBL_SERVICES['mailspike'])
+}
+
+def check_dns_list(target: str, service: str, logger: logging.Logger) -> DNSBLResult:
+    """
+    Check if a target is listed in a DNSBL service.
+    
+    Args:
+        target: The target to check (IP or domain)
+        service: The service name to check against
+        logger: Logger instance for debugging
+        
+    Returns:
+        DNSBLResult: The result of the check
+    """
+    logger.debug(f"Checking {target} against {service}")
+    service_config = DNSBL_SERVICES[service]
+    logger.debug(f"Service config: {service_config}")
+    
+    if service_config.special and service in HANDLERS:
+        logger.debug(f"Using special handler for {service}")
+        handler = HANDLERS[service]
+        is_listed, details = handler.check(target)
+        logger.debug(f"Special handler result: listed={is_listed}, details={details}")
+    else:
+        logger.debug(f"Using generic DNSBL check for {service}")
+        if is_valid_ip(target):
+            lookup = f"{reverse_ip(target)}.{service_config.dnsbl}"
+        else:
+            lookup = f"{target}.{service_config.dnsbl}"
+        logger.debug(f"DNS lookup: {lookup}")
+            
+        try:
+            answers = dns.resolver.resolve(lookup, 'A')
+            return_ip = str(answers[0])
+            is_listed = True
+            details = f"Listed (Return IP: {return_ip})"
+            logger.debug(f"DNS lookup successful: {return_ip}")
+        except dns.resolver.NXDOMAIN:
+            is_listed = False
+            details = "Not listed"
+            logger.debug("Target not listed")
+        except Exception as e:
+            is_listed = False
+            details = f"Error: {str(e)}"
+            logger.error(f"DNS lookup error: {str(e)}", exc_info=True)
+    
+    result = DNSBLResult(
+        list_name=service,
+        description=service_config.description,
+        category=service_config.category,
+        status="Listed" if is_listed else "Not Listed",
+        details=details
+    )
+    logger.debug(f"Created result: {result}")
+    return result
+
+def group_results_by_category(results: List[DNSBLResult], logger: logging.Logger) -> Dict[str, List[DNSBLResult]]:
+    """
+    Group results by their category.
+    
+    Args:
+        results: List of DNSBLResult objects
+        logger: Logger instance for debugging
+        
+    Returns:
+        Dict[str, List[DNSBLResult]]: Results grouped by category
+    """
+    logger.debug(f"Grouping {len(results)} results by category")
+    grouped = {}
+    for result in results:
+        if result.category not in grouped:
+            grouped[result.category] = []
+        grouped[result.category].append(result)
+    logger.debug(f"Grouped results: {grouped}")
+    return grouped
+
+def display_results(target: str, results: List[DNSBLResult], logger: logging.Logger):
+    """
+    Display the results in a formatted table.
+    
+    Args:
+        target: The target that was checked
+        results: List of DNSBLResult objects
+        logger: Logger instance for debugging
+    """
+    logger.debug(f"Displaying results for {target}")
+    table = Table(title=f"DNSBL Check Results for {target}")
+    table.add_column("Service", style="cyan")
+    table.add_column("Description", style="green")
+    table.add_column("Status", style="yellow")
+    table.add_column("Details", style="white")
+    
+    for result in results:
+        service_config = DNSBL_SERVICES.get(result.list_name)
+        handler = HANDLERS.get(result.list_name)
+        
+        status_style = "white"  
+        if handler and hasattr(handler, 'COLOR_LOGIC'):
+            return_ip = None
+            if "Return IP:" in result.details:
+                return_ip = result.details.split("Return IP:")[1].strip().split(")")[0].strip()
+            
+            if return_ip:
+                for color, ips in handler.COLOR_LOGIC.items():
+                    if return_ip in ips:
+                        status_style = color
+                        break
+        
+        if status_style == "white":
+            status_style = "red" if result.status == "Listed" else "green"
+        
+        details = result.details
+        if handler and hasattr(handler, 'RETURN_CODES'):
+            if "Return IP:" in result.details:
+                return_ip = result.details.split("Return IP:")[1].strip().split(")")[0].strip()
+                if return_ip in handler.RETURN_CODES:
+                    details = handler.RETURN_CODES[return_ip]
+        
+        table.add_row(
+            result.list_name,
+            result.description,
+            Text(result.status, style=status_style),
+            details
+        )
+        logger.debug(f"Added row: {result.list_name} - {result.status} (style: {status_style})")
+    
+    console.print(table)
+    
+    grouped_results = group_results_by_category(results, logger)
+    category_table = Table(title="Category Summary")
+    category_table.add_column("Category", style="cyan")
+    category_table.add_column("Total", style="white")
+    category_table.add_column("Listed", style="red")
+    category_table.add_column("Not Listed", style="green")
+    
+    for category, category_results in grouped_results.items():
+        listed_count = sum(1 for r in category_results if r.status == "Listed")
+        total_count = len(category_results)
+        not_listed_count = total_count - listed_count
+        
+        category_table.add_row(
+            category,
+            str(total_count),
+            str(listed_count),
+            str(not_listed_count)
+        )
+    
+    console.print(category_table)
+    
+    listed_count = sum(1 for r in results if r.status == "Listed")
+    total_count = len(results)
+    summary = f"Found {listed_count} out of {total_count} services listing the target"
+    logger.info(summary)
+    
+    if listed_count > 0:
+        console.print(Panel(summary, title="Overall Summary", style="red"))
+    else:
+        console.print(Panel(summary, title="Overall Summary", style="green"))
+
+@click.command()
+@click.argument('target')
+@click.option('--category', '-c', help='Filter results by category')
+@click.option('--verbose', '-v', count=True, help='Increase verbosity (can be used multiple times)')
+def main(target: str, category: str = None, verbose: int = 0):
+    """
+    Check if an IP address or domain is listed in various DNSBL services.
+    
+    TARGET can be either an IP address or domain name.
+    """
+    logger = setup_logging(verbose)
+    logger.info(f"Starting check for target: {target}")
+    
+    is_valid, error_msg = validate_target(target)
+    if not is_valid:
+        logger.error(f"Invalid target: {error_msg}")
+        console.print(f"[red]Error: {error_msg}[/red]")
+        return
+    
+    results = []
+    for service_name, service_config in DNSBL_SERVICES.items():
+        if category and service_config.category != category:
+            logger.debug(f"Skipping {service_name} - category mismatch")
+            continue
+            
+        try:
+            logger.info(f"Checking {service_name}")
+            result = check_dns_list(target, service_name, logger)
+            results.append(result)
+        except Exception as e:
+            logger.error(f"Error checking {service_name}: {str(e)}", exc_info=True)
+            console.print(f"[yellow]Warning: Error checking {service_name}: {str(e)}[/yellow]")
+    
+    display_results(target, results, logger)
+    logger.info("Check completed")
+
+if __name__ == '__main__':
+    main() 
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..34b12ed
--- /dev/null
@@ -0,0 +1,4 @@
+dnspython>=2.3.0
+click>=8.1.0
+rich>=13.0.0
+requests>=2.31.0 
\ No newline at end of file
diff --git a/utils/ip.py b/utils/ip.py
new file mode 100644 (file)
index 0000000..dbed4e0
--- /dev/null
@@ -0,0 +1,32 @@
+import ipaddress
+from typing import Tuple
+import re
+
+def is_valid_ip(ip: str) -> bool:
+    """Validate if a string is a valid IPv4 address."""
+    try:
+        ipaddress.ip_address(ip)
+        return True
+    except ValueError:
+        return False
+
+def reverse_ip(ip: str) -> str:
+    """Reverse an IP address for DNSBL lookups."""
+    return '.'.join(reversed(ip.split('.')))
+
+def validate_target(target: str) -> Tuple[bool, str]:
+    """Validate if a target is either a valid IP address or domain name."""
+    if is_valid_ip(target):
+        return True, ""
+    elif is_valid_domain(target):
+        return True, ""
+    else:
+        return False, "Target must be a valid IP address or domain name"
+
+def is_valid_domain(domain: str) -> bool:
+    """Validate if a string is a valid domain name."""
+    if not domain:
+        return False
+        
+    pattern = r'^([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}$'
+    return bool(re.match(pattern, domain)) 
\ No newline at end of file
diff --git a/utils/validators.py b/utils/validators.py
new file mode 100644 (file)
index 0000000..2b8dfdb
--- /dev/null
@@ -0,0 +1,51 @@
+import re
+from typing import Tuple
+
+def is_valid_ip(ip: str) -> bool:
+    """
+    Validate if a string is a valid IPv4 address.
+    
+    Args:
+        ip: The IP address to validate
+        
+    Returns:
+        bool: True if the IP is valid, False otherwise
+    """
+    try:
+        parts = ip.split('.')
+        return len(parts) == 4 and all(0 <= int(part) <= 255 for part in parts)
+    except (AttributeError, TypeError, ValueError):
+        return False
+
+def is_valid_domain(domain: str) -> bool:
+    """
+    Validate if a string is a valid domain name.
+    
+    Args:
+        domain: The domain name to validate
+        
+    Returns:
+        bool: True if the domain is valid, False otherwise
+    """
+    if not domain:
+        return False
+        
+    pattern = r'^([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}$'
+    return bool(re.match(pattern, domain))
+
+def validate_target(target: str) -> Tuple[bool, str]:
+    """
+    Validate if a target is either a valid IP address or domain name.
+    
+    Args:
+        target: The target to validate (IP or domain)
+        
+    Returns:
+        Tuple[bool, str]: (is_valid, error_message)
+    """
+    if is_valid_ip(target):
+        return True, ""
+    elif is_valid_domain(target):
+        return True, ""
+    else:
+        return False, "Target must be a valid IP address or domain name" 
\ No newline at end of file