add web UI: HTTP server, templates, dark theme CSS, D3.js graph visualization
This commit is contained in:
parent
2456047f8c
commit
0ee2f7da9a
11 changed files with 1376 additions and 0 deletions
0
eirescope/web/__init__.py
Normal file
0
eirescope/web/__init__.py
Normal file
287
eirescope/web/app.py
Normal file
287
eirescope/web/app.py
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
"""EireScope Web Server — Lightweight HTTP server with Jinja2 templates."""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
# Add project root to path
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
from eirescope.core.engine import InvestigationEngine
|
||||
from eirescope.core.results import summarize_investigation
|
||||
from eirescope.db.database import Database
|
||||
|
||||
logger = logging.getLogger("eirescope.web")
|
||||
|
||||
# Paths
|
||||
WEB_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
TEMPLATE_DIR = os.path.join(WEB_DIR, "templates")
|
||||
STATIC_DIR = os.path.join(WEB_DIR, "static")
|
||||
DATA_DIR = os.path.join(os.path.dirname(WEB_DIR), "data")
|
||||
|
||||
# Initialize Jinja2
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader(TEMPLATE_DIR),
|
||||
autoescape=True,
|
||||
)
|
||||
|
||||
# Initialize core components — always use /tmp for SQLite (avoids filesystem restrictions)
|
||||
import tempfile
|
||||
_db_path = os.path.join(tempfile.gettempdir(), "eirescope_investigations.db")
|
||||
db = Database(_db_path)
|
||||
engine = InvestigationEngine()
|
||||
|
||||
|
||||
class EireScopeHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP Request handler for EireScope web interface."""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
logger.info(f"{self.client_address[0]} - {format % args}")
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path
|
||||
query = urllib.parse.parse_qs(parsed.query)
|
||||
|
||||
# Static files
|
||||
if path.startswith("/static/"):
|
||||
self._serve_static(path[8:])
|
||||
return
|
||||
|
||||
# Routes
|
||||
if path == "/" or path == "":
|
||||
self._handle_index()
|
||||
elif path.startswith("/investigation/"):
|
||||
inv_id = path.split("/investigation/")[1].strip("/")
|
||||
self._handle_investigation(inv_id)
|
||||
elif path == "/history":
|
||||
self._handle_history()
|
||||
elif path == "/api/modules":
|
||||
self._json_response(engine.get_available_modules())
|
||||
elif path.startswith("/api/investigation/"):
|
||||
inv_id = path.split("/api/investigation/")[1].strip("/")
|
||||
self._handle_api_investigation(inv_id)
|
||||
elif path == "/api/history":
|
||||
self._json_response(db.list_investigations())
|
||||
elif path.startswith("/export/"):
|
||||
inv_id = path.split("/export/")[1].strip("/")
|
||||
self._handle_export(inv_id)
|
||||
else:
|
||||
self._send_error(404, "Page not found")
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
if path == "/api/search":
|
||||
self._handle_search()
|
||||
elif path == "/search":
|
||||
self._handle_search_form()
|
||||
else:
|
||||
self._send_error(404, "Endpoint not found")
|
||||
|
||||
def _handle_index(self):
|
||||
"""Render the search landing page."""
|
||||
modules = engine.get_available_modules()
|
||||
recent = db.list_investigations(limit=10)
|
||||
template = jinja_env.get_template("index.html")
|
||||
html = template.render(
|
||||
modules=modules,
|
||||
recent_investigations=recent,
|
||||
supported_types=engine.get_supported_types(),
|
||||
)
|
||||
self._html_response(html)
|
||||
|
||||
def _handle_search_form(self):
|
||||
"""Handle form-based search submission."""
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length).decode("utf-8")
|
||||
params = urllib.parse.parse_qs(body)
|
||||
|
||||
query = params.get("query", [""])[0].strip()
|
||||
entity_type = params.get("entity_type", [None])[0]
|
||||
if entity_type == "auto":
|
||||
entity_type = None
|
||||
|
||||
if not query:
|
||||
self._redirect("/")
|
||||
return
|
||||
|
||||
try:
|
||||
investigation = engine.investigate(query, entity_type)
|
||||
db.save_investigation(investigation)
|
||||
self._redirect(f"/investigation/{investigation.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Search failed: {e}")
|
||||
template = jinja_env.get_template("index.html")
|
||||
html = template.render(
|
||||
error=str(e),
|
||||
modules=engine.get_available_modules(),
|
||||
recent_investigations=db.list_investigations(limit=10),
|
||||
supported_types=engine.get_supported_types(),
|
||||
query=query,
|
||||
)
|
||||
self._html_response(html)
|
||||
|
||||
def _handle_search(self):
|
||||
"""Handle API search (JSON)."""
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(content_length).decode("utf-8"))
|
||||
|
||||
query = body.get("query", "").strip()
|
||||
entity_type = body.get("entity_type")
|
||||
module_filter = body.get("modules")
|
||||
|
||||
if not query:
|
||||
self._json_response({"error": "Query is required"}, status=400)
|
||||
return
|
||||
|
||||
try:
|
||||
investigation = engine.investigate(query, entity_type, module_filter)
|
||||
db.save_investigation(investigation)
|
||||
summary = summarize_investigation(investigation)
|
||||
self._json_response(summary)
|
||||
except Exception as e:
|
||||
self._json_response({"error": str(e)}, status=400)
|
||||
|
||||
def _handle_investigation(self, inv_id: str):
|
||||
"""Render investigation results page."""
|
||||
investigation = db.load_investigation(inv_id)
|
||||
if not investigation:
|
||||
self._send_error(404, "Investigation not found")
|
||||
return
|
||||
|
||||
summary = summarize_investigation(investigation)
|
||||
template = jinja_env.get_template("investigation.html")
|
||||
html = template.render(inv=summary, json_data=json.dumps(summary))
|
||||
self._html_response(html)
|
||||
|
||||
def _handle_api_investigation(self, inv_id: str):
|
||||
"""Return investigation data as JSON."""
|
||||
investigation = db.load_investigation(inv_id)
|
||||
if not investigation:
|
||||
self._json_response({"error": "Not found"}, status=404)
|
||||
return
|
||||
self._json_response(summarize_investigation(investigation))
|
||||
|
||||
def _handle_history(self):
|
||||
"""Render investigation history page."""
|
||||
investigations = db.list_investigations(limit=50)
|
||||
template = jinja_env.get_template("history.html")
|
||||
html = template.render(investigations=investigations)
|
||||
self._html_response(html)
|
||||
|
||||
def _handle_export(self, inv_id: str):
|
||||
"""Export investigation as HTML report."""
|
||||
investigation = db.load_investigation(inv_id)
|
||||
if not investigation:
|
||||
self._send_error(404, "Investigation not found")
|
||||
return
|
||||
|
||||
summary = summarize_investigation(investigation)
|
||||
report_template_path = os.path.join(
|
||||
os.path.dirname(WEB_DIR), "reporting", "templates", "report.html"
|
||||
)
|
||||
if os.path.exists(report_template_path):
|
||||
report_env = Environment(
|
||||
loader=FileSystemLoader(os.path.dirname(report_template_path)),
|
||||
autoescape=True,
|
||||
)
|
||||
template = report_env.get_template("report.html")
|
||||
else:
|
||||
template = jinja_env.get_template("report.html")
|
||||
|
||||
html = template.render(inv=summary, json_data=json.dumps(summary))
|
||||
self._html_response(html, headers={
|
||||
"Content-Disposition": f'attachment; filename="eirescope-report-{inv_id[:8]}.html"'
|
||||
})
|
||||
|
||||
def _serve_static(self, filepath: str):
|
||||
"""Serve static files (CSS, JS, images)."""
|
||||
full_path = os.path.join(STATIC_DIR, filepath)
|
||||
if not os.path.isfile(full_path):
|
||||
self._send_error(404, "File not found")
|
||||
return
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(full_path)
|
||||
mime_type = mime_type or "application/octet-stream"
|
||||
|
||||
with open(full_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", mime_type)
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.send_header("Cache-Control", "public, max-age=3600")
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
|
||||
def _html_response(self, html: str, status: int = 200, headers: dict = None):
|
||||
content = html.encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
if headers:
|
||||
for k, v in headers.items():
|
||||
self.send_header(k, v)
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
|
||||
def _json_response(self, data, status: int = 200):
|
||||
content = json.dumps(data, indent=2, default=str).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
|
||||
def _redirect(self, location: str):
|
||||
self.send_response(302)
|
||||
self.send_header("Location", location)
|
||||
self.end_headers()
|
||||
|
||||
def _send_error(self, code: int, message: str):
|
||||
try:
|
||||
template = jinja_env.get_template("error.html")
|
||||
html = template.render(code=code, message=message)
|
||||
self._html_response(html, status=code)
|
||||
except Exception:
|
||||
self._html_response(f"<h1>{code}</h1><p>{message}</p>", status=code)
|
||||
|
||||
|
||||
def create_server(host: str = "0.0.0.0", port: int = 5000) -> HTTPServer:
|
||||
"""Create and return the EireScope HTTP server."""
|
||||
server = HTTPServer((host, port), EireScopeHandler)
|
||||
logger.info(f"EireScope server ready at http://{host}:{port}")
|
||||
return server
|
||||
|
||||
|
||||
def run_server(host: str = "0.0.0.0", port: int = 5000):
|
||||
"""Start the EireScope web server."""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||
)
|
||||
server = create_server(host, port)
|
||||
print(f"""
|
||||
╔══════════════════════════════════════════════╗
|
||||
║ 🔍 EireScope OSINT v0.1.0 ║
|
||||
║ Open-Source Intelligence Dashboard ║
|
||||
╠══════════════════════════════════════════════╣
|
||||
║ Server running at: ║
|
||||
║ → http://localhost:{port} ║
|
||||
║ ║
|
||||
║ Press Ctrl+C to stop ║
|
||||
╚══════════════════════════════════════════════╝
|
||||
""")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down EireScope...")
|
||||
server.shutdown()
|
||||
340
eirescope/web/static/css/style.css
Normal file
340
eirescope/web/static/css/style.css
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
/* EireScope — OSINT Dashboard Stylesheet */
|
||||
/* Dark theme inspired by intelligence/law enforcement interfaces */
|
||||
|
||||
:root {
|
||||
--bg-primary: #0a0e17;
|
||||
--bg-secondary: #111827;
|
||||
--bg-card: #1a2332;
|
||||
--bg-input: #0d1421;
|
||||
--border: #1e2d3d;
|
||||
--border-focus: #3b82f6;
|
||||
--text-primary: #e2e8f0;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--accent-glow: rgba(59, 130, 246, 0.3);
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--info: #06b6d4;
|
||||
/* Entity type colors */
|
||||
--color-email: #8b5cf6;
|
||||
--color-username: #3b82f6;
|
||||
--color-phone: #10b981;
|
||||
--color-ip_address: #f59e0b;
|
||||
--color-domain: #06b6d4;
|
||||
--color-social_profile: #ec4899;
|
||||
--color-breach: #ef4444;
|
||||
--color-whois_info: #a78bfa;
|
||||
--color-geo_location: #34d399;
|
||||
--color-carrier_info: #fbbf24;
|
||||
--color-dns_record: #67e8f9;
|
||||
--color-company: #f472b6;
|
||||
--color-person: #c084fc;
|
||||
--color-url: #38bdf8;
|
||||
--color-hash: #fb923c;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
.mono { font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace; font-size: 0.9em; }
|
||||
.accent { color: var(--accent); }
|
||||
|
||||
/* Navbar */
|
||||
.navbar {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 12px 30px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky; top: 0; z-index: 100;
|
||||
}
|
||||
.nav-brand a {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
color: var(--text-primary); text-decoration: none; font-weight: 700; font-size: 18px;
|
||||
}
|
||||
.brand-icon { font-size: 22px; }
|
||||
.brand-tag {
|
||||
font-size: 10px; background: var(--accent); color: white;
|
||||
padding: 1px 6px; border-radius: 4px; text-transform: uppercase;
|
||||
letter-spacing: 1px; font-weight: 600;
|
||||
}
|
||||
.nav-links { display: flex; gap: 20px; }
|
||||
.nav-link {
|
||||
color: var(--text-secondary); font-size: 14px; font-weight: 500;
|
||||
padding: 6px 12px; border-radius: 6px; transition: all 0.2s;
|
||||
}
|
||||
.nav-link:hover { color: var(--text-primary); background: var(--bg-card); text-decoration: none; }
|
||||
|
||||
/* Container */
|
||||
.container { max-width: 1300px; margin: 0 auto; padding: 30px; }
|
||||
|
||||
/* Hero */
|
||||
.hero { text-align: center; padding: 40px 0 20px; }
|
||||
.hero h1 { font-size: 42px; font-weight: 800; letter-spacing: -1px; }
|
||||
.hero-sub { color: var(--text-secondary); font-size: 16px; margin-top: 6px; }
|
||||
|
||||
/* Search */
|
||||
.search-container { max-width: 800px; margin: 25px auto 40px; }
|
||||
.search-form { width: 100%; }
|
||||
.search-input-group {
|
||||
display: flex; gap: 8px;
|
||||
background: var(--bg-card); padding: 8px;
|
||||
border-radius: 12px; border: 1px solid var(--border);
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.search-input-group:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
.search-input {
|
||||
flex: 1; padding: 12px 16px;
|
||||
background: var(--bg-input); color: var(--text-primary);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
font-size: 15px; outline: none;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
}
|
||||
.search-input::placeholder { color: var(--text-muted); }
|
||||
.search-input:focus { border-color: var(--accent); }
|
||||
.type-select {
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-input); color: var(--text-primary);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
font-size: 13px; cursor: pointer; outline: none;
|
||||
}
|
||||
.type-select:focus { border-color: var(--accent); }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 10px 20px; border-radius: 8px;
|
||||
font-size: 14px; font-weight: 600; cursor: pointer;
|
||||
border: 1px solid transparent; transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: var(--accent-hover); text-decoration: none; }
|
||||
.btn-secondary { background: var(--bg-card); color: var(--text-primary); border-color: var(--border); }
|
||||
.btn-secondary:hover { border-color: var(--accent); text-decoration: none; }
|
||||
.btn-sm { padding: 5px 12px; font-size: 12px; border-radius: 6px; }
|
||||
.btn-ghost { background: transparent; color: var(--accent); border: none; padding: 4px 8px; }
|
||||
.search-btn { padding: 12px 28px; font-size: 15px; white-space: nowrap; }
|
||||
|
||||
/* Info Cards */
|
||||
.info-cards {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px; margin: 30px 0;
|
||||
}
|
||||
.card {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 24px; text-align: center;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
}
|
||||
.card:hover { border-color: var(--accent); transform: translateY(-2px); }
|
||||
.card-icon { font-size: 32px; margin-bottom: 10px; }
|
||||
.card h3 { font-size: 16px; margin-bottom: 6px; }
|
||||
.card p { color: var(--text-secondary); font-size: 13px; }
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px; margin: 20px 0;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 20px; text-align: center;
|
||||
}
|
||||
.stat-value { font-size: 36px; font-weight: 800; color: var(--accent); }
|
||||
.stat-label { color: var(--text-secondary); font-size: 12px; text-transform: uppercase; letter-spacing: 1px; }
|
||||
|
||||
/* Tables */
|
||||
.data-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||
.data-table th {
|
||||
background: var(--bg-card); color: var(--text-secondary);
|
||||
padding: 10px 14px; text-align: left; font-size: 12px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
.data-table td {
|
||||
padding: 10px 14px; border-bottom: 1px solid var(--border);
|
||||
font-size: 13px; vertical-align: middle;
|
||||
}
|
||||
.data-table tr:hover { background: rgba(59, 130, 246, 0.05); }
|
||||
.entity-value { max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block; padding: 2px 10px; border-radius: 6px;
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.badge-email { background: rgba(139,92,246,0.15); color: var(--color-email); }
|
||||
.badge-username { background: rgba(59,130,246,0.15); color: var(--color-username); }
|
||||
.badge-phone { background: rgba(16,185,129,0.15); color: var(--color-phone); }
|
||||
.badge-ip_address { background: rgba(245,158,11,0.15); color: var(--color-ip_address); }
|
||||
.badge-domain { background: rgba(6,182,212,0.15); color: var(--color-domain); }
|
||||
.badge-social_profile { background: rgba(236,72,153,0.15); color: var(--color-social_profile); }
|
||||
.badge-breach { background: rgba(239,68,68,0.15); color: var(--color-breach); }
|
||||
.badge-whois_info { background: rgba(167,139,250,0.15); color: var(--color-whois_info); }
|
||||
.badge-geo_location { background: rgba(52,211,153,0.15); color: var(--color-geo_location); }
|
||||
.badge-carrier_info { background: rgba(251,191,36,0.15); color: var(--color-carrier_info); }
|
||||
.badge-dns_record { background: rgba(103,232,249,0.15); color: var(--color-dns_record); }
|
||||
.badge-url { background: rgba(56,189,248,0.15); color: var(--color-url); }
|
||||
.badge-hash { background: rgba(251,146,60,0.15); color: var(--color-hash); }
|
||||
.badge-warning { background: rgba(245,158,11,0.15); color: var(--warning); }
|
||||
|
||||
/* Status */
|
||||
.status { font-size: 12px; font-weight: 600; }
|
||||
.status-completed { color: var(--success); }
|
||||
.status-running { color: var(--warning); }
|
||||
.status-failed { color: var(--danger); }
|
||||
.status-pending { color: var(--text-muted); }
|
||||
|
||||
/* Confidence bar */
|
||||
.confidence-bar {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--bg-input); border-radius: 4px; height: 20px;
|
||||
position: relative; min-width: 80px; overflow: hidden;
|
||||
}
|
||||
.confidence-fill {
|
||||
height: 100%; background: var(--accent); border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.confidence-bar span {
|
||||
position: absolute; right: 6px; font-size: 11px; font-weight: 600; color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; gap: 4px; margin: 25px 0 0; border-bottom: 2px solid var(--border); }
|
||||
.tab {
|
||||
padding: 10px 20px; background: none; border: none;
|
||||
color: var(--text-secondary); font-size: 14px; font-weight: 500;
|
||||
cursor: pointer; border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px; transition: all 0.2s;
|
||||
}
|
||||
.tab:hover { color: var(--text-primary); }
|
||||
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
.tab-content { display: none; padding: 20px 0; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
/* Graph */
|
||||
.graph-container {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: 12px; height: 500px; position: relative; overflow: hidden;
|
||||
}
|
||||
.graph-legend {
|
||||
display: flex; flex-wrap: wrap; gap: 14px;
|
||||
padding: 12px 0; justify-content: center;
|
||||
}
|
||||
.legend-item { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--text-secondary); }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
||||
.dot-email { background: var(--color-email); }
|
||||
.dot-username { background: var(--color-username); }
|
||||
.dot-domain { background: var(--color-domain); }
|
||||
.dot-ip_address { background: var(--color-ip_address); }
|
||||
.dot-social_profile { background: var(--color-social_profile); }
|
||||
.dot-phone { background: var(--color-phone); }
|
||||
.dot-breach { background: var(--color-breach); }
|
||||
.dot-whois_info { background: var(--color-whois_info); }
|
||||
.dot-geo_location { background: var(--color-geo_location); }
|
||||
|
||||
/* Filters */
|
||||
.entity-filters { display: flex; gap: 10px; margin-bottom: 15px; }
|
||||
.filter-input {
|
||||
flex: 1; padding: 8px 14px;
|
||||
background: var(--bg-input); color: var(--text-primary);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
font-size: 13px; outline: none;
|
||||
}
|
||||
.filter-input:focus { border-color: var(--accent); }
|
||||
|
||||
/* Entity details */
|
||||
.entity-details { margin-top: 8px; }
|
||||
.entity-details pre {
|
||||
background: var(--bg-input); padding: 10px;
|
||||
border-radius: 6px; font-size: 11px;
|
||||
color: var(--text-secondary); overflow-x: auto;
|
||||
max-height: 200px; overflow-y: auto;
|
||||
white-space: pre-wrap; word-break: break-all;
|
||||
}
|
||||
.hidden { display: none; }
|
||||
|
||||
/* Distribution bars */
|
||||
.type-distribution { margin: 10px 0 20px; }
|
||||
.dist-row { display: flex; align-items: center; gap: 12px; margin: 6px 0; }
|
||||
.dist-bar-container { flex: 1; background: var(--bg-input); height: 20px; border-radius: 4px; overflow: hidden; }
|
||||
.dist-bar { height: 100%; background: var(--accent); border-radius: 4px; min-width: 4px; transition: width 0.5s; }
|
||||
.dist-bar-rel { background: var(--info); }
|
||||
.dist-count { font-size: 13px; font-weight: 600; min-width: 30px; text-align: right; }
|
||||
.rel-type { font-size: 12px; color: var(--text-secondary); min-width: 200px; font-family: monospace; }
|
||||
|
||||
/* Module list */
|
||||
.module-list { list-style: none; }
|
||||
.module-item { padding: 10px 14px; border-bottom: 1px solid var(--border); font-size: 14px; }
|
||||
.module-failed { color: var(--danger); }
|
||||
.module-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; }
|
||||
.module-card {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 18px;
|
||||
}
|
||||
.module-card h4 { margin-bottom: 6px; }
|
||||
.module-card p { color: var(--text-secondary); font-size: 13px; margin-bottom: 10px; }
|
||||
.module-types { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
|
||||
/* Investigation header */
|
||||
.investigation-header {
|
||||
display: flex; justify-content: space-between; align-items: flex-start;
|
||||
flex-wrap: wrap; gap: 15px; margin-bottom: 20px;
|
||||
}
|
||||
.inv-info h1 { font-size: 24px; }
|
||||
.inv-meta { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 8px; }
|
||||
.meta-item { color: var(--text-muted); font-size: 12px; }
|
||||
.inv-actions { display: flex; gap: 8px; }
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 14px 18px; border-radius: 10px; margin: 15px 0;
|
||||
font-size: 14px; border: 1px solid;
|
||||
}
|
||||
.alert-error { background: rgba(239,68,68,0.1); border-color: var(--danger); color: var(--danger); }
|
||||
|
||||
/* Section */
|
||||
.section { margin: 35px 0; }
|
||||
.section h2 { font-size: 20px; margin-bottom: 15px; }
|
||||
|
||||
/* Error page */
|
||||
.error-page { text-align: center; padding: 80px 0; }
|
||||
.error-code { font-size: 72px; font-weight: 800; color: var(--accent); }
|
||||
.error-message { color: var(--text-secondary); font-size: 18px; margin: 10px 0 30px; }
|
||||
|
||||
/* Empty state */
|
||||
.empty-state { text-align: center; padding: 60px 0; color: var(--text-secondary); }
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
text-align: center; padding: 20px 30px; margin-top: 40px;
|
||||
border-top: 1px solid var(--border); color: var(--text-muted); font-size: 12px;
|
||||
}
|
||||
.footer-note { margin-top: 4px; font-size: 11px; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container { padding: 15px; }
|
||||
.search-input-group { flex-direction: column; }
|
||||
.hero h1 { font-size: 28px; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.investigation-header { flex-direction: column; }
|
||||
.tabs { overflow-x: auto; }
|
||||
.graph-container { height: 350px; }
|
||||
}
|
||||
42
eirescope/web/static/js/app.js
Normal file
42
eirescope/web/static/js/app.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* EireScope — Main application JavaScript */
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
const target = document.getElementById('tab-' + tab.dataset.tab);
|
||||
if (target) target.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle entity details
|
||||
function toggleDetails(btn) {
|
||||
const details = btn.nextElementSibling;
|
||||
if (details) {
|
||||
details.classList.toggle('hidden');
|
||||
btn.textContent = details.classList.contains('hidden') ? 'Show' : 'Hide';
|
||||
}
|
||||
}
|
||||
|
||||
// Entity filtering
|
||||
const searchInput = document.getElementById('entity-search');
|
||||
const typeFilter = document.getElementById('entity-type-filter');
|
||||
|
||||
function filterEntities() {
|
||||
const search = (searchInput ? searchInput.value : '').toLowerCase();
|
||||
const type = typeFilter ? typeFilter.value : 'all';
|
||||
const rows = document.querySelectorAll('#entities-table tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const rowType = row.dataset.type || '';
|
||||
const rowValue = row.dataset.value || '';
|
||||
const matchesType = type === 'all' || rowType === type;
|
||||
const matchesSearch = !search || rowValue.includes(search) || rowType.includes(search);
|
||||
row.style.display = matchesType && matchesSearch ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
if (searchInput) searchInput.addEventListener('input', filterEntities);
|
||||
if (typeFilter) typeFilter.addEventListener('change', filterEntities);
|
||||
197
eirescope/web/static/js/graph.js
Normal file
197
eirescope/web/static/js/graph.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/* EireScope — D3.js Entity Relationship Graph Visualization */
|
||||
|
||||
(function() {
|
||||
if (typeof investigationData === 'undefined' || !investigationData.graph) return;
|
||||
|
||||
const graph = investigationData.graph;
|
||||
if (!graph.nodes.length) return;
|
||||
|
||||
const container = document.getElementById('entity-graph');
|
||||
if (!container) return;
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight || 500;
|
||||
|
||||
// Color mapping for entity types
|
||||
const typeColors = {
|
||||
email: '#8b5cf6',
|
||||
username: '#3b82f6',
|
||||
phone: '#10b981',
|
||||
ip_address: '#f59e0b',
|
||||
domain: '#06b6d4',
|
||||
social_profile: '#ec4899',
|
||||
breach: '#ef4444',
|
||||
whois_info: '#a78bfa',
|
||||
geo_location: '#34d399',
|
||||
carrier_info: '#fbbf24',
|
||||
dns_record: '#67e8f9',
|
||||
company: '#f472b6',
|
||||
person: '#c084fc',
|
||||
url: '#38bdf8',
|
||||
hash: '#fb923c'
|
||||
};
|
||||
|
||||
// Node size by type importance
|
||||
const typeSizes = {
|
||||
email: 14, username: 14, phone: 14, ip_address: 12,
|
||||
domain: 12, social_profile: 8, breach: 10, whois_info: 8,
|
||||
geo_location: 9, carrier_info: 8, dns_record: 7, url: 7
|
||||
};
|
||||
|
||||
const svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('viewBox', [0, 0, width, height]);
|
||||
|
||||
// Background
|
||||
svg.append('rect')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('fill', '#1a2332');
|
||||
|
||||
// Zoom behavior
|
||||
const g = svg.append('g');
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.3, 5])
|
||||
.on('zoom', (event) => g.attr('transform', event.transform));
|
||||
svg.call(zoom);
|
||||
|
||||
// Build simulation
|
||||
const simulation = d3.forceSimulation(graph.nodes)
|
||||
.force('link', d3.forceLink(graph.links).id(d => d.id).distance(100))
|
||||
.force('charge', d3.forceManyBody().strength(-200))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(d => (typeSizes[d.type] || 8) + 5));
|
||||
|
||||
// Links
|
||||
const link = g.append('g')
|
||||
.selectAll('line')
|
||||
.data(graph.links)
|
||||
.join('line')
|
||||
.attr('stroke', '#2d3748')
|
||||
.attr('stroke-width', d => Math.max(1, d.confidence * 2))
|
||||
.attr('stroke-opacity', 0.6);
|
||||
|
||||
// Link labels
|
||||
const linkLabel = g.append('g')
|
||||
.selectAll('text')
|
||||
.data(graph.links)
|
||||
.join('text')
|
||||
.text(d => d.type.replace(/_/g, ' '))
|
||||
.attr('font-size', '8px')
|
||||
.attr('fill', '#4a5568')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', -4);
|
||||
|
||||
// Nodes
|
||||
const node = g.append('g')
|
||||
.selectAll('g')
|
||||
.data(graph.nodes)
|
||||
.join('g')
|
||||
.call(d3.drag()
|
||||
.on('start', dragStarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragEnded));
|
||||
|
||||
// Node circles
|
||||
node.append('circle')
|
||||
.attr('r', d => typeSizes[d.type] || 8)
|
||||
.attr('fill', d => typeColors[d.type] || '#666')
|
||||
.attr('stroke', '#0a0e17')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('opacity', 0.9);
|
||||
|
||||
// Glow effect for seed entity (first node)
|
||||
node.filter((d, i) => i === 0)
|
||||
.select('circle')
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 3)
|
||||
.attr('r', 18);
|
||||
|
||||
// Node labels
|
||||
node.append('text')
|
||||
.text(d => d.label.length > 25 ? d.label.substring(0, 25) + '...' : d.label)
|
||||
.attr('x', d => (typeSizes[d.type] || 8) + 6)
|
||||
.attr('y', 4)
|
||||
.attr('font-size', '11px')
|
||||
.attr('fill', '#cbd5e1')
|
||||
.attr('font-family', "'JetBrains Mono', monospace");
|
||||
|
||||
// Tooltip
|
||||
const tooltip = d3.select(container)
|
||||
.append('div')
|
||||
.style('position', 'absolute')
|
||||
.style('background', '#1e293b')
|
||||
.style('border', '1px solid #334155')
|
||||
.style('border-radius', '8px')
|
||||
.style('padding', '10px 14px')
|
||||
.style('font-size', '12px')
|
||||
.style('color', '#e2e8f0')
|
||||
.style('pointer-events', 'none')
|
||||
.style('opacity', 0)
|
||||
.style('z-index', 10)
|
||||
.style('max-width', '300px');
|
||||
|
||||
node.on('mouseover', function(event, d) {
|
||||
tooltip.transition().duration(200).style('opacity', 1);
|
||||
tooltip.html(`
|
||||
<div style="font-weight:700;margin-bottom:4px;color:${typeColors[d.type] || '#fff'}">${d.type.toUpperCase()}</div>
|
||||
<div style="font-family:monospace;word-break:break-all">${d.label}</div>
|
||||
<div style="margin-top:4px;color:#94a3b8">Source: ${d.source}</div>
|
||||
<div style="color:#94a3b8">Confidence: ${Math.round(d.confidence * 100)}%</div>
|
||||
`)
|
||||
.style('left', (event.offsetX + 15) + 'px')
|
||||
.style('top', (event.offsetY - 10) + 'px');
|
||||
|
||||
d3.select(this).select('circle')
|
||||
.transition().duration(200)
|
||||
.attr('r', (typeSizes[d.type] || 8) * 1.4);
|
||||
})
|
||||
.on('mouseout', function(event, d) {
|
||||
tooltip.transition().duration(200).style('opacity', 0);
|
||||
const isFirst = graph.nodes.indexOf(d) === 0;
|
||||
d3.select(this).select('circle')
|
||||
.transition().duration(200)
|
||||
.attr('r', isFirst ? 18 : (typeSizes[d.type] || 8));
|
||||
});
|
||||
|
||||
// Simulation tick
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
linkLabel
|
||||
.attr('x', d => (d.source.x + d.target.x) / 2)
|
||||
.attr('y', d => (d.source.y + d.target.y) / 2);
|
||||
|
||||
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Drag functions
|
||||
function dragStarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x; d.fy = d.y;
|
||||
}
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x; d.fy = event.y;
|
||||
}
|
||||
function dragEnded(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null; d.fy = null;
|
||||
}
|
||||
|
||||
// Fit to view on load
|
||||
setTimeout(() => {
|
||||
const bounds = g.node().getBBox();
|
||||
const dx = bounds.width, dy = bounds.height;
|
||||
const x = bounds.x + dx / 2, y = bounds.y + dy / 2;
|
||||
const scale = 0.85 / Math.max(dx / width, dy / height);
|
||||
const translate = [width / 2 - scale * x, height / 2 - scale * y];
|
||||
svg.transition().duration(750)
|
||||
.call(zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
|
||||
}, 2000);
|
||||
})();
|
||||
37
eirescope/web/templates/base.html
Normal file
37
eirescope/web/templates/base.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}EireScope{% endblock %} — OSINT Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">
|
||||
<a href="/">
|
||||
<span class="brand-icon">🔍</span>
|
||||
<span class="brand-text">EireScope</span>
|
||||
<span class="brand-tag">OSINT</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link">Search</a>
|
||||
<a href="/history" class="nav-link">History</a>
|
||||
<a href="/api/modules" class="nav-link">Modules</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>EireScope v0.1.0 — Open-Source Intelligence Dashboard</p>
|
||||
<p class="footer-note">All data from publicly available sources.</p>
|
||||
</footer>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
10
eirescope/web/templates/error.html
Normal file
10
eirescope/web/templates/error.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Error {{ code }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-page">
|
||||
<h1 class="error-code">{{ code }}</h1>
|
||||
<p class="error-message">{{ message }}</p>
|
||||
<a href="/" class="btn btn-primary">Back to Search</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
eirescope/web/templates/history.html
Normal file
40
eirescope/web/templates/history.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Investigation History{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Investigation History</h1>
|
||||
|
||||
{% if investigations %}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Query</th>
|
||||
<th>Type</th>
|
||||
<th>Entities</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for inv in investigations %}
|
||||
<tr>
|
||||
<td class="mono">{{ inv.initial_query }}</td>
|
||||
<td><span class="badge badge-{{ inv.initial_type }}">{{ inv.initial_type }}</span></td>
|
||||
<td>{{ inv.entity_count }}</td>
|
||||
<td><span class="status status-{{ inv.status }}">{{ inv.status }}</span></td>
|
||||
<td>{{ inv.created_at[:16] }}</td>
|
||||
<td>
|
||||
<a href="/investigation/{{ inv.id }}" class="btn btn-sm">View</a>
|
||||
<a href="/export/{{ inv.id }}" class="btn btn-sm btn-secondary">Export</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p>No investigations yet. <a href="/">Start your first investigation</a>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
115
eirescope/web/templates/index.html
Normal file
115
eirescope/web/templates/index.html
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hero">
|
||||
<h1>EireScope <span class="accent">OSINT</span></h1>
|
||||
<p class="hero-sub">Open-Source Intelligence Investigation Dashboard</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error">
|
||||
<strong>Error:</strong> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="search-container">
|
||||
<form action="/search" method="POST" class="search-form">
|
||||
<div class="search-input-group">
|
||||
<input
|
||||
type="text"
|
||||
name="query"
|
||||
class="search-input"
|
||||
placeholder="Enter email, username, phone, IP address, or domain..."
|
||||
value="{{ query or '' }}"
|
||||
autofocus
|
||||
required
|
||||
>
|
||||
<select name="entity_type" class="type-select">
|
||||
<option value="auto">Auto-detect</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="username">Username</option>
|
||||
<option value="phone">Phone Number</option>
|
||||
<option value="ip_address">IP Address</option>
|
||||
<option value="domain">Domain</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary search-btn">
|
||||
Investigate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="info-cards">
|
||||
<div class="card">
|
||||
<div class="card-icon">👤</div>
|
||||
<h3>Username Search</h3>
|
||||
<p>Scan 40+ platforms for matching profiles</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📧</div>
|
||||
<h3>Email Enrichment</h3>
|
||||
<p>Breach checks, domain analysis, provider detection</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📱</div>
|
||||
<h3>Phone Analysis</h3>
|
||||
<p>Carrier detection, Irish number classification</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🌐</div>
|
||||
<h3>IP/Domain Recon</h3>
|
||||
<p>WHOIS, GeoIP, DNS records, subdomain enumeration</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if recent_investigations %}
|
||||
<div class="section">
|
||||
<h2>Recent Investigations</h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Query</th>
|
||||
<th>Type</th>
|
||||
<th>Entities</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for inv in recent_investigations %}
|
||||
<tr>
|
||||
<td class="mono">{{ inv.initial_query }}</td>
|
||||
<td><span class="badge badge-{{ inv.initial_type }}">{{ inv.initial_type }}</span></td>
|
||||
<td>{{ inv.entity_count }}</td>
|
||||
<td><span class="status status-{{ inv.status }}">{{ inv.status }}</span></td>
|
||||
<td>{{ inv.created_at[:16] }}</td>
|
||||
<td><a href="/investigation/{{ inv.id }}" class="btn btn-sm">View</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="section modules-section">
|
||||
<h2>Available Modules</h2>
|
||||
<div class="module-grid">
|
||||
{% for module in modules %}
|
||||
<div class="module-card">
|
||||
<h4>{{ module.name }}</h4>
|
||||
<p>{{ module.description }}</p>
|
||||
<div class="module-types">
|
||||
{% for t in module.supported_types %}
|
||||
<span class="badge badge-{{ t }}">{{ t }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if module.requires_api_key %}
|
||||
<span class="badge badge-warning">API Key Required</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
173
eirescope/web/templates/investigation.html
Normal file
173
eirescope/web/templates/investigation.html
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Investigation: {{ inv.query }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="investigation-header">
|
||||
<div class="inv-info">
|
||||
<h1>Investigation: <span class="mono accent">{{ inv.query }}</span></h1>
|
||||
<div class="inv-meta">
|
||||
<span class="badge badge-{{ inv.query_type }}">{{ inv.query_type }}</span>
|
||||
<span class="status status-{{ inv.status }}">{{ inv.status }}</span>
|
||||
<span class="meta-item">Started: {{ inv.created_at[:19] }}</span>
|
||||
{% if inv.completed_at %}
|
||||
<span class="meta-item">Completed: {{ inv.completed_at[:19] }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inv-actions">
|
||||
<a href="/export/{{ inv.id }}" class="btn btn-secondary">Export Report</a>
|
||||
<a href="/" class="btn btn-primary">New Search</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ inv.total_entities }}</div>
|
||||
<div class="stat-label">Entities Found</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ inv.total_relationships }}</div>
|
||||
<div class="stat-label">Relationships</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ inv.modules_run|length }}</div>
|
||||
<div class="stat-label">Modules Run</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ inv.entity_counts|length }}</div>
|
||||
<div class="stat-label">Entity Types</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="graph">Relationship Graph</button>
|
||||
<button class="tab" data-tab="entities">Entities ({{ inv.total_entities }})</button>
|
||||
<button class="tab" data-tab="details">Details</button>
|
||||
<button class="tab" data-tab="modules">Modules</button>
|
||||
</div>
|
||||
|
||||
<!-- Graph Tab -->
|
||||
<div class="tab-content active" id="tab-graph">
|
||||
<div class="graph-container" id="entity-graph"></div>
|
||||
<div class="graph-legend">
|
||||
<span class="legend-item"><span class="dot dot-email"></span> Email</span>
|
||||
<span class="legend-item"><span class="dot dot-username"></span> Username</span>
|
||||
<span class="legend-item"><span class="dot dot-domain"></span> Domain</span>
|
||||
<span class="legend-item"><span class="dot dot-ip_address"></span> IP Address</span>
|
||||
<span class="legend-item"><span class="dot dot-social_profile"></span> Social Profile</span>
|
||||
<span class="legend-item"><span class="dot dot-phone"></span> Phone</span>
|
||||
<span class="legend-item"><span class="dot dot-breach"></span> Breach</span>
|
||||
<span class="legend-item"><span class="dot dot-whois_info"></span> WHOIS</span>
|
||||
<span class="legend-item"><span class="dot dot-geo_location"></span> Location</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entities Tab -->
|
||||
<div class="tab-content" id="tab-entities">
|
||||
<div class="entity-filters">
|
||||
<input type="text" id="entity-search" placeholder="Filter entities..." class="filter-input">
|
||||
<select id="entity-type-filter" class="type-select">
|
||||
<option value="all">All Types</option>
|
||||
{% for type_name, count in inv.entity_counts.items() %}
|
||||
<option value="{{ type_name }}">{{ type_name }} ({{ count }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<table class="data-table" id="entities-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Value</th>
|
||||
<th>Source</th>
|
||||
<th>Confidence</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entity in inv.entities %}
|
||||
<tr data-type="{{ entity.entity_type }}" data-value="{{ entity.value|lower }}">
|
||||
<td><span class="badge badge-{{ entity.entity_type }}">{{ entity.entity_type }}</span></td>
|
||||
<td class="mono entity-value">
|
||||
{% if entity.value.startswith('http') %}
|
||||
<a href="{{ entity.value }}" target="_blank" rel="noopener">{{ entity.value }}</a>
|
||||
{% else %}
|
||||
{{ entity.value }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ entity.source_module }}</td>
|
||||
<td>
|
||||
<div class="confidence-bar">
|
||||
<div class="confidence-fill" style="width: {{ (entity.confidence * 100)|int }}%"></div>
|
||||
<span>{{ (entity.confidence * 100)|int }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if entity.metadata %}
|
||||
<button class="btn btn-sm btn-ghost" onclick="toggleDetails(this)">Show</button>
|
||||
<div class="entity-details hidden">
|
||||
<pre>{{ entity.metadata|tojson(indent=2) }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Details Tab -->
|
||||
<div class="tab-content" id="tab-details">
|
||||
<h3>Entity Type Distribution</h3>
|
||||
<div class="type-distribution">
|
||||
{% for type_name, count in inv.entity_counts.items() %}
|
||||
<div class="dist-row">
|
||||
<span class="badge badge-{{ type_name }}">{{ type_name }}</span>
|
||||
<div class="dist-bar-container">
|
||||
<div class="dist-bar" style="width: {{ (count / inv.total_entities * 100)|int }}%"></div>
|
||||
</div>
|
||||
<span class="dist-count">{{ count }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h3>Relationship Types</h3>
|
||||
<div class="type-distribution">
|
||||
{% for rel_type, count in inv.relationship_counts.items() %}
|
||||
<div class="dist-row">
|
||||
<span class="rel-type">{{ rel_type }}</span>
|
||||
<div class="dist-bar-container">
|
||||
<div class="dist-bar dist-bar-rel" style="width: {{ (count / inv.total_relationships * 100)|int }}%"></div>
|
||||
</div>
|
||||
<span class="dist-count">{{ count }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modules Tab -->
|
||||
<div class="tab-content" id="tab-modules">
|
||||
<h3>Modules Executed</h3>
|
||||
<ul class="module-list">
|
||||
{% for mod in inv.modules_run %}
|
||||
<li class="module-item {% if 'FAILED' in mod %}module-failed{% endif %}">
|
||||
{% if 'FAILED' in mod %}❌{% else %}✅{% endif %}
|
||||
{{ mod }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const investigationData = {{ json_data|safe }};
|
||||
</script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/graph.js"></script>
|
||||
{% endblock %}
|
||||
135
eirescope/web/templates/report.html
Normal file
135
eirescope/web/templates/report.html
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>EireScope Report — {{ inv.query }}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #1a1a2e; line-height: 1.6; padding: 40px; max-width: 1000px; margin: 0 auto; }
|
||||
.report-header { border-bottom: 3px solid #16213e; padding-bottom: 20px; margin-bottom: 30px; }
|
||||
.report-header h1 { font-size: 28px; color: #16213e; }
|
||||
.report-header .subtitle { color: #666; font-size: 14px; margin-top: 5px; }
|
||||
.report-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px; font-size: 13px; }
|
||||
.report-meta dt { font-weight: bold; color: #16213e; }
|
||||
.report-meta dd { color: #444; }
|
||||
h2 { color: #16213e; border-bottom: 1px solid #ddd; padding-bottom: 8px; margin: 25px 0 15px; font-size: 20px; }
|
||||
h3 { color: #333; margin: 20px 0 10px; font-size: 16px; }
|
||||
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
|
||||
.stat { background: #f5f6fa; padding: 15px; border-radius: 8px; text-align: center; }
|
||||
.stat-val { font-size: 28px; font-weight: bold; color: #0f3460; }
|
||||
.stat-label { font-size: 12px; color: #666; text-transform: uppercase; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 13px; }
|
||||
th { background: #16213e; color: white; padding: 10px 12px; text-align: left; }
|
||||
td { padding: 8px 12px; border-bottom: 1px solid #eee; }
|
||||
tr:nth-child(even) { background: #f9f9fa; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; background: #e0e7ff; color: #1e3a5f; }
|
||||
.confidence { display: inline-block; min-width: 40px; text-align: center; }
|
||||
.mono { font-family: 'Courier New', monospace; font-size: 12px; }
|
||||
.disclaimer { margin-top: 40px; padding: 15px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; font-size: 12px; }
|
||||
.footer-report { margin-top: 30px; text-align: center; color: #999; font-size: 11px; border-top: 1px solid #eee; padding-top: 15px; }
|
||||
@media print { body { padding: 20px; } .stats { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="report-header">
|
||||
<h1>EireScope Investigation Report</h1>
|
||||
<p class="subtitle">Open-Source Intelligence Analysis</p>
|
||||
<dl class="report-meta">
|
||||
<dt>Investigation ID</dt>
|
||||
<dd class="mono">{{ inv.id[:12] }}...</dd>
|
||||
<dt>Target Query</dt>
|
||||
<dd class="mono">{{ inv.query }}</dd>
|
||||
<dt>Entity Type</dt>
|
||||
<dd>{{ inv.query_type }}</dd>
|
||||
<dt>Status</dt>
|
||||
<dd>{{ inv.status }}</dd>
|
||||
<dt>Started</dt>
|
||||
<dd>{{ inv.created_at }}</dd>
|
||||
<dt>Completed</dt>
|
||||
<dd>{{ inv.completed_at or 'N/A' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h2>Summary</h2>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-val">{{ inv.total_entities }}</div>
|
||||
<div class="stat-label">Entities</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val">{{ inv.total_relationships }}</div>
|
||||
<div class="stat-label">Relationships</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val">{{ inv.modules_run|length }}</div>
|
||||
<div class="stat-label">Modules Run</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val">{{ inv.entity_counts|length }}</div>
|
||||
<div class="stat-label">Entity Types</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Modules Executed</h2>
|
||||
<ul>
|
||||
{% for mod in inv.modules_run %}
|
||||
<li>{% if 'FAILED' in mod %}❌{% else %}✅{% endif %} {{ mod }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Discovered Entities</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Value</th>
|
||||
<th>Source Module</th>
|
||||
<th>Confidence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entity in inv.entities %}
|
||||
<tr>
|
||||
<td><span class="badge">{{ entity.entity_type }}</span></td>
|
||||
<td class="mono">{{ entity.value }}</td>
|
||||
<td>{{ entity.source_module }}</td>
|
||||
<td class="confidence">{{ (entity.confidence * 100)|int }}%</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Entity Relationships</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Relationship</th>
|
||||
<th>Target</th>
|
||||
<th>Confidence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rel in inv.relationships %}
|
||||
<tr>
|
||||
<td class="mono">{{ rel.source_entity_id[:8] }}...</td>
|
||||
<td>{{ rel.relationship_type }}</td>
|
||||
<td class="mono">{{ rel.target_entity_id[:8] }}...</td>
|
||||
<td class="confidence">{{ (rel.confidence * 100)|int }}%</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="disclaimer">
|
||||
<strong>Disclaimer:</strong> This report contains information gathered from publicly available
|
||||
sources only. The accuracy of this data depends on the reliability of external sources.
|
||||
All intelligence should be verified through independent means before use in any legal proceedings.
|
||||
</div>
|
||||
|
||||
<div class="footer-report">
|
||||
<p>Generated by EireScope v0.1.0 — Open-Source Intelligence Dashboard</p>
|
||||
<p>Report generated at {{ inv.completed_at or inv.created_at }}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue