2025-07-24 10:04:48 -07:00
|
|
|
|
#!/usr/bin/env python3
|
2025-07-24 10:36:24 -07:00
|
|
|
|
# Description: Monitor a website for changes.
|
2025-07-24 20:31:24 -07:00
|
|
|
|
# Usage: python3 diff-web.py --domain example.com
|
2025-07-24 10:04:48 -07:00
|
|
|
|
# Author: Justin Oros
|
|
|
|
|
|
# Source: https://github.com/JustinOros
|
|
|
|
|
|
|
2025-07-24 19:55:52 -07:00
|
|
|
|
import argparse
|
2025-07-24 10:04:48 -07:00
|
|
|
|
import hashlib
|
|
|
|
|
|
import os
|
2025-07-24 19:55:52 -07:00
|
|
|
|
import re
|
2025-07-24 11:00:56 -07:00
|
|
|
|
import sys
|
2025-07-24 19:55:52 -07:00
|
|
|
|
import time
|
2025-07-24 10:04:48 -07:00
|
|
|
|
from datetime import datetime
|
2025-07-24 19:55:52 -07:00
|
|
|
|
from getpass import getpass
|
|
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
|
|
import requests
|
2025-07-24 20:31:24 -07:00
|
|
|
|
from requests.exceptions import ConnectionError, HTTPError, Timeout, RequestException
|
2025-07-24 10:36:24 -07:00
|
|
|
|
from bs4 import BeautifulSoup
|
2025-07-24 10:21:51 -07:00
|
|
|
|
|
2025-07-24 19:55:52 -07:00
|
|
|
|
def clean_text(text):
|
2025-07-24 20:40:33 -07:00
|
|
|
|
junk_phrases_pattern = re.compile(
|
|
|
|
|
|
r'\b(?:Menu|Close|Sign ?In|Sign ?Out|Login|Logout|Back|Next|More|Search|Cart|Settings|Help|Contact|Language|'
|
|
|
|
|
|
r'Profile|Account|Support|US|EN|FR|DE|JP|ES|IT|CN|≡|×)\b',
|
|
|
|
|
|
flags=re.IGNORECASE
|
|
|
|
|
|
)
|
|
|
|
|
|
text = junk_phrases_pattern.sub('', text)
|
|
|
|
|
|
text = re.sub(r'(?:\bchevron_right\b\s*){1,}', '', text, flags=re.IGNORECASE)
|
2025-07-24 19:55:52 -07:00
|
|
|
|
text = re.sub(r'\s+', ' ', text)
|
|
|
|
|
|
return text.strip()
|
2025-07-24 10:04:48 -07:00
|
|
|
|
|
2025-07-24 20:31:24 -07:00
|
|
|
|
def fetch_content_with_fallback(domain):
|
|
|
|
|
|
parsed = urlparse(domain)
|
2025-07-24 20:40:33 -07:00
|
|
|
|
netloc = parsed.netloc if parsed.netloc else parsed.path
|
2025-07-24 10:21:51 -07:00
|
|
|
|
|
2025-07-24 20:31:24 -07:00
|
|
|
|
https_url = f"https://{netloc}"
|
|
|
|
|
|
http_url = f"http://{netloc}"
|
2025-07-24 19:55:52 -07:00
|
|
|
|
|
2025-07-24 20:31:24 -07:00
|
|
|
|
for url in [https_url, http_url]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = requests.get(url, timeout=10)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
soup = BeautifulSoup(response.text, 'html.parser')
|
2025-07-24 19:55:52 -07:00
|
|
|
|
|
2025-07-24 20:31:24 -07:00
|
|
|
|
for tag in soup(["script", "style", "noscript", "iframe", "svg", "canvas", "header", "footer", "nav", "form", "button"]):
|
|
|
|
|
|
tag.decompose()
|
|
|
|
|
|
|
|
|
|
|
|
keep_tags = ['h1', 'h2', 'h3', 'h4', 'p', 'li', 'article', 'section', 'span', 'div']
|
|
|
|
|
|
for tag in soup.find_all(True):
|
|
|
|
|
|
if tag.name not in keep_tags:
|
|
|
|
|
|
tag.decompose()
|
|
|
|
|
|
|
|
|
|
|
|
raw_text = soup.get_text(separator=' ', strip=True)
|
|
|
|
|
|
cleaned = clean_text(raw_text)
|
|
|
|
|
|
return url, cleaned
|
|
|
|
|
|
|
|
|
|
|
|
except (ConnectionError, HTTPError, Timeout, RequestException) as e:
|
|
|
|
|
|
print(f"[!] Failed to fetch {url}: {e}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
raise Exception(f"Could not fetch content from HTTPS or HTTP for domain: {domain}")
|
2025-07-24 19:55:52 -07:00
|
|
|
|
|
|
|
|
|
|
def hash_content(content):
|
2025-07-24 10:04:48 -07:00
|
|
|
|
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
|
|
|
|
|
|
2025-07-24 19:55:52 -07:00
|
|
|
|
def save_log(domain, content):
|
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
timestamp = now.strftime("%Y%m%d.%H%M%S")
|
|
|
|
|
|
domain_name = domain.replace("https://", "").replace("http://", "").split("/")[0]
|
|
|
|
|
|
filename = f"{domain_name}.{timestamp}.log"
|
|
|
|
|
|
with open(filename, "w", encoding="utf-8") as f:
|
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
print(f"[+] Change detected. Log saved to: {os.path.abspath(filename)}")
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_credentials(args):
|
|
|
|
|
|
try:
|
2025-07-24 20:18:40 -07:00
|
|
|
|
if args.email is not None and not args.email.strip():
|
|
|
|
|
|
args.email = input("Enter your email: ").strip()
|
|
|
|
|
|
if args.email and not args.password:
|
|
|
|
|
|
args.password = getpass("Enter your password: ")
|
2025-07-24 19:55:52 -07:00
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
print("\n[!] Input canceled by user. Exiting.")
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Monitor a webpage for changes.")
|
2025-07-24 20:07:17 -07:00
|
|
|
|
parser.add_argument("-d", "--domain", type=str, required=True, help="Domain to monitor (e.g., example.com)")
|
2025-07-24 20:18:40 -07:00
|
|
|
|
parser.add_argument("--time", type=int, default=60, help="Polling interval in seconds (default: 60)")
|
2025-07-24 19:55:52 -07:00
|
|
|
|
parser.add_argument("-e", "--email", nargs="?", const="", help="Your email address (optional prompt)")
|
2025-07-24 20:18:40 -07:00
|
|
|
|
parser.add_argument("-p", "--password", nargs="?", const="", help="Your password (optional; will prompt if not provided)")
|
2025-07-24 19:55:52 -07:00
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
args = prompt_credentials(args)
|
|
|
|
|
|
|
2025-07-24 20:07:17 -07:00
|
|
|
|
domain = args.domain
|
2025-07-24 20:31:24 -07:00
|
|
|
|
print(f"[+] Monitoring: {domain} every {args.time} seconds")
|
2025-07-24 19:55:52 -07:00
|
|
|
|
if args.email:
|
|
|
|
|
|
print(f"[+] Email: {args.email}")
|
2025-07-24 10:04:48 -07:00
|
|
|
|
|
2025-07-24 19:55:52 -07:00
|
|
|
|
previous_hash = None
|
2025-07-24 20:40:33 -07:00
|
|
|
|
current_url = None
|
2025-07-24 10:04:48 -07:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
2025-07-24 20:31:24 -07:00
|
|
|
|
url_used, content = fetch_content_with_fallback(domain)
|
|
|
|
|
|
current_url = url_used
|
2025-07-24 19:55:52 -07:00
|
|
|
|
current_hash = hash_content(content)
|
|
|
|
|
|
|
|
|
|
|
|
if previous_hash and current_hash != previous_hash:
|
2025-07-24 20:31:24 -07:00
|
|
|
|
save_log(current_url, content)
|
2025-07-24 19:55:52 -07:00
|
|
|
|
else:
|
|
|
|
|
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] No change.")
|
|
|
|
|
|
|
|
|
|
|
|
previous_hash = current_hash
|
2025-07-24 20:31:24 -07:00
|
|
|
|
|
2025-07-24 10:04:48 -07:00
|
|
|
|
except Exception as e:
|
2025-07-24 19:55:52 -07:00
|
|
|
|
print(f"[!] Error: {e}")
|
2025-07-24 10:04:48 -07:00
|
|
|
|
|
2025-07-24 19:55:52 -07:00
|
|
|
|
try:
|
|
|
|
|
|
time.sleep(args.time)
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
print("\n[!] Monitoring stopped by user. Goodbye!")
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
try:
|
|
|
|
|
|
main()
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
print("\n[!] Monitoring stopped by user. Goodbye!")
|
|
|
|
|
|
sys.exit(0)
|
2025-07-24 10:04:48 -07:00
|
|
|
|
|