From 7d286ca9f24f69ac474801ed78783cf0c5e6a94d Mon Sep 17 00:00:00 2001 From: Translator Date: Sat, 17 May 2025 05:01:09 +0000 Subject: [PATCH] Translated ['src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting --- .../gcp-to-workspace-pivoting/README.md | 36 +-- theme/ai.js | 236 ++++++++++-------- 2 files changed, 149 insertions(+), 123 deletions(-) diff --git a/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md b/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md index 37e546bc6..1bdb69ef0 100644 --- a/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md +++ b/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md @@ -4,12 +4,12 @@ ## **De GCP para GWS** -### **Noções básicas de Delegação em Domínio** +### **Noções básicas de Delegação em Larga Escala** -A delegação em domínio do Google Workspace permite que um objeto de identidade, seja um **aplicativo externo** do Google Workspace Marketplace ou uma **Conta de Serviço GCP** interna, **acesse dados em todo o Workspace em nome dos usuários**. +A delegação em larga escala do Google Workspace permite que um objeto de identidade, seja um **aplicativo externo** do Google Workspace Marketplace ou uma **Conta de Serviço GCP** interna, **acesse dados em todo o Workspace em nome dos usuários**. > [!NOTE] -> Isso basicamente significa que **contas de serviço** dentro dos projetos GCP de uma organização podem ser capazes de **fingir ser usuários do Workspace** da mesma organização (ou até mesmo de uma diferente). +> Isso basicamente significa que **contas de serviço** dentro dos projetos GCP de uma organização podem ser capazes de **impersonar usuários do Workspace** da mesma organização (ou até mesmo de uma diferente). Para mais informações sobre como isso funciona exatamente, consulte: @@ -19,11 +19,11 @@ gcp-understanding-domain-wide-delegation.md ### Comprometendo a delegação existente -Se um atacante **comprometeu algum acesso ao GCP** e **conhece um e-mail de usuário válido do Workspace** (preferencialmente **super admin**) da empresa, ele poderia **enumerar todos os projetos** aos quais tem acesso, **enumerar todas as SAs** dos projetos, verificar a quais **contas de serviço ele tem acesso** e **repetir** todas essas etapas com cada SA que pode fingir.\ -Com uma **lista de todas as contas de serviço** às quais ele tem **acesso** e a lista de **e-mails do Workspace**, o atacante poderia tentar **fingir ser o usuário com cada conta de serviço**. +Se um atacante **comprometeu algum acesso ao GCP** e **conhece um e-mail de usuário válido do Workspace** (preferencialmente **super admin**) da empresa, ele poderia **enumerar todos os projetos** aos quais tem acesso, **enumerar todas as SAs** dos projetos, verificar a quais **contas de serviço ele tem acesso** e **repetir** todas essas etapas com cada SA que pode impersonar.\ +Com uma **lista de todas as contas de serviço** às quais ele tem **acesso** e a lista de **e-mails do Workspace**, o atacante poderia tentar **impersonar o usuário com cada conta de serviço**. > [!CAUTION] -> Note que ao configurar a delegação em domínio, nenhum usuário do Workspace é necessário, portanto, apenas saber que **um válido é suficiente e necessário para a impersonação**.\ +> Note que ao configurar a delegação em larga escala, nenhum usuário do Workspace é necessário, portanto, apenas saber **um válido é suficiente e necessário para a impersonação**.\ > No entanto, os **privilégios do usuário impersonado serão utilizados**, então se for Super Admin, você poderá acessar tudo. Se não tiver nenhum acesso, isso será inútil. #### [GCP Generate Delegation Token](https://github.com/carlospolop/gcp_gen_delegation_token) @@ -36,22 +36,26 @@ python3 gen_delegation_token.py --user-email --key-file --key-file --scopes "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.domain, https://mail.google.com/, https://www.googleapis.com/auth/drive, openid" ``` +#### [**DelePwn**](https://github.com/n0tspam/delepwn) + +Baseado na ferramenta DeleFriend, mas com algumas adições, como a capacidade de enumerar o domínio, drive, gmail, calendário e realizar outras operações. + #### [**DeleFriend**](https://github.com/axon-git/DeleFriend) Esta é uma ferramenta que pode realizar o ataque seguindo estes passos: 1. **Enumerar Projetos GCP** usando a API Resource Manager. -2. Iterar sobre cada recurso do projeto e **enumerar recursos de conta de serviço GCP** aos quais o usuário IAM inicial tem acesso usando _GetIAMPolicy_. -3. Iterar sobre **cada função de conta de serviço** e encontrar funções integradas, básicas e personalizadas com permissão _**serviceAccountKeys.create**_ no recurso de conta de serviço alvo. Deve-se notar que a função Editor possui essa permissão por padrão. -4. Criar uma **nova chave privada `KEY_ALG_RSA_2048`** para cada recurso de conta de serviço que for encontrado com a permissão relevante na política IAM. +2. Iterar sobre cada recurso do projeto e **enumerar os recursos da conta de serviço GCP** aos quais o usuário IAM inicial tem acesso usando _GetIAMPolicy_. +3. Iterar sobre **cada função da conta de serviço** e encontrar funções integradas, básicas e personalizadas com permissão _**serviceAccountKeys.create**_ no recurso da conta de serviço alvo. Deve-se notar que a função Editor possui inerentemente essa permissão. +4. Criar uma **nova chave privada `KEY_ALG_RSA_2048`** para cada recurso da conta de serviço que for encontrado com a permissão relevante na política IAM. 5. Iterar sobre **cada nova conta de serviço e criar um `JWT`** **objeto** para ela, que é composto pelas credenciais da chave privada da SA e um escopo OAuth. O processo de criação de um novo objeto _JWT_ irá **iterar sobre todas as combinações existentes de escopos OAuth** da lista **oauth_scopes.txt**, a fim de encontrar todas as possibilidades de delegação. A lista **oauth_scopes.txt** é atualizada com todos os escopos OAuth que consideramos relevantes para abusar das identidades do Workspace. -6. O método `_make_authorization_grant_assertion` revela a necessidade de declarar um **usuário de workspace alvo**, referido como _subject_, para gerar JWTs sob DWD. Embora isso possa parecer exigir um usuário específico, é importante perceber que **DWD influencia toda identidade dentro de um domínio**. Consequentemente, criar um JWT para **qualquer usuário de domínio** afeta todas as identidades nesse domínio, consistente com nossa verificação de enumeração de combinações. Simplificando, um usuário válido do Workspace é suficiente para prosseguir.\ -Esse usuário pode ser definido no arquivo _config.yaml_ do DeleFriend. Se um usuário de workspace alvo não for conhecido, a ferramenta facilita a identificação automática de usuários válidos do workspace, escaneando usuários de domínio com funções em projetos GCP. É fundamental notar (novamente) que os JWTs são específicos do domínio e não gerados para cada usuário; portanto, o processo automático visa uma única identidade única por domínio. +6. O método `_make_authorization_grant_assertion` revela a necessidade de declarar um **usuário de workspace alvo**, referido como _subject_, para gerar JWTs sob DWD. Embora isso possa parecer exigir um usuário específico, é importante perceber que **DWD influencia toda identidade dentro de um domínio**. Consequentemente, criar um JWT para **qualquer usuário do domínio** afeta todas as identidades nesse domínio, consistente com nossa verificação de enumeração de combinações. Simplificando, um usuário válido do Workspace é suficiente para prosseguir.\ +Esse usuário pode ser definido no arquivo _config.yaml_ do DeleFriend. Se um usuário de workspace alvo não for conhecido, a ferramenta facilita a identificação automática de usuários válidos do workspace, escaneando usuários do domínio com funções em projetos GCP. É fundamental notar (novamente) que os JWTs são específicos do domínio e não gerados para cada usuário; portanto, o processo automático visa uma única identidade única por domínio. 7. **Enumerar e criar um novo token de acesso bearer** para cada JWT e validar o token contra a API tokeninfo. #### [Script Python do Gitlab](https://gitlab.com/gitlab-com/gl-security/threatmanagement/redteam/redteam-public/gcp_misc/-/blob/master/gcp_delegation.py) -O Gitlab criou [este script Python](https://gitlab.com/gitlab-com/gl-security/gl-redteam/gcp_misc/blob/master/gcp_delegation.py) que pode fazer duas coisas - listar o diretório de usuários e criar uma nova conta administrativa enquanto indica um json com credenciais de SA e o usuário a ser personificado. Aqui está como você o utilizaria: +O Gitlab criou [este script Python](https://gitlab.com/gitlab-com/gl-security/gl-redteam/gcp_misc/blob/master/gcp_delegation.py) que pode fazer duas coisas - listar o diretório de usuários e criar uma nova conta administrativa enquanto indica um json com credenciais SA e o usuário a ser personificado. Aqui está como você o utilizaria: ```bash # Install requirements pip install --upgrade --user oauth2client @@ -77,9 +81,9 @@ pip install --upgrade --user oauth2client É possível **verificar Delegações de Domínio Amplo em** [**https://admin.google.com/u/1/ac/owl/domainwidedelegation**](https://admin.google.com/u/1/ac/owl/domainwidedelegation)**.** -Um atacante com a capacidade de **criar contas de serviço em um projeto GCP** e **privilégios de super administrador no GWS poderia criar uma nova delegação permitindo que SAs impersonem alguns usuários do GWS:** +Um atacante com a capacidade de **criar contas de serviço em um projeto GCP** e **privilégios de super administrador no GWS pode criar uma nova delegação permitindo que SAs se façam passar por alguns usuários do GWS:** -1. **Gerando uma Nova Conta de Serviço e Par de Chaves Correspondente:** No GCP, novos recursos de conta de serviço podem ser produzidos interativamente via console ou programaticamente usando chamadas diretas de API e ferramentas CLI. Isso requer o **papel `iam.serviceAccountAdmin`** ou qualquer papel personalizado equipado com a **permissão `iam.serviceAccounts.create`**. Uma vez que a conta de serviço é criada, procederemos a gerar um **par de chaves relacionado** (**permissão `iam.serviceAccountKeys.create`**). +1. **Gerando uma Nova Conta de Serviço e Par de Chaves Correspondente:** No GCP, novos recursos de conta de serviço podem ser produzidos interativamente via console ou programaticamente usando chamadas diretas de API e ferramentas de CLI. Isso requer o **papel `iam.serviceAccountAdmin`** ou qualquer papel personalizado equipado com a **permissão `iam.serviceAccounts.create`**. Uma vez que a conta de serviço é criada, procederemos a gerar um **par de chaves relacionado** (**permissão `iam.serviceAccountKeys.create`**). 2. **Criação de nova delegação**: É importante entender que **apenas o papel de Super Admin possui a capacidade de configurar delegação global de Domínio Amplo no Google Workspace** e a delegação de Domínio Amplo **não pode ser configurada programaticamente,** ela pode ser criada e ajustada **manualmente** através do **console** do Google Workspace. - A criação da regra pode ser encontrada na página **Controles de API → Gerenciar delegação de Domínio Amplo no console de administração do Google Workspace**. 3. **Anexando privilégios de escopos OAuth**: Ao configurar uma nova delegação, o Google requer apenas 2 parâmetros, o ID do Cliente, que é o **ID OAuth do recurso de Conta de Serviço do GCP**, e **escopos OAuth** que definem quais chamadas de API a delegação requer. @@ -89,7 +93,7 @@ Um atacante com a capacidade de **criar contas de serviço em um projeto GCP** e #### Delegação Interorganizacional -O ID SA OAuth é global e pode ser usado para **delegação interorganizacional**. Não houve restrição implementada para impedir a delegação global cruzada. Em termos simples, **contas de serviço de diferentes organizações GCP podem ser usadas para configurar delegação de domínio amplo em outras organizações Workspace**. Isso resultaria em **apenas precisar de acesso de Super Admin ao Workspace**, e não acesso à mesma conta GCP, já que o adversário pode criar Contas de Serviço e chaves privadas em sua conta GCP pessoalmente controlada. +O ID SA OAuth é global e pode ser usado para **delegação interorganizacional**. Não houve restrição implementada para impedir a delegação global cruzada. Em termos simples, **contas de serviço de diferentes organizações GCP podem ser usadas para configurar delegação de domínio amplo em outras organizações do Workspace**. Isso resultaria em **apenas precisar de acesso de Super Admin ao Workspace**, e não acesso à mesma conta GCP, já que o adversário pode criar Contas de Serviço e chaves privadas em sua conta GCP pessoalmente controlada. ### Criando um Projeto para enumerar o Workspace @@ -150,7 +154,7 @@ Se um atacante comprometer o computador de um usuário, ele também poderá modi Se um atacante tiver acesso completo ao GWS, ele poderá acessar grupos com acesso privilegiado ao GCP ou até mesmo usuários, portanto, mover-se do GWS para o GCP é geralmente mais "simples" apenas porque **os usuários no GWS têm altos privilégios sobre o GCP**. -### Escalada de Privilégios do Google Groups +### Escalonamento de Privilégios do Google Groups Por padrão, os usuários podem **entrar livremente em grupos do Workspace da Organização** e esses grupos **podem ter permissões do GCP** atribuídas (verifique seus grupos em [https://groups.google.com/](https://groups.google.com/)). diff --git a/theme/ai.js b/theme/ai.js index bb8af53b7..c94992d5f 100644 --- a/theme/ai.js +++ b/theme/ai.js @@ -1,27 +1,28 @@ /** - * 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) - * ------------------------------------------------------------------------ + * HackTricks AI Chat Widget v1.16 – resizable sidebar + * --------------------------------------------------- + * ❶ Markdown rendering + sanitised (same as before) + * ❷ NEW: drag‑to‑resize panel, width persists via localStorage */ (function () { - const LOG = "[HackTricks-AI]"; - - /* ---------------- User-tunable constants ---------------- */ - const MAX_CONTEXT = 3000; // highlighted-text char limit - const MAX_QUESTION = 500; // question char limit + const LOG = "[HackTricks‑AI]"; + /* ---------------- User‑tunable constants ---------------- */ + const MAX_CONTEXT = 3000; // highlighted‑text char limit + const MAX_QUESTION = 500; // question char limit + const MIN_W = 250; // ← resize limits → + const MAX_W = 600; + const DEF_W = 350; // default width (if nothing saved) const TOOLTIP_TEXT = - "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it"; + "💡 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 + const BRAND_RED = "#b31328"; /* ------------------------------ State ------------------------------ */ let threadId = null; let isRunning = false; + /* ---------- helpers ---------- */ const $ = (sel, ctx = document) => ctx.querySelector(sel); if (document.getElementById("ht-ai-btn")) { console.warn(`${LOG} Widget already injected.`); @@ -31,44 +32,37 @@ ? document.addEventListener("DOMContentLoaded", init) : init()); - /* ==================================================================== */ - /* 🔗 1. 3rd-party libs → Markdown & sanitiser */ - /* ==================================================================== */ + /* =================================================================== */ + /* 🔗 1. 3rd‑party libs → Markdown & sanitiser */ + /* =================================================================== */ function loadScript(src) { - return new Promise((resolve, reject) => { + return new Promise((res, rej) => { const s = document.createElement("script"); s.src = src; - s.onload = resolve; - s.onerror = () => reject(new Error(`Failed to load ${src}`)); + s.onload = res; + s.onerror = () => rej(new Error(`Failed to load ${src}`)); document.head.appendChild(s); }); } - async function ensureDeps() { const deps = []; - if (typeof marked === "undefined") { + if (typeof marked === "undefined") deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js")); - } - if (typeof DOMPurify === "undefined") { + 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); } + const mdToSafeHTML = (md) => + DOMPurify.sanitize(marked.parse(md, { mangle: false, headerIds: false }), { + USE_PROFILES: { html: true } + }); - 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) { @@ -76,14 +70,14 @@ return; } - console.log(`${LOG} Injecting widget… v1.15`); + console.log(`${LOG} Injecting widget… v1.16`); await ensureThreadId(); injectStyles(); const btn = createFloatingButton(); createTooltip(btn); - const panel = createSidebar(); + const panel = createSidebar(); // ← panel with resizer const chatLog = $("#ht-ai-chat"); const sendBtn = $("#ht-ai-send"); const inputBox = $("#ht-ai-question"); @@ -100,15 +94,8 @@ 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; - } - + b[cls === "ht-ai" ? "innerHTML" : "textContent"] = + cls === "ht-ai" ? mdToSafeHTML(text) : text; chatLog.appendChild(b); chatLog.scrollTop = chatLog.scrollHeight; return b; @@ -116,30 +103,28 @@ const LOADER_HTML = ''; - function setInputDisabled(d) { + const setInputDisabled = (d) => { inputBox.disabled = d; sendBtn.disabled = d; - } - function clearThreadCookie() { + }; + const clearThreadCookie = () => { document.cookie = "threadId=; Path=/; Max-Age=0"; - threadId = null; - } - function resetConversation() { + threadId = null; + }; + const 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."); + alert("Please highlight some text first."); return; } if (savedSelection.length > MAX_CONTEXT) { - alert( - `Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.` - ); + alert(`Highlighted text is too long. Max ${MAX_CONTEXT} chars.`); return; } chatLog.innerHTML = ""; @@ -157,11 +142,10 @@ 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 loading = addMsg("", "ht-ai"); + loading.innerHTML = LOADER_HTML; const content = context ? `### Context:\n${context}\n\n### Question to answer:\n${question}` @@ -178,43 +162,39 @@ 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."; + else if (res.status === 429) err = "Rate limit exceeded."; } catch (_) {} - loadingBubble.textContent = err; + loading.textContent = err; return; } const data = await res.json(); - loadingBubble.remove(); + loading.remove(); if (Array.isArray(data.response)) - data.response.forEach((p) => { + 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."; + loading.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}.` - ); + alert(`Question too long (${q.length}). Max ${MAX_QUESTION}.`); return; } inputBox.value = ""; @@ -228,9 +208,9 @@ handleSend(); } }); - } + } /* end init */ - /* ==================================================================== */ + /* =================================================================== */ async function ensureThreadId() { const m = document.cookie.match(/threadId=([^;]+)/); if (m && m[1]) { @@ -241,62 +221,67 @@ 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; + 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."); + alert("Failed to initialise the conversation. Please refresh."); 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}`; +#ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);min-width:60px;height:60px;border-radius:30px;background:linear-gradient(45deg, #b31328, #d42d3f, #2d5db4, #3470e4);background-size:300% 300%;animation:gradientShift 8s ease infinite;color:#fff;font-size:18px;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;padding:0 20px} +#ht-ai-btn span{margin-left:8px;font-weight:bold} +@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}} +#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%;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 */ +.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} +/* NEW: resizer handle */ +#ht-ai-resizer{position:absolute;left:0;top:0;width:6px;height:100%;cursor:ew-resize;background:transparent} +#ht-ai-resizer:hover{background:rgba(255,255,255,.05)}`; 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 = "🤖"; + d.innerHTML = "🤖HackTricksAI"; document.body.appendChild(d); return d; } - function createTooltip(btn) { const t = document.createElement("div"); t.id = "ht-ai-tooltip"; @@ -311,11 +296,16 @@ btn.addEventListener("mouseleave", () => t.classList.remove("show")); } + /* =================================================================== */ function createSidebar() { + const saved = parseInt(localStorage.getItem("htAiWidth") || DEF_W, 10); + const width = Math.min(Math.max(saved, MIN_W), MAX_W); + const p = document.createElement("div"); p.id = "ht-ai-panel"; + p.style.width = width + "px"; // ← applied width p.innerHTML = ` -
HackTricks AI Chat +
HackTricks AI Chat
@@ -326,7 +316,39 @@
`; + /* NEW: resizer strip */ + const resizer = document.createElement("div"); + resizer.id = "ht-ai-resizer"; + p.appendChild(resizer); document.body.appendChild(p); + addResizeLogic(resizer, p); return p; } + + /* ---------------- resize behaviour ---------------- */ + function addResizeLogic(handle, panel) { + let startX, startW, dragging = false; + + const onMove = (e) => { + if (!dragging) return; + const dx = startX - e.clientX; // dragging leftwards ⇒ +dx + let newW = startW + dx; + newW = Math.min(Math.max(newW, MIN_W), MAX_W); + panel.style.width = newW + "px"; + }; + const onUp = () => { + if (!dragging) return; + dragging = false; + localStorage.setItem("htAiWidth", parseInt(panel.style.width, 10)); + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + }; + handle.addEventListener("mousedown", (e) => { + dragging = true; + startX = e.clientX; + startW = parseInt(window.getComputedStyle(panel).width, 10); + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + }); + } })();