Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| import uuid | |
| import hashlib | |
| import hmac | |
| import secrets | |
| import base64 | |
| import subprocess | |
| from typing import * | |
| import os | |
| import sys | |
| import re | |
| import json | |
| import time | |
| import urllib3 | |
| import requests | |
| import socket | |
| import logging | |
| import threading | |
| import concurrent.futures | |
| import ipaddress | |
| import ssl | |
| from urllib.parse import urlparse, urljoin, urlencode, quote | |
| from collections import defaultdict | |
| from bs4 import BeautifulSoup | |
| from datetime import datetime, timezone | |
| # Suppress insecure request warnings for self-signed certificates | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| class WebCrawler: | |
| def __init__(self, target_url, max_depth=2, delay=0.5, auth_headers=None, log_fn=None, | |
| exclude_paths=None, max_urls=None): | |
| self.target_url = target_url | |
| self.max_depth = max_depth | |
| self.delay = delay | |
| self.auth_headers = auth_headers or {} | |
| self.log_fn = log_fn | |
| self.exclude_paths = exclude_paths or [] | |
| self.max_urls = max_urls or max(50, max_depth * 25) | |
| self.visited_urls = set() | |
| self.discovered_urls = [] | |
| self.discovered_forms = [] | |
| self.domain = urlparse(target_url).netloc | |
| def log(self, level, message): | |
| """Helper to write to the scanner logger if provided, else console.""" | |
| if self.log_fn: | |
| self.log_fn(level, message) | |
| else: | |
| print(f"[{level}] {message}") | |
| def is_valid_url(self, url): | |
| """Check if URL is valid and belongs to the target domain""" | |
| parsed = urlparse(url) | |
| return parsed.netloc == self.domain and parsed.scheme in ['http', 'https'] | |
| def is_excluded(self, url): | |
| """Skip URLs matching user-defined path exclusions.""" | |
| if not self.exclude_paths: | |
| return False | |
| path = urlparse(url).path or "/" | |
| for pattern in self.exclude_paths: | |
| if not pattern: | |
| continue | |
| normalized = pattern if pattern.startswith("/") else f"/{pattern}" | |
| if path.startswith(normalized) or normalized in path: | |
| return True | |
| return False | |
| def get_all_links(self, url, soup): | |
| """Extract all links from page""" | |
| links = set() | |
| for tag in soup.find_all('a', href=True): | |
| link = urljoin(url, tag['href']) | |
| link = link.split('#')[0] | |
| if self.is_valid_url(link): | |
| links.add(link) | |
| return links | |
| def extract_forms(self, url, soup): | |
| """Extract all forms from page""" | |
| forms_data = [] | |
| forms = soup.find_all('form') | |
| for form in forms: | |
| form_details = { | |
| 'url': url, | |
| 'action': urljoin(url, form.get('action', '')), | |
| 'method': form.get('method', 'get').lower(), | |
| 'inputs': [] | |
| } | |
| for input_tag in form.find_all(['input', 'textarea', 'select']): | |
| input_type = input_tag.get('type', 'text') | |
| input_name = input_tag.get('name', '') | |
| if input_name: | |
| form_details['inputs'].append({ | |
| 'type': input_type, | |
| 'name': input_name, | |
| 'value': input_tag.get('value', '') | |
| }) | |
| if form_details['inputs']: | |
| forms_data.append(form_details) | |
| return forms_data | |
| def crawl(self, url, depth=0): | |
| """Recursively crawl website""" | |
| if depth > self.max_depth or url in self.visited_urls: | |
| return | |
| if self.is_excluded(url): | |
| self.log("INFO", f"[Crawler] Skipping excluded path: {url}") | |
| return | |
| if len(self.visited_urls) >= self.max_urls: | |
| self.log("WARNING", f"[Crawler] Max URL limit ({self.max_urls}) reached — stopping crawl") | |
| return | |
| self.log("INFO", f"[Crawler] Crawling depth {depth}: {url}") | |
| self.visited_urls.add(url) | |
| try: | |
| # Inject auth_headers for authenticated crawling | |
| headers = {"User-Agent": "LarShield/2.0 Crawler"} | |
| headers.update(self.auth_headers) | |
| response = requests.get(url, headers=headers, timeout=10, allow_redirects=True, verify=False) | |
| self.discovered_urls.append({ | |
| 'url': url, | |
| 'status': response.status_code, | |
| 'depth': depth | |
| }) | |
| if response.status_code == 200 and 'text/html' in response.headers.get('Content-Type', ''): | |
| soup = BeautifulSoup(response.content, 'html.parser') | |
| forms = self.extract_forms(url, soup) | |
| self.discovered_forms.extend(forms) | |
| if forms: | |
| self.log("SUCCESS", f"[Crawler] Found {len(forms)} form(s) on {url}") | |
| links = self.get_all_links(url, soup) | |
| for link in links: | |
| if link not in self.visited_urls: | |
| time.sleep(self.delay) | |
| self.crawl(link, depth + 1) | |
| except Exception as e: | |
| self.log("WARNING", f"[Crawler] Error crawling {url}: {str(e)}") | |
| def start(self): | |
| """Start crawling from target URL""" | |
| exclusion_note = f", Exclusions: {len(self.exclude_paths)}" if self.exclude_paths else "" | |
| self.log("INFO", | |
| f"[Crawler] Starting Web Crawler (Target: {self.target_url}, " | |
| f"Max Depth: {self.max_depth}, Max URLs: {self.max_urls}{exclusion_note})") | |
| self.crawl(self.target_url) | |
| self.log("SUCCESS", f"[Crawler] Crawl complete. Discovered {len(self.discovered_urls)} URLs and {len(self.discovered_forms)} forms.") | |
| return { | |
| 'urls': self.discovered_urls, | |
| 'forms': self.discovered_forms | |
| } | |