v4.5 Demo initial commit

This commit is contained in:
DaStra3003
2026-06-24 14:53:14 +02:00
commit 3de8ded7c2
110 changed files with 14363112 additions and 0 deletions
@@ -0,0 +1,626 @@
import subprocess
import os
import re
import math
import logging
from IoTAnalyzer.models import BinwalkRaw, StringsRaw, RawDataset, Tool
from IoTAnalyzer.db_create import (
create_binwalk_raw,
create_strings_raw,
create_analyzed_dataset,
create_binwalk_analyzed,
create_strings_analyzed,
)
import json
logger = logging.getLogger(__name__)
def analyze_firmware_file(firmware_file, raw_dataset_id):
"""
Main function for firmware analysis using binwalk and strings.
"""
print(f"Start analyzing firmware: {firmware_file}")
logger.info(f"Firmware-Analyse gestartet: {firmware_file} (RawDataset ID: {raw_dataset_id})")
# Robust path resolution for the firmware file (multiple candidates)
# repo_root = project root (3 levels up from analyze_firmware.py)
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
candidates = [
# preferred: central files directory in the repo root
os.path.join(repo_root, 'files', firmware_file),
# Fallback: frontend public folder (Next.js)
os.path.join(repo_root, 'Frontend', 'iot-pre-tester-frontend', 'public', 'filesAttachment', firmware_file),
# Relative (in case the working directory is already the repo root)
os.path.join('files', firmware_file),
os.path.join('Frontend', 'iot-pre-tester-frontend', 'public', 'filesAttachment', firmware_file),
]
filepath = None
for cand in candidates:
if os.path.exists(cand):
filepath = cand
break
if filepath is None:
logger.error(
"Firmware-Datei nicht gefunden. Geprüfte Pfade: " + ", ".join(candidates)
)
print("ERROR: File not found. Checked paths:\n" + "\n".join(candidates))
return
try:
# 1. Run binwalk analysis
print("Running binwalk analysis...")
binwalk_results = run_binwalk_analysis(filepath)
save_binwalk_to_database(binwalk_results, raw_dataset_id)
# 2. Run strings extraction (EXTENDED with key-value pairing)
print("Running strings extraction with key-value pairing...")
strings_results = run_strings_extraction_with_pairing(filepath)
save_strings_to_database(strings_results, raw_dataset_id)
# 3. Create analyzed dataset for further analysis
create_analyzed_dataset_for_firmware(raw_dataset_id, binwalk_results, strings_results)
print("Firmware analysis completed successfully.")
logger.info(f"Firmware-Analyse erfolgreich abgeschlossen: {firmware_file}")
except Exception as e:
logger.error(f"Fehler bei Firmware-Analyse: {e}")
print(f"ERROR during firmware analysis: {e}")
def run_binwalk_analysis(filepath):
"""
Runs binwalk on the firmware.
"""
results = []
try:
cmd = ['binwalk', filepath]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
lines = output.split('\n')
for line in lines:
if line.startswith('DECIMAL') or line.startswith('----') or not line.strip():
continue
parts = line.split(None, 2)
if len(parts) >= 3:
decimal = parts[0]
hexadecimal = parts[1]
description = parts[2]
results.append({
'offset': hexadecimal,
'description': description,
'file_path': None
})
logger.info(f"Binwalk fand {len(results)} Signaturen")
except subprocess.CalledProcessError as e:
logger.error(f"Binwalk-Fehler: {e.output}")
except FileNotFoundError:
logger.error("Binwalk nicht installiert. Bitte installieren: sudo apt-get install binwalk")
print("ERROR: binwalk not found. Install with: sudo apt-get install binwalk")
return results
def calculate_entropy(text):
"""Calculates the Shannon entropy of a string."""
if not text:
return 0
entropy = 0
for x in range(256):
p_x = float(text.count(chr(x))) / len(text)
if p_x > 0:
entropy += - p_x * math.log(p_x, 2)
return entropy
def looks_like_secret(text):
"""
Checks whether a string looks like a secret (high entropy, mixed case, digits).
"""
if len(text) < 16 or len(text) > 256:
return False
# Must contain letters AND digits
if not (re.search(r'[a-zA-Z]', text) and re.search(r'[0-9]', text)):
return False
entropy = calculate_entropy(text)
return entropy > 3.5
def is_blacklisted(text):
"""
Checks whether a string is on the blacklist.
"""
text = text.strip()
# Too short
if len(text) < 3:
return True
# Pure numbers
if re.match(r'^\d+$', text):
return True
# Boolean / Null
if text.lower() in ['true', 'false', 'null', 'none', 'undefined']:
return True
# Versions (standalone)
if re.match(r'^v?\d+(\.\d+)+$', text):
return True
# Only special characters
if re.match(r'^[^a-zA-Z0-9]+$', text):
return True
return False
def run_strings_extraction_with_pairing(filepath, min_length=4):
"""
EXTENDED strings extraction with KEY-VALUE PAIRING and FILTERING.
"""
results = []
# CRITICAL KEYWORDS that expect a value in the next string
CRITICAL_KEYS = {
# WiFi Credentials
'wifi-ssid': 'ssid',
'ssid': 'ssid',
'network-name': 'ssid',
'ap-name': 'ssid',
'wifi_ssid': 'ssid',
# WiFi Passwords
'wifi-pass': 'password',
'wifi-password': 'password',
'wifi_pass': 'password',
'wpa-psk': 'password',
'wpa_psk': 'password',
'pre-shared-key': 'password',
'network-password': 'password',
'password': 'password',
'passwd': 'password',
# Location
'location': 'location',
'geo-location': 'location',
'coordinates': 'location',
'latitude': 'location',
'longitude': 'location',
# Cloud Tokens
'cloud-token': 'cloud_token',
'cloud_token': 'cloud_token',
'auth-token': 'cloud_token',
'access-token': 'cloud_token',
'bearer-token': 'cloud_token',
'refresh-token': 'cloud_token',
# API Keys
'api-key': 'api_key',
'api_key': 'api_key',
'apikey': 'api_key',
'secret-key': 'api_key',
'secret_key': 'api_key',
'access-key': 'api_key',
'private-key': 'api_key',
'auth': 'api_key',
'bearer': 'cloud_token',
# Device Identifiers
'device-id': 'device_id',
'device_id': 'device_id',
'serial-num': 'serial',
'serial_num': 'serial',
'mac-address': 'mac',
'mac_address': 'mac',
# Certificates
'cert-dclrn': 'certificate',
'certificate': 'certificate',
}
# Trigger keywords for context search (simplified)
TRIGGER_KEYWORDS = set(CRITICAL_KEYS.keys())
try:
# Extract strings with offset
cmd = ['strings', '-a', '-t', 'x', '-n', str(min_length), filepath]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
lines = output.split('\n')
# Statistics
stats_total = 0
stats_kept = 0
stats_filtered = 0
i = 0
while i < len(lines):
line = lines[i].strip()
if not line:
i += 1
continue
# Parse: "offset string_value"
parts = line.split(None, 1)
if len(parts) < 2:
i += 1
continue
offset = parts[0]
string_value = parts[1].strip()
stats_total += 1
# 1. Blacklist Check
if is_blacklisted(string_value):
stats_filtered += 1
i += 1
continue
# 2. Context / key-value check
string_lower = string_value.lower().replace('-', '_').replace(' ', '_')
found_key_type = None
# Check whether the current string is a key
for key in TRIGGER_KEYWORDS:
if key.replace('-', '_') in string_lower:
found_key_type = CRITICAL_KEYS.get(key)
break
if found_key_type:
# KEY found! Store key and try to find value
results.append({
'string_value': f"[KEY] {string_value}",
'string_type': found_key_type
})
stats_kept += 1
# Get NEXT string as value (look ahead up to 2 strings)
lookahead_count = 0
j = i + 1
while j < len(lines) and lookahead_count < 2:
next_line = lines[j].strip()
if next_line:
next_parts = next_line.split(None, 1)
if len(next_parts) >= 2:
next_value = next_parts[1].strip()
# If the next value is NOT on the blacklist, we take it
if not is_blacklisted(next_value):
results.append({
'string_value': f"[VALUE] {next_value}",
'string_type': found_key_type
})
print(f" ✓ Found {found_key_type}: {string_value}{next_value}")
stats_kept += 1
j += 1
lookahead_count += 1
else:
# No key -> check whether it is a secret or an interesting category
string_type = categorize_string(string_value)
is_secret = looks_like_secret(string_value)
keep = False
if string_type != 'generic':
keep = True
elif is_secret:
string_type = 'potential_secret'
keep = True
if keep:
results.append({
'string_value': string_value,
'string_type': string_type
})
stats_kept += 1
else:
stats_filtered += 1
i += 1
logger.info(f"Strings Analyse: {stats_total} total, {stats_filtered} gefiltert, {stats_kept} behalten")
print(f"Strings Analysis: Total {stats_total} | Filtered {stats_filtered} | Kept {stats_kept}")
# Output statistics
stats = {}
for r in results:
stats[r['string_type']] = stats.get(r['string_type'], 0) + 1
print("\nString-Kategorien gefunden:")
for category, count in sorted(stats.items(), key=lambda x: x[1], reverse=True):
print(f" {category}: {count}")
except subprocess.CalledProcessError as e:
logger.error(f"Strings-Fehler: {e.output}")
except FileNotFoundError:
logger.error("strings nicht installiert. Bitte installieren: sudo apt-get install binutils")
print("ERROR: strings not found. Install with: sudo apt-get install binutils")
return results
def categorize_string(string_value):
"""
Categorizes a string based on its content.
"""
def save_binwalk_to_database(binwalk_results, raw_dataset_id):
"""
Stores binwalk results in the BinwalkRaw table.
"""
for result in binwalk_results:
try:
response = create_binwalk_raw(
raw_dataset_id=raw_dataset_id,
offset=result['offset'],
description=result['description'],
file_path=result.get('file_path')
)
if hasattr(response, 'content'):
response_data = json.loads(response.content.decode('utf-8'))
if response_data['status'] != 'success':
logger.warning(f"Binwalk-Eintrag konnte nicht gespeichert werden: {result}")
except Exception as e:
logger.error(f"Fehler beim Speichern von Binwalk-Daten: {e}")
def save_strings_to_database(strings_results, raw_dataset_id):
"""
Stores ALL strings results in the StringsRaw table.
NO LIMIT ANYMORE - all strings are stored!
"""
# Prioritize interesting strings for the ordering
priority_order = ['password', 'ssid', 'cloud_token', 'api_key', 'location', 'credentials', 'certificate', 'email',
'url', 'ip', 'mac', 'path', 'device_id', 'serial', 'generic']
sorted_results = sorted(
strings_results,
key=lambda x: priority_order.index(x['string_type']) if x['string_type'] in priority_order else 999
)
saved_count = 0
for result in sorted_results:
try:
response = create_strings_raw(
raw_dataset_id=raw_dataset_id,
string_value=result['string_value'],
string_type=result['string_type']
)
if hasattr(response, 'content'):
response_data = json.loads(response.content.decode('utf-8'))
if response_data['status'] == 'success':
saved_count += 1
except Exception as e:
logger.error(f"Fehler beim Speichern von Strings-Daten: {e}")
logger.info(f"Strings gespeichert: {saved_count} von {len(strings_results)}")
print(f"✓ Saved {saved_count} strings to database")
def create_analyzed_dataset_for_firmware(raw_dataset_id, binwalk_results, strings_results):
"""
Creates an AnalyzedDataset and performs extended analysis.
"""
try:
raw_dataset = RawDataset.objects.get(id=raw_dataset_id)
file_id = raw_dataset.file.id
# Create/get tool
binwalk_tool = get_or_create_tool("binwalk", "2.3.4")
strings_tool = get_or_create_tool("strings", "2.37")
# Create AnalyzedDataset
analyzed_response = create_analyzed_dataset(file_id)
if hasattr(analyzed_response, 'content'):
analyzed_data = json.loads(analyzed_response.content.decode('utf-8'))
else:
analyzed_data = analyzed_response
analyzed_dataset_id = analyzed_data['data']['id']
# Binwalk analysis: identify important components
analyze_binwalk_components(analyzed_dataset_id, binwalk_tool.id, binwalk_results)
# Strings analysis: identify critical strings (EXTENDED)
analyze_critical_strings(analyzed_dataset_id, strings_tool.id, strings_results)
logger.info(f"Analyzed Dataset für Firmware erstellt: ID {analyzed_dataset_id}")
except Exception as e:
logger.error(f"Fehler beim Erstellen des Analyzed Datasets: {e}")
def get_or_create_tool(tool_name, version):
"""
Helper function: gets an existing tool or creates a new one.
"""
try:
tool = Tool.objects.get(tool_name=tool_name, version=version)
logger.info(f"Tool gefunden: {tool_name} {version}")
return tool
except Tool.DoesNotExist:
tool = Tool.objects.create(
tool_name=tool_name,
version=version
)
logger.info(f"Tool erstellt: {tool_name} {version}")
return tool
def analyze_binwalk_components(analyzed_dataset_id, tool_id, binwalk_results):
"""
Analyzes binwalk results and stores important components.
"""
important_keywords = {
'Linux kernel': 'Linux Kernel',
'U-Boot': 'U-Boot Bootloader',
'filesystem': 'Filesystem',
'certificate': 'TLS Certificate',
'private key': 'Private Key',
'public key': 'Public Key',
'compressed': 'Compressed Data',
'encrypted': 'Encrypted Data',
'gzip': 'GZIP Archive',
'zlib': 'ZLIB Data',
'lzma': 'LZMA Archive',
'squashfs': 'SquashFS Filesystem',
'jffs2': 'JFFS2 Filesystem',
'cramfs': 'CramFS Filesystem',
}
for result in binwalk_results:
description = result['description'].lower()
for keyword, component_type in important_keywords.items():
if keyword.lower() in description:
try:
create_binwalk_analyzed(
analyzed_dataset_id=analyzed_dataset_id,
tool_id=tool_id,
component_type=component_type,
offset=result['offset'],
size=None,
entropy=None
)
logger.info(f"Wichtige Komponente gefunden: {component_type} @ {result['offset']}")
except Exception as e:
logger.error(f"Fehler beim Speichern von Binwalk-Analyse: {e}")
break
def analyze_critical_strings(analyzed_dataset_id, tool_id, strings_results):
"""
EXTENDED analysis of critical strings with KEY-VALUE pairs.
Severity levels:
- critical: passwords, cloud tokens, API keys, SSID+password combos
- high: SSIDs, device IDs, certificates
- medium: URLs, email addresses, location
- low: paths, MAC addresses, serial numbers
"""
# Extended severity mapping
severity_map = {
'password': 'critical',
'cloud_token': 'critical',
'api_key': 'critical',
'ssid': 'high',
'device_id': 'high',
'credentials': 'high',
'certificate': 'high',
'location': 'medium',
'email': 'medium',
'url': 'medium',
'ip': 'medium',
'serial': 'low',
'mac': 'low',
'path': 'low',
'generic': 'info'
}
for result in strings_results:
string_value = result['string_value']
string_type = result['string_type']
# Base severity from type
severity = severity_map.get(string_type, 'info')
category = string_type
# Store ALL critical, high and medium strings
if severity in ['critical', 'high', 'medium']:
try:
create_strings_analyzed(
analyzed_dataset_id=analyzed_dataset_id,
tool_id=tool_id,
string_value=string_value,
category=category,
severity=severity
)
except Exception as e:
logger.error(f"Fehler beim Speichern von Strings-Analyse: {e}")
def extract_with_binwalk(filepath, output_dir):
"""
Optional: runs binwalk -e to extract files.
"""
try:
cmd = ['binwalk', '-e', '-C', output_dir, filepath]
subprocess.run(cmd, check=True, capture_output=True, text=True)
logger.info(f"Binwalk-Extraktion nach {output_dir} erfolgreich")
return output_dir
except subprocess.CalledProcessError as e:
logger.error(f"Binwalk-Extraktion fehlgeschlagen: {e.stderr}")
return None
def categorize_string(string_value):
"""
Categorizes a string based on its content.
"""
# URLs
if re.search(r'https?://', string_value, re.IGNORECASE):
return 'url'
# Email addresses
if re.search(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', string_value):
return 'email'
# AWS Keys
if re.match(r'AKIA[0-9A-Z]{16}', string_value):
return 'api_key'
# JWT Tokens (Basic check: 3 parts separated by dots, starts with eyJ)
if re.match(r'eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+', string_value):
return 'cloud_token'
# Private Keys
if 'PRIVATE KEY' in string_value:
return 'api_key'
# IP addresses (IPv4)
if re.match(r'^(?:\d{1,3}\.){3}\d{1,3}$', string_value.strip()):
return 'ip'
# MAC addresses
if re.match(r'^(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$', string_value.strip()):
return 'mac'
# File paths
if re.match(r'/(?:bin|etc|usr|var|tmp|opt|home|dev|proc|sys|lib|mnt)/', string_value):
return 'path'
# Certificates
if re.search(r'-----BEGIN [A-Z ]+-----', string_value, re.IGNORECASE):
return 'certificate'
# Base64 Heuristic (length multiple of 4, valid chars, ends with = or == optionally)
# Only if length > 20 to avoid false positives on short strings
if len(string_value) > 20 and len(string_value) % 4 == 0 and re.match(r'^[A-Za-z0-9+/]+={0,2}$', string_value):
return 'base64_data'
return 'generic'
@@ -0,0 +1,107 @@
import time
import os
import shutil
import subprocess
from django.db import transaction
from IoTAnalyzer.db_create import create_analyzed_hash
from IoTAnalyzer.models import HashRaw, HashAnalyzed, Tool, HashType, AnalyzedDataset
WORDLIST_PATH = "/app/django_project/IoTAnalyzer/management/commands/rockyou.txt"
HASHCAT_PATH = "hashcat" # Update this if hashcat is in a different location
def crack_hashes_from_db(raw_hash_id):
""" Crack hashes from the database using hashcat """
if not os.path.exists(WORDLIST_PATH):
return {"error": f"The wordlist file {WORDLIST_PATH} does not exist."}
if not shutil.which(HASHCAT_PATH):
return {"error": "Hashcat is not installed or not in PATH."}
results = {}
try:
raw_hash = HashRaw.objects.get(id=raw_hash_id)
hash_value = raw_hash.hash_value.strip()
# Identify hash type and mode
mode_info = identify_hash_mode(hash_value)
if mode_info is None:
return {"error": f"Unknown hash type for hash: {hash_value}"}
mode, hash_type = mode_info
print(f"Processing hash: {hash_value} (Type: {hash_type}, Mode: {mode})")
temp_file_path = f"temp_hash_{raw_hash_id}.txt"
with open(temp_file_path, "w", encoding="utf-8") as temp_file:
temp_file.write(hash_value + "\n")
# Run hashcat
crack_command = [
HASHCAT_PATH, "-m", str(mode), "-a", "0", "-D", "1,2", temp_file_path, WORDLIST_PATH, "--potfile-path", "hashcat.potfile", "--force"
]
start_time = time.time()
process = subprocess.run(crack_command, text=True, capture_output=True)
end_time = time.time()
duration = end_time - start_time
crack_time_str = f"{duration:.2f}s"
print("Hashcat Output:", process.stdout)
print("Hashcat Errors:", process.stderr)
analyzed_dataset, _ = AnalyzedDataset.objects.get_or_create(file=raw_hash.raw_dataset.file)
tool, _ = Tool.objects.get_or_create(tool_name="Hashcat", defaults={"version": "6.x.x"})
# Retrieve cracked hashes
show_command = [
HASHCAT_PATH, "-m", str(mode), "--show", "--potfile-path", "hashcat.potfile", temp_file_path
]
show_process = subprocess.run(show_command, text=True, capture_output=True)
print("Hashcat Show Output:", show_process.stdout)
print("Hashcat Show Errors:", show_process.stderr)
with transaction.atomic():
if show_process.returncode == 0 and show_process.stdout.strip():
cracked_hashes = show_process.stdout.strip().split("\n")
for line in cracked_hashes:
parts = line.split(":", 1) # Split only on the first colon
if len(parts) == 2:
full_hash, plaintext = parts
results[full_hash] = {"plaintext": plaintext, "type": hash_type}
print(f"Cracked Hash: {full_hash} -> {plaintext}")
create_analyzed_hash(analyzed_dataset.id, tool.id, hash_type, full_hash, True, plaintext, crack_time=crack_time_str)
print(
f"DEBUG: Saved hash {full_hash}, Cracked Value: {plaintext}")
else:
create_analyzed_hash(analyzed_dataset.id, tool.id, hash_type, hash_value, False, crack_time=crack_time_str)
print(f"DEBUG: Saved uncracked hash {hash_value} ")
os.remove(temp_file_path)
except HashRaw.DoesNotExist:
return {"error": f"No HashRaw found with ID: {raw_hash_id}"}
except FileNotFoundError:
return {"error": "Hashcat is not installed or not in PATH."}
except Exception as e:
return {"error": f"Error running hashcat: {str(e)}"}
return results
def identify_hash_mode(hash_value):
"""
Identify hashcat mode based on the hash length and known structures.
"""
hash_modes = {
32: (0, "md5"),
40: (100, "sha1"),
56: (300, "sha224"),
64: (1400, "sha256"),
96: (6100, "sha384"),
128: (1700, "sha512")
}
return hash_modes.get(len(hash_value), None)
@@ -0,0 +1,131 @@
from scapy.all import rdpcap, Raw, DNS, DNSQR
try:
from scapy.layers.tls.all import TLS
except ImportError:
# Fallback or mock if TLS not available, though we use a try-except block in usage
TLS = None
import re
import os
from IoTAnalyzer.models import UrlRaw, HashRaw
def analyze_pcap_file(pcap_file, raw_dataset_id):
"""
Analyzes a PCAP file and stores found hashes and URLs in the database.
"""
print("Start analyzing...")
# Adjust the path to the file
# Check multiple locations for the file
possible_paths = [
os.path.join('/app/files/uploads', pcap_file), # Mounted frontend uploads
os.path.join('/app/files', pcap_file), # Shared files
os.path.join('Frontend', 'iot-pre-tester-frontend', 'public', 'filesAttachment', pcap_file), # Local dev fallback
]
filepath = None
for path in possible_paths:
if os.path.exists(path):
filepath = path
break
if not filepath:
print(f"Fehler: Datei {pcap_file} nicht gefunden in: {possible_paths}")
return
# Regular expressions for hashes and URLs
hash_pattern = re.compile(r"\b[a-fA-F0-9]{32,64}\b") # MD5, SHA1, SHA256
# Stricter URL pattern:
# 1. Requires http://, https://, or www. (Standard)
# 2. OR Naked domain with AT LEAST 2 dots (e.g. sub.domain.com) to catch cloud URLs but avoid "J.mINT"
url_pattern = re.compile(
r"\b(?:"
r"(?:https?://|www\.)(?:[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})" # Protocol/www required
r"|"
r"(?:[a-zA-Z0-9-]+\.){2,}[a-zA-Z]{2,6}" # Naked domain with >= 2 dots
r"|"
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" # IPv4
r")"
r"(?:/[a-zA-Z0-9._%/-]*)?\b"
)
# Load PCAP file
try:
packets = rdpcap(filepath)
except Exception as e:
print(f"Fehler beim Öffnen der PCAP-Datei: {e}")
return
found_hashes = set()
found_urls = set()
# Analyze packets
for pkt in packets:
# 1. DNS Queries (Reliable source for domains)
if pkt.haslayer(DNS) and pkt[DNS].qd:
try:
qname = pkt[DNS].qd.qname.decode('utf-8', errors='ignore')
if qname.endswith('.'):
qname = qname[:-1]
if qname:
found_urls.add(qname)
except Exception:
pass
# 2. TLS Server Name Indication (SNI)
# Attempt to extract SNI if TLS layer is present
if TLS and pkt.haslayer(TLS):
try:
# Iterate through TLS messages to find ClientHello and extensions
if hasattr(pkt[TLS], 'msg'):
for msg in pkt[TLS].msg:
if hasattr(msg, 'ext'):
for ext in msg.ext:
if hasattr(ext, 'servernames'):
for servername in ext.servernames:
if hasattr(servername, 'servername'):
sni = servername.servername.decode('utf-8', errors='ignore')
if sni:
found_urls.add(sni)
except Exception:
pass
# 3. Raw Payload (Fallback with strict regex)
if pkt.haslayer(Raw): # Check whether the packet has a payload
payload = pkt[Raw].load.decode(errors='ignore')
# Search for hashes
found_hashes.update(hash_pattern.findall(payload))
# Search for URLs
found_urls.update(url_pattern.findall(payload))
# Store results in the database
save_urls_to_database(found_urls, raw_dataset_id)
save_hashes_to_database(found_hashes, raw_dataset_id)
print("Done analyzing.")
def save_urls_to_database(urls, raw_dataset_id):
"""
Stores extracted URLs in the database.
"""
for url in urls:
UrlRaw.objects.create(
raw_dataset_id=raw_dataset_id,
url=url
)
def save_hashes_to_database(hashes, raw_dataset_id):
"""
Stores extracted hashes in the database.
"""
for hash_value in hashes:
HashRaw.objects.create(
raw_dataset_id=raw_dataset_id,
hash_value=hash_value
)
@@ -0,0 +1,212 @@
import dns.resolver
import requests
import ssl
import socket
import nmap
from django.db import transaction
import os
import re
from IoTAnalyzer.db_create import create_analyzed_url
from IoTAnalyzer.models import Tool, TlsVersion, CipherSuite, UrlRaw, AnalyzedDataset
from urllib.parse import urlparse
from datetime import datetime
LOG_FILE = "errors.log" # Log file for errors
def log_error(message):
"""
Logs an error message to the log file with a timestamp.
"""
with open(LOG_FILE, "a") as log:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log.write(f"[{timestamp}] {message}\n")
def validate_url(url):
"""
Validates if a given string is a properly formatted URL or domain.
"""
try:
result = urlparse(url)
return result.netloc or result.path
except Exception:
return False
def resolve_dns(query):
"""
Resolves a DNS query and returns a list of resolved IPs.
"""
try:
result = dns.resolver.resolve(query, 'A')
resolved_ips = [str(rdata) for rdata in result]
if not resolved_ips:
log_error(f"No IPs resolved for DNS query: {query}")
return resolved_ips
except dns.resolver.NXDOMAIN:
log_error(f"NXDOMAIN: No such domain {query}")
except dns.resolver.Timeout:
log_error(f"Timeout: DNS query for {query} timed out")
except dns.resolver.NoNameservers:
log_error(f"NoNameservers: No name servers found for {query}")
except Exception as e:
log_error(f"General DNS error for {query}: {e}")
return []
def scan_ports(target):
"""
Scans the given IP for open ports using Nmap.
Returns a list of open ports.
"""
nm = nmap.PortScanner()
open_ports = []
try:
nm.scan(hosts=target, arguments='-sT -Pn -p 22,80,443,8000,8080 --scan-delay 500ms --max-retries 1')
#nm.scan(hosts=ip, arguments='-sS -Pn -F')
if target in nm.all_hosts():
for port in nm[target].all_tcp():
if 'open' in nm[target]['tcp'][port]['state']:
open_ports.append(port)
except Exception as e:
log_error(f"Error scanning IP {target}: {e}")
if not open_ports:
print("Python-Nmap failed, using OS-level Nmap...")
os.system(f"nmap -sT -Pn -p 22,80,443,8000,8080 {target} > nmap_output.txt")
with open("nmap_output.txt", "r") as f:
output = f.read()
if "open" in output:
open_ports = []
for port in [22, 80, 443, 8000, 8080]:
pattern = rf"^{port}/tcp\s+open\s"
if re.search(pattern, output, re.MULTILINE):
open_ports.append(port)
return open_ports
def scan_ssl(target):
"""
Performs an SSL scan on the given domain or IP.
Retrieves TLS version, cipher suite, and certificate presence.
"""
context = ssl.create_default_context()
context.check_hostname = True
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.verify_mode = ssl.CERT_REQUIRED
context.load_default_certs()
context.set_alpn_protocols(["http/1.1", "h2"])
context.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 # Disable old protocols
scan_result = {
'tls_version': None,
'cipher': None,
'certificate': None,
}
try:
with socket.create_connection((target, 443), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname=target) as ssock:
cert = ssock.getpeercert()
cipher = ssock.cipher()
tls_version = ssock.version()
scan_result['certificate'] = 'Present' if cert else 'Not Present'
scan_result['cipher'] = cipher[0] if cipher else None
scan_result['tls_version'] = tls_version
except ssl.SSLError as e:
log_error(f"SSL Error during scan for {target}: {e}")
except socket.timeout:
log_error(f"Timeout during SSL scan for {target}")
except Exception as e:
log_error(f"General error during SSL scan for {target}: {e}")
return scan_result
def fetch_important_headers(url):
"""
Fetches only important headers (e.g., Server, Content-Type).
"""
try:
response = requests.head(url, timeout=5)
headers = response.headers
important_headers = {
"Server": headers.get("Server", "N/A"),
"Content-Type": headers.get("Content-Type", "N/A"),
"Strict-Transport-Security": headers.get("Strict-Transport-Security", "N/A"),
}
return important_headers
except Exception as e:
log_error(f"Error fetching headers for {url}: {e}")
return {}
def analyze_target(query, targets, analyzed_dataset):
"""
Ensures each URL is analyzed only once.
Combines SSL, HTTP headers, and port scanning for a DNS query and its targets.
Saves the results into the database with detailed logging.
"""
target = query.replace("http://", "").replace("https://", "").split('/')[0]
ssl_info = scan_ssl(target)
headers = fetch_important_headers(f"https://{target}")
open_ports = scan_ports(target)
print(f"Analyzing URL: {query}")
print(f"SSL Info: {ssl_info}")
print(f"Headers: {headers}")
print(f"Open Ports: {open_ports}")
with transaction.atomic():
tool, _ = Tool.objects.get_or_create(tool_name="DNS Analyzer", version="1.0")
if ssl_info['tls_version'] == None:
tls_version, _ = TlsVersion.objects.get_or_create(tls_version="N/A")
else:
tls_version = ssl_info['tls_version']
if ssl_info['cipher'] == None:
cipher, _ = CipherSuite.objects.get_or_create(cipher_suite="N/A")
else:
cipher = ssl_info['cipher']
create_analyzed_url(analyzed_dataset.id, tool.id, query, open_ports, [tls_version], [cipher])
log_error(f"Data saved for URL: {query}")
def get_urls_from_db(raw_url_id):
"""
Main function to handle the URL analysis for a given raw_url_id.
"""
try:
raw_url = UrlRaw.objects.get(id=raw_url_id)
except UrlRaw.DoesNotExist:
log_error(f"Raw URL with ID {raw_url_id} not found.")
return
queries = [raw_url.url]
analysis_targets = {}
for query in queries:
if not validate_url(query):
log_error(f"Invalid URL: {query}")
continue
ips = resolve_dns(query)
analysis_targets[query] = ips if ips else [query]
# analyzed_dataset = raw_url.raw_dataset.file.analyzeddataset_set.last()
analyzed_dataset, _ = AnalyzedDataset.objects.get_or_create(file=raw_url.raw_dataset.file)
if not analyzed_dataset:
log_error(f"No analyzed dataset found for the file associated with URL: {raw_url.url}")
return
for query, targets in analysis_targets.items():
analyze_target(query, targets, analyzed_dataset)