Upload files to "redflare/modules"
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from redflare.core.models import Finding, ModuleResult, Target
|
||||
from .base import Module, ModuleContext
|
||||
from .headers import SECURITY_HEADERS
|
||||
from .http import request
|
||||
|
||||
INTERESTING = ("api", "graphql", "admin", "auth", "token", "upload", "config", "internal")
|
||||
|
||||
|
||||
class NativeBrowserRuntimeModule(Module):
|
||||
name = "browser_runtime"
|
||||
description = "Natively capture browser/runtime requests, responses, redirects, console events, and application endpoints"
|
||||
|
||||
def run(self, target: Target, context: ModuleContext) -> ModuleResult:
|
||||
started = time.monotonic(); result = ModuleResult(self.name, target.url)
|
||||
try:
|
||||
capture = asyncio.run(self._capture(target, context))
|
||||
except Exception as exc:
|
||||
capture = self._fallback(target, context, f"{type(exc).__name__}: {exc}")
|
||||
requests_seen = capture.get("requests", []); responses = capture.get("responses", [])
|
||||
for item in requests_seen:
|
||||
url = str(item.get("url") or ""); parsed = urlsplit(url)
|
||||
if parsed.hostname == target.host:
|
||||
context.surface_graph.add_endpoint(target.url, url, method=str(item.get("method") or "GET"), source=self.name,
|
||||
authentication="observed" if item.get("authentication") else None)
|
||||
headers = {k.lower(): v for k, v in (capture.get("main_headers") or {}).items()}
|
||||
missing = [name for name in SECURITY_HEADERS if name not in headers]
|
||||
if missing:
|
||||
result.findings.append(Finding(context.run_id, target.url, self.name, "browser-security-headers",
|
||||
"Browser-runtime response headers need hardening", "low", .95,
|
||||
"Native runtime capture confirmed missing common security headers.", {"missing": missing}, ["browser", "headers", "native"]))
|
||||
interesting = [item for item in requests_seen if any(term in str(item.get("url", "")).lower() for term in INTERESTING)]
|
||||
if interesting:
|
||||
result.findings.append(Finding(context.run_id, target.url, self.name, "browser-endpoints",
|
||||
"Interesting endpoints observed during browser execution", "info", .8,
|
||||
"Native runtime capture observed assessment-relevant endpoints.", {"endpoints": interesting[:50]}, ["browser", "endpoints", "native"]))
|
||||
result.observations = {"engine": capture.get("engine"), "requests": len(requests_seen), "responses": len(responses),
|
||||
"console_events": len(capture.get("console", [])), "final_url": capture.get("final_url"),
|
||||
"fallback_reason": capture.get("fallback_reason")}
|
||||
directory = context.artifact_dir / self.name / target.host; directory.mkdir(parents=True, exist_ok=True)
|
||||
artifact = directory / "network_capture.json"; artifact.write_text(json.dumps(capture, indent=2), encoding="utf-8")
|
||||
result.artifacts.append(str(artifact)); result.duration_seconds = round(time.monotonic() - started, 4); return result
|
||||
|
||||
async def _capture(self, target: Target, context: ModuleContext) -> dict:
|
||||
from playwright.async_api import async_playwright
|
||||
captured = {"engine": "native-playwright", "target": target.url, "requests": [], "responses": [], "console": [], "main_headers": {}}
|
||||
async with async_playwright() as runtime:
|
||||
browser = await runtime.chromium.launch(headless=True)
|
||||
browser_context = await browser.new_context(ignore_https_errors=True)
|
||||
page = await browser_context.new_page()
|
||||
page.on("request", lambda req: captured["requests"].append({"url": req.url, "method": req.method, "resource_type": req.resource_type}))
|
||||
page.on("response", lambda res: captured["responses"].append({"url": res.url, "status": res.status}))
|
||||
page.on("console", lambda msg: captured["console"].append({"type": msg.type, "text": msg.text[:500]}))
|
||||
response = await page.goto(target.url, wait_until="domcontentloaded", timeout=int(max(5, context.timeout) * 1000))
|
||||
if response: captured["main_headers"] = await response.all_headers()
|
||||
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
await page.wait_for_timeout(1500)
|
||||
captured["final_url"] = page.url; captured["title"] = await page.title()
|
||||
await page.screenshot(path=str(context.artifact_dir / f"{target.host}_runtime.png"), full_page=True)
|
||||
await browser.close()
|
||||
return captured
|
||||
|
||||
@staticmethod
|
||||
def _fallback(target: Target, context: ModuleContext, reason: str) -> dict:
|
||||
response = request(target.url, context.timeout, max_body=500_000)
|
||||
urls = context.surface_graph.request_urls(target.url)
|
||||
return {"engine": "native-http-fallback", "fallback_reason": reason, "target": target.url,
|
||||
"final_url": response.url, "main_headers": response.headers,
|
||||
"requests": [{"url": url, "method": "GET", "resource_type": "mapped"} for url in urls] or [{"url": response.url, "method": "GET", "resource_type": "document"}],
|
||||
"responses": [{"url": response.url, "status": response.status}], "console": []}
|
||||
Reference in New Issue
Block a user