ShadowSec Panel v13

Shadow DOM UI with advanced OWASP-aligned checks: v10.3 UI + v5 depth + intrusive probes (SQLi/IDOR/SSRF/Rate-limit) and heuristics (ports/cache/fingerprinting). Live summary, filters, search, export, copy, and a Settings page for wordlists and options.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ShadowSec Panel v13
// @namespace    http://tampermonkey.net/
// @version      13.0.1
// @description  Shadow DOM UI with advanced OWASP-aligned checks: v10.3 UI + v5 depth + intrusive probes (SQLi/IDOR/SSRF/Rate-limit) and heuristics (ports/cache/fingerprinting). Live summary, filters, search, export, copy, and a Settings page for wordlists and options.
// @author       AbyssBite
// @license      MIT
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_notification
// ==/UserScript==

(function () {
  'use strict';
  if (window.top !== window.self) return;
  if (window.__SEC_TOOL_LOADED) return;
  window.__SEC_TOOL_LOADED = true;

  // ------------------ Persistent Settings ------------------
  const getVal = (k, d) => {
    try { return GM_getValue(k, d); } catch { return d; }
  };
  const setVal = (k, v) => {
    try { GM_setValue(k, v); } catch {}
  };

  const settings = {
    wordlistUrl: getVal('sec_wordlist_url', ''),      // external wordlist raw URL (optional)
    dirProbeLimit: getVal('sec_dir_limit', 200),      // max paths to probe in Extended Directory Probing
    execXss: getVal('sec_exec_xss', false),           // execute XSS payloads (default false)
    extraPorts: getVal('sec_extra_ports', ''),        // e.g., "9090, 9200"
    graphqlPaths: getVal('sec_graphql_paths', ''),    // e.g., "/graphql,/api/graphql"
    rateBurst: getVal('sec_rate_burst', 8)            // burst requests for Rate Limiting Test
  };

  // ------------------ Config (some overridable via settings) ------------------
  let EXECUTE_XSS_PAYLOADS = !!settings.execXss;

  const DIRECTORY_PROBE_PATHS_BASE = [
    "/robots.txt","/sitemap.xml","/admin/","/backup/","/.git/","/.env","/phpinfo.php",
    "/.htaccess","/config.php","/config/","/wp-admin/","/wp-login.php","/vendor/","/logs/","/old/","/test/","/staging/","/dev/",
    "/_next/","/graphql","/api","/api/graphql","/server-status"
  ];

  const OUTDATED_LIBS = [
    { name: "jQuery", regex: /jquery(?:\.min)?-([0-9.]+)\.js/i, min: "3.5.0" },
  ];

  const OPEN_REDIRECT_KEYS = ["url","next","redirect","return","rurl","dest","goto","redir","u","continue"];

  const SQLI_PAYLOADS = [
    "' OR 1=1 --", "' OR '1'='1", "'; WAITFOR DELAY '0:0:3' --", "') OR ('1'='1", "SLEEP(3))--", "' OR 1=1#"
  ];
  const SQL_ERROR_PATTERNS = [
    /you have an error in your sql syntax/i,
    /unclosed quotation mark/i,
    /warning: mysql/i, /mysqli?/i,
    /psql:.+ERROR/i, /postgresql/i,
    /sql server/i, /odbc/i,
    /ORA-\d+/i, /SQLite\/JDBCDriver/i
  ];

  const SSRF_PARAM_HINTS = /(url|uri|target|dest|image|feed|proxy|callback|return|endpoint)/i;

  let GRAPHQL_PATHS = ["/graphql", "/api/graphql"];
  if (settings.graphqlPaths && typeof settings.graphqlPaths === 'string') {
    const extra = settings.graphqlPaths.split(',').map(s=>s.trim()).filter(Boolean);
    GRAPHQL_PATHS = [...new Set([...GRAPHQL_PATHS, ...extra])];
  }

  const PORTS_WEB_HTTP = [80, 8080, 8000, 3000];
  const PORTS_WEB_HTTPS = [443, 8443];
  const PORTS_MISC = [9200, 27017, 5432, 3306, 6379, 15672, 5000];
  const SETTINGS_EXTRA_PORTS = (settings.extraPorts || '')
    .split(',').map(s=>parseInt(s.trim(),10))
    .filter(n=>Number.isInteger(n) && n>0);
  const EXTRA_PORTS = [...new Set(SETTINGS_EXTRA_PORTS)];

  const REFLECTION_PARAM_LIMIT = 10;

  // ------------------ Shadow DOM UI ------------------
  const wrapper = document.createElement('div');
  wrapper.id = 'sec_wrapper';
  document.body.appendChild(wrapper);
  const shadow = wrapper.attachShadow({ mode: 'open' });

  const panel = document.createElement('div');
  panel.id = 'sec_panel';
  shadow.appendChild(panel);

  panel.innerHTML = `
    <div id="sec_header">
      <span>🔐 ShadowSec Panel v13</span>
      <div>
        <button id="sec_settings" title="Settings">⚙️</button>
        <button id="sec_minimize" title="Minimize">−</button>
        <button id="sec_close" title="Close">×</button>
      </div>
    </div>

    <div id="sec_controls">
      <button id="sec_clear" title="Clear logs">Clear</button>
      <button id="sec_run_all" title="Run all tests">Run All</button>
      <button id="sec_export" title="Export report (JSON)">Export</button>
      <button id="sec_copy" title="Copy report (JSON)">Copy</button>
      <label><input type="checkbox" id="sec_intrusive"> Intrusive</label>
      <label>Filter:
        <select id="sec_filter">
          <option>All</option><option>High</option><option>Medium</option><option>Low</option>
        </select>
      </label>
      <input type="text" id="sec_search" placeholder="Search logs...">
      <button id="sec_theme" title="Toggle Dark/Light">☀️</button>
    </div>

    <div id="sec_summary">
      <div class="chip high">High: <span id="sum_high">0</span></div>
      <div class="chip medium">Medium: <span id="sum_medium">0</span></div>
      <div class="chip low">Low: <span id="sum_low">0</span></div>
    </div>

    <div id="sec_buttons"></div>
    <div id="sec_output"></div>
    <div id="sec_loading" style="display:none;">⏳ Running tests...</div>

    <!-- Settings Modal -->
    <div id="sec_modal_backdrop" hidden>
      <div id="sec_modal">
        <div id="sec_modal_header">
          <span>Settings</span>
          <button id="sec_modal_close" title="Close">×</button>
        </div>
        <div id="sec_modal_body">
          <label class="field">
            <span>External wordlist URL (raw):</span>
            <input type="url" id="inp_wordlist_url" placeholder="https://raw.githubusercontent.com/.../common.txt">
          </label>
          <label class="field">
            <span>Directory probe limit:</span>
            <input type="number" id="inp_dir_limit" min="10" max="2000" step="10">
          </label>
          <label class="field">
            <span>Execute XSS payloads (dangerous):</span>
            <input type="checkbox" id="inp_exec_xss">
          </label>
          <label class="field">
            <span>Extra probe ports (comma-separated):</span>
            <input type="text" id="inp_extra_ports" placeholder="9090, 9200, 5001">
          </label>
          <label class="field">
            <span>GraphQL paths (comma-separated):</span>
            <input type="text" id="inp_graphql_paths" placeholder="/graphql, /api/graphql, /gql">
          </label>
          <label class="field">
            <span>Rate-limit burst size:</span>
            <input type="number" id="inp_rate_burst" min="1" max="50" step="1">
          </label>
        </div>
        <div id="sec_modal_actions">
          <button id="sec_modal_save">Save</button>
          <button id="sec_modal_cancel">Cancel</button>
        </div>
      </div>
    </div>
  `;

  const style = document.createElement('style');
  style.textContent = `
    :host { all: initial; font-family: Inter, Segoe UI, system-ui, sans-serif !important; }
    #sec_panel { position:fixed; bottom:16px; right:16px; width:580px; height:600px;
      display:flex; flex-direction:column; background:#181818; color:#eee;
      border-radius:10px; box-shadow:0 6px 20px rgba(0,0,0,0.55); z-index:2147483647;
      overflow:hidden; transition:all .3s ease; font-size:13px; }
    #sec_panel.light { background:#fdfdfd; color:#222; }
    #sec_header { display:flex; justify-content:space-between; align-items:center;
      background:#202020; padding:8px 12px; font-weight:600; cursor:move; user-select:none; }
    #sec_panel.light #sec_header { background:#e6e6e6; }
    #sec_header button { all: unset; cursor:pointer; margin-left:6px; padding:2px 6px;
      border-radius:4px; background:#444; color:#fff; font-weight:bold; }
    #sec_header button:hover { background:#666; }
    #sec_panel.light #sec_header button { background:#ccc; color:#222; }

    #sec_controls { display:flex; flex-wrap:wrap; gap:6px; padding:8px;
      background:#252525; align-items:center; }
    #sec_panel.light #sec_controls { background:#f1f1f1; }
    #sec_controls button, #sec_buttons button, #sec_controls select {
      all: unset; cursor:pointer; padding:6px 10px; border-radius:6px;
      background:linear-gradient(180deg,#444,#333); color:#fff; font-size:13px; text-align:center; }
    #sec_controls button:hover, #sec_buttons button:hover { background:#666; }
    #sec_panel.light #sec_controls button, #sec_panel.light #sec_buttons button {
      background:linear-gradient(180deg,#ccc,#bbb); color:#222; }
    #sec_search { all: unset; flex:1; padding:6px; border:1px solid #444;
      border-radius:6px; background:#2a2a2a; color:#eee; font-size:13px; }
    #sec_panel.light #sec_search { background:#fff; color:#222; border:1px solid #ccc; }

    #sec_summary { display:flex; gap:8px; padding:8px; background:#1d1d1d; }
    #sec_panel.light #sec_summary { background:#ececec; }
    .chip { padding:4px 8px; border-radius:999px; font-weight:600; }
    .chip.high { background:#3b1212; color:#ff6969; }
    .chip.medium { background:#3b2a12; color:#ffc56a; }
    .chip.low { background:#123b18; color:#75d98a; }

    #sec_buttons { display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr));
      gap:6px; padding:8px; max-height:160px; overflow-y:auto; }
    #sec_output { flex:1; background:#101010; padding:8px; overflow-y:auto; }
    #sec_panel.light #sec_output { background:#fff; }
    #sec_output details { margin:4px 0; border:1px solid #333; border-radius:6px; overflow:hidden; }
    #sec_panel.light #sec_output details { border:1px solid #ccc; }
    #sec_output summary { background:#1f1f1f; padding:4px 8px; cursor:pointer; font-weight:500; }
    #sec_panel.light #sec_output summary { background:#eaeaea; }
    .log-entry { padding:3px 8px; border-top:1px solid rgba(255,255,255,0.05); }
    .high { color:#ff4c4c; }
    .medium { color:#ffb347; }
    .low { color:#4caf50; }

    #sec_loading { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
      background:rgba(0,0,0,0.75); padding:10px 18px; border-radius:6px; font-size:14px; color:#fff; }

    .minimized { height:34px !important; overflow:hidden; }

    /* Settings modal */
    #sec_modal_backdrop[hidden] { display:none; }
    #sec_modal_backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5);
      display:flex; align-items:center; justify-content:center; z-index:2147483648; }
    #sec_modal { width: 520px; background: #222; color:#eee; border-radius: 10px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); overflow:hidden; }
    #sec_panel.light #sec_modal { background: #fff; color:#222; }
    #sec_modal_header { display:flex; justify-content:space-between; align-items:center; padding:10px 12px; background:#333; font-weight:700; }
    #sec_panel.light #sec_modal_header { background:#eee; }
    #sec_modal_header button { all:unset; cursor:pointer; padding:4px 8px; background:#444; color:#fff; border-radius:6px; }
    #sec_panel.light #sec_modal_header button { background:#ccc; color:#222; }
    #sec_modal_body { padding:12px; display:flex; flex-direction:column; gap:10px; max-height: 50vh; overflow:auto; }
    .field { display:grid; grid-template-columns: 200px 1fr; gap:8px; align-items:center; }
    .field input[type="text"], .field input[type="url"], .field input[type="number"] {
      all:unset; border:1px solid #444; border-radius:6px; padding:8px; background:#2a2a2a; color:#eee; }
    #sec_panel.light .field input[type="text"], #sec_panel.light .field input[type="url"], #sec_panel.light .field input[type="number"] {
      background:#fff; color:#222; border:1px solid #ccc; }
    .field input[type="checkbox"] { transform: scale(1.2); }
    #sec_modal_actions { display:flex; gap:8px; justify-content:flex-end; padding:10px 12px; background:#2b2b2b; }
    #sec_panel.light #sec_modal_actions { background:#f2f2f2; }
    #sec_modal_actions button { all:unset; cursor:pointer; padding:8px 12px; border-radius:6px; background:#444; color:#fff; font-weight:600; }
    #sec_panel.light #sec_modal_actions button { background:#ccc; color:#222; }
    #sec_modal_actions button:hover { filter:brightness(1.1); }
  `;
  shadow.appendChild(style);

  // ------------------ Elements & State ------------------
  const outputDiv = panel.querySelector('#sec_output');
  const buttonsDiv = panel.querySelector('#sec_buttons');
  const loadingDiv = panel.querySelector('#sec_loading');
  const filterSelect = panel.querySelector('#sec_filter');
  const intrusiveCheckbox = panel.querySelector('#sec_intrusive');
  const searchInput = panel.querySelector('#sec_search');
  const themeButton = panel.querySelector('#sec_theme');
  const settingsBtn = panel.querySelector('#sec_settings');

  const sumHighEl = panel.querySelector('#sum_high');
  const sumMedEl  = panel.querySelector('#sum_medium');
  const sumLowEl  = panel.querySelector('#sum_low');

  const modalBackdrop = panel.querySelector('#sec_modal_backdrop');
  const modalClose = panel.querySelector('#sec_modal_close');
  const modalSave = panel.querySelector('#sec_modal_save');
  const modalCancel = panel.querySelector('#sec_modal_cancel');

  const inpWordlist = panel.querySelector('#inp_wordlist_url');
  const inpDirLimit = panel.querySelector('#inp_dir_limit');
  const inpExecXss = panel.querySelector('#inp_exec_xss');
  const inpExtraPorts = panel.querySelector('#inp_extra_ports');
  const inpGraphqlPaths = panel.querySelector('#inp_graphql_paths');
  const inpRateBurst = panel.querySelector('#inp_rate_burst');

  let findings = [];
  let isLightMode = false;

  // ------------------ Utils ------------------
  const now = () => new Date().toLocaleTimeString();
  const sanitize = (s) => (s ?? "").toString().replace(/[<>]/g, (c) => ({'<':'&lt;','>':'&gt;'}[c]));
  const debounce = (fn, d=100) => { let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a), d); } };
  const sameOrigin = (u) => { try { const x = new URL(u, location.href); return x.origin === location.origin; } catch { return false; } };
  const base64urlDecode = (b64u) => {
    try {
      const pad = '='.repeat((4 - (b64u.length % 4)) % 4);
      const s = (b64u + pad).replace(/-/g,'+').replace(/_/g,'/');
      return JSON.parse(decodeURIComponent(escape(atob(s))));
    } catch { return null; }
  };

  const log = (msg, severity="Low", testName="General") => {
    findings.push({ msg, severity, testName, timestamp: now() });
    renderLogs();
  };
  const toggleLoading = (s) => loadingDiv.style.display = s ? "block" : "none";
  const updateSummary = () => {
    const stats = { High:0, Medium:0, Low:0 };
    findings.forEach(f => { if (stats[f.severity] != null) stats[f.severity]++; });
    sumHighEl.textContent = stats.High; sumMedEl.textContent = stats.Medium; sumLowEl.textContent = stats.Low;
  };
  const renderLogs = debounce(() => {
    const filter = filterSelect.value;
    const needle = (searchInput.value || "").toLowerCase();
    outputDiv.innerHTML = "";
    const grouped = {};
    findings
      .filter(f => (filter === "All" || f.severity === filter) && f.msg.toLowerCase().includes(needle))
      .forEach(f => { grouped[f.testName] = grouped[f.testName] || []; grouped[f.testName].push(f); });

    for (const [test, logs] of Object.entries(grouped)) {
      const details = document.createElement('details'); details.open = true;
      const summary = document.createElement('summary'); summary.textContent = `${test} (${logs.length})`;
      details.appendChild(summary);
      logs.forEach(f => {
        const div = document.createElement('div');
        div.className = `log-entry ${f.severity.toLowerCase()}`;
        div.textContent = `[${f.timestamp}] [${f.severity}] ${sanitize(f.msg)}`;
        details.appendChild(div);
      });
      outputDiv.appendChild(details);
    }
    updateSummary();
  });

  // ------------------ Tests (merged baseline + advanced) ------------------
  const tests = {
    "HTTPS & Mixed Content": async () => {
      const T = "HTTPS";
      if (location.protocol !== "https:") {
        log("Page not served over HTTPS", "High", T);
      } else {
        let mixed = false;
        document.querySelectorAll("img,script,link,iframe,source,object,embed,video,audio").forEach(el => {
          const src = el.src || el.href || el.data;
          if (src && src.startsWith("http:")) { log(`Mixed content: ${src}`, "High", T); mixed = true; }
        });
        if (!mixed) log("No mixed content detected", "Low", T);
      }
    },

    "Cookies (Heuristics)": () => {
      const T = "Cookies";
      const cookies = document.cookie ? document.cookie.split(";").map(s => s.trim()) : [];
      if (!cookies.length) { log("No cookies visible via document.cookie", "Low", T); return; }
      cookies.forEach(c => {
        const [name, ...rest] = c.split('=');
        const value = rest.join('=');
        log(`Cookie readable by JS (not HttpOnly): ${name}=${(value||'').slice(0,40)}${(value||'').length>40?'...':''}`, "Medium", T);
        if (name.startsWith("__Host-")) log(`Cookie ${name} uses __Host- prefix (requires Secure, path=/, no Domain)`, "Low", T);
        else if (name.startsWith("__Secure-")) log(`Cookie ${name} uses __Secure- prefix (should be Secure over HTTPS)`, "Low", T);
      });
      log("Note: Secure/SameSite/HttpOnly flags are not exposed to JS; above are heuristics only.", "Low", T);
    },

    "Forms & CSRF": () => {
      const T = "Forms";
      const forms = Array.from(document.forms);
      if (!forms.length) { log("No forms found", "Low", T); return; }
      forms.forEach((form, i) => {
        const idx = `Form #${i+1}`;
        const hasCSRF = Array.from(form.elements).some(e => /csrf|_token/i.test(e.name));
        if (!hasCSRF) log(`${idx} lacks CSRF token`, "High", T);
        const action = form.getAttribute('action') || "(same-page)";
        if (/^http:\/\//i.test(action)) log(`${idx} action over HTTP: ${action}`, "High", T);
        try {
          const url = action === "(same-page)" ? location : new URL(action, location.href);
          if (url.origin !== location.origin) log(`${idx} posts cross-origin to ${url.origin}`, "Medium", T);
        } catch {}
        Array.from(form.elements).forEach(el => {
          if (el.type === 'password') {
            const ac = (el.getAttribute('autocomplete') || form.getAttribute('autocomplete') || "").toLowerCase();
            if (ac !== "off" && ac !== "new-password") {
              log(`${idx} password input without autocomplete="off/new-password"`, "Medium", T);
            }
          }
          if (el.type === 'file' && !el.getAttribute('accept')) {
            log(`${idx} file input lacks accept attribute`, "Medium", T);
          }
        });
      });
    },

    "Inline Event Handlers": () => {
      const T = "DOM";
      let found = false;
      document.querySelectorAll("*").forEach(el => {
        for (const a of el.attributes) {
          if (a.name.startsWith("on")) { log(`Inline handler ${a.name} on <${el.tagName.toLowerCase()}>`, "Medium", T); found = true; }
        }
      });
      if (!found) log("No inline handlers found", "Low", T);
    },

    "Security Headers (deep)": async () => {
      const T = "Headers";
      try {
        const res = await fetch(location.href, { method: "HEAD", credentials: "same-origin" });
        const h = (k) => res.headers.get(k);
        const lower = {}; for (const [k,v] of res.headers.entries()) lower[k.toLowerCase()] = v;
        const csp=h("content-security-policy"), xfo=h("x-frame-options"), hsts=h("strict-transport-security");
        const xcto=h("x-content-type-options"), refp=h("referrer-policy"), pp=h("permissions-policy");
        const coop=h("cross-origin-opener-policy"), coep=h("cross-origin-embedder-policy"), corp=h("cross-origin-resource-policy");
        const cache=h("cache-control");
        ["content-security-policy","x-frame-options","referrer-policy","x-content-type-options"].forEach(n => { if (!lower[n]) log(`Missing header: ${n}`, "High", T); });
        if (csp) {
          if (!/default-src/i.test(csp)) log("CSP lacks default-src directive", "Medium", T);
          if (/unsafe-inline/i.test(csp)) log("CSP allows 'unsafe-inline'", "High", T);
          if (/unsafe-eval/i.test(csp)) log("CSP allows 'unsafe-eval'", "High", T);
          if (!/object-src\s+('none'|none)/i.test(csp)) log("CSP should set object-src 'none'", "Medium", T);
          if (!xfo && !/frame-ancestors/i.test(csp)) log("No X-Frame-Options and CSP lacks frame-ancestors", "High", T);
        }
        if (xcto && xcto.toLowerCase() !== 'nosniff') log("X-Content-Type-Options should be 'nosniff'", "Medium", T);
        if (!refp) log("Missing Referrer-Policy", "Medium", T);
        else if (/unsafe-url/i.test(refp)) log("Referrer-Policy 'unsafe-url' is weak", "Medium", T);
        if (location.protocol === "https:" && !hsts) log("Missing Strict-Transport-Security on HTTPS", "Medium", T);
        if (!pp) log("Missing Permissions-Policy", "Medium", T);
        if (!coop) log("Missing Cross-Origin-Opener-Policy", "Medium", T);
        if (!coep) log("Missing Cross-Origin-Embedder-Policy", "Medium", T);
        if (!corp) log("Missing Cross-Origin-Resource-Policy", "Medium", T);
        if (cache && !/no-store|no-cache|must-revalidate/i.test(cache)) { log(`Cache-Control may allow caching: ${cache}`, "Medium", T); }
      } catch (e) {
        log("Could not read headers (CORS or opaque). Falling back to meta tags.", "Low", T);
        const metaCSP = document.querySelector('meta[http-equiv="Content-Security-Policy"]')?.content;
        if (!metaCSP) log("No CSP meta found", "High", T);
        else {
          if (/unsafe-inline/i.test(metaCSP)) log("CSP meta allows 'unsafe-inline'", "High", T);
          if (/unsafe-eval/i.test(metaCSP)) log("CSP meta allows 'unsafe-eval'", "High", T);
          if (!/default-src/i.test(metaCSP)) log("CSP meta lacks default-src", "Medium", T);
        }
      }
    },

    "OWASP Headers Compliance": async () => {
      const T = "OWASP Headers";
      try {
        const res = await fetch(location.href, { method: "HEAD", credentials: "same-origin" });
        const h = k => (res.headers.get(k) || "");
        const csp=h("content-security-policy"), xfo=h("x-frame-options"), hsts=h("strict-transport-security");
        const xcto=h("x-content-type-options"), refp=h("referrer-policy"), pp=h("permissions-policy");
        const coop=h("cross-origin-opener-policy"), coep=h("cross-origin-embedder-policy"), corp=h("cross-origin-resource-policy");
        if (!csp) log("Missing CSP", "High", T);
        if (!xfo && !(csp && /frame-ancestors/i.test(csp))) log("Missing X-Frame-Options and CSP frame-ancestors", "High", T);
        if (!xcto || xcto.toLowerCase() !== "nosniff") log("X-Content-Type-Options should be 'nosniff'", "Medium", T);
        if (!refp) log("Missing Referrer-Policy", "Medium", T);
        if (location.protocol === "https:") {
          if (!hsts) log("Missing HSTS on HTTPS", "High", T);
          else {
            const m = hsts.match(/max-age=(\d+)/i);
            const age = m ? parseInt(m[1], 10) : 0;
            if (age < 31536000) log(`HSTS max-age too low (${age})`, "Medium", T);
            if (!/includeSubDomains/i.test(hsts)) log("HSTS missing includeSubDomains", "Low", T);
          }
        }
        if (!pp) log("Missing Permissions-Policy", "Medium", T);
        if (!coop) log("Missing COOP", "Medium", T);
        if (!coep) log("Missing COEP", "Medium", T);
        if (!corp) log("Missing CORP", "Medium", T);
        if (csp) {
          if (/unsafe-inline/i.test(csp)) log("CSP allows 'unsafe-inline'", "High", T);
          if (/unsafe-eval/i.test(csp))   log("CSP allows 'unsafe-eval'", "High", T);
          if (!/default-src/i.test(csp))  log("CSP lacks default-src", "Medium", T);
          if (!/object-src\s+('none'|none)/i.test(csp)) log("CSP should set object-src 'none'", "Medium", T);
        }
        log("OWASP header audit complete", "Low", T);
      } catch {
        log("Header audit failed (CORS/opaque). Consider server-side check.", "Low", T);
      }
    },

    "CORS Policy": async () => {
      const T = "CORS";
      try {
        const res = await fetch(location.origin + "/", {
          method: "GET",
          headers: { "Origin": "https://evil.example" },
          credentials: "omit",
          cache: "no-store"
        });
        const acao = res.headers.get("access-control-allow-origin");
        const acac = res.headers.get("access-control-allow-credentials");
        if (acao === "*") log("Wildcard ACAO '*'", "Medium", T);
        else if (acao) log(`ACAO: ${acao}`, "Low", T);
        else log("No CORS headers on GET", "Low", T);
        if (acac === "true" && acao === "*") log("ACAC true with ACAO '*' is invalid/insecure", "High", T);
      } catch {
        log("CORS test failed (blocked or network error)", "Low", T);
      }
    },

    "Open Redirects": () => {
      const T = "Redirects";
      const params = new URLSearchParams(location.search);
      let cnt = 0;
      for (const key of OPEN_REDIRECT_KEYS) {
        if (params.has(key)) {
          const v = params.get(key) || "";
          cnt++;
          if (/^https?:\/\//i.test(v) || /^\/\//.test(v) || v.includes("..")) {
            log(`Potential open redirect param: ${key}=${v}`, "High", T);
          } else {
            log(`Redirect-like param present: ${key}=${v}`, "Medium", T);
          }
        }
        if (cnt >= REFLECTION_PARAM_LIMIT) break;
      }
      if (!cnt) log("No typical redirect parameters found", "Low", T);
    },

    "Exposed Storage": () => {
      const T = "Storage";
      const patterns = [/token/i, /key/i, /pass/i, /secret/i, /credential/i, /email/i];
      [["localStorage", localStorage], ["sessionStorage", sessionStorage]].forEach(([name, store]) => {
        if (!store || Object.keys(store).length === 0) { log(`${name}: empty`, "Low", T); return; }
        for (const k in store) {
          const v = String(store[k] ?? "").slice(0, 80);
          const flag = patterns.some(p => p.test(k) || p.test(v)) ? "High" : "Medium";
          log(`${name}: ${k}=${v}${(store[k] && store[k].length>80)?'...':''}`, flag, T);
        }
      });
    },

    "Clickjacking": () => {
      const T = "Clickjacking";
      if (window.top !== window.self) log("Page is inside an iframe", "High", T);
      const iframes = document.querySelectorAll('iframe');
      iframes.forEach((iframe, i) => {
        if (!iframe.hasAttribute('sandbox')) log(`Iframe #${i+1} lacks sandbox`, "Medium", T);
        else log(`Iframe #${i+1} sandbox="${iframe.getAttribute('sandbox')}"`, "Low", T);
        const allow = iframe.getAttribute('allow');
        if (allow && /camera|microphone|geolocation|payment|usb/i.test(allow)) {
          log(`Iframe #${i+1} allow contains powerful features: ${allow}`, "Medium", T);
        }
      });
      if (!iframes.length) log("No iframes found", "Low", T);
    },

    "WebSocket Security": () => {
      const T = "WebSockets";
      const WS = window.WebSocket;
      window.WebSocket = function (url, ...rest) {
        try {
          const u = new URL(url, location.href);
          if (u.protocol === "ws:") log(`Insecure WebSocket: ${url}`, "High", T);
          else log(`WebSocket: ${url}`, "Low", T);
        } catch { log(`Invalid WebSocket URL: ${url}`, "Medium", T); }
        return new WS(url, ...rest);
      };
      setTimeout(() => { window.WebSocket = WS; log("WebSocket hook complete", "Low", T); }, 1200);
    },

    "Service Workers": async () => {
      const T = "SW";
      if (!("serviceWorker" in navigator)) { log("Service Workers not supported", "Low", T); return; }
      const regs = await navigator.serviceWorker.getRegistrations();
      if (!regs.length) { log("No Service Workers registered", "Low", T); return; }
      regs.forEach((r, i) => {
        const scope = r.scope;
        const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL || "(unknown)";
        log(`SW #${i+1}: scope=${scope}, script=${url}`, "Medium", T);
      });
    },

    "Third-Party Scripts": () => {
      const T = "Scripts";
      let found = false;
      document.querySelectorAll("script[src]").forEach(s => {
        try {
          const u = new URL(s.src, location.href);
          if (u.hostname !== location.hostname) {
            found = true;
            const hasSRI = !!s.integrity;
            log(`3rd-party script: ${s.src} ${hasSRI?'(with SRI)':'(no SRI)'}`, hasSRI ? "Low" : "Medium", T);
          }
        } catch {}
      });
      if (!found) log("No 3rd-party scripts", "Low", T);
    },

    "DOM-based XSS (reflections)": () => {
      const T = "DOM-XSS";
      const params = new URLSearchParams(location.search);
      const docHTML = document.documentElement.innerHTML;
      let hits = 0;
      params.forEach((v, k) => {
        if (!v) return;
        if (docHTML.includes(v)) { log(`Param reflected in DOM: ${k}=${v.slice(0,80)}`, "High", T); hits++; }
      });
      if (!hits) log("No obvious param reflections in DOM", "Low", T);
    },

    "CSP Effectiveness": () => {
      const T = "CSP";
      const csp = document.querySelector('meta[http-equiv="Content-Security-Policy"]')?.content;
      if (!csp) { log("No CSP meta tag found (header may still exist)", "Medium", T); return; }
      log(`CSP meta: ${csp.slice(0,180)}${csp.length>180?'...':''}`, "Low", T);
      if (/unsafe-inline/i.test(csp)) log("CSP allows 'unsafe-inline'", "High", T);
      if (/unsafe-eval/i.test(csp))   log("CSP allows 'unsafe-eval'", "High", T);
      if (!/default-src/i.test(csp))  log("CSP lacks default-src", "Medium", T);
      if (!/object-src\s+('none'|none)/i.test(csp)) log("CSP should set object-src 'none'", "Medium", T);
    },

    "Subresource Integrity": () => {
      const T = "SRI";
      let flagged = 0;
      document.querySelectorAll("script[src], link[rel=stylesheet][href]").forEach(el => {
        const url = el.src || el.href;
        if (!url) return;
        const xorigin = !sameOrigin(url);
        if (xorigin && !el.integrity) { log(`Cross-origin resource without SRI: ${url}`, "Medium", T); flagged++; }
        else if (el.integrity) { log(`SRI present: ${url}`, "Low", T); }
      });
      if (!flagged) log("No cross-origin resources missing SRI", "Low", T);
    },

    "Privacy APIs": () => {
      const T = "Privacy";
      const canvas = document.createElement("canvas");
      if (canvas.getContext("2d")) log("Canvas API available (fingerprinting vector)", "Medium", T);
      try {
        const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
        if (gl) log("WebGL available (fingerprinting vector)", "Medium", T);
      } catch {}
      if (navigator.getBattery) log("Battery Status API available (fingerprinting vector)", "Low", T);
      if ("RTCPeerConnection" in window) log("WebRTC available (IP leak vector)", "Medium", T);
      if ("geolocation" in navigator) log("Geolocation API available", "Low", T);
    },

    "JavaScript Eval": () => {
      const T = "JS";
      const origEval = window.eval;
      const origSetTimeout = window.setTimeout;
      const origSetInterval = window.setInterval;
      window.eval = (...args) => { log("eval() used", "High", T); return origEval(...args); };
      window.setTimeout = (...args) => {
        if (typeof args[0] === "string") log("setTimeout called with string", "High", T);
        return origSetTimeout(...args);
      };
      window.setInterval = (...args) => {
        if (typeof args[0] === "string") log("setInterval called with string", "High", T);
        return origSetInterval(...args);
      };
      setTimeout(() => {
        window.eval = origEval;
        window.setTimeout = origSetTimeout;
        window.setInterval = origSetInterval;
        log("Eval hooks restored", "Low", T);
      }, 1200);
    },

    "HTTP Methods": async () => {
      const T = "Methods";
      for (const m of ["PUT","DELETE","TRACE"]) {
        try {
          const r = await fetch(location.origin + "/", { method: m });
          if (r.status < 400) log(`${m} allowed (potential risk)`, "High", T);
          else log(`${m} rejected with status ${r.status}`, "Low", T);
        } catch { log(`${m} request blocked`, "Low", T); }
      }
    },

    "Sensitive Attributes": () => {
      const T = "Attributes";
      const patterns = /token|key|pass|secret|cred|email/i;
      const all = document.querySelectorAll("*");
      let found = 0;
      all.forEach(el => {
        for (const a of el.attributes) {
          if (a.name === "value" || a.name.startsWith("data-")) {
            if (patterns.test(String(a.value))) {
              log(`Sensitive data in ${a.name} on <${el.tagName.toLowerCase()}>: ${String(a.value).slice(0,80)}`, "High", T);
              found++;
            }
          }
        }
      });
      if (!found) log("No sensitive-looking attribute values detected", "Low", T);
    },

    "XSS Payload Fuzzing": () => {
      const T = "XSS-Fuzz";
      if (!intrusiveCheckbox.checked) { log("Fuzzing skipped (Intrusive disabled)", "Low", T); return; }
      const nonExecPayloads = [
        `"><img src=x onerror=1>`,
        `<svg onload=1>`,
        `"><svg><desc>__XSS__</desc></svg>`,
        `"><b id="__XSS__">x</b>`,
        `</textarea>__XSS__<textarea>`,
        `' onmouseover=1 '`,
        `javascript:1`,
        `"><iframe srcdoc="__XSS__"></iframe>`
      ];
      const execPayloads = [
        `"><img src=x onerror=alert(1)>`,
        `<svg onload=alert(1)>`,
        `<a href="javascript:alert(1)">x</a>`,
        `<input value="x" onfocus=alert(1)>`
      ];
      const payloads = EXECUTE_XSS_PAYLOADS ? nonExecPayloads.concat(execPayloads) : nonExecPayloads;
      const inputs = Array.from(document.querySelectorAll("input[type=text],input[type=search],textarea"));
      if (!inputs.length) { log("No inputs found to fuzz", "Low", T); return; }
      inputs.forEach((input, i) => {
        const rn = input.getRootNode && input.getRootNode();
        if (rn && rn !== document) return; // avoid touching Shadow DOM (panel)
        payloads.forEach(p => {
          try {
            const original = input.value;
            input.value = p;
            input.dispatchEvent(new Event("input", { bubbles: true }));
            input.dispatchEvent(new Event("change", { bubbles: true }));
            input.value = original;
            log(`Fuzzed input #${i+1} with payload: ${p}`, "Low", T);
          } catch (e) {
            log(`Error fuzzing input #${i+1}: ${e.message}`, "Medium", T);
          }
        });
      });
      log("XSS fuzzing complete (check UI for unexpected reflections/popups)", "Low", T);
    },

    "Outdated Libraries": () => {
      const T = "Libs";
      document.querySelectorAll("script[src]").forEach(s => {
        const src = s.getAttribute("src") || "";
        OUTDATED_LIBS.forEach(lib => {
          const m = src.match(lib.regex);
          if (m) {
            const ver = m[1];
            log(`${lib.name} detected: ${ver} (recommended >= ${lib.min})`, "Medium", T);
          }
        });
      });
    },

    "Directory Probing": async () => {
      const T = "Dirs";
      for (const p of ["/robots.txt", "/sitemap.xml", "/admin/", "/backup/"]) {
        try {
          const r = await fetch(p, { cache: "no-store" });
          if (r.ok) log(`Accessible: ${p} (status ${r.status})`, "Medium", T);
          else if (r.status === 403) log(`Forbidden but present: ${p}`, "Low", T);
        } catch {}
      }
    },

    "Extended Directory Probing": async () => {
      const T = "Dirs+";
      const set = new Set(DIRECTORY_PROBE_PATHS_BASE);

      // Optional external wordlist from settings
      const WORDLIST_URL = settings.wordlistUrl;
      if (WORDLIST_URL) {
        try {
          log(`Fetching external wordlist from ${WORDLIST_URL}`, "Low", T);
          const txt = await fetch(WORDLIST_URL, { cache:"no-store" }).then(r => r.text());
          txt.split(/\r?\n/).forEach(line => {
            const l = line.trim();
            if (!l || l.startsWith("#")) return;
            const norm = l.startsWith("/") ? l : "/" + l;
            set.add(norm);
          });
          log(`Wordlist loaded, total candidates so far: ${set.size}`, "Low", T);
        } catch (e) {
          log(`Failed to fetch wordlist: ${e.message}`, "Medium", T);
        }
      }

      // Harvest from page links/scripts for path candidates
      const harvest = (url) => {
        try {
          const u = new URL(url, location.href);
          if (u.origin !== location.origin) return;
          const path = u.pathname;
          const parts = path.split('/').filter(Boolean);
          if (parts.length) {
            for (let i=1; i<=parts.length; i++) set.add("/" + parts.slice(0,i).join("/") + "/");
            if (/\.[a-z0-9]{1,6}$/i.test(parts[parts.length-1])) {
              set.add(path + ".bak");
              set.add(path + "~");
              set.add(path.replace(/\.[^.]+$/, ".old"));
            }
          }
        } catch {}
      };
      document.querySelectorAll('a[href],link[href],script[src]').forEach(el => {
        const u = el.getAttribute('href') || el.getAttribute('src');
        if (u) harvest(u);
      });

      const limit = Math.max(10, Math.min(2000, Number(settings.dirProbeLimit) || 200));
      const paths = Array.from(set).slice(0, limit);
      for (const p of paths) {
        try {
          const r = await fetch(p, { cache:"no-store" });
          if (r.ok) log(`Accessible: ${p} (status ${r.status})`, "Medium", T);
          else if (r.status === 403) log(`Forbidden but present: ${p}`, "Low", T);
        } catch {}
      }
      log(`Directory probing complete (${paths.length} checks)`, "Low", T);
    },

    "Reflected Params": () => {
      const T = "Reflections";
      const params = new URLSearchParams(location.search);
      let any = 0;
      params.forEach((v, k) => {
        if (!v) return;
        if (document.body.innerHTML.includes(v)) {
          any++;
          log(`Param ${k} appears reflected in body`, "High", T);
        }
      });
      if (!any) log("No param reflections found in body", "Low", T);
    },

    "Weak Form Inputs": () => {
      const T = "Weak Inputs";
      document.querySelectorAll("input[type=text],input[type=password]").forEach(el => {
        if (!el.hasAttribute("maxlength")) log(`Input "${el.name || el.id || '(unnamed)'}" missing maxlength`, "Low", T);
        if (el.type === "password" && !el.hasAttribute("minlength")) log(`Password input "${el.name || el.id || '(unnamed)'}" missing minlength`, "Low", T);
      });
    },

    "Links (_blank) Hygiene": () => {
      const T = "Links";
      document.querySelectorAll('a[target="_blank"]').forEach((a, i) => {
        const rel = (a.getAttribute('rel') || "").toLowerCase();
        if (!/\bnoopener\b/.test(rel) && !/\bnoreferrer\b/.test(rel)) {
          log(`Link #${i+1} opens new tab without rel="noopener"`, "Medium", T);
        }
      });
    },

    "Meta / Version Disclosure": async () => {
      const T = "Meta";
      const gen = document.querySelector('meta[name="generator"]')?.getAttribute('content');
      if (gen) log(`Meta generator: ${gen}`, "Medium", T);
      try {
        const res = await fetch(location.href, { method: "HEAD", credentials: "same-origin" });
        const xp = res.headers.get("x-powered-by");
        if (xp) log(`Header X-Powered-By: ${xp}`, "Medium", T);
      } catch {}
    },

    // -------- Advanced Additions --------

    "Open Ports (Heuristic)": async () => {
      const T = "Ports";
      if (!intrusiveCheckbox.checked) { log("Skipped (Intrusive disabled)", "Low", T); return; }
      const host = location.hostname;
      const isHTTPS = location.protocol === "https:";
      const candidates = [
        ...(isHTTPS ? PORTS_WEB_HTTPS : PORTS_WEB_HTTP),
        ...PORTS_MISC,
        ...EXTRA_PORTS
      ];
      log(`Probing ports on ${host} (heuristics)`, "Low", T);
      for (const p of candidates) {
        try {
          const proto = isHTTPS ? "https" : "http";
          if (isHTTPS && (p === 80 || p === 8080 || p === 8000 || p === 3000)) {
            log(`Skipping http://${host}:${p} due to mixed-content restrictions`, "Low", T);
          } else {
            const resp = await fetch(`${proto}://${host}:${p}/`, { mode: "no-cors", cache:"no-store", redirect:"follow" });
            log(`Fetch to ${proto}://${host}:${p} resolved (possible service)`, (p===3306||p===5432||p===6379||p===27017) ? "High":"Medium", T);
          }
        } catch {
          try {
            const wsProto = isHTTPS ? "wss" : "ws";
            const ws = new WebSocket(`${wsProto}://${host}:${p}`);
            ws.onopen = () => { log(`WebSocket open on port ${p}`, "Medium", T); ws.close(); };
            ws.onerror = () => {};
            await new Promise(r => setTimeout(r, 150));
          } catch {}
        }
      }
      log("Port probing completed (best-effort client heuristic)", "Low", T);
    },

    "SQL Injection Hints": async () => {
      const T = "SQLi";
      if (!intrusiveCheckbox.checked) { log("Skipped (Intrusive disabled)", "Low", T); return; }
      const url = new URL(location.href);
      const baseline = await fetch(url.toString(), { credentials:"include", cache:"no-store" }).then(r => r.text()).catch(()=>null);
      if (!baseline) { log("Baseline fetch failed", "Low", T); return; }
      const params = Array.from(url.searchParams.keys()).slice(0, 8);
      if (!params.length) { log("No query params to fuzz", "Low", T); }
      for (const k of params) {
        for (const p of SQLI_PAYLOADS) {
          const u = new URL(url.toString());
          u.searchParams.set(k, p);
          const t0 = performance.now();
          try {
            const txt = await fetch(u.toString(), { credentials:"include", cache:"no-store" }).then(r => r.text());
            const dt = (performance.now()-t0)|0;
            if (SQL_ERROR_PATTERNS.some(rx => rx.test(txt))) log(`DB error pattern after ${k}='${p}'`, "High", T);
            if (dt > 2500) log(`Slow response (${dt}ms) after ${k}='${p}' (time-based hint)`, "Medium", T);
          } catch {
            log(`Request failed for ${k}='${p}'`, "Low", T);
          }
        }
      }
      log("SQLi fuzzing complete (heuristics only)", "Low", T);
    },

    "Insecure Direct Object References (IDOR)": async () => {
      const T = "IDOR";
      if (!intrusiveCheckbox.checked) { log("Skipped (Intrusive disabled)", "Low", T); return; }
      const url = new URL(location.href);
      const baseline = await fetch(url.toString(), { credentials:"include", cache:"no-store" }).then(r=>r.text()).catch(()=>null);
      if (!baseline) { log("Baseline fetch failed", "Low", T); return; }
      let candidates = [];
      url.searchParams.forEach((v,k) => { const m = (v||"").match(/^\d{2,8}$/); if (m) candidates.push({type:"param", key:k, val:parseInt(v,10)}); });
      const segs = url.pathname.split('/').filter(Boolean);
      segs.forEach((s, idx) => { if (/^\d{2,8}$/.test(s)) candidates.push({type:"path", index:idx, val:parseInt(s,10)}); });
      if (!candidates.length) { log("No obvious numeric IDs detected in URL", "Low", T); return; }
      for (const c of candidates.slice(0,5)) {
        for (const delta of [1, -1, 100]) {
          const u = new URL(location.href);
          if (c.type === "param") u.searchParams.set(c.key, String(c.val + delta));
          else {
            const parts = u.pathname.split('/');
            const nonEmpty = parts.filter(Boolean);
            nonEmpty[c.index] = String(c.val + delta);
            u.pathname = "/" + nonEmpty.join("/") + (location.pathname.endsWith('/')?'/':'');
          }
          try {
            const txt = await fetch(u.toString(), { credentials:"include", cache:"no-store" }).then(r=>r.text());
            if (txt && Math.abs(txt.length - baseline.length) > 500) {
              log(`Response size changed for ID mutation (${c.type}:${c.val}→${c.val+delta})`, "High", T);
            }
          } catch {}
        }
      }
      log("IDOR heuristic probing complete", "Low", T);
    },

    "Server-Side Request Forgery (SSRF)": async () => {
      const T = "SSRF";
      if (!intrusiveCheckbox.checked) { log("Skipped (Intrusive disabled)", "Low", T); return; }
      const endpoints = new Set();
      Array.from(document.forms).forEach(f => {
        const a = f.getAttribute('action') || location.href;
        try { const u = new URL(a, location.href); if (u.origin === location.origin) endpoints.add(u.pathname + (u.search||"")); } catch {}
      });
      Array.from(document.querySelectorAll('a[href]')).forEach(a => {
        try { const u = new URL(a.href, location.href); if (u.origin === location.origin && SSRF_PARAM_HINTS.test(u.search)) endpoints.add(u.pathname + u.search); } catch {}
      });
      Array.from(document.querySelectorAll('script[src],link[href]')).forEach(el => {
        const href = el.getAttribute('src') || el.getAttribute('href');
        try { const u = new URL(href, location.href); if (u.origin === location.origin && SSRF_PARAM_HINTS.test(u.search)) endpoints.add(u.pathname + u.search); } catch {}
      });
      const targets = ["http://127.0.0.1", "http://169.254.169.254/latest/meta-data/"];
      if (!endpoints.size) { log("No candidate endpoints with URL-like params found", "Low", T); return; }
      for (const path of Array.from(endpoints).slice(0,6)) {
        for (const target of targets) {
          try {
            const u = new URL(path, location.origin);
            const sp = new URLSearchParams(u.search);
            let hit = false;
            for (const [k,v] of sp.entries()) { if (SSRF_PARAM_HINTS.test(k)) { sp.set(k, target); hit = true; } }
            if (!hit) continue;
            u.search = sp.toString();
            const r = await fetch(u.toString(), { credentials:"include", cache:"no-store" });
            if (r.ok) {
              const text = await r.text();
              if (/ami-|instance|meta-|availability-zone|hostname/i.test(text) || text.length > 0) {
                log(`Potential SSRF via ${u.pathname}?${sp} → returned ${text.length} bytes`, "High", T);
              } else {
                log(`Endpoint responded (status ${r.status}) to internal URL probe`, "Medium", T);
              }
            } else if (r.status === 403) {
              log(`Endpoint forbids internal URL (403) — SSRF mitigated at ${u.pathname}`, "Low", T);
            }
          } catch {}
        }
      }
      log("SSRF probing complete (same-origin endpoints only)", "Low", T);
    },

    "Prototype Pollution": async () => {
      const T = "Prototype Pollution";
      const cleanUp = (key) => { try { delete Object.prototype[key]; } catch {} };
      const payload = JSON.parse('{"__proto__":{"__sec_polluted__":true}}');
      let polluted = false;
      if (window._ && typeof _.merge === "function") {
        try {
          _.merge({}, payload);
          if (({}).__sec_polluted__ === true) { polluted = true; log("_.merge produced prototype pollution", "High", T); }
          cleanUp("__sec_polluted__");
        } catch {}
      }
      if (window.jQuery && typeof jQuery.extend === "function") {
        try {
          jQuery.extend(true, {}, payload);
          if (({}).__sec_polluted__ === true) { polluted = true; log("$.extend(true, ...) produced prototype pollution", "High", T); }
          cleanUp("__sec_polluted__");
        } catch {}
      }
      Array.from(document.querySelectorAll('script:not([src])')).forEach(s => {
        const t = (s.textContent || "").slice(0, 4000);
        if (/extend\s*\(\s*true|merge\s*\(/i.test(t) && /__proto__|constructor|prototype/i.test(t)) {
          log("Inline script contains risky deep-merge patterns", "Medium", T);
        }
      });
      if (!polluted) log("No prototype pollution detected in quick checks", "Low", T);
    },

    "Web Cache Poisoning (Heuristics)": async () => {
      const T = "Cache Poisoning";
      try {
        const r = await fetch(location.href, { method:"GET", cache:"no-store", headers: { "X-SEC-TEST": "A" } });
        const vary = r.headers.get("vary") || "";
        const cache = r.headers.get("cache-control") || "";
        if (!/no-store|no-cache|private/i.test(cache)) log(`Cache-Control may allow caching HTML: ${cache}`, "Medium", T);
        if (vary && !/accept-encoding|accept-language|origin/i.test(vary)) log(`Vary header lacks common keys: ${vary}`, "Low", T);
        else if (!vary) log("No Vary header present", "Low", T);
        log("Note: Browser cannot confirm poisoning; review headers and CDN config", "Low", T);
      } catch {
        log("Cache heuristic failed (request error)", "Low", T);
      }
    },

    "GraphQL Introspection": async () => {
      const T = "GraphQL";
      const q = { query: "query { __schema { queryType { name } types { name } } }" };
      let tried = 0, hits = 0;
      for (const path of GRAPHQL_PATHS) {
        tried++;
        try {
          const r = await fetch(path, {
            method:"POST",
            headers: { "content-type":"application/json" },
            credentials:"include",
            body: JSON.stringify(q),
          });
          const js = await r.json().catch(()=> ({}));
          if (js.data && js.data.__schema) { log(`${path}: Introspection ENABLED`, "High", T); hits++; }
          else if (js.errors) { log(`${path}: Errors returned (likely disabled)`, "Low", T); }
          else { log(`${path}: No GraphQL response`, "Low", T); }
        } catch {}
      }
      if (!tried) log("No /graphql endpoints tried", "Low", T);
      if (!hits) log("No introspection enabled detected", "Low", T);
    },

    "Advanced Fingerprinting Detection": async () => {
      const T = "Fingerprinting";
      let flagged = 0;
      Array.from(document.querySelectorAll('script:not([src])')).forEach(s => {
        const t = (s.textContent || "");
        if (/toDataURL\s*\(|getImageData\s*\(|AudioContext|webkitAudioContext|getChannelData/i.test(t)) {
          flagged++; log("Inline script uses canvas/audio APIs (fingerprinting vector)", "Medium", T);
        }
        if (/fingerprint|fp2|device.+id/i.test(t)) { flagged++; log("Inline script mentions fingerprint identifiers", "Medium", T); }
      });
      let canvasCalls = 0;
      const canvasProto = HTMLCanvasElement.prototype;
      const _toDataURL = canvasProto.toDataURL;
      canvasProto.toDataURL = function(...a){ canvasCalls++; try { return _toDataURL.apply(this,a); } catch(e){ throw e; } };
      await new Promise(r => setTimeout(r, 1000));
      canvasProto.toDataURL = _toDataURL;
      if (canvasCalls>0) log(`Canvas toDataURL invoked ${canvasCalls}x during probe window`, "Medium", T);
      if (!flagged && canvasCalls===0) log("No obvious fingerprinting signals in brief probe", "Low", T);
    },

    "Broken Authentication (Heuristics)": () => {
      const T = "Auth";
      const jwtRx = /eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]*/g;
      const scanStr = (s) => { const found = (s||"").match(jwtRx) || []; return [...new Set(found)]; };
      let any = 0;
      [["localStorage", localStorage], ["sessionStorage", sessionStorage]].forEach(([name, store]) => {
        try {
          for (const k in store) {
            const vals = scanStr(String(store[k] || ""));
            vals.forEach(tok => {
              any++; log(`${name} contains JWT-like token (${k})`, "Medium", T);
              const [h,p] = tok.split('.'); const header = base64urlDecode(h); const payload = base64urlDecode(p);
              if (header && header.alg && /none/i.test(header.alg)) log("JWT alg=none (weak)", "High", T);
              if (payload && payload.exp && (payload.exp*1000 < Date.now())) log("JWT appears expired", "Low", T);
            });
          }
        } catch {}
      });
      (document.cookie || "").split(';').forEach(c => {
        const parts = c.trim().split('=');
        if (!parts[0]) return;
        const vals = scanStr(decodeURIComponent(parts.slice(1).join('=')));
        vals.forEach(tok => {
          any++; log(`Cookie contains JWT-like token: ${parts[0]} (readable ⇒ not HttpOnly)`, "High", T);
        });
      });
      if (!any) log("No JWT-like tokens detected in quick scan", "Low", T);
      log("Session fixation requires auth flow observation; not auto-tested", "Low", T);
    },

    "Rate Limiting Test": async () => {
      const T = "Rate Limit";
      if (!intrusiveCheckbox.checked) { log("Skipped (Intrusive disabled)", "Low", T); return; }
      const url = new URL(location.href);
      url.searchParams.set("sec_rate_test", String(Date.now()%100000));
      const N = Math.max(1, Math.min(50, Number(settings.rateBurst) || 8));
      let got429 = 0;
      await Promise.all(Array.from({length:N}, async () => {
        try {
          const r = await fetch(url.toString(), { credentials:"include", cache:"no-store" });
          if (r.status === 429) got429++;
        } catch {}
      }));
      if (got429>0) log(`429 detected (${got429}/${N}) — rate limiting present`, "Low", T);
      else log(`No 429 responses in ${N} rapid requests — rate limit possibly absent`, "Medium", T);
    },

    "Client-Side Template Injection (CSTI)": async () => {
      const T = "CSTI";
      const inputs = Array.from(document.querySelectorAll("input[type=text],input[type=search],textarea"))
        .filter(el => (el.getRootNode && el.getRootNode() === document));
      if (!inputs.length) { log("No candidate inputs", "Low", T); return; }
      const exprs = ["{{7*7}}", "<%= 7*7 %>", "${{7*7}}", "${7*7}"];
      const beforeHTML = document.documentElement.innerHTML;
      for (const el of inputs.slice(0,8)) {
        const orig = el.value;
        for (const ex of exprs) {
          try {
            el.value = ex;
            el.dispatchEvent(new Event("input", { bubbles:true }));
            el.dispatchEvent(new Event("change", { bubbles:true }));
          } catch {}
        }
        el.value = orig;
      }
      await new Promise(r => setTimeout(r, 300));
      const afterHTML = document.documentElement.innerHTML;
      if (afterHTML.includes("49") && !beforeHTML.includes("49")) {
        log("Found '49' after injecting '{{7*7}}' expressions — CSTI hint", "High", T);
      } else {
        log("No clear CSTI signal from simple expressions", "Low", T);
      }
    }
  };

  // ------------------ Buttons ------------------
  for (const [name, fn] of Object.entries(tests)) {
    const b = document.createElement("button");
    b.textContent = name;
    b.onclick = async () => {
      findings = []; renderLogs(); toggleLoading(true);
      try { await fn(); }
      catch (e) { log(`Unexpected error in ${name}: ${e.message}`, "Medium", name); }
      toggleLoading(false);
    };
    buttonsDiv.appendChild(b);
  }

  // ------------------ Controls ------------------
  panel.querySelector('#sec_clear').onclick = () => { findings = []; renderLogs(); };
  panel.querySelector('#sec_close').onclick = () => wrapper.remove();
  panel.querySelector('#sec_minimize').onclick = () => panel.classList.toggle("minimized");

  panel.querySelector('#sec_run_all').onclick = async () => {
    findings = []; renderLogs(); toggleLoading(true);
    for (const [name, fn] of Object.entries(tests)) {
      try { await fn(); }
      catch (e) { log(`Unexpected error in ${name}: ${e.message}`, "Medium", name); }
    }
    const stats = { High:0, Medium:0, Low:0 };
    findings.forEach(f => { if (stats[f.severity] != null) stats[f.severity]++; });
    log(`Summary: High=${stats.High}, Medium=${stats.Medium}, Low=${stats.Low}`, "Low", "Summary");
    toggleLoading(false);
  };

  panel.querySelector('#sec_export').onclick = () => {
    const report = { url: location.href, time: new Date().toISOString(), findings };
    GM_download({
      url: "data:application/json," + encodeURIComponent(JSON.stringify(report, null, 2)),
      name: `sec_report_${Date.now()}.json`,
      saveAs: true
    });
  };

  panel.querySelector('#sec_copy').onclick = () => {
    const report = { url: location.href, time: new Date().toISOString(), findings };
    navigator.clipboard.writeText(JSON.stringify(report, null, 2)).then(
      () => GM_notification("Report copied to clipboard!", "Success"),
      () => GM_notification("Failed to copy report.", "Error")
    );
  };

  filterSelect.onchange = renderLogs;
  searchInput.oninput = renderLogs;
  themeButton.onclick = () => {
    isLightMode = !isLightMode;
    panel.classList.toggle("light", isLightMode);
    themeButton.textContent = isLightMode ? "🌙" : "☀️";
  };

  // ------------------ Settings Modal Logic ------------------
  const openSettings = () => {
    inpWordlist.value = settings.wordlistUrl || '';
    inpDirLimit.value = Number(settings.dirProbeLimit || 200);
    inpExecXss.checked = !!settings.execXss;
    inpExtraPorts.value = settings.extraPorts || '';
    inpGraphqlPaths.value = settings.graphqlPaths || '';
    inpRateBurst.value = Number(settings.rateBurst || 8);
    modalBackdrop.hidden = false;
  };
  const closeSettings = () => { modalBackdrop.hidden = true; };

  settingsBtn.onclick = openSettings;
  modalClose.onclick = closeSettings;
  modalCancel.onclick = closeSettings;

  modalSave.onclick = () => {
    const wl = inpWordlist.value.trim();
    const dl = Math.max(10, Math.min(2000, Number(inpDirLimit.value) || 200));
    const ex = !!inpExecXss.checked;
    const ep = inpExtraPorts.value.trim();
    const gp = inpGraphqlPaths.value.trim();
    const rb = Math.max(1, Math.min(50, Number(inpRateBurst.value) || 8));

    settings.wordlistUrl = wl;
    settings.dirProbeLimit = dl;
    settings.execXss = ex;
    settings.extraPorts = ep;
    settings.graphqlPaths = gp;
    settings.rateBurst = rb;

    setVal('sec_wordlist_url', wl);
    setVal('sec_dir_limit', dl);
    setVal('sec_exec_xss', ex);
    setVal('sec_extra_ports', ep);
    setVal('sec_graphql_paths', gp);
    setVal('sec_rate_burst', rb);

    // apply in-memory for current session
    EXECUTE_XSS_PAYLOADS = ex;

    // Ports
    const newExtra = (ep || '').split(',').map(s=>parseInt(s.trim(),10)).filter(n=>Number.isInteger(n) && n>0);
    EXTRA_PORTS.length = 0;
    newExtra.forEach(n => { if (!EXTRA_PORTS.includes(n)) EXTRA_PORTS.push(n); });

    // GraphQL paths
    GRAPHQL_PATHS = ["/graphql", "/api/graphql"];
    if (gp) {
      const extra = gp.split(',').map(s=>s.trim()).filter(Boolean);
      GRAPHQL_PATHS = [...new Set([...GRAPHQL_PATHS, ...extra])];
    }

    closeSettings();
    GM_notification("Settings saved.", "Security Panel");
  };

  // ------------------ Dragging ------------------
  let dragging = false, offX=0, offY=0;
  panel.querySelector('#sec_header').addEventListener('mousedown', e => {
    dragging = true; const r = panel.getBoundingClientRect();
    offX = e.clientX - r.left; offY = e.clientY - r.top; e.preventDefault();
  });
  document.addEventListener('mousemove', e => {
    if (!dragging) return;
    panel.style.left = `${e.clientX - offX}px`;
    panel.style.top  = `${e.clientY - offY}px`;
    panel.style.right = "auto"; panel.style.bottom = "auto";
  });
  document.addEventListener('mouseup', () => dragging = false);

  // ------------------ Init ------------------
  log("Security Panel v13 initialized", "Low", "General");
})();