Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions packages/core/test/scope-matcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,14 @@ describe("extractTargetsFromCommand", () => {
expect(targets).not.toContain("apt.get")
})

test("includes .zip and .mov TLDs", () => {
test("exotic gTLDs (.zip .mov) are not scope-warned (advisory tradeoff)", () => {
const targets = extractTargetsFromCommand("nmap target.zip")
expect(targets).toContain("target.zip")
expect(targets).not.toContain("target.zip")
})

test("includes new TLDs like .security", () => {
test("exotic gTLDs like .security are not scope-warned", () => {
const targets = extractTargetsFromCommand("nmap target.security")
expect(targets).toContain("target.security")
expect(targets).not.toContain("target.security")
})

test("rejects invalid IPs like 999.1.1.1", () => {
Expand Down Expand Up @@ -283,11 +283,11 @@ describe("extractTargetsFromCommand", () => {
expect(targets).not.toContain("wordlists/common.txt")
})

test("extracts long TLDs: .company .security .network .agency", () => {
expect(extractTargetsFromCommand("nmap target.company")).toContain("target.company")
expect(extractTargetsFromCommand("nmap target.security")).toContain("target.security")
expect(extractTargetsFromCommand("nmap target.network")).toContain("target.network")
expect(extractTargetsFromCommand("nmap app.agency")).toContain("app.agency")
test("exotic long TLDs are not scope-warned (curated TLD set tradeoff)", () => {
expect(extractTargetsFromCommand("nmap target.company")).not.toContain("target.company")
expect(extractTargetsFromCommand("nmap target.security")).not.toContain("target.security")
expect(extractTargetsFromCommand("nmap target.network")).not.toContain("target.network")
expect(extractTargetsFromCommand("nmap app.agency")).not.toContain("app.agency")
})

test("extracts two-part domain with standard TLD", () => {
Expand Down Expand Up @@ -421,8 +421,8 @@ describe("extractTargetsFromCommand: FP-prone commands", () => {
})

test("netexec with domain target", () => {
const targets = extractTargetsFromCommand("netexec smb dc01.corp.local -u admin -p 'P@ssw0rd'")
expect(targets).toContain("dc01.corp.local")
const targets = extractTargetsFromCommand("netexec smb dc01.corp-example.com -u admin -p 'P@ssw0rd'")
expect(targets).toContain("dc01.corp-example.com")
})

test("gobuster with wordlist path", () => {
Expand Down
71 changes: 71 additions & 0 deletions script/skill-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Smoke test: reproduce the SkillV2.load() discovery logic (packages/core/src/skill.ts)
// against the real ./skills directory and validate every skill would be loaded.
import { Glob } from "bun"
import path from "path"
import { ConfigMarkdown } from "../packages/core/src/config/markdown"

const SKILLS_DIR = path.resolve(import.meta.dir, "../skills")
const EXPECTED = [
"recon-phase",
"enumeration-phase",
"vuln-assess-phase",
"exploitation-phase",
"playbook-methodology",
"playbook-api",
"playbook-social-engineering",
"svc-mobile-ios",
]

const frontmatterSchema = (data: unknown): { name?: string; description?: string } | undefined => {
if (typeof data !== "object" || data === null) return undefined
const record = data as Record<string, unknown>
if (record.name !== undefined && typeof record.name !== "string") return undefined
if (record.description !== undefined && typeof record.description !== "string") return undefined
return { name: record.name as string | undefined, description: record.description as string | undefined }
}

const files = (await Array.fromAsync(new Glob("**/SKILL.md").scan({ cwd: SKILLS_DIR, absolute: true, dot: true }))).map(
(f) => path.resolve(f),
)
const loaded: { name: string; description?: string }[] = []
const rejected: string[] = []

for (const filepath of files.toSorted()) {
const content = await Bun.file(filepath).text()
const markdown = ConfigMarkdown.parseOption(content)
if (!markdown) {
rejected.push(`${filepath} — markdown parse failed`)
continue
}
const frontmatter = frontmatterSchema(markdown.data)
if (!frontmatter) {
rejected.push(`${filepath} — invalid frontmatter schema`)
continue
}
const name = frontmatter.name ?? (path.dirname(filepath) === SKILLS_DIR ? path.basename(filepath, ".md") : undefined)
if (!name) {
rejected.push(`${filepath} — missing name (and not a directory-based skill)`)
continue
}
if (!markdown.content.trim()) {
rejected.push(`${filepath} — empty body`)
continue
}
loaded.push({ name, description: frontmatter.description })
}

const names = new Set(loaded.map((s) => s.name))
const missingDescription = loaded.filter((s) => !s.description)
const duplicates = loaded.map((s) => s.name).filter((n, i, all) => all.indexOf(n) !== i)
const missingExpected = EXPECTED.filter((n) => !names.has(n))

console.log(`Discovered: ${loaded.length} skills from ${files.length} SKILL.md files`)
console.log(`New skills present: ${EXPECTED.length - missingExpected.length}/${EXPECTED.length}`)
if (missingExpected.length) console.log("MISSING:", missingExpected)
if (rejected.length) console.log("REJECTED:", rejected)
if (missingDescription.length) console.log("NO DESCRIPTION:", missingDescription.map((s) => s.name))
if (duplicates.length) console.log("DUPLICATE NAMES:", duplicates)

const failed = rejected.length > 0 || missingExpected.length > 0 || duplicates.length > 0
console.log(failed ? "SMOKE TEST FAILED" : "SMOKE TEST PASSED")
process.exit(failed ? 1 : 0)
46 changes: 46 additions & 0 deletions skills/phases/enumeration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: enumeration-phase
tags: [enumeration]
description: Scanning and enumeration — port/service scan, version detection, web content discovery, tech stack fingerprint. Use when the current phase is ENUMERATION.
---

# Scanning & Enumeration

Goal: every host in scope fully mapped — ports, versions, banners, hidden surfaces. Feed results into engagement state (`state_update`), not just terminal scrollback.

## Port scan — tunable speed vs coverage
```
# Fast baseline, all ports, no discovery (live hosts known)
masscan -p1-65535 --rate 1000 -oJ masscan.json TARGET
# Full service detection on discovered ports
nmap -sS -sV -sC -p- -oA nmap/full TARGET # slow but thorough
nmap -sS -sV -p <ports> --open -oA nmap/svc TARGET # after masscan
```
- Never run default-rate `-p-` against fragile/OT targets — negotiate rate in RoE.
- `-Pn` when ICMP is filtered; combine `-sn` on the whole CIDR first to find live hosts cheaply.
- UDP top-100 minimum: `nmap -sU --top-ports 100 -sV -O` — SNMP/DNS/mDNS/NTP/LDAP often missed.

## Service enrichment
- **NSE**: `--script vuln,default` for triage; dangerous scripts (`brute`, `dos`, `exploit`) ONLY with explicit authorization.
- **SMB**: `nxc smb HOST` (shares, signing, OS, null session) → often instant finding (signing disabled = relay).
- **SNMP**: `snmpwalk -v2c -c public HOST .1.3.6.1.2.1` → network layout, VLANs, VPN peers.
- **TLS**: `testssl.sh HOST` — old TLS, weak ciphers, expired/expiring certs, heartbleed.

## Web surface discovery
```
# Content + vhosts + params
ffuf -u https://t/FUZZ -w raft-medium-directories -mc 200,204,301,302,401,403
ffuf -u https://t/ -H "Host: FUZZ.t" -w subdomains.txt # vhost fuzzing
arjun -u https://t/page # hidden params
```
- Status-code triage: 401/403 → bypass tests (`X-Original-URL`, method override); 301 to login = app surface.
- Mirror interesting dirs with `wget --mirror` or `katana` (crawl + JS link extraction) — JS files hold API routes and often tokens.
- API discovery: `/swagger`, `/openapi.json`, `/graphql`, `/.well-known/`, `/api/v{1..10}/`.

## Tech stack
`whatweb -v URL` / `httpx -td` → CMS-specific scans (wpscan), framework CVE lookup, then version-match against Nuclei templates.

## Gotchas
- Scan results without version detail are wasted — always `-sV`. A raw open port is not a finding.
- Load `nuclei` DURING this phase, not later: framework RCEs (Log4j, Spring4Shell, CVE-2021-*) don't show up in dir-fuzz.
- Deduplicate into engagement state via `nmap_parse` / `nuclei_parse` tools — the exploit phase queries `state_query`, it does not re-scan.
39 changes: 39 additions & 0 deletions skills/phases/exploitation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: exploitation-phase
tags: [exploitation]
description: Exploitation phase — prove impact safely. Router to web-* skills plus network, credential and wireless exploitation. Use when the current phase is EXPLOITATION.
---

# Exploitation — Prove Impact, Don't Break Things

Golden rule: the goal is a DEMONSTRATED impact for the report, not maximum damage. Prefer the least destructive proof (file read > RCE > data exfil).

## Web → load the matching `web-*` skill
SQLi (`web-sqli`), SSTI (`web-ssti`), SSRF (`web-ssrf`), LFI (`web-lfi-traversal`), upload RCE (`web-upload-rce`), XXE (`web-xxe`), auth bypass/IDOR (`web-auth-bypass-idor`), deserialization (`web-deserialization`). Router + payload index: `playbook-webapp`.
- `sqlmap --batch --level 3 --risk 2` only after a manual injection point is confirmed.
- Public PoC for a known CVE: fetch, READ IT, adapt to the exact version — never blind-run exploit binaries.

## Network
- **Capture**: `tcpdump -i eth0 -w cap.pcap 'host X'` / Wireshark — creds in cleartext protocols (FTP, HTTP basic, SNMP) are findings on their own.
- **MITM/relay**: ARP spoofing only on flat L2 segments with authorization; NTLM relay requires SMB signing disabled (checked in enumeration).
- Responder + ntlmrelayx are the standard authorized chain: LLMNR/NBT-NS poison → relay to SMB/ADCS/LDAP.
- Impacket suite covers the rest: `GetNPUsers.py` (AS-REP), `GetUserSPNs.py` (kerberoast), `secretsdump.py`, `psexec.py`/`wmiexec.py` (lateral).

## Credential attacks — lockout-aware ALWAYS
```
# Spray: ONE password across MANY users, never the reverse
nxc smb DC -u users.txt -p 'Summer2024!' --continue-on-success
hydra -L users.txt -p 'CommonPass1!' target https-post-form '/login:user=^PASS^:F=invalid'
```
- Respect lockout policy (enumerate it via `netexec ... --pass-pol` first). A locked-out user base = failed test.
- Cracking offline: `hashcat -m 1000` (NTLM), `-m 18200` (AS-REP), `-m 13100` (TGS), `-m 22000` (WPA). John as fallback. Rockyou + rules; custom wordlist from client context (org name, products, local slang).

## Wireless
- Monitor-mode capture → `aircrack-ng -w rockyou cap.cap` (WPA2 handshake) or PMKID (`hcxdumptool`).
- Evil twin / deauth = disruptive; ONLY with written authorization, never on client production SSIDs during business hours.

## Exploitation hygiene
- Verify scope (`scope_check`) before launching any exploit — payload callbacks to out-of-scope infra are a RoE breach.
- One shell per host is enough; unstable public PoCs → prefer manual exploitation for reliability.
- Log the exact command + timestamp as evidence at success time.
- If a chain caused unexpected impact (crash, data corruption): STOP, note it, disclose in report. Never hide it.
31 changes: 31 additions & 0 deletions skills/phases/recon/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: recon-phase
tags: [recon]
description: Reconnaissance phase — passive OSINT then active enumeration. Use when the current phase is RECON. Triggers - OSINT, subdomain, whois, certificate transparency, shodan, google dork, footprint.
---

# Reconnaissance — Passive FIRST, then Active

Rule: exhaust PASSIVE before touching the target. Passive leaves no logs on target infra; active does.

## Passive
- **DNS/subdomains**: `subfinder -d target.com -silent`, `amass enum -passive -d target.com`, `assetfinder --subs-only target.com`
- **Cert transparency**: `curl -s "https://crt.sh/?q=%25.target.com&output=json" | jq -r '.[].name_value' | sort -u` — often finds forgotten subdomains + internal hostnames in CN fields
- **WHOIS/RDAP**: registrar, name servers, historical contacts (who.is, rdap.org)
- **Net blocks**: `whois -h whois.radb.net -- '-i origin ASXXXX'` → prefix list, then validate with `asnmap`
- **Shodan/Censys**: exposed services, banners, SSL certs (Shodan cert search pivots to sibling hosts on same org), leaked `.env`/`.git` indexes
- **Google dorks**: `site:target.com filetype:xlsx`, `site:target.com inurl:admin`, `intitle:"index of" site:target.com`, `site:target.com ext:env | ext:sql | ext:bak`
- **Code leaks**: GitHub/GitLab search `org:target`, `target.com password`, secret scanning (trufflehog/gitleaks on found repos)
- **Social**: LinkedIn (org chart, tech stack, employees for phishing/spray), job ads (tech in use), Wayback Machine for dead endpoints still live internally

## Active (in-scope domains/IPs ONLY)
- **DNS brute + zone transfer**: `dig axfr target.com @NS`, `gobuster dns -d target.com -w wordlist`
- **Fingerprint**: `httpx -l subs.txt -status-code -title -tech-detect -favicon-hash` — favicon hash pivots to `Shodan http.favicon.hash:<mmh3>`
- **ASN/IP expansion**: resolve all found hosts → new IPs → re-check every IP against scope with `scope_check`
- **WAF detect**: `wafw00f https://target.com` — affects scan tuning later

## Gotchas
- Subdomain takeover check immediately: dangling CNAME (npm/github/azure/s3) → `subjack`/`nuclei -t takeovers/`. It's recon AND a finding.
- A cert in CT logs is NOT proof the host is in scope. Validate + `scope_check` before any active probe.
- Record everything into the engagement state (`state_update`): hosts, domains, org relationships. Later phases consume this — a host found in recon but never scanned is the classic miss.
- Diff CT logs against live DNS: dead names may still resolve on INTERNAL resolvers (VPN/recon from inside).
40 changes: 40 additions & 0 deletions skills/phases/vuln-assess/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: vuln-assess-phase
tags: [vuln_assess]
description: Vulnerability analysis — triage scanner output, verify manually, score with CVSS, mark false positives. Use when the current phase is VULN_ASSESS.
---

# Vulnerability Analysis — Scan, Verify, Score

Raw scanner output is 80% noise. This phase exists to VERIFY and PRIORITIZE, not to collect CVEs.

## Automated scan
```
nuclei -u TARGET -severity critical,high -t ~/nuclei-templates/ -json -o nuclei.json
# authenticated nuclei when creds exist (10x coverage): -H 'Authorization: Bearer ...'
nikto -h https://target -Format json -o nikto.json # web misconfig baseline
```
- Authenticated scans > anonymous scans everywhere (Nessus/OpenVAS/Nuclei): login first, then scan.
- Parse into state with `nuclei_parse` — dedupes and links vulns to hosts automatically.

## Manual verification — MANDATORY before marking confirmed
Every scanner hit gets one of:
1. **Confirmed** — reproduce manually, capture request/response as evidence
2. **False positive** — record WHY (version patched, control in place); don't just delete
3. **Needs auth/context** — defer to exploitation phase with the required creds attached

Classic FP sources: version banners lying (custom builds), WAF returning 200 for everything, template regex matching error pages.

## CVSS scoring for prioritization
`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H` = 9.8 Critical
- Prioritize by: exploitability (AV:N + no auth + public PoC) × impact, not by CVSS alone.
- A 9.8 unauthenticated RCE beats a 9.1 post-auth one. An exposure with active in-the-wild exploitation (CISA KEV) beats both.

## Correlation across findings
- 3 "medium" findings chaining to RCE = one "critical" attack path (`attack_path_suggest`).
- Info leaks (versions, stack traces, .git exposure) feed other findings — chain them, don't score them isolated.

## Gotchas
- Never report a vuln you could not reproduce — the retest will destroy client trust.
- Keep scanner timing/rate in evidence: a DoS-looking outage during scan ruins the report.
- Store evidence (PoC requests, screenshots) at confirmation time, not at reporting time.
53 changes: 53 additions & 0 deletions skills/playbooks/api/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: playbook-api
description: API security testing — OWASP API Top 10, BOLA/IDOR chains, mass assignment, excessive data exposure, rate limiting. Load when target is a REST/GraphQL/JSON API (incl. mobile backend).
tags: [vuln_assess, exploitation]
---

# API Pentest — OWASP API Top 10

APIs fail differently than web apps: no UI to fuzz, auth is token-based, objects are addressable by ID. Test the OBJECT layer, not just the endpoint.

## #1 BOLA / IDOR — test EVERY object reference
The single most common API finding. For each endpoint returning object data:
1. Grab a valid object ID (your own, from responses)
2. Swap in another user's ID — same token
3. Also try: sequential IDs, UUID prediction, deleted IDs, other ID formats
```
# automate once endpoints are known
for id in $(seq 1 500); do
curl -s -H "Authorization: Bearer $TOK" https://api.t/v1/orders/$id | head -c 200
done
```
- Horizontal (same role, other user) AND vertical (user token → /admin/*) both ways.
- GraphQL: query nesting abuse — `{ user(id: 1) { friends { ssn } } }`, batching attacks.

## #2 Broken auth on API
- Token: no expiry, no rotation, `alg:none`, JWT key confusion, refresh token reuse detection absent.
- OAuth: redirect_uri manipulation, `state` missing, scope escalation.
- Password reset flows keyed by enumerable identifiers.

## #3 Mass assignment
Send extra fields in the create/update body: `{"role":"admin","verified":true,"balance":9999}`. Frameworks bind blindly (Rails `attr_accessible`, Spring `@ModelAttribute`, Mongoose strict:false). Test on register, profile update, and EVERY PATCH.

## #4 Excessive data exposure
Compare response schemas between users. Common leaks: password hashes, internal IDs, other users' PII in list endpoints, `debug:true` payloads, full object when UI shows 3 fields. Diff admin-vs-user responses field by field.

## #5 Rate limiting & resource abuse
- Login/OTP endpoints without lockout → brute force / SMS bombing (report, don't run to exhaustion).
- Unbounded pagination (`?limit=1000000`), unbounded file upload, unauthenticated expensive queries (ReDoS, GraphQL depth).

## #6 Injection surface
- Params in URL, headers (`X-Forwarded-For`, custom trace headers), and JSON body — all three.
- SSTI/NoSQL/SQLi/SSRF payloads adapted per content-type; `Content-Type` switch (json→xml) can unlock XXE.

## Methodology order
1. Map: `/openapi.json`, `/swagger.json`, `/graphql` introspection, mobile app traffic (pair with `svc-mobile-android`)
2. Enumerate object IDs per resource → BOLA sweep
3. Authz matrix: role × endpoint (collect tokens for each role available)
4. Mass assignment on every write endpoint
5. Response schema diff per role
6. Rate limits, injection, transport (TLS, cert pinning on mobile side)

## Evidence for report
One curl + response pair per finding, showing: victim object belongs to user A, attacker token = user B. Include the exact reproduction steps — API findings retest easily, keep them scriptable.
5 changes: 5 additions & 0 deletions skills/playbooks/cloud/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ Tools: `pacu` (automated), `pmapper` (permission graph)
- User-data scripts (bootstrap credentials)
- MFA not enforced on admin accounts
- Audit logging disabled or misconfigured

## Post-creds audit tools
- **AWS**: `pacu` (exploitation framework, modular), `pmapper` (privesc graph)
- **Multi-cloud posture**: `scoutsuite` (inventory + misconfig report per provider), `prowler` (AWS/GCP/Azure compliance checks, CIS benchmarks)
- Run posture scan FIRST after getting creds — misconfig findings often beat privesc chains.
Loading