From 563408b51e0bfdcaa371ac6e7846bf4a254b89ab Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 21:22:20 +0000 Subject: [PATCH 1/2] feat(ai): add BYOK AI assistant with bug-hunting triage skills Adds an optional bring-your-own-key LLM layer that helps triage data the recon pipeline already collected for an in-scope target. It never runs scans or generates exploit payloads itself - it reads a target's stored report (findings/secrets/services/endpoints), optionally grounds the review in the analyst's own knowledge-base notes, and asks the configured provider to prioritize, flag likely false positives, and point to the next manual-verification step. - ai_assistant.py: provider-agnostic client (anthropic | openai | any OpenAI-compatible endpoint via AI_BASE_URL), skill loader, and a knowledge-base bridge that pulls methodology/checklist sections (not payload dictionaries, unless --kb-payloads is passed) from a local clone of the user's own vuln-notes repo, keyed off each finding's vuln_type. - ai_skills/*.skill.md: four starter skills - triage_findings, js_secrets_review, recon_summary, kb_crossref - as plain Markdown templates so new skills need no code changes. - db_sqlite.py / back/schema.sql: new ai_reviews table + save/get helpers for review history, following the existing findings-table patterns. - cHack.sh: --ai-review/--ai-skill/--ai-kb/--ai-save/--ai-history flags. - .env.example: AI_PROVIDER/AI_API_KEY/AI_MODEL/AI_BASE_URL/AI_KB_PATH (BYOK - no key ships with cHack). - README.md: usage section + project-structure entries. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01S4RPaDzDNabPSHexgxVdoB --- .env.example | 15 ++ README.md | 69 +++++ ai_assistant.py | 360 +++++++++++++++++++++++++++ ai_skills/README.md | 40 +++ ai_skills/js_secrets_review.skill.md | 36 +++ ai_skills/kb_crossref.skill.md | 32 +++ ai_skills/recon_summary.skill.md | 26 ++ ai_skills/triage_findings.skill.md | 39 +++ back/schema.sql | 15 ++ cHack.sh | 49 ++++ db_sqlite.py | 31 +++ 11 files changed, 712 insertions(+) create mode 100755 ai_assistant.py create mode 100644 ai_skills/README.md create mode 100644 ai_skills/js_secrets_review.skill.md create mode 100644 ai_skills/kb_crossref.skill.md create mode 100644 ai_skills/recon_summary.skill.md create mode 100644 ai_skills/triage_findings.skill.md diff --git a/.env.example b/.env.example index 7b124fb..776cb5d 100644 --- a/.env.example +++ b/.env.example @@ -27,3 +27,18 @@ RUNNER_TOKEN= # Must match the Worker's RUNNER_TOKEN (set via: wrangler secret put RUNNER_TOKEN). # Generate one with: openssl rand -hex 32 RUNNER_TOKEN=your_shared_runner_token + +# --- BYOK AI Assistant (optional; see ai_assistant.py) --- +# Bring-your-own-key triage layer over your own stored scan data. Nothing +# here is required to run cHack's scans — only `--ai-review` / `--ai-history`. +# AI_PROVIDER: anthropic (default) | openai | generic (any OpenAI-compatible +# endpoint, e.g. a local model server — set AI_BASE_URL for it). +AI_PROVIDER=anthropic +AI_API_KEY= +AI_MODEL= +AI_BASE_URL= +# Path to your own knowledge-base repo (vuln methodology notes), used by +# skills that set requires_kb: true when run with --ai-kb. Expects a +# Vulns/.md note per vuln class, matching ai_assistant.py's +# VULN_TYPE_TO_KB_NOTE mapping. +AI_KB_PATH= diff --git a/README.md b/README.md index dddff2e..a6d16b6 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,73 @@ snippets are masked in results. ASN is not yet captured by the pipeline, so the --- +## 🧠 BYOK AI Assistant + +An optional, bring-your-own-key triage layer over data the pipeline **already +collected** for one of your own in-scope targets. It never runs scans or +produces new exploit payloads — it reads your stored report, optionally +grounds itself in your own methodology notes, and helps you prioritize and +decide what to verify by hand next. Your key, your provider, your bill; cHack +never ships or proxies a key for you. + +```bash +# List available skills +python3 ai_assistant.py skills + +# Triage a target's stored findings (dedupe, re-rank, flag likely noise, +# suggest a next manual-verification step per finding) +./cHack.sh --ai-review acme.com + +# Same, but ground it in your own knowledge-base notes (see AI_KB_PATH below) +./cHack.sh --ai-review acme.com --ai-kb --ai-save + +# Review JS-secret scanner output for true-positive likelihood +./cHack.sh --ai-review acme.com --ai-skill js_secrets_review + +# Short exec summary suitable for pasting into Telegram/a teammate +./cHack.sh --ai-review acme.com --ai-skill recon_summary + +# Past AI reviews stored for a target (requires --ai-save at review time) +./cHack.sh --ai-history acme.com +``` + +**Configure (`.env`):** + +```bash +AI_PROVIDER=anthropic # anthropic (default) | openai | generic +AI_API_KEY= # your own key — never committed, never shipped +AI_MODEL= # optional override; sane per-provider default +AI_BASE_URL= # required for AI_PROVIDER=generic (self-hosted / + # OpenAI-compatible endpoint) +AI_KB_PATH= # path to a local clone of your own vuln-notes + # repo (one Vulns/.md per class) for + # skills that take --ai-kb +``` + +**Skills** live as plain Markdown prompt templates in `ai_skills/*.skill.md` — +drop in a new file to add one, no code changes needed (see +`ai_skills/README.md`). Shipped skills: + +| Skill | What it does | +| :--- | :--- | +| `triage_findings` (default) | Re-ranks findings by real risk, flags likely false positives, gives one next-step per finding | +| `js_secrets_review` | Sanity-checks `js_secrets.py`'s confirmed-live/candidate/informational tiering, groups by provider | +| `recon_summary` | Short, non-technical exec summary of a target's current report | +| `kb_crossref` | Maps each finding's vuln class to your own KB checklist and flags unverified checklist items | + +When `--ai-kb` is set, only the methodology/overview/impact/remediation/ +testing-checklist sections of the matching `Vulns/.md` note are sent to +the model by default — the payload-dictionary sections are left out unless +you also pass `--ai-kb --kb-payloads` (via `python3 ai_assistant.py review ... +--kb --kb-payloads` directly), since that's denser content you may not want +leaving your machine by default. + +> ⚠️ Same authorized-use scope as the rest of cHack: only point this at +> targets you already have a right to test, and treat whatever text you send +> to a third-party AI provider accordingly. + +--- + ## 🤖 Telegram Bot Once the Worker is deployed and a runner daemon is online, control everything from Telegram: @@ -446,6 +513,8 @@ cHack/ ├── js_secrets.py # JavaScript secret/credential scanner ├── client_bugs.py # Client-side bug analyzer (DOM XSS, CORS, open redirect, ...) ├── db_sqlite.py # local SQLite store + D1 sync layer +├── ai_assistant.py # BYOK AI triage assistant over stored scan data +├── ai_skills/ # Markdown prompt templates used by ai_assistant.py ├── install.sh # VPS provisioning (Go, toolchain, Python deps) ├── back/ # Cloudflare Worker: Telegram bot + GraphQL API │ ├── src/index.js diff --git a/ai_assistant.py b/ai_assistant.py new file mode 100755 index 0000000..a6b38b4 --- /dev/null +++ b/ai_assistant.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 +""" +BYOK AI Assistant for cHack — authorized-use only. + +Bring-your-own-key LLM layer that helps a human analyst triage cHack's own +stored scan data (findings, secrets, services, endpoints) faster. It never +runs scans or generates exploit payloads itself: it reads what the recon +pipeline already collected for an in-scope target, optionally grounds the +review in the analyst's own knowledge-base notes, and asks the configured +model to summarize / prioritize / point to the next manual-verification +step. + +Configuration (env vars, normally set in .env and sourced by cHack.sh): + + AI_PROVIDER anthropic (default) | openai | generic + AI_API_KEY your own API key for that provider (BYOK — never shipped + with cHack, never phoned home anywhere but the provider + you configure) + AI_MODEL model id (sensible per-provider default if unset) + AI_BASE_URL override endpoint (required for provider=generic, i.e. + any OpenAI-compatible endpoint: local model server, + self-hosted gateway, etc.) + AI_KB_PATH path to your own knowledge-base repo (e.g. a local clone + of your personal vuln-notes vault) used for grounding + skills that set requires_kb: true + +Usage: + python3 ai_assistant.py skills + python3 ai_assistant.py review [--skill NAME] [--kb] [--kb-payloads] [--save] + python3 ai_assistant.py history [--limit N] +""" +import argparse +import glob +import json +import os +import sys +import urllib.error +import urllib.request + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +SKILLS_DIR = os.path.join(SCRIPT_DIR, "ai_skills") + +DEFAULT_MODELS = { + "anthropic": "claude-sonnet-5-5", + "openai": "gpt-5", + "generic": "", +} + +# Maps cHack's `vuln_type` finding values to a knowledge-base note filename +# (relative to AI_KB_PATH/Vulns/). Extend as your own KB's taxonomy grows. +VULN_TYPE_TO_KB_NOTE = { + "xss": "XSS", "dom_xss": "XSS", + "sqli": "SQLi", "sql_injection": "SQLi", "nosql": "NoSQL", "nosqli": "NoSQL", + "cors": "CORS", "ssrf": "SSRF", "ssti": "SSTI", "xxe": "XXE", + "csrf": "CSRF", "clickjacking": "Clickjacking", + "idor": "AccessControl", "access_control": "AccessControl", "bac": "AccessControl", + "auth": "Authentication", "authentication": "Authentication", + "jwt": "JWT", "graphql": "GraphQL", "api": "API", + "prototype_pollution": "PrototypePollution", + "command_injection": "CommandInjection", "rce": "CommandInjection", + "path_traversal": "PathTraversal", "lfi": "PathTraversal", + "file_upload": "FileUpload", + "request_smuggling": "RequestSmuggling", + "cache_poisoning": "WebCachePoisoning", "cache_deception": "WebCacheDeception", + "deserialization": "InsecureDeserialization", + "websocket": "WebSockets", "websockets": "WebSockets", + "race_condition": "RaceConditions", "race": "RaceConditions", + "business_logic": "BusinessLogic", + "secret": "InformationDisclosure", "exposure": "InformationDisclosure", + "info_disclosure": "InformationDisclosure", + "dos": "DoS", "webllm": "WebLLM", "llm": "WebLLM", +} + +# KB note sections considered "methodology" (safe default context): overview, +# attack-vector explanation, impact, remediation, testing checklist. Payload +# dictionaries are excluded unless --kb-payloads is passed explicitly, since +# that content is denser/more sensitive to ship to a third-party API. +_SAFE_SECTION_PREFIXES = ( + "## 1.", "## 2.", "## 5.", "## 6.", "## 7.", +) +_PAYLOAD_SECTION_PREFIXES = ("## 4.", "## 8.") + + +def _load_env_file(): + """Best-effort .env loader so `python3 ai_assistant.py ...` works even + when invoked directly (cHack.sh already sources .env for its own calls).""" + env_path = os.path.join(SCRIPT_DIR, ".env") + if not os.path.exists(env_path): + return + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip('"').strip("'") + os.environ.setdefault(key, value) + + +def list_skills(): + skills = [] + for path in sorted(glob.glob(os.path.join(SKILLS_DIR, "*.skill.md"))): + meta, _ = _parse_skill_file(path) + skills.append(meta) + return skills + + +def _parse_skill_file(path): + with open(path) as f: + raw = f.read() + meta = {"name": os.path.basename(path).replace(".skill.md", ""), + "title": "", "description": "", "requires_kb": False} + body = raw + if raw.startswith("---"): + end = raw.find("\n---", 3) + if end != -1: + frontmatter = raw[3:end].strip() + body = raw[end + 4:].lstrip("\n") + for line in frontmatter.splitlines(): + if ":" not in line: + continue + k, _, v = line.partition(":") + k, v = k.strip(), v.strip() + if k == "requires_kb": + meta[k] = v.lower() in ("true", "yes", "1") + else: + meta[k] = v + return meta, body + + +def load_skill(name): + path = os.path.join(SKILLS_DIR, f"{name}.skill.md") + if not os.path.exists(path): + available = ", ".join(s["name"] for s in list_skills()) + raise SystemExit(f"Unknown skill '{name}'. Available: {available}") + return _parse_skill_file(path) + + +def build_context(domain_or_slug, max_findings=60, max_secrets=40, max_endpoints=30): + """Pull the target's own stored report via db_sqlite and trim it to a + size reasonable to hand to an LLM in one call.""" + sys.path.insert(0, SCRIPT_DIR) + import db_sqlite # local import: reuses cHack's existing report builder + + report = db_sqlite.generate_report(domain_or_slug) + if report.get("target") is None: + raise SystemExit(f"No stored target found for '{domain_or_slug}'. " + f"Run a scan / --sync first.") + + report["findings"] = report["findings"][:max_findings] + report["secrets"] = report["secrets"][:max_secrets] + report["endpoints"] = report["endpoints"][:max_endpoints] + return report + + +def _kb_note_path(kb_path, note_name): + for ext_dir in ("Vulns",): + candidate = os.path.join(kb_path, ext_dir, f"{note_name}.md") + if os.path.exists(candidate): + return candidate + return None + + +def _strip_frontmatter(md_text): + if md_text.startswith("---"): + end = md_text.find("\n---", 3) + if end != -1: + return md_text[end + 4:].lstrip("\n") + return md_text + + +def _extract_safe_sections(md_text, include_payloads=False): + lines = _strip_frontmatter(md_text).splitlines() + out, keep = [], True # keep the frontmatter/intro before the first "## " + for line in lines: + if line.startswith("## "): + is_payload = line.startswith(_PAYLOAD_SECTION_PREFIXES) + keep = include_payloads or not is_payload + if not include_payloads and is_payload: + continue + if keep: + out.append(line) + return "\n".join(out).strip() + + +def kb_context_for_report(report, kb_path, include_payloads=False, max_chars=6000): + """Pull the relevant knowledge-base note(s) for the vuln types present + in a report, trimmed to methodology/checklist content by default.""" + if not kb_path or not os.path.isdir(kb_path): + return "(no knowledge base configured — set AI_KB_PATH to your notes repo path)" + + vuln_types = {f.get("vuln_type", "").lower() for f in report.get("findings", [])} + note_names = sorted({VULN_TYPE_TO_KB_NOTE[v] for v in vuln_types if v in VULN_TYPE_TO_KB_NOTE}) + + if not note_names: + return "(no matching knowledge-base notes for the vuln types in this report)" + + chunks = [] + for note in note_names: + path = _kb_note_path(kb_path, note) + if not path: + continue + with open(path, encoding="utf-8", errors="ignore") as f: + text = f.read() + snippet = _extract_safe_sections(text, include_payloads=include_payloads) + chunks.append(f"### {note}\n{snippet}") + + combined = "\n\n".join(chunks) + if len(combined) > max_chars: + combined = combined[:max_chars] + "\n...(truncated)" + return combined or "(matching notes found but could not be read)" + + +def _call_anthropic(api_key, model, system, user, base_url=None): + url = (base_url or "https://api.anthropic.com") + "/v1/messages" + payload = { + "model": model, + "max_tokens": 4096, + "system": system, + "messages": [{"role": "user", "content": user}], + } + req = urllib.request.Request( + url, data=json.dumps(payload).encode(), + headers={ + "Content-Type": "application/json", + "x-api-key": api_key, + "anthropic-version": "2023-06-01", + }, + ) + with urllib.request.urlopen(req, timeout=120) as resp: + data = json.loads(resp.read().decode()) + return "".join(block.get("text", "") for block in data.get("content", [])) + + +def _call_openai_compatible(api_key, model, system, user, base_url=None): + url = (base_url or "https://api.openai.com/v1") + "/chat/completions" + payload = { + "model": model, + "messages": [ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + } + req = urllib.request.Request( + url, data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, + ) + with urllib.request.urlopen(req, timeout=120) as resp: + data = json.loads(resp.read().decode()) + return data["choices"][0]["message"]["content"] + + +def call_llm(system_prompt, user_prompt): + _load_env_file() + provider = os.environ.get("AI_PROVIDER", "anthropic").lower() + api_key = os.environ.get("AI_API_KEY", "") + if not api_key: + raise SystemExit( + "AI_API_KEY is not set. This is BYOK: put your own provider API " + "key in .env (AI_API_KEY=...) — cHack never ships or proxies a key." + ) + model = os.environ.get("AI_MODEL") or DEFAULT_MODELS.get(provider, "") + base_url = os.environ.get("AI_BASE_URL") or None + + try: + if provider == "anthropic": + return _call_anthropic(api_key, model, system_prompt, user_prompt, base_url) + elif provider in ("openai", "generic"): + if provider == "generic" and not base_url: + raise SystemExit("AI_PROVIDER=generic requires AI_BASE_URL.") + return _call_openai_compatible(api_key, model, system_prompt, user_prompt, base_url) + else: + raise SystemExit(f"Unknown AI_PROVIDER '{provider}' (use anthropic | openai | generic).") + except urllib.error.HTTPError as e: + body = e.read().decode(errors="ignore") + raise SystemExit(f"AI provider returned HTTP {e.code}: {body[:500]}") + except urllib.error.URLError as e: + raise SystemExit(f"Could not reach AI provider ({base_url or provider}): {e}") + + +def run_skill(domain_or_slug, skill_name, use_kb=False, kb_payloads=False): + meta, prompt_template = load_skill(skill_name) + report = build_context(domain_or_slug) + + kb_text = "" + if meta.get("requires_kb") and use_kb: + kb_path = os.environ.get("AI_KB_PATH", "") + kb_text = kb_context_for_report(report, kb_path, include_payloads=kb_payloads) + elif meta.get("requires_kb") and not use_kb: + kb_text = "(knowledge-base context not requested — pass --kb to include it)" + + user_prompt = prompt_template.format( + context=json.dumps(report, indent=2, default=str), + kb_context=kb_text, + ) + system_prompt = ( + "You are an authorized-use security triage assistant embedded in the " + "cHack recon platform. Only ever analyze data the user's own tooling " + "already collected on their own in-scope targets; never produce new " + "exploit payloads or instructions to attack out-of-scope systems." + ) + output = call_llm(system_prompt, user_prompt) + return meta, report, output + + +def save_review(domain_or_slug, skill_name, output): + sys.path.insert(0, SCRIPT_DIR) + import db_sqlite + model = os.environ.get("AI_MODEL") or DEFAULT_MODELS.get( + os.environ.get("AI_PROVIDER", "anthropic").lower(), "") + return db_sqlite.save_ai_review(domain_or_slug, skill_name, model, output) + + +def main(): + parser = argparse.ArgumentParser(description="cHack BYOK AI assistant") + sub = parser.add_subparsers(dest="cmd", required=True) + + sub.add_parser("skills", help="List available AI skills") + + p_review = sub.add_parser("review", help="Run an AI skill against a stored target report") + p_review.add_argument("target") + p_review.add_argument("--skill", default="triage_findings") + p_review.add_argument("--kb", action="store_true", help="Include knowledge-base context") + p_review.add_argument("--kb-payloads", action="store_true", + help="Also include the KB's payload-dictionary sections (sent to the configured AI provider)") + p_review.add_argument("--save", action="store_true", help="Store the output in local.db") + + p_hist = sub.add_parser("history", help="Show past AI reviews for a target") + p_hist.add_argument("target") + p_hist.add_argument("--limit", type=int, default=10) + + args = parser.parse_args() + + if args.cmd == "skills": + for s in list_skills(): + kb = " [needs --kb]" if s.get("requires_kb") else "" + print(f"{s['name']:<20} {s.get('description','')}{kb}") + return + + if args.cmd == "review": + meta, _report, output = run_skill(args.target, args.skill, args.kb, args.kb_payloads) + print(f"\n=== {meta.get('title', args.skill)} — {args.target} ===\n") + print(output) + if args.save: + save_review(args.target, args.skill, output) + print("\n[saved to local.db — see `ai_assistant.py history`]") + return + + if args.cmd == "history": + sys.path.insert(0, SCRIPT_DIR) + import db_sqlite + for row in db_sqlite.get_ai_reviews(args.target, args.limit): + print(f"[{row['created_at']}] {row['skill']} ({row['model']})") + print(row["output"]) + print("-" * 60) + + +if __name__ == "__main__": + main() diff --git a/ai_skills/README.md b/ai_skills/README.md new file mode 100644 index 0000000..863fd94 --- /dev/null +++ b/ai_skills/README.md @@ -0,0 +1,40 @@ +# AI Skills + +Each `*.skill.md` file here is a prompt template used by `ai_assistant.py` +(the BYOK AI review layer — see `../README.md` → "BYOK AI Assistant"). + +## Format + +```markdown +--- +name: triage_findings +title: Findings Triage +description: One line shown by `ai_assistant.py skills` +requires_kb: true +--- + +``` + +- `{context}` is replaced with the compact JSON report for the target + (`db_sqlite.generate_report()` — findings, secrets, services, endpoints). +- `{kb_context}` is replaced with methodology notes pulled from your own + knowledge-base repo (see `AI_KB_PATH` in `.env.example`), when + `requires_kb: true` and the user passes `--kb`. + +## Scope + +These skills are triage/analysis aids for data your own pipeline already +collected on in-scope, authorized targets. They summarize, prioritize, and +point to the next manual-verification step — they never ask the model to +generate exploit payloads or attack code; that's what your KB's payload +sheets and your own judgment are for. + +## Adding a skill + +Drop a new `.skill.md` file here following the format above. It's +picked up automatically — no code changes needed. List available skills +with: + +``` +python3 ai_assistant.py skills +``` diff --git a/ai_skills/js_secrets_review.skill.md b/ai_skills/js_secrets_review.skill.md new file mode 100644 index 0000000..992e190 --- /dev/null +++ b/ai_skills/js_secrets_review.skill.md @@ -0,0 +1,36 @@ +--- +name: js_secrets_review +title: JS Secrets Review +description: Review js_secrets.py output and classify confirmed-live/candidate/noise, no payloads +requires_kb: false +--- +You are reviewing output from `js_secrets.py`, cHack's JavaScript secret +scanner, for a single authorized target. Each item already carries a +tiered classification (confirmed-live / candidate / informational) from +the scanner's own entropy and vendor-pattern checks. + +For the secrets/findings JSON below: + +1. Sanity-check the scanner's tiering: does anything tagged "candidate" or + "informational" look, from the surrounding evidence (variable name, + file path, vendor prefix), like it's actually live and worth escalating + — or does anything tagged higher look like a placeholder/test/example + key that should be downgraded? +2. Group by credential type/provider (cloud, payment, analytics, internal + API, etc.) and note any provider whose *read-only* verification API + could confirm liveness without taking any write/destructive action — + name the provider and what a read-only check would look like in plain + English, not code. +3. Call out any secret that appears in a client-bundled file (vs. + server-only context) as a higher-priority exposure regardless of the + credential's own blast radius, since it's already public. +4. Produce a short remediation note per confirmed/escalated item: rotate + credential, scope it down, move it server-side, etc. + +Do not fabricate API calls, write code to exfiltrate or use any secret, +and do not output the raw secret value beyond what's already redacted in +the input. Output is for the human operator's own authorized review only. + +## Secrets / findings (JSON) + +{context} diff --git a/ai_skills/kb_crossref.skill.md b/ai_skills/kb_crossref.skill.md new file mode 100644 index 0000000..d13125f --- /dev/null +++ b/ai_skills/kb_crossref.skill.md @@ -0,0 +1,32 @@ +--- +name: kb_crossref +title: Knowledge-Base Cross-Reference +description: Map each finding's vuln class to your own KB checklist and flag unverified checklist items +requires_kb: true +--- +You are cross-referencing a target's findings against the human analyst's +own personal methodology notes (their knowledge-base repo). You are not +being asked for exploit payloads — the KB's own payload sheets already +hold those; your job is purely to connect "what the scanner found" to +"what the analyst's own checklist says to verify next." + +For each distinct vulnerability class present in the findings below: + +1. Name the matching knowledge-base note (from "Knowledge base context"). +2. From that note's testing checklist, list the 2-4 checklist items most + relevant to THIS specific finding (its URL/parameter/context) that are + not yet obviously confirmed by the scanner's own evidence. +3. Note any related vulnerability class the KB note cross-links (e.g. XSS + note links CSRF/CORS/Clickjacking) that's worth a quick look on the + same target even if the scanner didn't flag it directly. + +If a finding's vuln class has no matching KB note in the context provided, +say so plainly instead of guessing. + +## Findings (JSON) + +{context} + +## Knowledge base context (your own methodology notes) + +{kb_context} diff --git a/ai_skills/recon_summary.skill.md b/ai_skills/recon_summary.skill.md new file mode 100644 index 0000000..602644c --- /dev/null +++ b/ai_skills/recon_summary.skill.md @@ -0,0 +1,26 @@ +--- +name: recon_summary +title: Executive Recon Summary +description: Turn a target's stored report into a short exec summary (for Telegram/team sharing) +requires_kb: false +--- +Summarize the following cHack scan report for a non-technical stakeholder +or teammate who needs the headline, not the raw data. This is an +authorized engagement/bug-bounty target. + +Write: + +- One paragraph: overall exposure picture (attack surface size, worst + finding class present, anything time-sensitive like a live credential). +- A short bullet list: counts by severity, and the single riskiest finding + with a one-line "why it matters" (impact, not technical mechanism). +- One line: suggested next action for the team (re-scan cadence, escalate + to manual testing, nothing urgent, etc.) + +Keep it under ~150 words total, no markdown tables, plain sentences — this +gets pasted into a Telegram message. Do not include secret values, raw +URLs with tokens, or exploit instructions. + +## Report (JSON) + +{context} diff --git a/ai_skills/triage_findings.skill.md b/ai_skills/triage_findings.skill.md new file mode 100644 index 0000000..4f25508 --- /dev/null +++ b/ai_skills/triage_findings.skill.md @@ -0,0 +1,39 @@ +--- +name: triage_findings +title: Findings Triage +description: Prioritize stored findings, flag likely false positives, suggest next manual-verification step per finding +requires_kb: true +--- +You are a senior application-security reviewer triaging findings that an +authorized reconnaissance pipeline (cHack) already collected for a single +in-scope target. You are NOT asked to exploit anything or produce new +attack payloads — only to help a human analyst prioritize and verify their +own tool output faster. + +For the JSON report below: + +1. Group findings by vulnerability class and re-rank by real-world exploit + risk (not just the tool's reported severity) — call out where you'd + raise or lower a finding's priority and why. +2. Flag findings that look like likely false positives or noise (e.g. + generic security-header warnings on a static asset host, low-entropy + "secrets", reflected input that is already output-encoded) and say + what signal makes you suspect that. +3. For each distinct finding (not each duplicate), give ONE concrete next + manual-verification step a human should try — grounded in the + methodology notes under "Knowledge base context" below where relevant. +4. Close with a short "work next" ordered list: the 3-5 findings worth a + human's time first. + +Be concise. Use the finding IDs/URLs so output maps back to the report. +Do not invent findings that aren't in the data, and do not write exploit +code or payloads — point to the matching knowledge-base checklist item +instead and let the human take it from there. + +## Report (JSON) + +{context} + +## Knowledge base context (your own methodology notes) + +{kb_context} diff --git a/back/schema.sql b/back/schema.sql index afee7d4..99928db 100644 --- a/back/schema.sql +++ b/back/schema.sql @@ -165,6 +165,21 @@ CREATE INDEX IF NOT EXISTS idx_findings_company_id ON findings(company_id); CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity); CREATE INDEX IF NOT EXISTS idx_findings_vuln_type ON findings(vuln_type); +-- BYOK AI assistant review history (see ai_assistant.py). Stores the +-- analyst-facing output of a skill run against a target's own stored +-- report; never holds provider API keys or raw secret values. +CREATE TABLE IF NOT EXISTS ai_reviews ( + id TEXT PRIMARY KEY, + domain_id TEXT REFERENCES domains(id) ON DELETE CASCADE, + company_id TEXT REFERENCES companies(id) ON DELETE CASCADE, + skill TEXT NOT NULL, + model TEXT, + output TEXT NOT NULL, + created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); + +CREATE INDEX IF NOT EXISTS idx_ai_reviews_domain_id ON ai_reviews(domain_id); + -- Discovered Endpoints / URLs table CREATE TABLE IF NOT EXISTS endpoints ( id TEXT PRIMARY KEY, diff --git a/cHack.sh b/cHack.sh index d1fc884..ae6f87a 100755 --- a/cHack.sh +++ b/cHack.sh @@ -2903,6 +2903,14 @@ show_usage() { echo " --format FMT Report format: json (default) | sarif | stix" echo " --include-secrets Include secret values in reports (default: redacted)" echo "" + echo "BYOK AI Assistant (triage aid over your own stored data; see .env.example):" + echo " --ai-review TARGET Run an AI triage skill against a target's stored report" + echo " --ai-skill NAME Skill to run (default: triage_findings; list with" + echo " 'python3 ai_assistant.py skills')" + echo " --ai-kb Ground the review in your AI_KB_PATH notes repo" + echo " --ai-save Store the AI output in local.db (see --ai-history)" + echo " --ai-history TARGET Show past AI reviews stored for a target" + echo "" echo "Target & Entity Management:" echo " --sync DOMAIN Sync on-disk scan data to Cloudflare D1 & local SQLite" echo " --sync-all Sync all targets from disk to Cloudflare D1 & SQLite" @@ -3123,6 +3131,11 @@ main() { local SYNC_ALL=false local REPORT_TARGET="" local REPORT_FORMAT="json" + local AI_REVIEW_TARGET="" + local AI_SKILL="triage_findings" + local AI_USE_KB=false + local AI_SAVE=false + local AI_HISTORY_TARGET="" SENDER_ID="" while [[ $# -gt 0 ]]; do @@ -3177,6 +3190,29 @@ main() { INCLUDE_SECRETS=true shift ;; + --ai-review) + if [ $# -lt 2 ]; then log_error "Option $1 requires an argument."; exit 1; fi + AI_REVIEW_TARGET="$2" + shift 2 + ;; + --ai-skill) + if [ $# -lt 2 ]; then log_error "Option $1 requires an argument."; exit 1; fi + AI_SKILL="$2" + shift 2 + ;; + --ai-kb) + AI_USE_KB=true + shift + ;; + --ai-save) + AI_SAVE=true + shift + ;; + --ai-history) + if [ $# -lt 2 ]; then log_error "Option $1 requires an argument."; exit 1; fi + AI_HISTORY_TARGET="$2" + shift 2 + ;; --sync-all) SYNC_ALL=true shift @@ -3274,6 +3310,19 @@ main() { exit 1 fi + # BYOK AI Assistant (triage aid over the target's own stored report) + if [ -n "$AI_REVIEW_TARGET" ]; then + local ai_args=(review "$AI_REVIEW_TARGET" --skill "$AI_SKILL") + [ "$AI_USE_KB" = true ] && ai_args+=(--kb) + [ "$AI_SAVE" = true ] && ai_args+=(--save) + python3 "$SCRIPT_DIR/ai_assistant.py" "${ai_args[@]}" + exit $? + fi + if [ -n "$AI_HISTORY_TARGET" ]; then + python3 "$SCRIPT_DIR/ai_assistant.py" history "$AI_HISTORY_TARGET" + exit $? + fi + # Report Export (json | sarif | stix) if [ -n "$REPORT_TARGET" ]; then local report_args=() diff --git a/db_sqlite.py b/db_sqlite.py index 1590b67..bfd7e88 100644 --- a/db_sqlite.py +++ b/db_sqlite.py @@ -953,6 +953,37 @@ def get_findings_for_domain(domain_or_slug, severity=None): conn.close() return findings +def save_ai_review(domain_or_slug, skill, model, output): + """Store a BYOK AI assistant review (ai_assistant.py) against whichever + company the domain/slug resolves to. Never stores the provider API key + or raw secret values — only the model's analyst-facing output text.""" + conn = get_db() + company_id = _resolve_company_id(conn, domain_or_slug.lower()) + review_id = str(uuid.uuid4()) + conn.execute( + "INSERT INTO ai_reviews (id, domain_id, company_id, skill, model, output) " + "VALUES (?, NULL, ?, ?, ?, ?)", + (review_id, company_id, skill, model, output), + ) + conn.commit() + conn.close() + return review_id + + +def get_ai_reviews(domain_or_slug, limit=10): + conn = get_db() + company_id = _resolve_company_id(conn, domain_or_slug.lower()) + cur = conn.cursor() + cur.execute( + "SELECT skill, model, output, created_at FROM ai_reviews " + "WHERE company_id = ? ORDER BY created_at DESC LIMIT ?", + (company_id, limit), + ) + rows = [dict(r) for r in cur.fetchall()] + conn.close() + return rows + + def get_secrets_for_domain(domain_or_slug): conn = get_db() cursor = conn.cursor() From 983a33ddb887e47bfaf39f09bf74fcdde48f954a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 21:38:36 +0000 Subject: [PATCH 2/2] docs(skills): add a Claude Code skill for agents working in this repo Adds .claude/skills/chack-recon/SKILL.md so any Claude Code agent (or other agent reading this convention) that touches the repo picks up the authorized-use scope rule, the cHack.sh CLI surface, where scan data lives (db_sqlite.py query helpers, redaction defaults), how to drive the BYOK AI assistant, and the repo's own conventions for extending the pipeline (finding schema, supply-chain pinning, shared D1/SQLite schema). Carves out .claude/skills/ from the existing .claude/ gitignore entry so the skill is checked in and visible to any agent, while local workspace state (worktrees, plans, settings) stays ignored. Also adds ai_assistant.py to the lint workflow's py_compile syntax check, which previously only covered the other three Python modules. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01S4RPaDzDNabPSHexgxVdoB --- .claude/skills/chack-recon/SKILL.md | 114 ++++++++++++++++++++++++++++ .github/workflows/lint.yml | 2 +- .gitignore | 7 +- 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/chack-recon/SKILL.md diff --git a/.claude/skills/chack-recon/SKILL.md b/.claude/skills/chack-recon/SKILL.md new file mode 100644 index 0000000..ed0563b --- /dev/null +++ b/.claude/skills/chack-recon/SKILL.md @@ -0,0 +1,114 @@ +--- +name: chack-recon +description: Use when working inside the cHack repo — driving a scan, reading stored recon/finding data, running the BYOK AI assistant, or touching cHack.sh / db_sqlite.py / js_secrets.py / client_bugs.py / ai_assistant.py. Covers scope enforcement, the CLI surface, where data lives, and how to query or report on it safely. +--- + +# cHack recon platform + +cHack is an **authorized-use-only** offensive-security recon/DAST pipeline +(see root `README.md`). Any agent working in this repo — running it, +extending it, or just answering questions about a target's data — follows +the same scope rule a human operator does. + +## Hard rule: scope enforcement + +Every scan path reads `scope.txt` (see `scope.example.txt`) and refuses +targets outside it unless `ALLOW_OUT_OF_SCOPE=true` is explicitly set. Do not +set that flag, add a target to `scope.txt`, or otherwise bypass this check on +your own initiative — if a user asks you to scan something, confirm it's +their own system or a signed/in-scope engagement first; if that's unclear, +ask rather than assume. + +Never disable or weaken the scope check, the active-secret-verification +opt-in, or any other authorized-use gate as a way to "get a scan working." + +## Driving a scan (`cHack.sh`) + +All scanning goes through `./cHack.sh`. Key entry points (`--help` for the +full list): + +```bash +./cHack.sh -d example.com # full 6-phase pipeline +./cHack.sh --info-gather example.com # passive recon only (phases 1-4) +./cHack.sh --web example.com # crawl + JS secret mining +./cHack.sh --network example.com # port scan +./cHack.sh --fuzz example.com # content/backup discovery +./cHack.sh --vuln example.com # nuclei + DAST +./cHack.sh --client example.com # DOM XSS / postMessage / CORS / etc. +./cHack.sh -D # daemon: poll central queue, execute jobs +``` + +Each phase is independently resumable and writes into local SQLite +(`local.db`), then optionally syncs to the Cloudflare D1 backend in `back/` +when `BACKEND_URL` is configured. Don't invent new scan phases by shelling +out to tools directly — add a phase function in `cHack.sh` (see `run_phase`) +so results land in the standard finding schema and get stored consistently. + +## Where results live + +Everything funnels into `local.db` (SQLite) via `db_sqlite.py`. Prefer its +existing query helpers over raw SQL when reading data: + +```bash +python3 db_sqlite.py summary # DB-wide stats +python3 db_sqlite.py target # one target's record +python3 db_sqlite.py findings [severity] +python3 db_sqlite.py secrets # values are redacted +python3 db_sqlite.py search [kind] # all|domain|service|tech|secret|finding|risk +python3 db_sqlite.py report # full JSON report +./cHack.sh --report --format sarif|stix|json +``` + +Secrets are redacted by default everywhere except `--include-secrets` on a +report export — don't print raw credential values into chat, commits, or +PRs; treat anything pulled from `secrets`/`findings` as sensitive even +though it's already redacted at the DB layer. + +## The BYOK AI assistant (`ai_assistant.py`) + +A separate, optional triage layer over data the pipeline already collected +— it never scans and never generates exploit payloads itself. See +`ai_skills/README.md` for the skill-template format and the root README's +"BYOK AI Assistant" section for full usage. + +```bash +python3 ai_assistant.py skills # list triage skills +./cHack.sh --ai-review [--ai-skill NAME] [--ai-kb] [--ai-save] +./cHack.sh --ai-history +``` + +It requires the user's own key (`AI_API_KEY` in `.env`) — never hardcode a +key, never call a different provider than `AI_PROVIDER` says, and don't add +a skill that asks the model to produce exploit code/payloads; that violates +the existing skills' own stated scope (see each `ai_skills/*.skill.md` +frontmatter + body) and the point of keeping this a *triage* layer, not an +attack-automation one. + +When adding a new AI skill: drop a `.skill.md` file in `ai_skills/` +following the existing frontmatter format (`name`, `title`, `description`, +`requires_kb`) — no code changes needed, `ai_assistant.py` picks it up. + +## Extending the pipeline + +- New finding types/tools: emit the standard schema cHack already uses — + `{title, severity, url, description, evidence, poc, cve, tool, vuln_type}` + — see `client_bugs.py`'s module docstring for the convention. +- Touching `back/schema.sql`: it's shared between local SQLite and + Cloudflare D1 — use `CREATE TABLE IF NOT EXISTS` / idempotent `ALTER`s + (see `db_sqlite.py`'s `init_db`/`migrate_db`) so existing installs upgrade + cleanly, and mirror any new table into `back/schema.sql` too. +- Don't pin a Go tool to `latest` in `cHack.sh`'s `TOOL_VERSIONS` — the repo + deliberately pins to verified release tags as a supply-chain control + (see the `fix(supply-chain)` history); only `go install ...@latest` for + tools with no semver-compatible module path, matching the existing + comments next to `gowitness`/`hakrawler`/`assetfinder`/`waybackurls`. +- Run `.github/workflows/lint.yml`'s checks (ShellCheck on `cHack.sh`, Ruff + on the Python files) before considering a change done. + +## Telegram / team-workflow surfaces + +`back/` is a separate Cloudflare Worker (grammY bot + GraphQL API over D1). +Changes there affect every connected runner and operator — read the root +README's "Access Control & Audit" and "Team Workflows" sections before +changing role checks, the audit log, or API-token enforcement +(`AUTH_ENFORCED`). diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ec49384..4099ce4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,7 +33,7 @@ jobs: - name: Lint run: ruff check --output-format=github . || true - name: Syntax check - run: python -m py_compile js_secrets.py db_sqlite.py client_bugs.py + run: python -m py_compile js_secrets.py db_sqlite.py client_bugs.py ai_assistant.py secrets: name: Secret Scan (gitleaks) diff --git a/.gitignore b/.gitignore index 49370af..9cd37f4 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,8 @@ temp_job_*.json # Authorized-scope allowlist (per-operator; never commit real scope) scope.txt -# Claude Code local workspace (worktrees, plans, settings) -.claude/ +# Claude Code local workspace (worktrees, plans, settings) — but keep +# shared, repo-level skills (.claude/skills/) checked in and visible to +# any agent working in this repo. +.claude/* +!.claude/skills/