Translated ['src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting

This commit is contained in:
Translator
2025-05-17 05:01:15 +00:00
parent 22a841300e
commit b96d5e4853
2 changed files with 150 additions and 124 deletions

View File

@@ -9,7 +9,7 @@
La délégation à l'échelle du domaine de Google Workspace permet à un objet d'identité, soit une **application externe** du Google Workspace Marketplace, soit un **compte de service GCP** interne, d'**accéder aux données à travers le Workspace au nom des utilisateurs**.
> [!NOTE]
> Cela signifie essentiellement que les **comptes de service** à l'intérieur des projets GCP d'une organisation pourraient être capables d'**usurper l'identité des utilisateurs de Workspace** de la même organisation (ou même d'une différente).
> Cela signifie essentiellement que les **comptes de service** à l'intérieur des projets GCP d'une organisation pourraient être capables d'**imiter les utilisateurs de Workspace** de la même organisation (ou même d'une différente).
Pour plus d'informations sur le fonctionnement exact de cela, consultez :
@@ -19,12 +19,12 @@ gcp-understanding-domain-wide-delegation.md
### Compromettre une délégation existante
Si un attaquant **a compromis un accès sur GCP** et **connaît un email d'utilisateur Workspace valide** (de préférence **super admin**) de l'entreprise, il pourrait **énumérer tous les projets** auxquels il a accès, **énumérer tous les SA** des projets, vérifier à quels **comptes de service il a accès**, et **répéter** toutes ces étapes avec chaque SA qu'il peut usurper.\
Avec une **liste de tous les comptes de service** auxquels il a **accès** et la liste des **emails Workspace**, l'attaquant pourrait essayer d'**usurper l'identité de l'utilisateur avec chaque compte de service**.
Si un attaquant **compromet un accès sur GCP** et **connaît un email d'utilisateur Workspace valide** (de préférence **super admin**) de l'entreprise, il pourrait **énumérer tous les projets** auxquels il a accès, **énumérer tous les SA** des projets, vérifier à quels **comptes de service il a accès**, et **répéter** toutes ces étapes avec chaque SA qu'il peut imiter.\
Avec une **liste de tous les comptes de service** auxquels il a **accès** et la liste des **emails Workspace**, l'attaquant pourrait essayer d'**imiter l'utilisateur avec chaque compte de service**.
> [!CAUTION]
> Notez que lors de la configuration de la délégation à l'échelle du domaine, aucun utilisateur Workspace n'est nécessaire, donc il suffit de **connaître un valide pour l'usurpation**.\
> Cependant, les **privilèges de l'utilisateur usurpé seront utilisés**, donc s'il s'agit d'un Super Admin, vous pourrez accéder à tout. S'il n'a aucun accès, cela sera inutile.
> Notez que lors de la configuration de la délégation à l'échelle du domaine, aucun utilisateur Workspace n'est nécessaire, donc il suffit de **connaître un valide pour l'imitation**.\
> Cependant, les **privilèges de l'utilisateur imité seront utilisés**, donc s'il s'agit d'un Super Admin, vous pourrez accéder à tout. S'il n'a aucun accès, cela sera inutile.
#### [GCP Générer un jeton de délégation](https://github.com/carlospolop/gcp_gen_delegation_token)
@@ -36,16 +36,20 @@ python3 gen_delegation_token.py --user-email <user-email> --key-file <path-to-ke
# Impersonate indicated user and add additional scopes
python3 gen_delegation_token.py --user-email <user-email> --key-file <path-to-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)
Basé sur l'outil DeleFriend suivant, mais avec quelques ajouts comme la capacité d'énumérer le domaine, le drive, gmail, le calendrier et d'effectuer d'autres opérations.
#### [**DeleFriend**](https://github.com/axon-git/DeleFriend)
Ceci est un outil qui peut effectuer l'attaque en suivant ces étapes :
C'est un outil qui peut effectuer l'attaque en suivant ces étapes :
1. **Énumérer les projets GCP** en utilisant l'API Resource Manager.
2. Itérer sur chaque ressource de projet, et **énumérer les ressources de compte de service GCP** auxquelles l'utilisateur IAM initial a accès en utilisant _GetIAMPolicy_.
3. Itérer sur **chaque rôle de compte de service**, et trouver les rôles intégrés, de base et personnalisés avec la permission _**serviceAccountKeys.create**_ sur la ressource de compte de service cible. Il convient de noter que le rôle d'éditeur possède intrinsèquement cette permission.
4. Créer une **nouvelle clé privée `KEY_ALG_RSA_2048`** pour chaque ressource de compte de service qui est trouvée avec la permission pertinente dans la politique IAM.
5. Itérer sur **chaque nouveau compte de service et créer un objet `JWT`** pour celui-ci qui est composé des informations d'identification de la clé privée SA et d'une portée OAuth. Le processus de création d'un nouvel objet _JWT_ **itérera sur toutes les combinaisons existantes de portées OAuth** de la liste **oauth_scopes.txt**, afin de trouver toutes les possibilités de délégation. La liste **oauth_scopes.txt** est mise à jour avec toutes les portées OAuth que nous avons trouvées pertinentes pour abuser des identités Workspace.
6. La méthode `_make_authorization_grant_assertion` révèle la nécessité de déclarer un **utilisateur de workspace cible**, appelé _subject_, pour générer des JWT sous DWD. Bien que cela puisse sembler nécessiter un utilisateur spécifique, il est important de réaliser que **DWD influence chaque identité au sein d'un domaine**. Par conséquent, créer un JWT pour **n'importe quel utilisateur de domaine** affecte toutes les identités dans ce domaine, conformément à notre vérification d'énumération de combinaison. En d'autres termes, un utilisateur valide de Workspace est suffisant pour avancer.\
5. Itérer sur **chaque nouveau compte de service et créer un objet `JWT`** pour celui-ci qui est composé des informations d'identification de la clé privée SA et d'un scope OAuth. Le processus de création d'un nouvel objet _JWT_ **itérera sur toutes les combinaisons existantes de scopes OAuth** de la liste **oauth_scopes.txt**, afin de trouver toutes les possibilités de délégation. La liste **oauth_scopes.txt** est mise à jour avec tous les scopes OAuth que nous avons trouvés pertinents pour abuser des identités Workspace.
6. La méthode `_make_authorization_grant_assertion` révèle la nécessité de déclarer un **utilisateur de workspace cible**, appelé _subject_, pour générer des JWT sous DWD. Bien que cela puisse sembler nécessiter un utilisateur spécifique, il est important de réaliser que **DWD influence chaque identité au sein d'un domaine**. Par conséquent, créer un JWT pour **tout utilisateur de domaine** affecte toutes les identités dans ce domaine, conformément à notre vérification d'énumération de combinaison. En d'autres termes, un utilisateur valide de Workspace est suffisant pour avancer.\
Cet utilisateur peut être défini dans le fichier _config.yaml_ de DeleFriend. Si un utilisateur de workspace cible n'est pas déjà connu, l'outil facilite l'identification automatique des utilisateurs de workspace valides en scannant les utilisateurs de domaine avec des rôles sur les projets GCP. Il est essentiel de noter (encore une fois) que les JWT sont spécifiques au domaine et ne sont pas générés pour chaque utilisateur ; par conséquent, le processus automatique cible une seule identité unique par domaine.
7. **Énumérer et créer un nouveau jeton d'accès bearer** pour chaque JWT et valider le jeton contre l'API tokeninfo.
@@ -77,23 +81,23 @@ pip install --upgrade --user oauth2client
Il est possible de **vérifier les délégations à l'échelle du domaine dans** [**https://admin.google.com/u/1/ac/owl/domainwidedelegation**](https://admin.google.com/u/1/ac/owl/domainwidedelegation)**.**
Un attaquant ayant la capacité de **créer des comptes de service dans un projet GCP** et **des privilèges d'administrateur super pour GWS pourrait créer une nouvelle délégation permettant aux SAs d'usurper l'identité de certains utilisateurs GWS :**
Un attaquant ayant la capacité de **créer des comptes de service dans un projet GCP** et **des privilèges de super administrateur sur GWS pourrait créer une nouvelle délégation permettant aux SAs d'usurper l'identité de certains utilisateurs de GWS :**
1. **Génération d'un nouveau compte de service et d'une paire de clés correspondante :** Sur GCP, de nouvelles ressources de compte de service peuvent être produites soit de manière interactive via la console, soit de manière programmatique en utilisant des appels API directs et des outils CLI. Cela nécessite le **rôle `iam.serviceAccountAdmin`** ou tout rôle personnalisé équipé de la **permission `iam.serviceAccounts.create`**. Une fois le compte de service créé, nous procéderons à la génération d'une **paire de clés associée** (**permission `iam.serviceAccountKeys.create`**).
2. **Création d'une nouvelle délégation :** Il est important de comprendre que **seul le rôle d'administrateur super possède la capacité de configurer une délégation à l'échelle du domaine dans Google Workspace** et que la délégation à l'échelle du domaine **ne peut pas être configurée de manière programmatique.** Elle ne peut être créée et ajustée **manuellement** via la **console** Google Workspace.
- La création de la règle peut être trouvée sous la page **Contrôles API → Gérer la délégation à l'échelle du domaine dans la console d'administration Google Workspace**.
3. **Attachement des privilèges des scopes OAuth :** Lors de la configuration d'une nouvelle délégation, Google nécessite seulement 2 paramètres, l'ID client, qui est l'**ID OAuth de la ressource de compte de service GCP**, et les **scopes OAuth** qui définissent quels appels API la délégation nécessite.
- La **liste complète des scopes OAuth** peut être trouvée [**ici**](https://developers.google.com/identity/protocols/oauth2/scopes), mais voici une recommandation : `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`
4. **Agir au nom de l'identité cible :** À ce stade, nous avons un objet délégué fonctionnel dans GWS. Maintenant, **en utilisant la clé privée du compte de service GCP, nous pouvons effectuer des appels API** (dans le cadre défini dans le paramètre de scope OAuth) pour le déclencher et **agir au nom de toute identité existant dans Google Workspace**. Comme nous l'avons appris, le compte de service générera des jetons d'accès selon ses besoins et en fonction des permissions qu'il a pour les applications REST API.
- Consultez la **section précédente** pour quelques **outils** à utiliser avec cette délégation.
2. **Création d'une nouvelle délégation :** Il est important de comprendre que **seul le rôle de Super Administrateur possède la capacité de configurer une délégation à l'échelle du domaine dans Google Workspace** et que la délégation à l'échelle du domaine **ne peut pas être configurée de manière programmatique.** Elle ne peut être créée et ajustée **manuellement** via la **console** de Google Workspace.
- La création de la règle peut être trouvée sous la page **Contrôles API → Gérer la délégation à l'échelle du domaine dans la console d'administration de Google Workspace**.
3. **Attacher les privilèges des portées OAuth :** Lors de la configuration d'une nouvelle délégation, Google exige seulement 2 paramètres, l'ID client, qui est l'**ID OAuth de la ressource de compte de service GCP**, et les **portées OAuth** qui définissent quels appels API la délégation nécessite.
- La **liste complète des portées OAuth** peut être trouvée [**ici**](https://developers.google.com/identity/protocols/oauth2/scopes), mais voici une recommandation : `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`
4. **Agir au nom de l'identité cible :** À ce stade, nous avons un objet délégué fonctionnel dans GWS. Maintenant, **en utilisant la clé privée du compte de service GCP, nous pouvons effectuer des appels API** (dans la portée définie dans le paramètre de portée OAuth) pour le déclencher et **agir au nom de toute identité existant dans Google Workspace**. Comme nous l'avons appris, le compte de service générera des jetons d'accès selon ses besoins et en fonction des permissions qu'il a pour les applications REST API.
- Consultez la **section précédente** pour quelques **outils** permettant d'utiliser cette délégation.
#### Délégation inter-organisationnelle
L'ID SA OAuth est global et peut être utilisé pour la **délégation inter-organisationnelle**. Aucune restriction n'a été mise en place pour empêcher la délégation inter-groupe. En termes simples, **les comptes de service de différentes organisations GCP peuvent être utilisés pour configurer une délégation à l'échelle du domaine sur d'autres organisations Workspace**. Cela signifierait **qu'il suffit d'avoir un accès d'administrateur super à Workspace**, et non d'accéder au même compte GCP, car l'adversaire peut créer des comptes de service et des clés privées sur son compte GCP personnel.
L'ID SA OAuth est global et peut être utilisé pour la **délégation inter-organisationnelle**. Aucune restriction n'a été mise en place pour empêcher la délégation inter-groupe. En termes simples, **les comptes de service de différentes organisations GCP peuvent être utilisés pour configurer une délégation à l'échelle du domaine sur d'autres organisations Workspace**. Cela signifierait **qu'il suffit d'un accès de Super Administrateur à Workspace**, et non d'un accès au même compte GCP, car l'adversaire peut créer des comptes de service et des clés privées sur son compte GCP personnel.
### Création d'un projet pour énumérer Workspace
Par **défaut**, les **utilisateurs** de Workspace ont la permission de **créer de nouveaux projets**, et lorsqu'un nouveau projet est créé, le **créateur obtient le rôle de propriétaire** sur celui-ci.
Par **défaut**, les **utilisateurs** de Workspace ont la permission de **créer de nouveaux projets**, et lorsqu'un nouveau projet est créé, le **créateur obtient le rôle de Propriétaire** sur celui-ci.
Par conséquent, un utilisateur peut **créer un projet**, **activer** les **API** pour énumérer Workspace dans son nouveau projet et essayer de **l'énumérer**.

View File

@@ -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: dragtoresize 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 = "[HackTricksAI]";
/* ---------------- Usertunable constants ---------------- */
const MAX_CONTEXT = 3000; // highlightedtext 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. 3rdparty 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 =
'<span class="ht-loading"><span></span><span></span><span></span></span>';
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 = "🤖<span>HackTricksAI</span>";
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 = `
<div id="ht-ai-header"><strong>HackTricks AI Chat</strong>
<div id="ht-ai-header"><strong>HackTricks AI Chat</strong>
<div class="ht-actions">
<button id="ht-ai-reset" title="Reset">↺</button>
<span id="ht-ai-close" title="Close">✖</span>
@@ -326,7 +316,39 @@
<textarea id="ht-ai-question" placeholder="Type your question…"></textarea>
<button id="ht-ai-send">Send</button>
</div>`;
/* 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);
});
}
})();