176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
METATRON - search.py
|
|
Free web search via DuckDuckGo — no API key needed.
|
|
Also fetches and extracts plain text from URLs.
|
|
Used by LLM tool dispatch when AI writes [SEARCH: query]
|
|
"""
|
|
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
from ddgs import DDGS # pip install duckduckgo-search
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# DDG SEARCH
|
|
# ─────────────────────────────────────────────
|
|
|
|
def web_search(query: str, max_results: int = 5) -> str:
|
|
"""
|
|
Search DuckDuckGo and return formatted results.
|
|
No API key. No rate limit issues for reasonable usage.
|
|
Returns a string ready to paste into LLM prompt.
|
|
"""
|
|
print(f" [*] Searching: {query}")
|
|
try:
|
|
with DDGS() as ddgs:
|
|
results = list(ddgs.text(query, max_results=max_results))
|
|
|
|
if not results:
|
|
return "[!] No search results found."
|
|
|
|
output = f"[WEB SEARCH RESULTS FOR: {query}]\n"
|
|
output += "─" * 50 + "\n"
|
|
for i, r in enumerate(results, 1):
|
|
output += f"\n[{i}] {r['title']}\n"
|
|
output += f" URL : {r['href']}\n"
|
|
output += f" Snippet : {r['body']}\n"
|
|
|
|
return output
|
|
|
|
except Exception as e:
|
|
return f"[!] Search failed: {e}"
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# CVE SPECIFIC SEARCH
|
|
# ─────────────────────────────────────────────
|
|
|
|
def search_cve(cve_id: str) -> str:
|
|
"""
|
|
Search for a specific CVE.
|
|
Queries DDG then also hits cve.mitre.org directly.
|
|
"""
|
|
print(f" [*] Looking up {cve_id}...")
|
|
|
|
# DDG search first
|
|
ddg_results = web_search(f"{cve_id} vulnerability exploit details", max_results=3)
|
|
|
|
# Direct MITRE fetch
|
|
mitre_url = f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve_id}"
|
|
mitre_data = fetch_page(mitre_url, max_chars=2000)
|
|
|
|
return f"{ddg_results}\n\n[MITRE CVE LOOKUP: {cve_id}]\n{mitre_data}"
|
|
|
|
|
|
def search_exploit(service: str, version: str) -> str:
|
|
"""
|
|
Search for known exploits for a service + version combo.
|
|
e.g. search_exploit("apache", "2.4.49")
|
|
"""
|
|
query = f"{service} {version} exploit CVE vulnerability 2023 2024"
|
|
return web_search(query, max_results=5)
|
|
|
|
|
|
def search_fix(vuln_name: str) -> str:
|
|
"""
|
|
Search for mitigation/fix for a vulnerability.
|
|
"""
|
|
query = f"how to fix {vuln_name} security mitigation patch"
|
|
return web_search(query, max_results=3)
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# PAGE FETCHER
|
|
# ─────────────────────────────────────────────
|
|
|
|
def fetch_page(url: str, max_chars: int = 3000) -> str:
|
|
"""
|
|
Fetch a URL and return extracted plain text.
|
|
Strips all HTML tags. Truncated to max_chars for LLM context.
|
|
"""
|
|
try:
|
|
headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/120.0"}
|
|
resp = requests.get(url, headers=headers, timeout=15)
|
|
resp.raise_for_status()
|
|
|
|
soup = BeautifulSoup(resp.text, "html.parser")
|
|
|
|
# remove nav/footer/script noise
|
|
for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
|
|
tag.decompose()
|
|
|
|
text = soup.get_text(separator="\n", strip=True)
|
|
|
|
# collapse blank lines
|
|
lines = [l for l in text.splitlines() if l.strip()]
|
|
clean = "\n".join(lines)
|
|
|
|
if len(clean) > max_chars:
|
|
clean = clean[:max_chars] + f"\n... [truncated at {max_chars} chars]"
|
|
|
|
return clean
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
return "[!] Could not connect to URL — check network."
|
|
except requests.exceptions.Timeout:
|
|
return "[!] Page fetch timed out."
|
|
except requests.exceptions.HTTPError as e:
|
|
return f"[!] HTTP error: {e}"
|
|
except Exception as e:
|
|
return f"[!] Fetch failed: {e}"
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# TOOL DISPATCH HANDLER
|
|
# ─────────────────────────────────────────────
|
|
|
|
def handle_search_dispatch(query: str) -> str:
|
|
"""
|
|
Called by llm.py when AI writes [SEARCH: something].
|
|
Smartly routes to CVE lookup, exploit search, or general search.
|
|
"""
|
|
query = query.strip()
|
|
|
|
# CVE pattern — CVE-YYYY-NNNNN
|
|
import re
|
|
cve_pattern = re.compile(r'CVE-\d{4}-\d{4,7}', re.IGNORECASE)
|
|
cve_match = cve_pattern.search(query)
|
|
if cve_match:
|
|
return search_cve(cve_match.group())
|
|
|
|
# exploit keywords
|
|
if any(word in query.lower() for word in ["exploit", "poc", "payload", "rce", "lfi", "sqli"]):
|
|
return web_search(query + " exploit poc github", max_results=5)
|
|
|
|
# fix/patch keywords
|
|
if any(word in query.lower() for word in ["fix", "patch", "mitigate", "harden", "secure"]):
|
|
return search_fix(query)
|
|
|
|
# default general search
|
|
return web_search(query, max_results=5)
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# QUICK TEST
|
|
# ─────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
print("[ search.py test ]\n")
|
|
print("[1] General search")
|
|
print("[2] CVE lookup")
|
|
print("[3] Fetch a URL")
|
|
choice = input("Choice: ").strip()
|
|
|
|
if choice == "1":
|
|
q = input("Query: ").strip()
|
|
print(web_search(q))
|
|
|
|
elif choice == "2":
|
|
cve = input("CVE ID (e.g. CVE-2021-44228): ").strip()
|
|
print(search_cve(cve))
|
|
|
|
elif choice == "3":
|
|
url = input("URL: ").strip()
|
|
print(fetch_page(url))
|