#!/usr/bin/env python3
# PCredz 2.1.0
# https://github.com/lgandx/PCredz
#
# Created by Laurent Gaffie
# Contact: lgaffie@secorizon.com
# X: @secorizon
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

try:
    import pcapy
except ImportError:
    print("pcapy-ng is not installed.")
    print("Install: pip3 install pcapy-ng")
    exit(1)

import datetime
import argparse
import os
import re
import time
import logging
import base64
import struct
import binascii

VERSION = 'PCredz 2.1.0'

# Global state
ntlm_challenge = {}
ftp_users = {}
seen_credentials = set()
logged_messages = set()
detected_offset = None
disabled_protocols = set()  # Protocols to skip
excluded_hosts = set()  # IPs to exclude from capture

# Pre-compiled regex patterns
REGEX_HTTP_BASIC = re.compile(rb'Authorization: Basic ([A-Za-z0-9+/=]+)', re.IGNORECASE)
REGEX_NTLM_CHALLENGE = re.compile(rb'(?:WWW|Proxy)-Authenticate: NTLM ([A-Za-z0-9+/=]+)', re.IGNORECASE)
REGEX_NTLM_AUTH = re.compile(rb'(?:Authorization|Proxy-Authorization): NTLM ([A-Za-z0-9+/=]+)', re.IGNORECASE)
REGEX_PASSWORD_FIELDS = re.compile(
    r'(?:^|&)(password|pass|_password|passwd|session_password|sessionpassword|'
    r'login_password|loginpassword|form_pw|pw|userpassword|pwd|upassword|'
    r'passwort|passwrd|wppassword|j_password|admin_password|admin_pass|'
    r'secret|api_key|token|key|auth)\s*=\s*([^&"\s]+)',
    re.IGNORECASE
)

def write_data(outfile, data, key):
    """Write credentials to file with deduplication"""
    outfile_path = os.path.join(output_path, "logs", outfile)
    os.makedirs(os.path.dirname(outfile_path), exist_ok=True)
    
    cache_key = (outfile, key)
    if cache_key in seen_credentials:
        return
    
    seen_credentials.add(cache_key)
    with open(outfile_path, 'a', encoding='utf-8') as f:
        f.write(data + '\n')

def get_packet_info(src, sport, dst, dport):
    """Generate packet info string with optional timestamp"""
    ts = f'[{datetime.datetime.fromtimestamp(time.time())}] ' if args.timestamp else ''
    return f'{ts}{src}:{sport} > {dst}:{dport}'

def print_and_log(src, sport, dst, dport, message, credential_key=None):
    """Print and log message with deduplication (unless verbose)
    
    Args:
        credential_key: Unique identifier for this credential (for deduplication)
                       If None, always prints
    """
    head = get_packet_info(src, sport, dst, dport)
    full_msg = f'{head}\n{message}\n'
    
    # Always log to file
    logger.warning(full_msg.rstrip('\n'))
    
    # Print to console: always if verbose, once per credential if not verbose
    if args.verbose:
        print(full_msg)
    elif credential_key is None:
        print(full_msg)
    elif credential_key not in logged_messages:
        print(full_msg)
        logged_messages.add(credential_key)

def get_flow_key(src, sport, dst, dport):
    """Generate consistent flow identifier"""
    return (src, sport, dst, dport) if sport > dport else (dst, dport, src, sport)

def is_protocol_disabled(protocol):
    """Check if a protocol is disabled"""
    return protocol.upper() in disabled_protocols

def extract_ntlm(payload, src, sport, dst, dport):
    """Extract NTLM authentication (Type 2 Challenge, Type 3 Response)"""
    if is_protocol_disabled('NTLM'):
        return
        
    flow = get_flow_key(src, sport, dst, dport)
    reverse_flow = get_flow_key(dst, dport, src, sport)
    pos = 0
    
    while pos < len(payload):
        idx = payload.find(b'NTLMSSP\x00', pos)
        if idx == -1:
            break
            
        blob = payload[idx:]
        if len(blob) < 20:
            break
            
        try:
            msg_type = struct.unpack('<I', blob[8:12])[0]
            
            if msg_type == 2 and len(blob) >= 32:
                # NTLM Type 2 (Challenge)
                challenge = binascii.hexlify(blob[24:32]).upper().decode()
                ntlm_challenge[flow] = challenge
                ntlm_challenge[reverse_flow] = challenge
                
            elif msg_type == 3 and len(blob) >= 64:
                # NTLM Type 3 (Response)
                lm_len = struct.unpack('<H', blob[12:14])[0]
                lm_off = struct.unpack('<H', blob[16:18])[0]
                nt_len = struct.unpack('<H', blob[20:22])[0]
                nt_off = struct.unpack('<H', blob[24:26])[0]
                dom_len = struct.unpack('<H', blob[28:30])[0]
                dom_off = struct.unpack('<H', blob[32:34])[0]
                user_len = struct.unpack('<H', blob[36:38])[0]
                user_off = struct.unpack('<H', blob[40:42])[0]
                
                domain = blob[dom_off:dom_off + dom_len].decode('utf-16le', errors='ignore').strip('\x00')
                user = blob[user_off:user_off + user_len].decode('utf-16le', errors='ignore').strip('\x00')
                lm_resp = binascii.hexlify(blob[lm_off:lm_off + lm_len]).upper().decode()
                nt_resp = binascii.hexlify(blob[nt_off:nt_off + nt_len]).upper().decode()
                
                challenge = ntlm_challenge.get(flow) or ntlm_challenge.get(reverse_flow) or '0000000000000000'
                
                if nt_len > 24:
                    # NTLMv2
                    hash_line = f'{user}::{domain}:{challenge}:{nt_resp[:32]}:{nt_resp[32:]}'
                    msg = f'NTLMv2 complete hash is: {hash_line}'
                    write_data('NTLMv2.txt', hash_line, user)
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'NTLMv2:{user}@{domain}')
                elif nt_len == 24:
                    # NTLMv1
                    hash_line = f'{user}::{domain}:{lm_resp}:{nt_resp}:{challenge}'
                    msg = f'NTLMv1 complete hash is: {hash_line}'
                    write_data('NTLMv1.txt', hash_line, user)
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'NTLMv1:{user}@{domain}')
                    
        except (struct.error, UnicodeDecodeError, IndexError):
            pass
            
        pos = idx + 8

def extract_ntlm_from_http(payload, src, sport, dst, dport):
    """Extract NTLM from HTTP headers"""
    if is_protocol_disabled('NTLM'):
        return
        
    # Challenge
    challenge_match = REGEX_NTLM_CHALLENGE.search(payload)
    if challenge_match:
        try:
            blob = base64.b64decode(challenge_match.group(1))
            if len(blob) >= 32 and blob[:8] == b'NTLMSSP\x00' and struct.unpack('<I', blob[8:12])[0] == 2:
                flow = get_flow_key(src, sport, dst, dport)
                reverse_flow = get_flow_key(dst, dport, src, sport)
                challenge = binascii.hexlify(blob[24:32]).upper().decode()
                ntlm_challenge[flow] = challenge
                ntlm_challenge[reverse_flow] = challenge
        except (base64.binascii.Error, struct.error, IndexError):
            pass
    
    # Response
    auth_match = REGEX_NTLM_AUTH.search(payload)
    if auth_match:
        try:
            blob = base64.b64decode(auth_match.group(1))
            if b'NTLMSSP\x00' in blob:
                extract_ntlm(blob, src, sport, dst, dport)
        except (base64.binascii.Error, ValueError):
            pass

def extract_http_basic(payload, src, sport, dst, dport):
    """Extract HTTP Basic authentication"""
    if is_protocol_disabled('HTTP'):
        return
        
    basic_match = REGEX_HTTP_BASIC.search(payload)
    if basic_match:
        try:
            creds_bytes = base64.b64decode(basic_match.group(1))
            creds = creds_bytes.decode('utf-8', errors='ignore')
            if ':' in creds:
                user, pwd = creds.split(':', 1)
                if user and pwd:
                    msg = f'Found HTTP Basic authentication: {creds}'
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'HTTP-Basic:{creds}')
                    write_data('HTTP-Basic.txt', creds, creds)
        except (base64.binascii.Error, UnicodeDecodeError):
            pass

def extract_http_password_fields(payload, src, sport, dst, dport):
    """Extract password-like fields from HTTP requests"""
    if is_protocol_disabled('HTTP'):
        return
        
    if dport in {443, 8443} or sport in {443, 8443}:
        return
        
    try:
        payload_str = payload.decode('utf-8', errors='ignore')
    except (UnicodeDecodeError, AttributeError):
        return
    
    for match in REGEX_PASSWORD_FIELDS.finditer(payload_str):
        value = match.group(2)
        if len(value) > 3 and value.isprintable() and not value.isspace():
            # Extract the full line for context
            lines = payload_str.split('\n')
            matched_line = None
            for line in lines:
                if match.group(0) in line:
                    matched_line = line.strip()
                    break
            
            if matched_line and len(matched_line) < 500:
                msg = f'Potential password submission:\nRequest: {matched_line}'
                display_line = matched_line
            else:
                # Fallback: show context
                match_start = match.start()
                match_end = match.end()
                context_start = max(0, match_start - 100)
                context_end = min(len(payload_str), match_end + 100)
                context = payload_str[context_start:context_end]
                msg = f'Potential password submission:\nRequest: ...{context}...'
                display_line = context
            
            # Use field name and value as credential key for deduplication
            field_name = match.group(1)
            print_and_log(src, sport, dst, dport, msg, credential_key=f'HTTP-Field:{field_name}={value}')
            write_data('HTTP-PasswordFields.txt', display_line, value)

def extract_smtp_auth(payload, src, sport, dst, dport):
    """Extract SMTP authentication"""
    if is_protocol_disabled('SMTP'):
        return
        
    if dport not in {25, 587, 465} and sport not in {25, 587, 465}:
        return
        
    lines = payload.split(b'\r\n')
    flow_key = get_flow_key(src, sport, dst, dport)
    
    for line in lines:
        line = line.strip()
        if line.upper().startswith(b'AUTH PLAIN '):
            try:
                creds_bytes = base64.b64decode(line[11:])
                creds = creds_bytes.decode('utf-8', errors='ignore')
                parts = creds.split('\x00')
                if len(parts) >= 3 and parts[1] and parts[2]:
                    user, pwd = parts[1], parts[2]
                    msg = f'SMTP AUTH PLAIN: {user}:{pwd}'
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'SMTP:{user}:{pwd}')
                    write_data('SMTP-Plaintext.txt', f'{user}:{pwd}', f'{user}:{pwd}')
            except (base64.binascii.Error, UnicodeDecodeError, IndexError):
                pass
                
        elif line.upper().startswith(b'AUTH LOGIN'):
            pass
            
        elif flow_key in ftp_users and line:
            try:
                pwd = base64.b64decode(line).decode('utf-8', errors='ignore')
                if pwd:
                    smtp_user = ftp_users[flow_key]
                    msg = f'SMTP AUTH LOGIN: {smtp_user}:{pwd}'
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'SMTP:{smtp_user}:{pwd}')
                    write_data('SMTP-Plaintext.txt', f'{smtp_user}:{pwd}', f'{smtp_user}:{pwd}')
                del ftp_users[flow_key]
            except (base64.binascii.Error, UnicodeDecodeError):
                pass
                
        elif line:
            try:
                user = base64.b64decode(line).decode('utf-8', errors='ignore')
                if user and '@' in user:
                    ftp_users[flow_key] = user
            except (base64.binascii.Error, UnicodeDecodeError):
                pass

def extract_ldap_simple_bind(payload, src, sport, dst, dport):
    """Extract LDAP Simple Bind credentials"""
    if is_protocol_disabled('LDAP'):
        return
        
    if dport not in {389, 636} and sport not in {389, 636}:
        return
        
    if len(payload) < 10 or payload[0] != 0x30:
        return
        
    try:
        pos = 1
        length = payload[pos]
        pos += 1
        
        if length & 0x80:
            len_bytes = length & 0x7F
            if pos + len_bytes > len(payload):
                return
            length = int.from_bytes(payload[pos:pos + len_bytes], 'big')
            pos += len_bytes
        
        if pos + 3 > len(payload) or payload[pos] != 0x02:
            return
        pos += 2 + payload[pos + 1]
        
        if pos + 1 > len(payload) or payload[pos] != 0x60:
            return
        pos += 1
        
        bind_length = payload[pos]
        pos += 1
        if bind_length & 0x80:
            len_bytes = bind_length & 0x7F
            if pos + len_bytes > len(payload):
                return
            pos += len_bytes
        
        if pos + 3 > len(payload) or payload[pos] != 0x02:
            return
        pos += 2 + payload[pos + 1]
        
        if pos + 2 > len(payload) or payload[pos] != 0x04:
            return
        dn_len = payload[pos + 1]
        pos += 2
        
        if pos + dn_len > len(payload):
            return
        dn = payload[pos:pos + dn_len].decode('utf-8', errors='ignore')
        pos += dn_len
        
        if pos + 2 > len(payload):
            return
        
        if payload[pos:pos + 2] == b'\x80\x00':
            msg = f'LDAP Simple Bind: {dn} : (empty password)'
            print_and_log(src, sport, dst, dport, msg, credential_key=f'LDAP:{dn}:(empty)')
            write_data('LDAP-Simple.txt', f'{dn}:(empty)', f'{dn}:(empty)')
        elif payload[pos] == 0x80:
            pwd_len = payload[pos + 1]
            pos += 2
            if pos + pwd_len > len(payload):
                return
            pwd = payload[pos:pos + pwd_len].decode('utf-8', errors='ignore')
            msg = f'LDAP Simple Bind: {dn} : {pwd}'
            print_and_log(src, sport, dst, dport, msg, credential_key=f'LDAP:{dn}:{pwd}')
            write_data('LDAP-Simple.txt', f'{dn}:{pwd}', f'{dn}:{pwd}')
    except (IndexError, UnicodeDecodeError, struct.error):
        pass

def extract_kerberos(payload, src, sport, dst, dport):
    """Extract Kerberos AS-REQ Pre-Auth (etype 23)"""
    if is_protocol_disabled('KERBEROS'):
        return
        
    if len(payload) < 50:
        return
        
    try:
        MsgType = payload[17:18]
        EncType = payload[39:40]
        
        if MsgType == b"\x0a" and EncType == b"\x17":
            if payload[40:44] in {b"\xa2\x36\x04\x34", b"\xa2\x35\x04\x33"}:
                HashLen = struct.unpack('<b', payload[41:42])[0]
                if HashLen in {53, 54}:
                    Hash = payload[44:44 + HashLen]
                    SwitchHash = Hash[16:] + Hash[0:16]
                    
                    name_offset = 144 if HashLen == 54 else 143
                    NameLen = struct.unpack('<b', payload[name_offset:name_offset + 1])[0]
                    Name = payload[name_offset + 1:name_offset + 1 + NameLen]
                    
                    domain_offset = name_offset + 1 + NameLen + 3
                    DomainLen = struct.unpack('<b', payload[domain_offset:domain_offset + 1])[0]
                    Domain = payload[domain_offset + 1:domain_offset + 1 + DomainLen]
                    
                    BuildHash = f'$krb5pa$23${Name.decode("latin-1")}${Domain.decode("latin-1").upper()}$dummy${binascii.hexlify(SwitchHash).decode("latin-1").upper()}'
                    msg = f'MSKerb hash found: {BuildHash}'
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'Kerberos:{Name.decode("latin-1")}')
                    write_data('MSKerb.txt', BuildHash, Name.decode('latin-1'))
            else:
                HashLen = struct.unpack('<b', payload[48:49])[0]
                Hash = payload[49:49 + HashLen]
                SwitchHash = Hash[16:] + Hash[0:16]
                
                NameLen = struct.unpack('<b', payload[HashLen + 97:HashLen + 98])[0]
                Name = payload[HashLen + 98:HashLen + 98 + NameLen]
                
                DomainLen = struct.unpack('<b', payload[HashLen + 98 + NameLen + 3:HashLen + 98 + NameLen + 4])[0]
                Domain = payload[HashLen + 98 + NameLen + 4:HashLen + 98 + NameLen + 4 + DomainLen]
                
                BuildHash = f'$krb5pa$23${Name.decode("latin-1")}${Domain.decode("latin-1").upper()}$dummy${binascii.hexlify(SwitchHash).decode("latin-1").upper()}'
                msg = f'MSKerb hash found: {BuildHash}'
                print_and_log(src, sport, dst, dport, msg, credential_key=f'Kerberos:{Name.decode("latin-1")}')
                write_data('MSKerb.txt', BuildHash, Name.decode('latin-1'))
    except (IndexError, struct.error, UnicodeDecodeError):
        pass

def extract_cleartext(payload, src, sport, dst, dport):
    """Extract cleartext credentials (IRC, FTP, SNMP, MSSQL)"""
    flow_key = get_flow_key(src, sport, dst, dport)
    lines = payload.split(b'\r\n')
    
    # Detect IRC vs FTP
    is_irc = (dport in range(6660, 6670) or sport in range(6660, 6670) or 
              dport in {6697, 7000} or sport in {6697, 7000})
    
    if not is_irc:
        for line in lines:
            if line.upper().startswith(b'NICK ') or line.upper().startswith(b'JOIN '):
                is_irc = True
                break
    
    for line in lines:
        line = line.strip()
        
        if is_irc and not is_protocol_disabled('IRC'):
            # IRC protocol
            if line.upper().startswith(b'NICK '):
                nick = line[5:].decode('utf-8', errors='ignore').strip()
                msg = f'IRC Nick: {nick}'
                print_and_log(src, sport, dst, dport, msg, credential_key=f'IRC:nick:{nick}')
            elif line.upper().startswith(b'USER '):
                parts = line[5:].decode('utf-8', errors='ignore').strip().split()
                if parts:
                    user = parts[0]
                    ftp_users[flow_key] = user
                    msg = f'IRC User: {user}'
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'IRC:user:{user}')
            elif line.upper().startswith(b'PASS '):
                pwd = line[5:].decode('utf-8', errors='ignore').strip()
                msg = f'IRC Pass: {pwd}'
                if flow_key in ftp_users:
                    creds = f'{ftp_users[flow_key]}:{pwd}'
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'IRC:{creds}')
                    write_data('IRC-Plaintext.txt', creds, creds)
                    del ftp_users[flow_key]
                else:
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'IRC:pass:{pwd}')
        elif not is_protocol_disabled('FTP'):
            # FTP protocol
            if line.upper().startswith(b'USER '):
                user = line[5:].decode('utf-8', errors='ignore').strip()
                ftp_users[flow_key] = user
                msg = f'FTP User: {user}'
                print_and_log(src, sport, dst, dport, msg, credential_key=f'FTP:user:{user}')
                
            elif line.upper().startswith(b'PASS '):
                pwd = line[5:].decode('utf-8', errors='ignore').strip()
                msg = f'FTP Pass: {pwd}'
                if flow_key in ftp_users:
                    creds = f'{ftp_users[flow_key]}:{pwd}'
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'FTP:{creds}')
                    write_data('FTP-Plaintext.txt', creds, creds)
                    del ftp_users[flow_key]
                else:
                    print_and_log(src, sport, dst, dport, msg, credential_key=f'FTP:pass:{pwd}')
    
    # SNMP
    if not is_protocol_disabled('SNMP') and (dport in {161, 162} or sport in {161, 162}):
        if len(payload) > 20 and payload[0:1] == b'\x30':
            try:
                snmpv1 = payload[4:6] == b'\x02\x01' and payload[6] in {0, 1}
                snmpv2c = payload[2:4] == b'\x02\x01' and payload[4] in {0, 1}
                if (snmpv1) or (snmpv2c):
                    comm_idx = payload.find(b'\x04', 7) if snmpv1 else payload.find(b'\x04', 5)
                    if comm_idx != -1:
                        comm_len = payload[comm_idx + 1]
                        if 0 < comm_len < 50:
                            comm = payload[comm_idx + 2:comm_idx + 2 + comm_len].decode('utf-8', errors='ignore')
                            if comm.isprintable() and not comm.isspace():
                                version = "SNMPv1" if snmpv1 else "SNMPv2c"
                                msg = f'Found {version} Community string: {comm}'
                                write_data(f'{version}.txt', comm, comm)
                                print_and_log(src, sport, dst, dport, msg, credential_key=f'SNMP:{comm}')
            except (IndexError, UnicodeDecodeError):
                pass
    
    # MSSQL
    if not is_protocol_disabled('MSSQL') and (dport == 1433 or sport == 1433):
        mssql_msg = parse_mssql_plaintext(payload)
        if mssql_msg:
            print_and_log(src, sport, dst, dport, mssql_msg)
            try:
                username = mssql_msg.split('Password: ')[0].split('Username: ')[1]
                write_data('MSSQL-Plaintext.txt', mssql_msg, username)
            except IndexError:
                pass

def parse_mssql_plaintext(payload):
    """Parse MSSQL TDS login packet"""
    try:
        if len(payload) < 58 or payload[0:2] != b'\x10\x01':
            return None
            
        username_offset = struct.unpack('<H', payload[48:50])[0]
        pwd_offset = struct.unpack('<H', payload[52:54])[0]
        app_offset = struct.unpack('<H', payload[56:58])[0]
        
        pwd_len = app_offset - pwd_offset
        username_len = pwd_offset - username_offset
        
        if pwd_len <= 0 or username_len <= 0 or pwd_len > 200 or username_len > 200:
            return None
        
        pwd_str = payload[8 + pwd_offset:8 + pwd_offset + pwd_len]
        pwd = bytes(b ^ 0xa5 for b in pwd_str)
        pwd = pwd[::-1].decode('utf-16le', errors='ignore').strip('\x00')
        
        username = payload[8 + username_offset:8 + username_offset + username_len].decode('utf-16le', errors='ignore').strip('\x00')
        
        if not username or not pwd:
            return None
            
        return f"MSSQL Username: {username} Password: {pwd}"
    except (IndexError, struct.error, UnicodeDecodeError):
        return None

def process_packet(packet):
    """Process a single packet"""
    global detected_offset
    
    if len(packet) < 40:
        return
    
    # Auto-detect link layer offset (cached after first packet)
    if detected_offset is not None:
        offset = detected_offset
        if len(packet) >= offset + 20:
            search_payload = packet[offset:]
        else:
            return
    else:
        # First packet: detect offset
        for offset in [16, 14, 0]:
            if len(packet) < offset + 20:
                continue
            search_payload = packet[offset:]
            if len(search_payload) >= 20 and (search_payload[0] & 0xF0) == 0x40:
                detected_offset = offset
                break
        else:
            if len(packet) < 54:
                return
            search_payload = packet[14:]
            detected_offset = 14
        
    if len(search_payload) < 20:
        return
        
    try:
        ip_hlen = (search_payload[0] & 0x0f) * 4
        if len(search_payload) < ip_hlen + 10:
            return
            
        proto = search_payload[9]
        src = '.'.join(str(b) for b in search_payload[12:16])
        dst = '.'.join(str(b) for b in search_payload[16:20])
        
        # Skip packets from/to excluded hosts
        if src in excluded_hosts or dst in excluded_hosts:
            return
        
        if proto == 6:  # TCP
            tcp_offset = ip_hlen
            if len(search_payload) < tcp_offset + 20:
                return
            tcp_hlen = (search_payload[tcp_offset + 12] >> 4) * 4
            payload = search_payload[tcp_offset + tcp_hlen:]
            sport, dport = struct.unpack('>HH', search_payload[tcp_offset:tcp_offset + 4])
            
        elif proto == 17:  # UDP
            payload = search_payload[ip_hlen + 8:]
            sport, dport = struct.unpack('>HH', search_payload[ip_hlen:ip_hlen + 4])
            
        else:
            payload = b''
            sport = dport = 0
        
        # Extract credentials from various protocols
        extract_ntlm(search_payload, src, sport, dst, dport)
        extract_ntlm_from_http(payload, src, sport, dst, dport)
        extract_http_basic(payload, src, sport, dst, dport)
        extract_http_password_fields(payload, src, sport, dst, dport)
        extract_smtp_auth(payload, src, sport, dst, dport)
        extract_ldap_simple_bind(payload, src, sport, dst, dport)
        
        if dport == 88 or sport == 88:
            extract_kerberos(payload, src, sport, dst, dport)
            
        extract_cleartext(payload, src, sport, dst, dport)
        
    except (IndexError, struct.error):
        pass

def process_pcap(fname):
    """Process a PCAP/PCAPNG file"""
    global detected_offset, ntlm_challenge, ftp_users, seen_credentials, logged_messages
    
    # Reset state for new file
    detected_offset = None
    ntlm_challenge.clear()
    ftp_users.clear()
    seen_credentials.clear()
    logged_messages.clear()
    
    start_time = time.time()
    file_size = os.path.getsize(fname) / (1024 * 1024)
    print(f'Parsing {fname}...')
    
    reader = pcapy.open_offline(fname)
    packet_count = 0
    
    while True:
        header, packet = reader.next()
        if not header:
            break
        process_packet(packet)
        packet_count += 1
    
    elapsed = time.time() - start_time
    if elapsed >= 60:
        print(f'\n{fname} parsed in: {elapsed/60:.3g} minutes ({packet_count:,} packets, {file_size:.3g} MB).\n')
    else:
        print(f'\n{fname} parsed in: {elapsed:.4g} seconds ({packet_count:,} packets, {file_size:.3g} MB).\n')

def run():
    """Main execution"""
    print(f'{VERSION}\nAuthor: Laurent Gaffie\nContact: lgaffie@secorizon.com\nX: @secorizon\n')
    
    if args.activate_cc:
        print("CC number scanning activated\n")
    else:
        print("CC number scanning deactivated\n")

    if args.interface:
        print(f'Live capture on {args.interface}\n')
        reader = pcapy.open_live(args.interface, 65536, True, 100)
        reader.loop(-1, lambda hdr, data: process_packet(data))
        
    elif args.fname:
        process_pcap(args.fname)
        
    elif args.dir_path:
        for root, _, files in os.walk(args.dir_path):
            for file in files:
                if file.lower().endswith(('.pcap', '.pcapng')):
                    path = os.path.join(root, file)
                    process_pcap(path)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=VERSION)
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('-f', dest='fname', help='Pcap file to parse')
    group.add_argument('-d', dest='dir_path', help='Pcap directory to parse recursively')
    group.add_argument('-i', dest='interface', help='Interface for live capture')
    parser.add_argument('-c', action='store_false', dest='activate_cc', default=True, help='Deactivate CC number scanning')
    parser.add_argument('-t', action='store_true', dest='timestamp', help='Print timestamps')
    parser.add_argument('-v', action='store_true', dest='verbose', help='Verbose mode (print duplicates)')
    parser.add_argument('-o', dest='output_path', default='./', help='Output directory for logs')
    parser.add_argument('--disable', dest='disable_protocols', action='append', 
                        help='Disable protocol (can be used multiple times). Options: NTLM, HTTP, FTP, IRC, LDAP, SMTP, Kerberos, SNMP, MSSQL')
    parser.add_argument('--exclude-host', dest='exclude_hosts', action='append',
                        help='Exclude host IP from capture (can be used multiple times)')
    args = parser.parse_args()
    
    # Process disabled protocols
    if args.disable_protocols:
        disabled_protocols.update(p.upper() for p in args.disable_protocols)
        print(f"Disabled protocols: {', '.join(sorted(disabled_protocols))}\n")
    
    # Process excluded hosts
    if args.exclude_hosts:
        excluded_hosts.update(args.exclude_hosts)
        print(f"Excluded hosts: {', '.join(sorted(excluded_hosts))}\n")

    output_path = args.output_path.rstrip('/')
    os.makedirs(os.path.join(output_path, 'logs'), exist_ok=True)
    session_log = os.path.join(output_path, 'CredentialDump-Session.log')

    logger = logging.getLogger('PCredz')
    logger.setLevel(logging.WARNING)
    logger.addHandler(logging.FileHandler(session_log, encoding='utf-8'))

    run()
