commit 5bec530a89f479fc9adb59a38fa102d7c7677e54
Author: pliny <133052465+elder-plinius@users.noreply.github.com>
Date: Thu Jul 2 13:56:02 2026 -0400
Add files via upload
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..9bfd3b3
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,131 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 1.x | :white_check_mark: |
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability in T3MP3ST, please report it responsibly:
+
+1. **Do NOT** open a public GitHub issue for security vulnerabilities.
+2. **Primary channel — GitHub private security advisory.** Open the repository's
+ **Security** tab and click **"Report a vulnerability"**, or go directly to
+ [`/security/advisories/new`](../../security/advisories/new). This creates a
+ private advisory visible only to you and the maintainers — no email inbox is
+ required, and it works from day one on a public repo.
+3. **Fallback channel.** If the Security tab is unavailable to you, open a public
+ issue that contains **only** the sentence "Requesting a private security
+ contact" (no vulnerability details) and a maintainer will open a private
+ advisory to continue the conversation.
+4. Include detailed steps to reproduce the vulnerability in the private advisory.
+5. Allow reasonable time for the issue to be addressed before public disclosure.
+
+## Security Considerations
+
+### Authorized Use Only
+
+T3MP3ST is a security testing framework designed for **authorized penetration testing and red team operations only**. Before using this tool:
+
+- Obtain explicit written authorization from the system owner
+- Define clear scope and rules of engagement
+- Ensure compliance with applicable laws and regulations
+- Document all testing activities
+
+See [docs/SCOPE_AND_AUTHORIZATION.md](docs/SCOPE_AND_AUTHORIZATION.md) for the working model used by the API and UI: human intent, scope receipts, tool gates, evidence, findings, retests, and accepted memory.
+
+### API Key Security
+
+- Never commit API keys to version control
+- Use environment variables for all sensitive credentials
+- Rotate API keys regularly
+- Use separate keys for development and production
+
+### Payload Database
+
+The framework includes comprehensive payload databases for educational and testing purposes:
+
+- SQL injection payloads
+- XSS payloads
+- SSTI payloads
+- Command injection payloads
+- XXE payloads
+
+These payloads are intended for:
+- Authorized security testing
+- Security research
+- Educational purposes
+- CTF competitions
+
+### Data Handling
+
+When using T3MP3ST:
+
+- Do not exfiltrate real sensitive data
+- Use test/dummy data for demonstrations
+- Properly dispose of any captured credentials after testing
+- Follow data protection regulations (GDPR, CCPA, etc.)
+
+### Network Security
+
+- Test only against authorized targets
+- Use isolated networks when possible
+- Be aware of potential lateral impact
+- Monitor for unintended effects
+
+## Known Limitations
+
+### Development Dependencies
+
+`npm audit` currently reports **0 vulnerabilities** (verified 2026-07-02). Development
+dependencies (build/test tooling such as the bundler) are not part of the production
+runtime regardless. Maintainers should re-run `npm audit` before each release tag.
+
+### Real Functionality
+
+All core arsenal tools perform **real operations** (not simulations):
+
+| Tool | Implementation |
+|------|----------------|
+| Port scanning | Real TCP connect scans via Node.js `net` module |
+| DNS lookup | Real DNS resolution via Node.js `dns` module |
+| Subdomain enumeration | Real DNS lookups for each subdomain candidate |
+| Directory bruteforce | Real HTTP requests to discover paths |
+| Technology detection | Real HTTP fingerprinting of headers/content |
+| XSS scanning | Real payload injection with reflection detection |
+| SQL injection scanning | Real error-based and boolean-based detection |
+| SSL/TLS scanning | Real TLS connections via Node.js `tls` module |
+| WHOIS lookup | Real WHOIS queries via TCP port 43 |
+| Password spraying | Real HTTP POST requests to login endpoints |
+| Hash cracking | Real dictionary attacks using `crypto` module |
+
+**Note:** Only use these tools against systems you have explicit authorization to test.
+
+## Security Best Practices
+
+1. **Run in isolated environments** - Use VMs or containers
+2. **Network segmentation** - Test networks should be isolated
+3. **Logging** - Maintain detailed logs of all testing activities
+4. **Authorization documentation** - Keep copies of all authorizations
+5. **Scope adherence** - Never exceed authorized scope
+
+## Compliance
+
+T3MP3ST usage should comply with:
+
+- Computer Fraud and Abuse Act (CFAA)
+- General Data Protection Regulation (GDPR)
+- PCI DSS (for payment card environments)
+- HIPAA (for healthcare environments)
+- Local and international cybersecurity laws
+
+## Contact
+
+Report vulnerabilities **privately** via the repository's **GitHub Security Advisories**
+("Report a vulnerability" under the **Security** tab) — this keeps the report confidential
+until a fix ships. Please do **not** open a public issue for a security problem.
+
+Target response: acknowledgement within **3 business days**, and a remediation plan within
+**30 days** of a confirmed report.
diff --git a/VISION.md b/VISION.md
new file mode 100644
index 0000000..4dbf699
--- /dev/null
+++ b/VISION.md
@@ -0,0 +1,692 @@
+# T3MP3ST: Research Directions & Evolutionary Vision
+
+## Beyond the Kill Chain — Toward Autonomous Adversarial Intelligence
+
+**Version 2.0 Research Prospectus | February 2026**
+
+---
+
+## Abstract
+
+T3MP3ST v1.0 established a multi-agent framework for orchestrating penetration testing across a structured kill chain. This document looks forward. It identifies seven evolutionary vectors that would transform T3MP3ST from a *tool framework* into something closer to an *adversarial intelligence system* — one capable of reasoning about novel attack surfaces, adapting strategies mid-engagement, learning from operational history, and coordinating swarm-scale operations across distributed infrastructure. Each vector is analyzed for technical feasibility, architectural implications, and open research questions.
+
+This is not a roadmap. It is a cartography of the possible.
+
+---
+
+## Table of Contents
+
+1. [Vector 1: Cognitive Architecture — From Prompt Engineering to Adversarial Reasoning](#vector-1-cognitive-architecture)
+2. [Vector 2: Swarm Dynamics — Emergent Coordination at Scale](#vector-2-swarm-dynamics)
+3. [Vector 3: Adversarial Machine Learning — The Meta-Game](#vector-3-adversarial-machine-learning)
+4. [Vector 4: Continuous Autonomous Operations — The Persistent Agent](#vector-4-continuous-autonomous-operations)
+5. [Vector 5: Knowledge Architecture — From Databases to Adversarial Ontologies](#vector-5-knowledge-architecture)
+6. [Vector 6: Distributed & Edge Execution — Operating Beyond the Perimeter](#vector-6-distributed-and-edge-execution)
+7. [Vector 7: Evaluation Science — Measuring What Matters](#vector-7-evaluation-science)
+8. [Synthesis: The Convergence Thesis](#synthesis-the-convergence-thesis)
+9. [Ethical Framework: Constraints That Enable](#ethical-framework)
+10. [Open Problems](#open-problems)
+
+---
+
+## Vector 1: Cognitive Architecture
+
+### From Prompt Engineering to Adversarial Reasoning
+
+#### 1.1 The Current State
+
+T3MP3ST v1.0 operators are prompt-specialized: each archetype receives a system prompt that constrains its reasoning to a domain (reconnaissance, exploitation, etc.). Task execution follows a simple pattern — the operator receives a task description, optionally enters a ReAct loop with tool access, and produces structured output. The LLM does the thinking, but the *architecture* of that thinking is flat.
+
+This works. It solves many tasks. But it has a ceiling.
+
+#### 1.2 The Ceiling
+
+Consider what a skilled human penetration tester does that the current architecture cannot:
+
+- **Hypothetico-deductive reasoning**: "If this server runs Apache 2.4.49, and path traversal CVE-2021-41773 exists, then requesting `/.%2e/.%2e/etc/passwd` should return shadow hashes. If it doesn't, the patch is applied or a WAF is blocking. Let me test both hypotheses."
+- **Counterfactual analysis**: "The XSS didn't fire. Was it filtered at the WAF, sanitized at the application layer, or is the reflection point not in an executable context? Each implies a different bypass strategy."
+- **Abductive inference**: "I see outbound DNS traffic to a weird domain. The most likely explanation is an existing compromise. Second most likely: a misconfigured service. I should investigate before I trip someone else's wire."
+
+These are not prompt engineering problems. They are *cognitive architecture* problems.
+
+#### 1.3 Proposed Architecture: The Adversarial Reasoning Engine
+
+```
+ +---------------------------+
+ | METACOGNITIVE LAYER |
+ | (Strategy selection, |
+ | confidence calibration, |
+ | reasoning mode switching) |
+ +---------------------------+
+ |
+ +------------------+------------------+
+ | | |
+ +-------v-------+ +------v------+ +--------v--------+
+ | HYPOTHESIS | | PLANNING | | ADVERSARIAL |
+ | ENGINE | | ENGINE | | IMAGINATION |
+ | (Generate, | | (PDDL-like | | (What would a |
+ | test, refine | | operators, | | defender see? |
+ | hypotheses | | precond + | | What would |
+ | about target) | | effects) | | break next?) |
+ +----------------+ +-------------+ +-----------------+
+ | | |
+ +------------------+------------------+
+ |
+ +----------v----------+
+ | WORKING MEMORY |
+ | (Structured state: |
+ | beliefs, goals, |
+ | open questions, |
+ | failed attempts) |
+ +---------------------+
+```
+
+**Hypothesis Engine**: Rather than executing tasks linearly, operators would maintain a *hypothesis tree* about the target. Each action is designed to confirm or refute a specific hypothesis. Failed actions are not failures — they're information that prunes the search space.
+
+Implementation path:
+- Structured hypothesis objects: `{ claim, evidence_for, evidence_against, confidence, test_plan }`
+- LLM generates hypotheses from observations; tool results update confidence
+- Tree pruning when confidence drops below threshold
+- Hypothesis merging when evidence converges
+
+**Planning Engine**: Current task generation is phase-based (recon tasks, then scan tasks, etc.). A true planning engine would reason about *preconditions and effects* — understanding that "to exploit SQLi, I need to know the DBMS type" creates an implicit dependency on fingerprinting, regardless of what phase we're nominally in.
+
+Implementation path:
+- PDDL-inspired operator definitions: `{ preconditions: [...], effects: [...], cost: N }`
+- Forward-chaining planner that generates task sequences from current state to goal
+- Replanning when actions fail or new information arrives
+- Integration with the existing kill chain as a *heuristic ordering*, not a hard constraint
+
+**Adversarial Imagination**: The most sophisticated human testers think from the *defender's perspective*. "If I were the SOC, what would I be alerting on right now?" This dual-perspective reasoning is what separates automated scanning from adversarial thinking.
+
+Implementation path:
+- Parallel "red" and "blue" reasoning threads
+- Blue thread estimates detection probability of proposed red actions
+- Cost function balances exploitation value against detection risk
+- Integrated with OPSEC controller for quantitative risk budgeting
+
+#### 1.4 Research Questions
+
+- What is the optimal granularity for hypotheses? Too coarse and they're useless; too fine and the search space explodes.
+- Can LLMs reliably perform counterfactual reasoning about network states they haven't observed, or does this require explicit world models?
+- How do you calibrate confidence? LLMs are notoriously poor at epistemic humility. Can we use ensemble disagreement or tool-verified predictions as calibration signals?
+- Is there a useful formal relationship between the hypothesis tree and MITRE ATT&CK's technique graph?
+
+---
+
+## Vector 2: Swarm Dynamics
+
+### Emergent Coordination at Scale
+
+#### 2.1 Beyond Team Factories
+
+T3MP3ST v1.0 coordinates operators through a centralized dispatcher: MissionControl assigns tasks to operators based on archetype matching. This is a *command hierarchy*. It works for small teams (5-10 operators) but has fundamental scaling limitations:
+
+- Single point of failure (the coordinator)
+- O(N) communication overhead for status updates
+- No mechanism for operators to discover collaboration opportunities independently
+- Rigid role boundaries prevent adaptive specialization
+
+#### 2.2 Stigmergic Coordination
+
+In nature, ant colonies coordinate millions of agents without central command through *stigmergy* — agents modify the shared environment (pheromone trails), and other agents respond to those modifications. The result is emergent collective intelligence that far exceeds any individual agent's capability.
+
+**Applied to T3MP3ST:**
+
+```
++----------+ writes to +------------------+ reads from +----------+
+| Operator | --------------> | SHARED EVIDENCE | <-------------- | Operator |
+| Alpha | | LANDSCAPE | | Beta |
++----------+ | | +----------+
+ | | - Findings (with | |
+ | | "heat" scores) | |
+ | | - Failed attempts | |
+ | | (anti-pheromone)| |
+ | | - Open questions | |
+ v | - Resource claims | v
+ selects next +------------------+ selects next
+ action based action based
+ on landscape on landscape
+```
+
+**Pheromone Analogs:**
+- **Discovery pheromone**: When an operator finds an open port/service, it deposits a "heat" signal. Other operators are attracted to high-heat areas (many services = rich attack surface).
+- **Exploitation pheromone**: Successfully exploited vulnerabilities create trails that attract post-exploitation operators.
+- **Danger pheromone** (anti-pheromone): Detection events create repulsion signals. Operators avoid areas with high danger concentrations.
+- **Exhaustion pheromone**: Failed attempts mark paths as explored, preventing redundant work.
+
+**Evaporation**: Pheromones decay over time, preventing the swarm from getting stuck on stale information. A port that was open 30 minutes ago might be firewalled now — the pheromone should weaken.
+
+#### 2.3 Emergent Specialization
+
+Instead of rigid archetypes, agents in a swarm model would *develop* specializations based on their history:
+
+- An agent that successfully cracks several passwords develops higher confidence for authentication tasks
+- An agent that repeatedly fails at web exploitation stops selecting those tasks (negative reinforcement via local history)
+- When the swarm has no specialist for a needed capability, a generalist agent *steps up* (role vacancy filling)
+
+This creates a system that is resilient to operator loss — if the designated scanner is "burned," other operators can absorb its responsibilities without centralized reassignment.
+
+#### 2.4 Scaling Properties
+
+| Property | Centralized (v1.0) | Stigmergic (v2.0) |
+|----------|--------------------|--------------------|
+| Max operators | ~10 (coordinator bottleneck) | Hundreds (limited by environment, not coordination) |
+| Fault tolerance | Low (coordinator SPOF) | High (any agent can fail) |
+| Communication | O(N) per tick | O(1) per agent (local reads) |
+| Adaptability | Manual reconfiguration | Emergent respecialization |
+| Overhead | High (status aggregation) | Low (distributed state) |
+
+#### 2.5 Research Questions
+
+- What pheromone evaporation rate produces optimal exploration/exploitation balance in network penetration testing?
+- How do you prevent swarm collapse (all agents converging on the same target)?
+- Can stigmergic coordination produce kill chain progression without explicit phase management?
+- What is the minimum swarm size needed for emergent specialization to be more effective than pre-assigned roles?
+
+---
+
+## Vector 3: Adversarial Machine Learning
+
+### The Meta-Game
+
+#### 3.1 The Arms Race
+
+Defensive AI is advancing rapidly. EDR systems use behavioral ML to detect anomalous process execution. WAFs use neural networks to identify attack payloads. SIEM platforms use graph neural networks to detect lateral movement patterns. Network detection systems use autoencoders to flag anomalous traffic.
+
+T3MP3ST currently addresses this through static OPSEC levels (silent/covert/loud) and hardcoded timing parameters. This is the equivalent of bringing a playbook to a chess match — it works against predictable defenses but fails against adaptive ones.
+
+#### 3.2 Defensive Model Awareness
+
+Future T3MP3ST operators need to reason about *what the defender's models see*:
+
+**Payload Mutation**: Rather than selecting from a static payload database, generate payloads that minimize the probability of detection by known WAF architectures:
+
+```
+Traditional: payload_db[category].random()
+Adversarial: argmin_p P(detected | p, waf_model) subject to P(exploits | p) > threshold
+```
+
+This is a constrained optimization problem. The payload must be *both* evasive and effective. Research in adversarial examples for NLP classifiers (TextFooler, BERT-Attack, etc.) provides a starting point, but security payloads have unique constraints — they must be syntactically valid in the target context (SQL, HTML, shell, etc.).
+
+**Traffic Shaping**: Rather than fixed timing jitter, shape the statistical properties of scanning traffic to match benign patterns:
+
+- Match the inter-request timing distribution of legitimate users (not uniform random — real traffic is bursty)
+- Mirror the User-Agent distribution of the target's actual visitor population
+- Sequence requests in patterns that match common crawlers/bots rather than vulnerability scanners
+- Adapt in real-time based on response patterns that might indicate rate limiting or blocking
+
+**Behavioral Mimicry**: For post-exploitation, generate process execution patterns that blend with the target's normal operational profile:
+
+- Time actions to coincide with regular administrative activity
+- Use tools already present on the target (living-off-the-land) prioritized by what's *actually installed and commonly executed*
+- Match file access patterns to normal application behavior
+
+#### 3.3 The GAN Analogy
+
+This entire dynamic can be modeled as a generative adversarial process:
+
+```
+Generator (T3MP3ST): Produces attack actions
+Discriminator (Blue): Classifies actions as malicious/benign
+Training signal: Did we get detected? Did we succeed?
+```
+
+But unlike a standard GAN, the discriminator is:
+- Not differentiable (can't backpropagate through a real WAF)
+- Only partially observable (we don't know the exact model architecture)
+- Adaptive (the blue team may retrain in response to our actions)
+
+This pushes toward *black-box adversarial ML* techniques: gradient-free optimization, evolutionary strategies, and bandit algorithms for payload selection.
+
+#### 3.4 Research Questions
+
+- Can reinforcement learning (PPO, SAC) train effective payload mutation policies using only binary detection/no-detection feedback?
+- What is the transferability of adversarial payloads across WAF products? (If we evade ModSecurity, do we also evade Cloudflare?)
+- How do you balance evasion against exploitation reliability? Heavily mutated payloads may evade detection but fail to trigger the vulnerability.
+- Can we model the defender's response function (will they block this IP, add a WAF rule, or escalate to incident response)?
+
+---
+
+## Vector 4: Continuous Autonomous Operations
+
+### The Persistent Agent
+
+#### 4.1 From Engagement to Continuous Assurance
+
+Current T3MP3ST operates in *engagement mode*: start mission, run kill chain, collect findings, generate report, stop. This mirrors traditional penetration testing — a point-in-time assessment.
+
+The industry is moving toward *continuous security validation*. Attack surface management, breach and attack simulation (BAS), and continuous penetration testing are growing categories. T3MP3ST's architecture is uniquely positioned for this shift, but requires fundamental changes.
+
+#### 4.2 The Continuous Operation Model
+
+```
+ +-----------------------------------+
+ | CONTINUOUS LOOP |
+ +-----------------------------------+
+ | |
+ +--------+ | +----------+ +-----------+ | +--------+
+ | DETECT |------>| PRIORITIZE|-->| VALIDATE |-------->| REPORT |
+ | CHANGE | | | & PLAN | | & EXPLOIT | | | DELTA |
+ +--------+ | +----------+ +-----------+ | +--------+
+ ^ | | | | |
+ | | v v | |
+ | | +---------+ +-----------+ | |
+ | | | COMPARE | | UPDATE | | |
+ | | | TO LAST | | KNOWLEDGE | | |
+ | | | STATE | | BASE | | |
+ | | +---------+ +-----------+ | |
+ | | | |
+ | +-----------------------------------+ |
+ | |
+ +-------- wait(interval) <-----------------------------+
+```
+
+**Change Detection**: Monitor the target's attack surface for changes:
+- New DNS records (subdomain monitoring)
+- New open ports (continuous port scanning at low rates)
+- New web content (crawl diffing)
+- Certificate changes (CT log monitoring)
+- Cloud resource changes (API polling)
+
+**Delta Reporting**: Instead of full reports each cycle, generate *deltas* — what changed since the last assessment. A new critical finding on a previously-clean host is more urgent than a persistent medium finding on a known-vulnerable one.
+
+**State Persistence**: The Evidence Vault becomes a *longitudinal database*:
+- Finding history with first-seen / last-seen timestamps
+- Vulnerability lifecycle tracking (discovered -> reported -> remediated -> regression)
+- Target evolution over time (new services, removed services, configuration changes)
+- Operational metrics (mean time to detect, mean time to exploit)
+
+#### 4.3 Autonomous Escalation Decisions
+
+In continuous mode, the system must make autonomous decisions about when to escalate:
+
+- **New critical finding**: Immediate notification via webhook/Slack/PagerDuty
+- **Regression**: Previously-remediated vulnerability reappears — high priority alert
+- **Attack surface expansion**: Significant new external exposure detected
+- **Anomaly**: Target behavior doesn't match historical baseline (possible ongoing attack by a third party)
+
+This requires a *notification policy engine* — configurable rules about what triggers alerts, who gets them, and through which channels.
+
+#### 4.4 Research Questions
+
+- What scanning rate produces adequate coverage without triggering IDS/rate limiting in production environments?
+- How do you distinguish between "vulnerability was remediated" and "vulnerability is temporarily unreachable" (e.g., due to deployment, maintenance)?
+- Can continuous operation data train predictive models? ("Based on this organization's patch velocity, this vulnerability will likely be remediated within N days")
+- What is the right abstraction for "security state" that enables meaningful diffing between assessment cycles?
+
+---
+
+## Vector 5: Knowledge Architecture
+
+### From Databases to Adversarial Ontologies
+
+#### 5.1 The Knowledge Problem
+
+T3MP3ST v1.0 contains several embedded knowledge stores:
+- 20 CVE entries in the KnowledgeBase
+- 40+ MITRE ATT&CK techniques
+- 200+ payload entries across categories
+- 50+ privilege escalation techniques
+- 15 secret detection patterns
+
+These are *static enumerations*. They encode *what* but not *why* or *when*. The system knows that DirtyCow is a kernel privilege escalation exploit, but it doesn't know *under what conditions DirtyCow is the optimal choice* versus a SUID binary exploit versus a sudo misconfiguration.
+
+#### 5.2 The Adversarial Ontology
+
+An ontology is a formal representation of knowledge that captures *relationships* between concepts. An adversarial ontology would encode:
+
+```
+Concepts:
+ - Vulnerability classes (not just individual CVEs)
+ - Defensive technologies (WAF types, EDR products, SIEM systems)
+ - Environmental conditions (OS version, patch level, configuration)
+ - Attack techniques (abstract methods, not just specific payloads)
+
+Relations:
+ - "exploits": technique T exploits vulnerability V under conditions C
+ - "detectedBy": technique T is detected by defense D with probability P
+ - "requires": technique T requires precondition P
+ - "enables": successful technique T enables follow-on capability C
+ - "mitigates": defense D mitigates technique T
+ - "bypasses": technique T2 bypasses defense D against technique T1
+```
+
+**Example query**: "Given a Linux server running kernel 5.4 with SELinux enforcing and no SUID binaries, what privilege escalation paths exist, ordered by detection probability?"
+
+The ontology can reason about this by traversing the graph: filter techniques by precondition match, exclude those mitigated by SELinux, rank by detection probability.
+
+#### 5.3 Dynamic Knowledge Acquisition
+
+Static knowledge becomes stale. CVEs are published daily. New bypass techniques emerge weekly. The knowledge architecture should support:
+
+**Ingestion Pipelines**:
+- NVD/CVE feeds — automatic ingestion of new vulnerabilities
+- MITRE ATT&CK updates — technique additions and modifications
+- ExploitDB/GitHub — new public exploits
+- Vendor advisories — patch information for remediation tracking
+- Threat intelligence feeds — active exploitation indicators
+
+**LLM-Assisted Structuring**: Raw vulnerability descriptions are unstructured text. An LLM can parse them into structured ontology entries:
+
+```
+Input: "CVE-2024-XXXX: A buffer overflow in libfoo 3.2.1 allows
+ remote code execution via a crafted PNG file. CVSS 9.8."
+
+Output: {
+ concept: "vulnerability",
+ class: "buffer_overflow",
+ component: "libfoo",
+ versions_affected: ["<= 3.2.1"],
+ vector: "remote",
+ trigger: "crafted_png_file",
+ impact: "code_execution",
+ cvss: 9.8,
+ relations: [
+ { type: "exploitable_via", target: "file_upload_endpoints" },
+ { type: "requires", target: "libfoo_installed" },
+ { type: "mitigated_by", target: ["ASLR", "stack_canaries", "libfoo >= 3.2.2"] }
+ ]
+}
+```
+
+#### 5.4 Experiential Knowledge
+
+Beyond external knowledge, T3MP3ST should build knowledge from its own operations:
+
+- **Technique effectiveness history**: "SQL injection via UNION SELECT worked on 73% of PHP/MySQL targets but only 12% of Node.js/PostgreSQL targets"
+- **Detection correlation**: "dir_bruteforce with >100 req/s triggers Cloudflare rate limiting 89% of the time, but <10 req/s succeeds undetected in 95% of cases"
+- **Environmental fingerprints**: "When we see `X-Powered-By: Express` and `Server: nginx`, the application is likely a Node.js app behind a reverse proxy — prioritize prototype pollution and SSRF over SQLi"
+
+This experiential knowledge creates a *feedback loop* that improves with each engagement.
+
+#### 5.5 Research Questions
+
+- What ontology formalism (OWL, RDF, property graphs, vector embeddings) best captures adversarial knowledge? The answer likely depends on query patterns.
+- How do you maintain ontology consistency when ingesting from multiple conflicting sources?
+- Can experiential knowledge generalize across organizations, or is each target environment sufficiently unique that transfer is limited?
+- What is the right balance between LLM-based reasoning over unstructured knowledge and formal reasoning over structured ontologies?
+
+---
+
+## Vector 6: Distributed & Edge Execution
+
+### Operating Beyond the Perimeter
+
+#### 6.1 The Architectural Constraint
+
+T3MP3ST v1.0 runs as a single Node.js process. All operators share the same event loop, the same network stack, and the same IP address. This creates several limitations:
+
+- **Attribution**: All traffic originates from one IP — trivial to block
+- **Performance**: CPU-bound operations (hash cracking, large scans) block the event loop
+- **Proximity**: Cannot test from inside the target network without full deployment
+- **Resilience**: Process crash loses all state
+
+#### 6.2 The Distributed Architecture
+
+```
++------------------------------------------------------------------+
+| T3MP3ST COMMAND CENTER |
+| (Orchestration, Knowledge Base, Evidence Vault, Reporting) |
++------------------------------------------------------------------+
+ | | | |
+ +----v----+ +----v----+ +----v----+ +----v----+
+ | RELAY | | RELAY | | RELAY | | RELAY |
+ | NODE | | NODE | | NODE | | NODE |
+ | (Cloud | | (VPN | | (Target | | (Tor |
+ | East) | | Exit) | | DMZ) | | Exit) |
+ +---------+ +---------+ +---------+ +---------+
+ | | | |
+ [Operators] [Operators] [Operators] [Operators]
+```
+
+**Relay Nodes**: Lightweight execution environments that host operators and execute tools. Each relay has:
+- Its own IP address and network context
+- A subset of Arsenal tools appropriate for its position
+- Encrypted communication channel back to Command Center
+- Local evidence cache with periodic sync
+
+**Command Center**: The brain. Maintains the global knowledge base, coordinates relay assignments, aggregates evidence, and makes strategic decisions. Does not execute tools directly — it delegates to relays.
+
+**Positioning Strategies**:
+- **External diversity**: Multiple cloud relays across regions/providers to avoid IP-based blocking
+- **Internal positioning**: A relay deployed inside the target's DMZ (with authorization) for internal testing
+- **Egress diversity**: Relays behind different VPN providers or Tor for attribution resistance during authorized red team exercises
+- **Proximity optimization**: Route scan traffic through the relay with lowest latency to the target
+
+#### 6.3 The Edge Agent
+
+For internal penetration testing, a stripped-down "edge agent" could be deployed on the target network:
+
+```
+Edge Agent (~5MB):
+ - Minimal operator runtime
+ - Core arsenal tools (port scan, HTTP client, DNS lookup)
+ - Encrypted relay back to Command Center
+ - Self-destruction timer
+ - Memory-only execution (no disk writes)
+```
+
+This enables authorized internal testing without deploying the full T3MP3ST stack inside the target environment. The edge agent acts as the "hands" while the Command Center provides the "brain."
+
+#### 6.4 State Synchronization
+
+Distributed operation creates a state synchronization challenge. The Conflict-free Replicated Data Type (CRDT) family offers a solution:
+
+- **Findings**: Grow-only set (findings are never deleted during an engagement)
+- **Target status**: Last-writer-wins register with timestamp (most recent observation is most accurate)
+- **Operator state**: Per-relay authority (each relay is authoritative for its own operators)
+- **Knowledge base**: Grow-only set with version vectors for conflict detection on updates
+
+CRDTs guarantee eventual consistency without coordination — perfect for a distributed system where relays may have intermittent connectivity.
+
+#### 6.5 Research Questions
+
+- What is the optimal relay topology for a given target architecture? (Graph optimization problem)
+- How do you handle relay compromise during a red team exercise? (The relay has evidence and encryption keys)
+- Can relay-to-relay communication enable collaborative scanning without routing through the Command Center?
+- What is the minimum viable edge agent? How small can it be while remaining operationally useful?
+
+---
+
+## Vector 7: Evaluation Science
+
+### Measuring What Matters
+
+#### 7.1 The Benchmark Gap
+
+T3MP3ST v1.0 integrates with Cybench (40 tasks) and NYU CTF Bench (200 tasks). These are primarily flag-capture CTF challenges — they test the ability to solve known puzzles with known answers.
+
+This is useful but insufficient. Real penetration testing requires capabilities that CTF benchmarks don't measure:
+
+- **Prioritization**: Given 100 targets, which do you test first? (No CTF tests this)
+- **Comprehensiveness**: Did you find *all* the vulnerabilities, or just the ones with flags?
+- **Stealth**: Did you achieve the objective without being detected?
+- **Reporting quality**: Can a human understand and act on your findings?
+- **Efficiency**: How many actions did you take? How much time/money did you spend?
+- **Adaptability**: Can you handle unexpected conditions (WAF, honeypot, non-standard configurations)?
+
+#### 7.2 The T3MP3ST Evaluation Framework
+
+A comprehensive evaluation framework should measure across five dimensions:
+
+**1. Discovery Rate (Recall)**
+```
+DR = |vulnerabilities_found| / |vulnerabilities_present|
+```
+Requires ground-truth vulnerable environments. Docker-based challenge infrastructure (like T3MP3ST's CTF system) can provide this, but must be expanded to include:
+- Multi-vulnerability applications (not just one vuln per challenge)
+- Realistic enterprise topologies (not just single hosts)
+- Configuration-dependent vulnerabilities (not just code bugs)
+
+**2. Precision**
+```
+P = |true_findings| / |total_findings|
+```
+False positive rate matters enormously for operational tools. A tool that reports 100 findings where 80 are false positives is *worse* than a tool that reports 20 findings where 18 are real — the analyst's time is the bottleneck.
+
+**3. Stealth Score**
+```
+SS = 1 - (|detections| / |detection_opportunities|)
+```
+Requires adversarial monitoring infrastructure: deploy IDS/EDR on benchmark targets and measure what T3MP3ST's operations trigger. This creates a principled OPSEC benchmark.
+
+**4. Operational Efficiency**
+```
+OE = |findings_value| / (|actions_taken| * cost_per_action)
+```
+Measures information-theoretic efficiency: how much security value per unit of effort? Penalizes redundant scanning, unnecessary tool invocations, and unfocused reconnaissance.
+
+**5. Report Quality (Human Evaluation)**
+```
+RQ = human_assessment(clarity, accuracy, actionability, completeness)
+```
+Requires human evaluators, which is expensive. Can be partially automated with LLM judges, but must be calibrated against human agreement.
+
+#### 7.3 The Living Benchmark
+
+Static benchmarks rot. The security landscape changes faster than benchmark authors can update. The evaluation system should include:
+
+**Procedural Generation**: Automatically generate new challenge environments by composing:
+- Base images (Ubuntu, Windows Server, Alpine, etc.)
+- Vulnerable applications (from a library of known-vulnerable versions)
+- Defensive configurations (WAF rules, firewall policies, EDR agents)
+- Network topologies (flat, segmented, air-gapped)
+
+Each generated environment is unique but its ground truth is known (we configured it). This prevents overfitting to specific challenges while maintaining a reliable scoring baseline.
+
+**Community Challenges**: A submission pipeline where researchers contribute new benchmark scenarios. Each scenario includes:
+- Docker/Terraform definition
+- Ground truth vulnerabilities
+- Expected findings
+- Difficulty rating
+- Tag taxonomy (web, binary, crypto, etc.)
+
+**Head-to-Head Evaluation**: Run multiple autonomous security testing systems against the same benchmark and publish comparative results. This creates competitive pressure that drives improvement across the field.
+
+#### 7.4 Research Questions
+
+- What is the right ground-truth representation for "all vulnerabilities present" in a realistic environment? Some vulnerabilities are chains — does the chain count as one finding or N?
+- How do you score partial credit? (Found the XSS but not the SQLi — is that 50% or 0% or depends-on-severity?)
+- Can procedurally generated environments achieve sufficient realism to be meaningful?
+- Is there a principled way to weight the five evaluation dimensions, or is it inherently context-dependent?
+
+---
+
+## Synthesis: The Convergence Thesis
+
+These seven vectors are not independent. They converge toward a single architectural vision:
+
+```
+VECTOR CONTRIBUTION
+─────────────────────────────────────────────────────────────────
+Cognitive Architecture → HOW each agent thinks
+Swarm Dynamics → HOW agents coordinate
+Adversarial ML → HOW agents evade detection
+Continuous Operations → WHEN agents operate (always)
+Knowledge Architecture → WHAT agents know (and learn)
+Distributed Execution → WHERE agents operate (everywhere)
+Evaluation Science → WHETHER agents are improving
+```
+
+The convergence point is an **adversarial intelligence system** that:
+
+1. **Reasons** about targets through hypothesis-driven exploration, not checklist scanning
+2. **Coordinates** through emergent stigmergic signals, not centralized dispatch
+3. **Evades** through adaptive strategy, not static configurations
+4. **Persists** through continuous monitoring, not point-in-time assessments
+5. **Knows** through a growing ontological knowledge base, not static databases
+6. **Operates** from distributed positions, not a single vantage point
+7. **Improves** through rigorous evaluation against living benchmarks
+
+No single vector achieves this alone. The compound effect of all seven transforms the system from a sophisticated tool into something qualitatively different — a system that *understands* security in an operational, adversarial sense rather than merely executing security testing procedures.
+
+### The Maturity Model
+
+| Level | Name | Characteristics | T3MP3ST Version |
+|-------|------|-----------------|-----------------|
+| L0 | Scanner | Fixed tools, fixed targets, fixed sequence | Pre-v1.0 |
+| L1 | Framework | Orchestrated tools, configurable targets, kill chain phases | **v1.0 (current)** |
+| L2 | Agent | LLM-driven reasoning, hypothesis testing, adaptive strategy | v2.0 target |
+| L3 | Swarm | Emergent coordination, distributed operation, experiential learning | v3.0 target |
+| L4 | Intelligence | Adversarial ontologies, continuous operation, meta-game awareness | Long-term vision |
+
+The jump from L1 to L2 is the most important and most tractable. It requires improvements to cognitive architecture (Vector 1) and knowledge systems (Vector 5) but can be achieved within the existing single-process architecture. The jump from L2 to L3 is the most architecturally demanding, requiring distributed execution (Vector 6) and swarm coordination (Vector 2). L4 is speculative — it requires breakthroughs in adversarial ML (Vector 3) and evaluation science (Vector 7) that are active research areas beyond T3MP3ST specifically.
+
+---
+
+## Ethical Framework
+
+### Constraints That Enable
+
+Every capability described in this document increases T3MP3ST's offensive potential. Without commensurate investment in ethical constraints, this potential becomes a liability. The ethical framework is not an afterthought — it is load-bearing architecture.
+
+### Principles
+
+**1. Authorization is non-negotiable.** No level of autonomy removes the requirement for explicit, documented authorization. The system must refuse to operate against targets not explicitly in-scope, regardless of what an operator or even an LLM suggests.
+
+**2. Transparency over capability.** A system that can explain its reasoning is more valuable than one that cannot, even if the explanation costs computational overhead. Every action should be traceable to a goal, every goal to an objective, every objective to an authorized mission.
+
+**3. Minimum necessary force.** The cognitive architecture should prefer information-gathering over exploitation, and exploitation over destruction. The system proves vulnerabilities exist; it does not maximize damage.
+
+**4. Defensive utility.** Every capability developed for offense should have a defensive dual. Adversarial payload mutation also generates WAF test cases. Swarm coordination also models attacker behavior for detection engineering. The knowledge ontology also powers vulnerability management.
+
+**5. Fail-safe, not fail-secure.** When uncertain, the system should stop and ask, not proceed and hope. The autonomous operation level should be a *ceiling* enforced by architecture, not a *floor* assumed by default.
+
+### Architectural Enforcement
+
+- **Scope engine**: Hard technical constraint on target scope. DNS resolution and IP connection functions check against the authorized scope before every request. This cannot be overridden by LLM reasoning.
+- **Action logging**: Every tool invocation, every LLM call, every operator decision is logged with full context. The log is append-only and integrity-protected.
+- **Kill switch**: Immediate, irrevocable stop of all operations. Available at CLI, API, and physical (keyboard interrupt) levels.
+- **Rate governor**: Hardware-enforced (not software-configurable) limits on maximum request rates to prevent accidental denial-of-service.
+- **Human-in-the-loop gates**: Configurable checkpoints where operations pause for human review before proceeding. Mandatory for exploitation and post-exploitation phases at default autonomy levels.
+
+---
+
+## Open Problems
+
+These are questions we don't have answers to. They represent the frontier.
+
+### 1. The Alignment Problem for Offensive AI
+
+How do you ensure an autonomous offensive agent pursues the *authorized objective* and not an objective that an LLM's training data suggests is useful? If the system prompt says "test this web application for SQLi" but the LLM decides that escalating to the underlying server would be more impressive, how do you constrain that without destroying the creative reasoning that makes the system useful?
+
+This is a domain-specific instance of the general AI alignment problem, and it's not solved.
+
+### 2. The Detection Equilibrium
+
+As AI-powered offensive tools improve, AI-powered defensive tools will improve in response. Is there a Nash equilibrium? Or is the dynamic more like an evolutionary arms race with no stable equilibrium? The answer has implications for how much investment in adversarial ML (Vector 3) is actually worthwhile — if defenses can always catch up, the marginal return on evasion innovation decreases.
+
+### 3. The Observation Problem
+
+Security testing fundamentally requires *interacting* with the target, and interaction creates observable artifacts. There is a theoretical minimum detection surface for any given assessment scope — you cannot find a vulnerability without testing for it, and testing creates evidence. What is this minimum, and how close can an autonomous system get to it?
+
+### 4. The Benchmark Paradox
+
+If we build systems that optimize for benchmark performance, and benchmarks are necessarily simplified models of reality, we may create systems that are excellent at benchmarks and poor at real engagements. How do you benchmark the *unbenchmarkable* — creativity, judgment, adaptability, operational intuition?
+
+### 5. The Knowledge Boundary
+
+LLMs trained on public data know about public techniques. The most valuable offensive techniques are *private* — zero-days, novel bypasses, unreported vulnerabilities. Can an LLM-based system discover genuinely novel techniques, or is it fundamentally limited to recombining known knowledge? The answer depends on whether creativity is interpolation or extrapolation, which is an open question in cognitive science.
+
+### 6. The Trust Calibration Problem
+
+When should a human trust the system's assessment and when should they verify? If the system reports "no critical findings," is the target secure or did the system fail? Calibrating trust requires calibrating confidence, which requires the system to accurately model its own uncertainty — a problem that even current frontier LLMs struggle with.
+
+---
+
+## Conclusion
+
+T3MP3ST v1.0 is a framework. It orchestrates tools and agents through a structured pipeline. The seven vectors described here trace a path from framework to intelligence system — a path that is technically demanding but increasingly tractable as LLM capabilities advance, distributed systems mature, and the security community builds shared evaluation infrastructure.
+
+The most important insight is that these vectors are *multiplicative*, not additive. A system with better reasoning (Vector 1) AND experiential knowledge (Vector 5) is not 2x better than one with just reasoning — it's qualitatively different, because the knowledge informs the reasoning which generates new knowledge.
+
+The second most important insight is that we don't need all seven vectors to make progress. The cognitive architecture improvements (Vector 1) are achievable now, with current LLM capabilities and the existing T3MP3ST codebase. They alone would represent a significant advance in autonomous security testing capability.
+
+The future is not a single breakthrough. It is a compound effect — many improvements, each modest, combining into something that doesn't yet exist.
+
+Build it.
+
+---
+
+*This document describes research directions for authorized security testing tools. All capabilities discussed are intended for use within legal, authorized, and ethical security testing engagements only.*
diff --git a/WHITEPAPER.md b/WHITEPAPER.md
new file mode 100644
index 0000000..6e20d8c
--- /dev/null
+++ b/WHITEPAPER.md
@@ -0,0 +1,799 @@
+# T3MP3ST: Tactical Execution Multi-agent Platform for Security Testing
+
+## A Technical Whitepaper
+
+**Version 1.0 | February 2026**
+
+---
+
+> **What is real vs. scaffolding — read this first.** T3MP3ST's live, tool-backed
+> engine is **reconnaissance**: the recon operator drives a real ReAct tool-calling
+> loop against a target and is what produces the re-derivable benchmark numbers (run
+> under a **single-agent loop**, not the full operator swarm). The rest of the
+> Cyber Kill Chain past recon — and the "advanced" / "Pliny Specials" modules — are
+> **honestly-labeled scaffolding**: framework surface, interface stubs, and
+> simulators, not autonomous exploitation. Reports from full-chain runs show `0`
+> executed exploits. See [`FEATURES.md`](FEATURES.md) for the per-module
+> `[x]` shipped / `[~]` partial / `[ ]` planned legend, and the README's
+> "What is real vs. scaffolding" section. This document describes the **framework**;
+> where it names a capability, check it against that legend before assuming it runs.
+
+---
+
+## Abstract
+
+T3MP3ST (TEMPEST) is an open-source, TypeScript-based multi-agent framework for
+penetration testing and red team operations. The platform models the workflow as a
+structured Cyber Kill Chain pipeline of specialized AI-powered agents ("operators"),
+backed by payload databases, real security tooling, and LLM-driven decision-making.
+Today its live engine is the **reconnaissance operator** (a real tool-calling ReAct
+loop); the downstream kill-chain phases and advanced modules are framework
+scaffolding rather than autonomous exploitation (see the note above and
+[`FEATURES.md`](FEATURES.md)). T3MP3ST exposes its capabilities through three
+interfaces: a command-line interface, an HTTP REST API, and a Model Context Protocol
+(MCP) server, for embedding recon-driven security testing into human-driven
+workflows and agent pipelines.
+
+This whitepaper details the architecture, agent model, execution pipeline, tooling, and integration surface of T3MP3ST, providing a comprehensive technical reference for security professionals, researchers, and developers.
+
+---
+
+## Table of Contents
+
+1. [Introduction](#1-introduction)
+2. [Architecture Overview](#2-architecture-overview)
+3. [The Operator Model](#3-the-operator-model)
+4. [Kill Chain Execution Pipeline](#4-kill-chain-execution-pipeline)
+5. [LLM Backbone](#5-llm-backbone)
+6. [The Pliny Specials](#6-the-pliny-specials)
+7. [Arsenal & Tooling](#7-arsenal--tooling)
+8. [Evidence & Intelligence Pipeline](#8-evidence--intelligence-pipeline)
+9. [OPSEC Layer](#9-opsec-layer)
+10. [Integration Surfaces](#10-integration-surfaces)
+11. [Web UI & War Room](#11-web-ui--war-room)
+12. [Benchmark System](#12-benchmark-system)
+13. [Security & Ethical Considerations](#13-security--ethical-considerations)
+14. [Comparison with Existing Tools](#14-comparison-with-existing-tools)
+15. [Future Directions](#15-future-directions)
+16. [Conclusion](#16-conclusion)
+
+---
+
+## 1. Introduction
+
+### 1.1 The Problem
+
+Modern penetration testing demands coordination across multiple specialized tools, techniques, and phases. A typical engagement requires:
+
+- Reconnaissance and OSINT across multiple vectors
+- Vulnerability discovery using diverse scanning methodologies
+- Exploit development and delivery
+- Post-exploitation and lateral movement
+- Evidence collection, correlation, and reporting
+
+Traditionally, these are performed sequentially by human operators switching between dozens of tools. This is slow, error-prone, and difficult to scale. Existing automation approaches tend to be either too narrow (single-tool wrappers) or too broad (full autopilot without human oversight).
+
+### 1.2 The T3MP3ST Approach
+
+T3MP3ST addresses this gap by modeling the penetration testing workflow as a **multi-agent coordination problem**. Rather than building a monolithic scanner, T3MP3ST decomposes security testing into specialized agent roles (operators) that communicate, share intelligence, and advance through a structured kill chain. Each operator is powered by an LLM backbone that enables autonomous reasoning, tool selection, and finding extraction.
+
+Key design principles:
+
+- **Specialization over generalization** - Each operator is tuned for a specific kill chain phase
+- **Intelligence sharing** - Findings from one phase feed directly into the next
+- **Operational security by default** - Detection risk, cooldowns, and abort thresholds are first-class concepts
+- **Multiple autonomy levels** - From fully manual to fully autonomous operation
+- **Open integration** - CLI, REST API, and MCP server for embedding in any workflow
+
+### 1.3 Scope of This Document
+
+This whitepaper covers T3MP3ST v1.0, encompassing the core framework (~12,000 lines of TypeScript), the web UI (~14,000 lines), and the supporting infrastructure. All code is MIT-licensed.
+
+---
+
+## 2. Architecture Overview
+
+### 2.1 System Topology
+
+```
++------------------------------------------------------------------+
+| T3MP3ST COMMAND |
+| (TempestCommand - Central EventEmitter-based Orchestrator) |
++------------------------------------------------------------------+
+| |
+| +------------+ +------------+ +------------+ +------------+ |
+| | OPERATOR | | MISSION | | TARGET | | EVIDENCE | |
+| | CELL | | CONTROL | | ENVIRON | | VAULT | |
+| | (Agent | | (Task | | (Attack | | (Findings, | |
+| | Pool) | | Queue, | | Surface | | Creds, | |
+| | | | Phases, | | Model) | | Evidence) | |
+| | | | RoE) | | | | | |
+| +------------+ +------------+ +------------+ +------------+ |
+| |
+| +------------+ +------------+ +------------+ +------------+ |
+| | ARSENAL | | OPSEC | | COMMS | | ANALYSIS | |
+| | (15+ Real | | (Detection | | (Inter- | | (Reporting | |
+| | Tools + | | Mgmt, | | Agent | | Engine, | |
+| | CLI Wrap) | | Cooldown) | | Channels) | | Summaries)| |
+| +------------+ +------------+ +------------+ +------------+ |
+| |
+| +------------------------------------------------------------+ |
+| | LLM BACKBONE | |
+| | OpenRouter | Anthropic | OpenAI | Ollama | Mock | |
+| | 50+ models | Fallback chain | safeLLMCall() | |
+| +------------------------------------------------------------+ |
+| |
+| +------------------------------------------------------------+ |
+| | ADVANCED MODULES (Stubs + Implemented) | |
+| | KnowledgeBase | EvasionEngine | ReasoningEngine | | |
+| | CognitionEngine | SwarmController | CloudSecurity | ... | |
+| +------------------------------------------------------------+ |
++------------------------------------------------------------------+
+ | | |
+ +----+----+ +----+----+ +----+----+
+ | CLI | | REST | | MCP |
+ | (REPL) | | API | | Server |
+ | | | :3333 | | (stdio) |
+ +---------+ +---------+ +---------+
+```
+
+### 2.2 Core Components
+
+**TempestCommand** (`src/index.ts`, 1039 lines) - The central orchestrator. Extends `EventEmitter` and owns all subsystems. Implements the tick-based execution loop, event forwarding, intelligence syncing, and lifecycle management.
+
+**OperatorCell** (`src/operators/index.ts`, 861 lines) - Manages the pool of operator agents. Handles spawning, capacity limits, status aggregation, and archetype-based querying.
+
+**MissionControl** (`src/mission/index.ts`) - Mission lifecycle, task queue, phase transitions, objective tracking, and Rules of Engagement enforcement.
+
+**TargetEnvironment** (`src/target/index.ts`) - Attack surface model. Tracks targets, their status transitions (discovered -> scanning -> vulnerable -> exploited -> owned), services, and vulnerabilities.
+
+**EvidenceVault** (`src/evidence/index.ts`) - Central repository for findings, credentials, and evidence artifacts. Supports severity classification and CVSS scoring.
+
+**Arsenal** (`src/arsenal/index.ts`) - Tool registry with 15+ built-in tools and external CLI tool wrappers. Category-based filtering and execution history tracking.
+
+**LLMBackbone** (`src/llm/index.ts`) - Multi-provider LLM client supporting OpenRouter (50+ models), Anthropic, OpenAI, Ollama, and a mock provider for testing.
+
+### 2.3 Technology Stack
+
+| Layer | Technology |
+|-------|-----------|
+| Language | TypeScript 5.3+ (strict mode) |
+| Runtime | Node.js 18+ |
+| Build | `tsc` (TypeScript compiler) |
+| Events | `eventemitter3` (typed events) |
+| CLI | `commander` + `inquirer` + `chalk` + `figlet` |
+| API Server | Express.js 4 with CORS |
+| MCP Server | `@modelcontextprotocol/sdk` (stdio transport) |
+| Config | `conf` (persistent storage) + `dotenv` |
+| Testing | Vitest |
+| Linting | ESLint 9 with `typescript-eslint` |
+
+---
+
+## 3. The Operator Model
+
+### 3.1 Agent Architecture
+
+Each operator is an instance of `OperatorAgent`, which is an `EventEmitter` with:
+
+- **Identity**: UUID, callsign, archetype
+- **Profile**: MITRE ATT&CK tactics, tool categories, capabilities, system prompt
+- **State machine**: idle -> tasked -> executing -> cooldown -> idle (or burned)
+- **Intelligence**: locally accumulated findings and credentials
+- **Execution**: LLM-powered task execution with optional AgentLoop (ReAct pattern)
+
+### 3.2 Eight Archetypes
+
+| Archetype | Kill Chain Phase | MITRE Tactics | Capabilities |
+|-----------|-----------------|---------------|-------------|
+| **Recon** | Reconnaissance | TA0043 | OSINT, DNS enum, subdomain discovery, port scanning |
+| **Scanner** | Discovery | TA0007 | Vulnerability scanning, service fingerprinting, config audit |
+| **Exploiter** | Initial Access | TA0001, TA0002 | Exploit dev, payload delivery, initial access |
+| **Infiltrator** | Lateral Movement | TA0008, TA0004 | Privilege escalation, lateral movement, credential access |
+| **Exfiltrator** | Exfiltration | TA0009, TA0010 | Data collection, exfiltration, staging |
+| **Ghost** | Persistence | TA0003, TA0005 | Persistence, evasion, cleanup, anti-forensics |
+| **Coordinator** | C2 | TA0011 | Orchestration, task management, decision making |
+| **Analyst** | Reporting | - | Analysis, reporting, recommendations, risk assessment |
+
+Each archetype has a dedicated system prompt (`src/prompts/index.ts`) that constrains the operator's reasoning to its domain. The prompts include:
+
+- Role definition and operational constraints
+- Preferred tools and techniques (mapped to MITRE ATT&CK technique IDs)
+- Output format expectations (structured findings, credentials, next steps)
+- OPSEC considerations specific to the phase
+
+### 3.3 Task Execution Pipeline
+
+When an operator receives a task, execution follows this priority order:
+
+1. **AgentLoop (ReAct)** - If an `AgentLoop` is attached (LLM + Arsenal), the operator uses autonomous tool-calling in a Reason-Act-Observe loop. The loop selects tools from the Arsenal, executes them, observes results, and iterates (up to 15 iterations, 50K token budget).
+
+2. **LLM-only** - If only an LLM is available (no Arsenal), the operator uses the LLM to analyze the task and produce structured output without executing real tools.
+
+3. **No LLM** - Returns a descriptive error explaining that an LLM provider is required.
+
+### 3.4 Team Factories
+
+Pre-built team compositions for common scenarios:
+
+- `createBalancedTeam()` - All 8 archetypes (one each), general-purpose
+- `createStealthTeam()` - Recon + Scanner + Ghost with low detection thresholds
+- `createBreachTeam()` - 2x Exploiter + Infiltrator + Exfiltrator with aggressive settings
+
+### 3.5 State Management
+
+```
+ assignTask()
+ idle ───────────> tasked ──────> executing
+ ^ |
+ | v
+ +──── cooldown <────────── (task complete)
+ |
+ +──── burned <────── (detection risk exceeded)
+```
+
+- **Detection risk** accumulates with failed tasks (+0.1 per failure)
+- When risk exceeds `maxDetectionRisk`, the operator is automatically "burned" (disabled)
+- Cooldown duration is configurable per operator (default 5s, stealth configs use 30s)
+
+---
+
+## 4. Kill Chain Execution Pipeline
+
+### 4.1 Phase Model
+
+T3MP3ST implements the 7-phase Cyber Kill Chain:
+
+```
+RECON -> WEAPONIZE -> DELIVER -> EXPLOIT -> INSTALL -> C2 -> ACTIONS
+```
+
+Each phase maps to specific operator archetypes, ensuring the right specialist handles each stage.
+
+### 4.2 Tick-Based Execution
+
+The `TempestCommand.tick()` method runs every 1 second and implements the core execution loop:
+
+1. **OPSEC check** - If abort is recommended, pause operations
+2. **Task seeding** - On first tick with targets, generate reconnaissance tasks
+3. **Phase advancement** - If all tasks for the current phase are complete, advance to the next phase and generate new tasks
+4. **Task dispatch** - Match pending tasks to idle operators by archetype, checking dependencies before assignment
+5. **Auto-spawning** - If no operator exists for a task's archetype, auto-spawn one (up to 3 per archetype)
+
+### 4.3 Intelligence Flow
+
+Findings from operators flow through an intelligence pipeline:
+
+```
+Operator discovers finding
+ -> EvidenceVault stores it
+ -> syncFindingToTarget() updates the Target model
+ -> Services extracted from port/service findings
+ -> Vulnerabilities added to target record
+ -> Target status elevated (discovered -> vulnerable -> exploited)
+ -> Next phase operators receive enriched target data
+```
+
+This ensures that a scanner's discovery of an open port is automatically available to the exploiter in the next phase.
+
+### 4.4 Mission Control
+
+Missions are created with:
+- **Objectives** - High-level goals (e.g., "Achieve initial foothold")
+- **Phases** - Ordered subset of kill chain phases
+- **Rules of Engagement** - Scope limits, forbidden techniques, detection thresholds
+- **Task Queue** - Priority-based, dependency-aware task scheduling
+
+Two RoE presets are provided:
+- `createDefaultRoE()` - Standard engagement rules
+- `createStrictRoE()` - Tight scope, low detection tolerance
+
+---
+
+## 5. LLM Backbone
+
+### 5.1 Multi-Provider Architecture
+
+The `LLMBackbone` class (`src/llm/index.ts`) abstracts LLM access across providers:
+
+| Provider | Models | Notes |
+|----------|--------|-------|
+| **OpenRouter** | 50+ models (Claude, GPT-4, Gemini, Grok, DeepSeek, Llama, Mistral) | Primary provider, recommended |
+| **Anthropic** | Claude Opus, Sonnet, Haiku | Direct API |
+| **OpenAI** | GPT-4o, GPT-4 Turbo, o1 | Direct API |
+| **Ollama** | Any local model | For air-gapped environments |
+| **Mock** | Deterministic responses | For testing |
+
+### 5.2 Fallback Chain
+
+The `safeLLMCall()` pattern implements resilient LLM communication:
+
+1. Attempt call with primary model
+2. If response is empty (<10 chars) or fails, retry with Hermes (via OpenRouter)
+3. 3-tier JSON parsing: code block extraction -> non-greedy match -> plain-text regex
+4. Empty response detection triggers automatic fallback
+
+### 5.3 Model Configuration
+
+```typescript
+// Model IDs use DOT format for OpenRouter
+const models = {
+ opus: 'anthropic/claude-opus-4.6',
+ sonnet: 'anthropic/claude-sonnet-4.5',
+ haiku: 'anthropic/claude-haiku-4.5',
+};
+```
+
+Configuration is managed through `src/config/index.ts` using the `conf` library for persistent storage, with environment variable override support.
+
+---
+
+## 6. The Pliny Specials
+
+### 6.1 Overview
+
+The "Pliny Specials" are T3MP3ST's nine elite capability organs. Each provides a specialized, high-level security operation backed by structured payload knowledge, local simulators, evidence contracts, and LLM-powered analysis. The intended shape is not a loose tool drawer: every Special should participate in the same loop of hypothesis, safe probe, evidence, finding, fix, and retest.
+
+### 6.2 Tool Catalog
+
+| Tool | Power | Domain | Primary Function |
+|------|-------|--------|-----------------|
+| **LEVIATHAN** | 99 | Orchestration | Autonomous kill chain orchestration - plans and executes full engagements |
+| **SPHINX** | 88 | Validation | Vulnerability validation - confirms exploitability, eliminates false positives |
+| **GORGON** | 92 | Exploitation | Precision strike engine - surgical exploitation with stealth priority |
+| **CERBERUS** | 85 | Privilege Escalation | Identifies and ranks escalation vectors (50+ techniques for Linux/Windows) |
+| **TYPHON** | 90 | Payload Engineering | Encoding, obfuscation, and WAF bypass (URL, Base64, hex, unicode, HTML entities) |
+| **GRIFFIN** | 95 | Credential Harvesting | Secret extraction - AWS keys, GitHub tokens, JWTs, private keys (15+ patterns) |
+| **SIMURGH** | 100 | Research | Zero-day research framework - attack surface mapping and primitive identification |
+| **HYDRA** | 85 | Multi-Vector | Parallel multi-vector attack coordination (14 attack vectors) |
+| **ARACHNE** | 87 | Chaining | Exploit chain builder - multi-step attack path construction (18 known chains) |
+
+### 6.3 Payload Databases
+
+The Pliny tools are backed by comprehensive, categorized payload databases (200+ total):
+
+**SQL Injection** (5 categories):
+- UNION-based (10 payloads)
+- Blind boolean (7 payloads)
+- Time-based blind (6 payloads)
+- Error-based (4 payloads)
+- Stacked queries (5 payloads)
+
+**Cross-Site Scripting** (4 categories):
+- HTML context, attribute injection, JavaScript context, polyglot
+
+**Server-Side Template Injection**:
+- Jinja2, Twig, ERB, generic detection
+
+**Local/Remote File Inclusion**:
+- Unix path traversal, Windows path traversal, PHP wrappers
+
+**SSRF**:
+- Localhost bypass, cloud metadata (AWS/GCP/Azure/DigitalOcean)
+
+**Command Injection**:
+- Unix and Windows variants
+
+**XXE**:
+- File read, SSRF, out-of-band exfiltration
+
+### 6.4 Privilege Escalation Database
+
+CERBERUS maintains 50+ escalation techniques:
+
+**Linux** (25+ techniques):
+- SUID/SGID binaries, sudo misconfigurations, capabilities
+- Kernel exploits (DirtyPipe, DirtyCow, PwnKit)
+- Cron jobs, writable paths, container escapes
+
+**Windows** (25+ techniques):
+- Potato family (JuicyPotato, PrintSpoofer, GodPotato)
+- UAC bypass, unquoted service paths
+- Token impersonation, registry autorun, DLL hijacking
+
+### 6.5 Secret Pattern Detection
+
+GRIFFIN uses 15+ regex patterns to detect:
+- AWS access keys, secret keys, session tokens
+- GitHub tokens (classic and fine-grained)
+- Google API keys and OAuth secrets
+- JWT tokens (with algorithm analysis)
+- Database connection URIs
+- Private keys (RSA, DSA, EC, OpenSSH)
+
+---
+
+## 7. Arsenal & Tooling
+
+### 7.1 Built-in Tools (Real Implementations)
+
+All built-in tools perform **real operations** using Node.js APIs:
+
+| Tool | Category | Implementation |
+|------|----------|---------------|
+| `dns_lookup` | Recon | `dns` module - A, AAAA, MX, NS, TXT, CNAME records |
+| `port_scan` | Recon | `net` module - TCP connect scan |
+| `subdomain_enum` | Recon | DNS brute-force with wordlist |
+| `whois_lookup` | Recon | TCP port 43 queries |
+| `http_request` | Web | Full HTTP client with custom headers |
+| `header_analysis` | Web | Security header scoring |
+| `dir_bruteforce` | Web | HTTP directory enumeration |
+| `technology_detect` | Web | Header/content fingerprinting |
+| `xss_scan` | Vuln | Payload injection + reflection detection |
+| `sqli_scan` | Vuln | Error-based and boolean-based detection |
+| `ssl_scan` | Vuln | `tls` module - cipher analysis |
+| `password_spray` | Auth | HTTP POST credential testing |
+| `hash_crack` | Auth | Dictionary attack via `crypto` |
+| `base64_decode` | Util | Base64 encoding/decoding |
+| `jwt_decode` | Util | JWT parsing with security analysis |
+
+### 7.2 External CLI Tool Wrappers
+
+The Arsenal also wraps common security CLI tools when available on the system:
+
+- **Network**: nmap, curl, wget, dig, host, whois
+- **Web**: nikto, gobuster, ffuf, dirb
+- **Exploitation**: sqlmap, wfuzz, hydra
+- **Crypto/Forensics**: openssl, base64, xxd, strings, file, exiftool, binwalk
+
+These are executed via `child_process.exec` with a whitelisted command set to prevent command injection.
+
+### 7.3 Tool Execution Model
+
+```typescript
+// Tools are registered with typed schemas
+arsenal.register({
+ name: 'port_scan',
+ category: 'recon',
+ description: 'TCP port scanner',
+ execute: async (params, context) => {
+ // Real implementation using Node.js net module
+ return { success: true, data: { openPorts: [...] } };
+ }
+});
+
+// Operators access tools via the AgentLoop
+agentLoop.run(task, systemPrompt, target);
+// -> LLM selects tools -> Arsenal executes -> Observe results -> Iterate
+```
+
+---
+
+## 8. Evidence & Intelligence Pipeline
+
+### 8.1 Evidence Vault
+
+The `EvidenceVault` centrally stores all operational intelligence:
+
+**Findings** include:
+- Title, description, severity (critical/high/medium/low/info)
+- CVSS score, CVE identifiers, CWE classification
+- Target ID, operator ID, kill chain phase
+- Evidence artifacts (screenshots, HTTP traces, command output)
+- Remediation recommendations
+- Timestamp and exploitation status
+
+**Credentials** are classified by type:
+- Passwords, hashes, tokens, API keys, certificates
+
+### 8.2 Intelligence Syncing
+
+The `syncFindingToTarget()` method automatically enriches the target model:
+
+1. **Service extraction** - Parses port/service findings and adds to target services
+2. **Vulnerability registration** - Maps findings to target vulnerabilities
+3. **Status escalation** - Automatically promotes target status based on severity
+4. **Credential attachment** - Links credentials to their source targets
+
+This creates a feedback loop where each operator's discoveries enrich the target model for subsequent operators.
+
+### 8.3 Reporting Engine
+
+The `AnalysisEngine` generates structured reports:
+
+- **Executive summary** - Risk rating, finding counts, top recommendations
+- **Technical findings** - Detailed vulnerability descriptions with evidence
+- **Full report** - Combined executive + technical + attack paths
+- **Export formats** - Markdown (implemented), with JSON support
+
+---
+
+## 9. OPSEC Layer
+
+### 9.1 Detection Management
+
+The `OpsecController` tracks operational security across the engagement:
+
+- **Detection events** - Each detected interaction is logged with type and severity
+- **Abort threshold** - Configurable limit; when exceeded, operations pause automatically
+- **IOC tracking** - Indicators of Compromise are cataloged for post-engagement review
+
+### 9.2 OPSEC Levels
+
+| Level | Max Detections | Cooldown | Jitter | Traffic Blending |
+|-------|---------------|----------|--------|-----------------|
+| **Silent** | 1 | 5 minutes | 5-15s | Yes |
+| **Covert** | 3 | 1 minute | 1-5s | Yes |
+| **Loud** | 20 | 2 seconds | 0.1-0.5s | No |
+
+### 9.3 Operator-Level OPSEC
+
+Each operator maintains an independent detection risk score:
+- Starts at 0
+- Increases by 0.1 on task failure
+- When it exceeds `maxDetectionRisk`, the operator is "burned" (permanently disabled)
+- Cooldown periods between tasks reduce observable patterns
+
+---
+
+## 10. Integration Surfaces
+
+### 10.1 Command-Line Interface
+
+The CLI (`src/cli.ts`, 20K lines) provides an interactive REPL with:
+
+- Operation presets (balanced, stealth, aggressive, custom)
+- Operator spawning and management
+- Target addition and status monitoring
+- Mission creation and execution
+- AI chat with the LLM backbone
+- Report generation
+- Full settings management
+
+### 10.2 HTTP REST API
+
+The API server (`src/server.ts`) exposes the mission-control + tool surface via Express.js:
+
+```
+POST /api/mission/start # Start an autonomous mission (targets + operators)
+GET /api/mission/report # Export the engagement report (markdown)
+POST /api/whitebox/analyze # White-box repo analysis (ingest -> decompose)
+POST /api/operators/spawn # Spawn an operator into the active mission
+POST /api/general/plan # Op Admiral -- draft an operation plan
+GET /api/codex/status # Codex CLI availability (read-only)
+POST /api/codex/probe # Codex exec readiness self-test
+GET /api/findings # Evidence-gated findings ledger
+POST /api/tools/execute # Direct tool execution
+POST /api/tools/recon # Network reconnaissance
+POST /api/llm/chat # LLM chat
+GET /api/health # Health check
+```
+
+The server also includes SSE endpoints for real-time event streaming to the web UI, plus mission dispatch endpoints for backend operator execution.
+
+### 10.3 Model Context Protocol (MCP) Server
+
+The MCP server (`src/mcp-server.ts`, 83K lines) implements the [Model Context Protocol](https://modelcontextprotocol.io/) specification, enabling AI assistants (Claude, etc.) to use T3MP3ST tools directly:
+
+```json
+{
+ "mcpServers": {
+ "t3mp3st-pliny": {
+ "command": "node",
+ "args": ["/path/to/t3mp3st/dist/mcp-server.js"],
+ "env": { "OPENROUTER_API_KEY": "sk-or-..." }
+ }
+ }
+}
+```
+
+Exposed MCP tools: `pliny_leviathan`, `pliny_sphinx`, `pliny_gorgon`, `pliny_cerberus`, `pliny_typhon`, `pliny_griffin`, `pliny_simurgh`, `pliny_hydra`, `pliny_arachne`, `security_recon`.
+
+---
+
+## 11. Web UI & War Room
+
+### 11.1 Architecture
+
+The web UI (`docs/index.html`, ~14K lines) is a single-page application with a dark cyberpunk theme. It communicates with the API server via REST and SSE for real-time updates.
+
+### 11.2 Pages
+
+| Page | Function |
+|------|----------|
+| **War Room** | Unified dashboard with kill chain visualization, operator status, live intel feed, OPSEC monitor |
+| **Operators** | Agent management - spawn, monitor, view dossiers |
+| **Missions** | Mission creation, phase tracking, objective management |
+| **Evidence** | Findings vault with severity filtering |
+| **Arsenal** | Tool registry with code viewer, stats, and demo capabilities |
+| **Terminal** | Direct command execution |
+| **Benchmarks** | Performance testing against Cybench and NYU CTF Bench |
+| **Config Library** | OPSEC presets and templates |
+| **CTF Range** | Practice targets for training |
+| **Settings** | API key management, model selection, preferences |
+
+### 11.3 Real-Time Features
+
+- **SSE event stream** - Findings, operator status changes, phase transitions, and periodic status updates stream to the UI
+- **Operator hover tooltips** - Full dossier on mouseenter
+- **Console event tooltips** - Full event text on hover
+- **Keyboard shortcuts** - Rapid navigation and actions
+- **Sound cues** - Audio feedback for critical events
+
+---
+
+## 12. Benchmark System
+
+### 12.1 Built-in Categories
+
+T3MP3ST includes a comprehensive benchmarking system across 9 categories:
+
+| Category | Description |
+|----------|-------------|
+| **web** | Web application security (XSS, SQLi, SSRF) |
+| **binary** | Binary exploitation |
+| **crypto** | Cryptographic challenges |
+| **reverse** | Reverse engineering |
+| **forensics** | Digital forensics |
+| **autonomous_ops** | Autonomous operation scenarios |
+| **owasp** | OWASP Top 10 coverage |
+| **mitre** | MITRE ATT&CK technique coverage |
+| **cwe** | CWE Top 25 vulnerability coverage |
+
+### 12.2 External Benchmark Integration
+
+The system integrates with two major CTF benchmark datasets:
+
+**Cybench** (40 tasks):
+- Sourced from `github.com/andyzorigin/cybench`
+- Tasks include metadata, challenge files, and expected flags
+- Fetched via `raw.githubusercontent.com` to avoid API rate limits
+
+**NYU CTF Bench** (200 tasks):
+- Sourced from NYU's CTF challenge repository
+- Tasks include `challenge.json` with flag fields
+- Fetched via a single GitHub git tree API call for efficiency
+
+### 12.3 Scoring
+
+- **Flag-based**: Exact flag match = instant 100%
+- **Keyword + LLM judge**: `evaluateResponseRigorous()` combines keyword matching with an LLM judge for partial credit scoring
+- Results are aggregated per category and displayed in the web UI
+
+---
+
+## 13. Security & Ethical Considerations
+
+### 13.1 Authorized Use Only
+
+T3MP3ST is designed exclusively for **authorized security testing**:
+
+- Penetration testing engagements with written authorization
+- Red team exercises with signed Rules of Engagement
+- Security research in controlled environments
+- CTF competitions and educational contexts
+
+### 13.2 Safety Mechanisms
+
+- **Rules of Engagement enforcement** - Scope limits, forbidden techniques
+- **Detection thresholds** - Automatic pause when limits are exceeded
+- **Operator burn mechanism** - Compromised agents are permanently disabled
+- **Whitelisted CLI commands** - External tool execution is restricted to known security tools
+- **No real exploitation by default** - Pliny tools provide analysis and planning; actual exploitation requires explicit configuration
+
+### 13.3 API Key Security
+
+- API keys are stored in persistent config, never in source code
+- Environment variable override supported
+- `.env` file loading via `dotenv`
+- `.gitignore` excludes sensitive files
+
+### 13.4 Compliance Considerations
+
+Usage should comply with:
+- Computer Fraud and Abuse Act (CFAA)
+- General Data Protection Regulation (GDPR)
+- PCI DSS, HIPAA as applicable
+- Local cybersecurity laws and regulations
+
+---
+
+## 14. Comparison with Existing Tools
+
+| Feature | T3MP3ST | Metasploit | Cobalt Strike | AutoPT Tools |
+|---------|---------|-----------|---------------|-------------|
+| Multi-agent architecture | Yes | No | Partial (teamserver) | Varies |
+| LLM-powered reasoning | Yes (50+ models) | No | No | Some |
+| Kill chain alignment | Full 7-phase | Exploit-focused | Post-exploitation | Varies |
+| MCP integration | Yes | No | No | No |
+| OPSEC management | Built-in | Limited | Strong | Minimal |
+| Web UI | Yes | Armitage | Yes | Varies |
+| Open source | MIT | BSD (Framework) | Commercial | Varies |
+| Language | TypeScript | Ruby | Java | Python/Go |
+| Payload databases | 200+ | 2000+ | Focused | Limited |
+| Evidence management | Full pipeline | Database | Logs | Minimal |
+
+T3MP3ST distinguishes itself through its **LLM-native multi-agent architecture** and **MCP integration**, enabling a new class of AI-assisted security testing workflows that don't exist in traditional tooling.
+
+---
+
+## 15. Future Directions
+
+### 15.1 Near-Term (P0)
+
+- Full REST API endpoint coverage for operators, targets, and missions
+- PDF report generation
+- Scanner import (Nmap XML, Nuclei JSON)
+- Real exploit execution in Pliny tools
+
+### 15.2 Mid-Term (P1-P2)
+
+- **Cloud security module** - AWS/GCP/Azure enumeration and testing
+- **Function calling** in LLM backbone for native tool use
+- **WebSocket** real-time updates (replacing SSE)
+- **Cognition patterns** - Chain-of-Thought, ReAct, Tree-of-Thought
+- **Swarm intelligence** - Pheromone-based multi-agent coordination
+- **Plugin system** for community-contributed tools
+
+### 15.3 Long-Term (P3)
+
+- Distributed operation across multiple nodes
+- SaaS deployment model
+- Marketplace for tools and operator profiles
+- AI-powered remediation guidance
+- Continuous security monitoring mode
+
+---
+
+## 16. Conclusion
+
+T3MP3ST represents a new paradigm in security testing tooling: **LLM-native, multi-agent, kill-chain-aligned penetration testing**. By decomposing the security testing workflow into specialized operators that communicate, share intelligence, and advance autonomously through structured phases, T3MP3ST enables both deeper analysis and broader coverage than traditional single-tool approaches.
+
+The platform's triple integration surface (CLI, REST API, MCP) makes it embeddable in any workflow, from human-driven engagements to fully autonomous AI agent pipelines. Its comprehensive payload databases, real tooling implementations, and OPSEC-first design philosophy provide a solid foundation for authorized security testing at scale.
+
+T3MP3ST is open source under the MIT license, welcoming contributions from the security research community.
+
+---
+
+## Appendix A: Codebase Metrics
+
+| Metric | Value |
+|--------|-------|
+| Backend TypeScript | ~12,000 lines |
+| Web UI (HTML/JS/CSS) | ~14,000 lines |
+| MCP Server | ~83,000 lines |
+| API Server | ~52,000 lines |
+| Total source files | 21 TypeScript modules |
+| Dependencies | 12 production, 8 development |
+| Built-in tools | 15 |
+| External tool wrappers | 15+ |
+| Payload database entries | 200+ |
+| MITRE ATT&CK techniques | 40+ |
+| CVE entries | 20+ |
+| Privilege escalation techniques | 50+ |
+| Secret detection patterns | 15+ |
+| Supported LLM models | 50+ |
+
+## Appendix B: Module Map
+
+```
+src/
+ index.ts # TempestCommand, factory functions, exports
+ cli.ts # Interactive CLI (commander + inquirer)
+ server.ts # Express API server + Pliny endpoints
+ mcp-server.ts # MCP server (stdio transport)
+ setup.ts # Configuration wizard
+ types/index.ts # Core TypeScript type definitions
+ operators/index.ts # OperatorAgent, OperatorCell, archetype profiles
+ mission/index.ts # MissionControl, TaskQueue, RoE
+ target/index.ts # TargetEnvironment, target factories
+ evidence/index.ts # EvidenceVault, finding/credential management
+ arsenal/index.ts # Arsenal, built-in tools, CLI wrappers
+ llm/index.ts # LLMBackbone, provider adapters
+ config/index.ts # Configuration, API key management
+ opsec/index.ts # OpsecController, detection management
+ comms/index.ts # CommsChannel, inter-agent messaging
+ analysis/index.ts # AnalysisEngine, reporting
+ agent/index.ts # AgentLoop (ReAct execution pattern)
+ benchmark/index.ts # Benchmark system, scoring
+ prompts/index.ts # System prompts, templates
+ stubs/index.ts # Advanced module stubs (KnowledgeBase, EvasionEngine, etc.)
+docs/
+ index.html # Single-page web UI
+ctf/
+ challenges/ # CTF practice challenges
+```
+
+---
+
+*T3MP3ST is developed for authorized security testing only. Never use this framework for unauthorized access to systems.*
diff --git a/src/__tests__/ai-redteam-playbook.test.ts b/src/__tests__/ai-redteam-playbook.test.ts
new file mode 100644
index 0000000..18b0397
--- /dev/null
+++ b/src/__tests__/ai-redteam-playbook.test.ts
@@ -0,0 +1,49 @@
+import { describe, it, expect } from 'vitest';
+import {
+ AI_REDTEAM_PLAYBOOK,
+ AI_REDTEAM_TECHNIQUE_IDS,
+ aiRedTeamBriefing,
+ redTeamTechnique,
+} from '../resources/ai-redteam-playbook.js';
+
+describe('AI red-team playbook (distilled from L1B3RT4S / P4RS3LT0NGV3)', () => {
+ it('has the full distilled technique set', () => {
+ expect(AI_REDTEAM_PLAYBOOK.length).toBeGreaterThanOrEqual(15);
+ expect(AI_REDTEAM_TECHNIQUE_IDS.length).toBe(AI_REDTEAM_PLAYBOOK.length);
+ });
+
+ it('every technique is well-formed (all fields, kebab-case id)', () => {
+ for (const t of AI_REDTEAM_PLAYBOOK) {
+ expect(t.id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
+ for (const field of ['category', 'principle', 'redteamUse', 'defense'] as const) {
+ expect(typeof t[field]).toBe('string');
+ expect(t[field].length).toBeGreaterThan(10);
+ }
+ }
+ });
+
+ it('ids are unique', () => {
+ expect(new Set(AI_REDTEAM_TECHNIQUE_IDS).size).toBe(AI_REDTEAM_TECHNIQUE_IDS.length);
+ });
+
+ it('covers the load-bearing technique classes', () => {
+ for (const id of ['refusal-suppression-inversion', 'prefill-affirmation', 'format-scaffold-hijack', 'stacked-composition']) {
+ expect(AI_REDTEAM_TECHNIQUE_IDS).toContain(id);
+ }
+ });
+
+ it('lookup resolves a known id and rejects unknown', () => {
+ expect(redTeamTechnique('format-scaffold-hijack')?.category).toMatch(/format/i);
+ expect(redTeamTechnique('does-not-exist')).toBeUndefined();
+ });
+
+ it('briefing is a non-empty multi-line summary', () => {
+ const brief = aiRedTeamBriefing();
+ expect(brief.length).toBeGreaterThan(100);
+ expect(brief.split('\n').length).toBe(AI_REDTEAM_PLAYBOOK.length);
+ });
+
+ it('every technique carries a defensive countermeasure (this is defensive methodology)', () => {
+ expect(AI_REDTEAM_PLAYBOOK.every(t => t.defense.trim().length > 0)).toBe(true);
+ });
+});
diff --git a/src/__tests__/code-ingest.test.ts b/src/__tests__/code-ingest.test.ts
new file mode 100644
index 0000000..b316465
--- /dev/null
+++ b/src/__tests__/code-ingest.test.ts
@@ -0,0 +1,352 @@
+/**
+ * code-ingest tests
+ *
+ * Builds a tiny temp Python repo with mkdtempSync, runs the full
+ * ingestRepository pipeline over it, and asserts the SELECTION behavior:
+ * - blocks extracted with correct names + plausible line ranges
+ * - entry points detected from @app.route decorators
+ * - call graph resolves an intra-repo call (handler -> helper)
+ * - classification precedence: route -> exposed_externally,
+ * fetch_url -> attack_surface, validate_token -> security_control,
+ * format_name -> neutral
+ * - priority: the exposed handler outranks the neutral util
+ * - packAnalysisUnits preserves priority order and respects a small budget
+ *
+ * Reminder: code-ingest is a PYTHON-ONLY regex "AST-lite" prototype (see the
+ * module header) — these tests exercise the constructs it is designed to handle.
+ */
+
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
+import { tmpdir } from 'os';
+import { join } from 'path';
+import {
+ ingestRepository,
+ createPythonIngestConfig,
+ parseFile,
+ buildCallGraph,
+ findEntryPoints,
+ packAnalysisUnits,
+ formatUnitForLLM,
+ crawl,
+ type IngestResult,
+ type AnalysisUnit,
+} from '../recon/code-ingest.js';
+import { estimateTokens } from '../orchestration/context-pack.js';
+
+// ---------------------------------------------------------------------------
+// Temp repo fixture
+// ---------------------------------------------------------------------------
+
+const APP_PY = `from flask import Flask, request
+from util import fetch_url, validate_token, format_name
+
+app = Flask(__name__)
+
+
+@app.route("/proxy")
+def proxy_view():
+ token = request.args.get("token")
+ if not validate_token(token):
+ return "no", 403
+ data = fetch_url(request.args.get("target"))
+ return data
+
+
+@app.route("/health")
+def health_view():
+ return "ok"
+`;
+
+const UTIL_PY = `import requests
+
+
+def fetch_url(url):
+ resp = requests.get(url)
+ return resp.text
+
+
+def validate_token(tok):
+ return tok == "secret"
+
+
+def format_name(x):
+ return str(x).strip().title()
+`;
+
+// A file that must be skipped because it lives in a test dir.
+const TEST_PY = `def test_something():
+ assert True
+`;
+
+let repoRoot: string;
+let result: IngestResult;
+
+beforeAll(() => {
+ repoRoot = mkdtempSync(join(tmpdir(), 'code-ingest-'));
+ writeFileSync(join(repoRoot, 'app.py'), APP_PY);
+ writeFileSync(join(repoRoot, 'util.py'), UTIL_PY);
+
+ // a dir that must be excluded by default
+ mkdirSync(join(repoRoot, 'tests'));
+ writeFileSync(join(repoRoot, 'tests', 'test_app.py'), TEST_PY);
+
+ // a non-python file that must be ignored by includeExts
+ writeFileSync(join(repoRoot, 'README.md'), '# not python\n');
+
+ const config = createPythonIngestConfig(repoRoot);
+ result = ingestRepository(config);
+});
+
+afterAll(() => {
+ rmSync(repoRoot, { recursive: true, force: true });
+});
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function unitByName(name: string): AnalysisUnit {
+ const u = result.analysisUnits.find((au) => au.block.name === name);
+ if (!u) throw new Error(`no analysis unit for ${name}`);
+ return u;
+}
+
+// ---------------------------------------------------------------------------
+// crawl
+// ---------------------------------------------------------------------------
+
+describe('crawl', () => {
+ it('keeps .py files and skips test dirs + non-python files', () => {
+ const files = crawl(createPythonIngestConfig(repoRoot));
+ const names = files.map((f) => f.split(/[\\/]/).pop());
+ expect(names).toContain('app.py');
+ expect(names).toContain('util.py');
+ // excluded by default test-dir rule
+ expect(names).not.toContain('test_app.py');
+ // excluded by includeExts (.py only)
+ expect(names).not.toContain('README.md');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// parseFile
+// ---------------------------------------------------------------------------
+
+describe('parseFile', () => {
+ it('extracts function blocks with names, params, and plausible line ranges', () => {
+ const blocks = parseFile('util.py', UTIL_PY);
+ const names = blocks.map((b) => b.name);
+ expect(names).toContain('fetch_url');
+ expect(names).toContain('validate_token');
+ expect(names).toContain('format_name');
+
+ const fetch = blocks.find((b) => b.name === 'fetch_url')!;
+ expect(fetch.kind).toBe('function');
+ expect(fetch.params).toEqual(['url']);
+ // def is on line 4 of UTIL_PY (1-indexed), body spans a couple lines
+ expect(fetch.lineStart).toBe(4);
+ expect(fetch.lineEnd).toBeGreaterThanOrEqual(fetch.lineStart);
+ expect(fetch.lineEnd).toBeLessThan(fetch.lineStart + 4);
+ expect(fetch.body).toContain('requests.get(url)');
+ // stable id shape
+ expect(fetch.id).toBe(`util.py::fetch_url@${fetch.lineStart}`);
+ });
+
+ it('captures decorators immediately preceding a def', () => {
+ const blocks = parseFile('app.py', APP_PY);
+ const proxy = blocks.find((b) => b.name === 'proxy_view')!;
+ expect(proxy.decorators.join('\n')).toContain('@app.route("/proxy")');
+ });
+
+ it('labels methods nested under a class as "method"', () => {
+ const src = [
+ 'class Service:',
+ ' def handle(self, req):',
+ ' return req',
+ '',
+ 'def free_function(a):',
+ ' return a',
+ ].join('\n');
+ const blocks = parseFile('svc.py', src);
+ const cls = blocks.find((b) => b.name === 'Service')!;
+ const method = blocks.find((b) => b.name === 'handle')!;
+ const fn = blocks.find((b) => b.name === 'free_function')!;
+ expect(cls.kind).toBe('class');
+ expect(method.kind).toBe('method');
+ // self dropped from params
+ expect(method.params).toEqual(['req']);
+ expect(fn.kind).toBe('function');
+ });
+
+ it('recovers a multiline def signature instead of dropping the function', () => {
+ // A security-relevant function whose params span lines must NOT vanish.
+ const src = [
+ 'def transfer(',
+ ' src_account,',
+ ' dst_account,',
+ ' amount=compute_fee(0),',
+ '):',
+ ' do_wire(src_account, dst_account, amount)',
+ ' return True',
+ '',
+ 'def after(x):',
+ ' return x',
+ ].join('\n');
+ const blocks = parseFile('bank.py', src);
+ const transfer = blocks.find((b) => b.name === 'transfer');
+ expect(transfer, 'transfer() must be captured, not dropped').toBeTruthy();
+ // params gathered across lines, defaults/nested-call stripped to bare names
+ expect(transfer!.params).toEqual(['src_account', 'dst_account', 'amount']);
+ expect(transfer!.lineStart).toBe(1);
+ // body reaches past the ")" line to the real statements
+ expect(transfer!.body).toContain('do_wire(');
+ // the following def is still parsed (signature lines were skipped, not eaten)
+ expect(blocks.find((b) => b.name === 'after')).toBeTruthy();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// entry points + call graph
+// ---------------------------------------------------------------------------
+
+describe('findEntryPoints', () => {
+ it('detects @app.route handlers as entry points', () => {
+ const blocks = parseFile('app.py', APP_PY);
+ const eps = findEntryPoints(blocks);
+ const proxy = blocks.find((b) => b.name === 'proxy_view')!;
+ const health = blocks.find((b) => b.name === 'health_view')!;
+ expect(eps).toContain(proxy.id);
+ expect(eps).toContain(health.id);
+ });
+});
+
+describe('buildCallGraph', () => {
+ it('resolves an intra-repo call from a handler to a helper', () => {
+ const blocks = [
+ ...parseFile('app.py', APP_PY),
+ ...parseFile('util.py', UTIL_PY),
+ ];
+ const graph = buildCallGraph(blocks);
+ const proxy = blocks.find((b) => b.name === 'proxy_view')!;
+ const fetch = blocks.find((b) => b.name === 'fetch_url')!;
+
+ // handler -> helper edge
+ expect(graph[proxy.id].callees).toContain(fetch.id);
+ // inverse: helper knows its caller
+ expect(graph[fetch.id].callers).toContain(proxy.id);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// full pipeline: classification, reachability, priority
+// ---------------------------------------------------------------------------
+
+describe('ingestRepository classification', () => {
+ it('classifies a @route handler as exposed_externally', () => {
+ expect(unitByName('proxy_view').exposure).toBe('exposed_externally');
+ expect(result.entryPoints).toContain(unitByName('proxy_view').block.id);
+ });
+
+ it('classifies fetch_url (outbound request sink) as attack_surface', () => {
+ const fetch = unitByName('fetch_url');
+ expect(fetch.exposure).toBe('attack_surface');
+ // evidence recorded
+ expect(fetch.riskSignals.some((s) => s.includes('requests.get'))).toBe(true);
+ // SSRF/IDOR shape: url param + outbound request
+ expect(fetch.riskSignals.some((s) => s.startsWith('ssrf-idor:'))).toBe(true);
+ });
+
+ it('classifies validate_token as security_control (by name)', () => {
+ expect(unitByName('validate_token').exposure).toBe('security_control');
+ });
+
+ it('classifies format_name as neutral', () => {
+ expect(unitByName('format_name').exposure).toBe('neutral');
+ });
+
+ it('reaches fetch_url from the proxy_view entry point', () => {
+ const fetch = unitByName('fetch_url');
+ expect(fetch.reachable).toBe(true);
+ expect(fetch.reachDepth).toBe(1);
+ // representative reachability path starts at the handler
+ expect(fetch.reachabilityPaths[0][0]).toBe(unitByName('proxy_view').block.id);
+ });
+
+ it('stats account for every block and every exposure class', () => {
+ const { stats } = result;
+ expect(stats.files).toBe(2); // app.py + util.py (tests dir excluded)
+ expect(stats.blocks).toBe(result.analysisUnits.length);
+ const sum =
+ stats.exposed_externally +
+ stats.exposed_internally +
+ stats.attack_surface +
+ stats.security_control +
+ stats.neutral;
+ expect(sum).toBe(stats.blocks);
+ });
+});
+
+describe('ingestRepository priority ordering', () => {
+ it('ranks the exposed handler above the neutral util', () => {
+ const handler = unitByName('proxy_view');
+ const neutral = unitByName('format_name');
+ expect(handler.priority).toBeGreaterThan(neutral.priority);
+ });
+
+ it('emits analysisUnits sorted by priority descending', () => {
+ const priorities = result.analysisUnits.map((u) => u.priority);
+ const sorted = [...priorities].sort((a, b) => b - a);
+ expect(priorities).toEqual(sorted);
+ });
+
+ it('never assigns a negative priority (unreachable just misses the bonus)', () => {
+ for (const u of result.analysisUnits) {
+ expect(u.priority).toBeGreaterThanOrEqual(0);
+ }
+ });
+});
+
+// ---------------------------------------------------------------------------
+// composition with context-pack
+// ---------------------------------------------------------------------------
+
+describe('packAnalysisUnits', () => {
+ it('respects a small token budget and drops the lowest-priority units', () => {
+ const units = result.analysisUnits;
+ // Budget large enough for the top unit but not all of them.
+ const topCost = estimateTokens(formatUnitForLLM(units[0]));
+ const budget = topCost + 5;
+
+ const packed = packAnalysisUnits(units, budget);
+
+ expect(packed.tokensUsed).toBeLessThanOrEqual(budget);
+ expect(packed.includedUnits.length).toBeGreaterThan(0);
+ expect(packed.droppedUnits.length).toBeGreaterThan(0);
+ // nothing vanishes off the books
+ expect(packed.includedUnits.length + packed.droppedUnits.length).toBe(
+ units.length,
+ );
+ // the highest-priority unit is included, a lower one is dropped
+ expect(packed.includedUnits[0].block.name).toBe(units[0].block.name);
+ });
+
+ it('preserves the incoming (security) priority order among included units', () => {
+ const units = result.analysisUnits;
+ const packed = packAnalysisUnits(units, 100_000); // everything fits
+ const includedNames = packed.includedUnits.map((u) => u.block.name);
+ const originalNames = units.map((u) => u.block.name);
+ // same order, all included
+ expect(includedNames).toEqual(originalNames);
+ expect(packed.droppedUnits).toHaveLength(0);
+ });
+
+ it('formatUnitForLLM renders path:lines, exposure, risk signals, and body', () => {
+ const fetch = unitByName('fetch_url');
+ const text = formatUnitForLLM(fetch);
+ expect(text).toContain(`util.py:${fetch.block.lineStart}-${fetch.block.lineEnd}`);
+ expect(text).toContain('exposure: attack_surface');
+ expect(text).toContain('risk signals:');
+ expect(text).toContain('requests.get(url)'); // body included
+ });
+});
diff --git a/src/__tests__/context-pack.test.ts b/src/__tests__/context-pack.test.ts
new file mode 100644
index 0000000..f0815a8
--- /dev/null
+++ b/src/__tests__/context-pack.test.ts
@@ -0,0 +1,150 @@
+/**
+ * context-pack tests
+ *
+ * Covers the token-budgeted, repo-map + relevance packer that replaced the old
+ * blind head-slice truncation:
+ * - packContext respects the token budget
+ * - the REPO MAP header is ALWAYS present
+ * - higher-relevance files are packed before lower ones when budget is tight
+ * - droppedFiles is reported when the bundle exceeds budget (no silent loss)
+ * - parseLooseSource splits a delimited blob and handles a plain blob
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ parseLooseSource,
+ packContext,
+ estimateTokens,
+ type SourceBundle,
+} from '../orchestration/context-pack.js';
+
+describe('estimateTokens', () => {
+ it('approximates chars / 4', () => {
+ expect(estimateTokens('')).toBe(0);
+ expect(estimateTokens('abcd')).toBe(1);
+ expect(estimateTokens('a'.repeat(40))).toBe(10);
+ });
+});
+
+describe('parseLooseSource', () => {
+ it('splits a delimited blob into files by delimiter path', () => {
+ const blob = [
+ '=== src/auth.py ===',
+ 'def login(u, p):',
+ ' return check(u, p)',
+ '--- FILE: src/util.js ---',
+ 'function noop() {}',
+ '## handlers/route.ts',
+ 'export const route = 1;',
+ ].join('\n');
+
+ const bundle = parseLooseSource(blob);
+ const paths = bundle.map(f => f.path);
+ expect(paths).toContain('src/auth.py');
+ expect(paths).toContain('src/util.js');
+ expect(paths).toContain('handlers/route.ts');
+
+ const auth = bundle.find(f => f.path === 'src/auth.py');
+ expect(auth?.content).toContain('def login');
+ expect(auth?.content).not.toContain('function noop'); // stays in util.js
+ });
+
+ it('returns a single pseudo-file for a plain blob with no delimiters', () => {
+ const blob = 'just some prose about a struct\nwith no file markers at all';
+ const bundle = parseLooseSource(blob);
+ expect(bundle).toHaveLength(1);
+ expect(bundle[0].path).toBe('(source)');
+ expect(bundle[0].content).toBe(blob);
+ });
+
+ it('does not split on ordinary horizontal rules or comment sentences', () => {
+ // "---" with no path, and a "// a comment." that is not a file path
+ const blob = 'line one\n---\nline two\n// this is a normal comment here\nline three';
+ const bundle = parseLooseSource(blob);
+ expect(bundle).toHaveLength(1);
+ expect(bundle[0].path).toBe('(source)');
+ });
+
+ it('preserves preamble content before the first delimiter', () => {
+ const blob = 'README notes here\n=== a/b.c ===\nint main(){}';
+ const bundle = parseLooseSource(blob);
+ expect(bundle.map(f => f.path)).toContain('(preamble)');
+ expect(bundle.find(f => f.path === '(preamble)')?.content).toContain('README notes');
+ });
+});
+
+describe('packContext', () => {
+ it('always emits a REPO MAP header, even for a huge repo on a tiny budget', () => {
+ const bundle: SourceBundle = Array.from({ length: 200 }, (_, i) => ({
+ path: `src/file_${i}.ts`,
+ content: 'x'.repeat(500),
+ }));
+ const packed = packContext(bundle, { tokenBudget: 300 });
+ expect(packed.text).toContain('=== REPO MAP');
+ // Huge inventory on a tiny budget must cap the map, not silently drop it.
+ expect(packed.text).toMatch(/more files/);
+ });
+
+ it('respects the token budget (uses at most ~the budget)', () => {
+ const bundle: SourceBundle = Array.from({ length: 30 }, (_, i) => ({
+ path: `mod_${i}.ts`,
+ content: 'const x = 1;\n'.repeat(200),
+ }));
+ const budget = 2000;
+ const packed = packContext(bundle, { tokenBudget: budget });
+ // tokensUsed is the reported accounting; assert it never overruns the budget.
+ expect(packed.tokensUsed).toBeLessThanOrEqual(budget);
+ // and the real assembled text is in the same ballpark (small map/newline slack)
+ expect(estimateTokens(packed.text)).toBeLessThanOrEqual(budget + 50);
+ });
+
+ it('reports droppedFiles when the bundle exceeds the budget', () => {
+ const bundle: SourceBundle = Array.from({ length: 20 }, (_, i) => ({
+ path: `big_${i}.ts`,
+ content: 'y'.repeat(4000), // ~1000 tokens each — cannot all fit in 1500
+ }));
+ const packed = packContext(bundle, { tokenBudget: 1500 });
+ expect(packed.droppedFiles.length).toBeGreaterThan(0);
+ // included + dropped account for every file (nothing vanishes off the books)
+ expect(packed.includedFiles.length + packed.droppedFiles.length).toBe(bundle.length);
+ });
+
+ it('packs higher-relevance files before lower ones when budget is tight', () => {
+ const bundle: SourceBundle = [
+ { path: 'unrelated/readme.md', content: 'z'.repeat(3000) },
+ { path: 'src/deserialize_handler.py', content: 'pickle.loads(data)\n'.repeat(150) },
+ { path: 'misc/notes.txt', content: 'w'.repeat(3000) },
+ ];
+ // Budget fits roughly one file body below the map. The security-relevant /
+ // objective-matching file must win.
+ const packed = packContext(bundle, {
+ tokenBudget: 1200,
+ objective: 'find an unsafe deserialize sink using pickle',
+ });
+ expect(packed.includedFiles).toContain('src/deserialize_handler.py');
+ // the winner appears in the body, not just the map
+ const bodyStart = packed.text.indexOf('=== FILE:');
+ expect(packed.text.slice(bodyStart)).toContain('src/deserialize_handler.py');
+ expect(packed.droppedFiles.length).toBeGreaterThan(0);
+ });
+
+ it('head+tail elides a single oversized file instead of dropping it', () => {
+ const marker = 'UNIQUE_TAIL_MARKER_9f3';
+ const huge = 'A'.repeat(20000) + marker;
+ const bundle: SourceBundle = [{ path: 'one/giant.c', content: huge }];
+ const packed = packContext(bundle, { tokenBudget: 1000 });
+ expect(packed.includedFiles).toContain('one/giant.c');
+ expect(packed.text).toContain('…[middle elided');
+ expect(packed.text).toContain('A'); // head present
+ expect(packed.text).toContain(marker); // tail present
+ });
+
+ it('echoes the budget and never drops the map for a plain single-file bundle', () => {
+ const bundle = parseLooseSource('int main() { return 0; }');
+ const packed = packContext(bundle, { tokenBudget: 5000 });
+ expect(packed.tokenBudget).toBe(5000);
+ expect(packed.text).toContain('=== REPO MAP');
+ expect(packed.includedFiles).toContain('(source)');
+ expect(packed.droppedFiles).toHaveLength(0);
+ });
+});
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
new file mode 100644
index 0000000..5be673e
--- /dev/null
+++ b/src/__tests__/index.test.ts
@@ -0,0 +1,455 @@
+/**
+ * T3MP3ST Core Tests
+ *
+ * Basic test suite for core functionality.
+ */
+
+import { describe, it, expect, beforeEach } from 'vitest';
+import { Arsenal, BUILTIN_TOOLS, successResult, failResult, createToolContext } from '../arsenal/index.js';
+import { FRONTIER_ARSENAL_MILESTONE, SAFE_COMMANDS, TOOL_ADAPTERS, adaptersForFamily, summarizeToolCatalog } from '../arsenal/catalog.js';
+import { createKnowledgeBase, createEvasionEngine, CVE_DATABASE, MITRE_TECHNIQUES } from '../stubs/index.js';
+import { AGENT_PROMPT_PACKS, FOREFRONT_PRESSURE_LANES, OPERATOR_RUNBOOKS, forefrontPressureForFamily, promptPacksForFamily, runbookForFamily } from '../resources/index.js';
+import { OpGeneral } from '../general/index.js';
+import { LLMBackbone } from '../llm/index.js';
+
+describe('Arsenal', () => {
+ let arsenal: Arsenal;
+
+ beforeEach(() => {
+ arsenal = new Arsenal();
+ });
+
+ describe('Tool Registration', () => {
+ it('should register built-in tools', () => {
+ arsenal.registerMany(BUILTIN_TOOLS);
+ expect(arsenal.getAllTools().length).toBe(BUILTIN_TOOLS.length);
+ });
+
+ it('should get tool by name', () => {
+ arsenal.registerMany(BUILTIN_TOOLS);
+ const tool = arsenal.getTool('dns_lookup');
+ expect(tool).toBeDefined();
+ expect(tool?.name).toBe('dns_lookup');
+ });
+
+ it('should filter tools by category', () => {
+ arsenal.registerMany(BUILTIN_TOOLS);
+ const reconTools = arsenal.getToolsByCategory('recon');
+ expect(reconTools.length).toBeGreaterThan(0);
+ reconTools.forEach(tool => expect(tool.category).toBe('recon'));
+ });
+
+ it('should return undefined for unknown tool', () => {
+ const tool = arsenal.getTool('nonexistent');
+ expect(tool).toBeUndefined();
+ });
+ });
+
+ describe('Tool Execution', () => {
+ it('should execute dns_lookup tool (may fail if no network)', async () => {
+ arsenal.registerMany(BUILTIN_TOOLS);
+ const result = await arsenal.execute('dns_lookup', createToolContext(undefined, { domain: 'example.com' }));
+ // DNS lookup now does real network requests - may succeed or fail depending on network
+ expect(result.output || result.error).toContain('example.com');
+ });
+
+ it('should execute base64_decode tool', async () => {
+ arsenal.registerMany(BUILTIN_TOOLS);
+ const result = await arsenal.execute('base64_decode', createToolContext(undefined, { data: 'SGVsbG8gV29ybGQ=' }));
+ expect(result.success).toBe(true);
+ expect(result.output).toContain('Hello World');
+ });
+
+ it('should execute jwt_decode tool', async () => {
+ arsenal.registerMany(BUILTIN_TOOLS);
+ // Valid JWT token structure (header.payload.signature)
+ const testJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
+ const result = await arsenal.execute('jwt_decode', createToolContext(undefined, { token: testJwt }));
+ expect(result.success).toBe(true);
+ expect(result.output).toContain('HS256');
+ });
+
+ it('should throw for unknown tool', async () => {
+ await expect(arsenal.execute('nonexistent', createToolContext())).rejects.toThrow();
+ });
+ });
+
+ describe('Helper Functions', () => {
+ it('should create success result', () => {
+ const result = successResult('Test output');
+ expect(result.success).toBe(true);
+ expect(result.output).toBe('Test output');
+ });
+
+ it('should create fail result', () => {
+ const result = failResult('Test error');
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('Test error');
+ });
+ });
+});
+
+describe('KnowledgeBase', () => {
+ const kb = createKnowledgeBase();
+
+ describe('CVE Database', () => {
+ it('should have CVE entries', () => {
+ expect(CVE_DATABASE.length).toBeGreaterThan(0);
+ });
+
+ it('should query CVEs by ID', () => {
+ const result = kb.query({ type: 'cve', query: 'Log4Shell' });
+ expect(result.results.length).toBeGreaterThan(0);
+ });
+
+ it('should get CVE by exact ID', () => {
+ const cve = kb.getCVE('CVE-2021-44228');
+ expect(cve).toBeDefined();
+ expect(cve?.description).toContain('Log4Shell');
+ });
+
+ it('should filter critical CVEs by CVSS', () => {
+ const critical = kb.getCriticalCVEs(9.0);
+ expect(critical.length).toBeGreaterThan(0);
+ critical.forEach(c => expect(c.cvss).toBeGreaterThanOrEqual(9.0));
+ });
+ });
+
+ describe('MITRE Techniques', () => {
+ it('should have MITRE techniques', () => {
+ expect(MITRE_TECHNIQUES.length).toBeGreaterThan(0);
+ });
+
+ it('should get technique by ID', () => {
+ const technique = kb.getTechnique('T1190');
+ expect(technique).toBeDefined();
+ expect(technique?.name).toBe('Exploit Public-Facing Application');
+ });
+
+ it('should filter techniques by tactic', () => {
+ const initialAccess = kb.getTechniquesByTactic('initial-access');
+ expect(initialAccess.length).toBeGreaterThan(0);
+ initialAccess.forEach(t => expect(t.tactic).toBe('initial-access'));
+ });
+ });
+
+ describe('Pattern Matching', () => {
+ it('should detect SQL injection patterns', () => {
+ const matches = kb.matchPatterns("SELECT * FROM users WHERE id = '1' OR '1'='1'");
+ expect(matches.some(m => m.type === 'sqli')).toBe(true);
+ });
+
+ it('should detect XSS patterns', () => {
+ const matches = kb.matchPatterns('');
+ expect(matches.some(m => m.type === 'xss')).toBe(true);
+ });
+
+ it('should detect path traversal patterns', () => {
+ const matches = kb.matchPatterns('../../etc/passwd');
+ expect(matches.some(m => m.type === 'path-traversal')).toBe(true);
+ });
+ });
+});
+
+describe('EvasionEngine', () => {
+ const evasion = createEvasionEngine();
+
+ describe('Encoding', () => {
+ it('should encode to base64', () => {
+ const result = evasion.encode('hello', 'base64');
+ expect(result.encoded).toBe('aGVsbG8=');
+ expect(result.encoding).toBe('base64');
+ });
+
+ it('should encode to hex', () => {
+ const result = evasion.encode('AB', 'hex');
+ expect(result.encoded).toBe('4142');
+ });
+
+ it('should encode to URL', () => {
+ const result = evasion.encode('');
+ expect(variants.length).toBeGreaterThan(3);
+ // All variants should be different
+ const uniqueEncoded = new Set(variants.map(v => v.encoded));
+ expect(uniqueEncoded.size).toBe(variants.length);
+ });
+ });
+
+ describe('WAF Bypass', () => {
+ it('should generate WAF bypass variants', () => {
+ const variants = evasion.getWAFBypassVariants('SELECT * FROM users');
+ expect(variants.length).toBeGreaterThan(5);
+ expect(variants).toContain('SELECT * FROM users'); // Original included
+ });
+ });
+
+ describe('Code Obfuscation', () => {
+ it('should obfuscate strings', () => {
+ const result = evasion.obfuscate("var x = 'hello'", 'string_split');
+ expect(result.obfuscated).toContain('String.fromCharCode');
+ });
+ });
+});
+
+describe('Operator Contracts', () => {
+ it('should expose family-specific prompt packs with evidence contracts', () => {
+ expect(AGENT_PROMPT_PACKS.length).toBeGreaterThanOrEqual(5);
+ for (const pack of AGENT_PROMPT_PACKS) {
+ expect(pack.roleFrame.length).toBeGreaterThan(20);
+ expect(pack.evidenceContract.length).toBeGreaterThanOrEqual(3);
+ expect(pack.escalationRules.length).toBeGreaterThan(0);
+ }
+ });
+
+ it('should return the AI boundary prompt pack for AI red-team missions', () => {
+ const packs = promptPacksForFamily('ai_red_team');
+ expect(packs.map(pack => pack.id)).toContain('prompt-ai-boundary-cartographer');
+ });
+
+ it('should expose operator runbooks with phase risks and exit criteria', () => {
+ expect(OPERATOR_RUNBOOKS.length).toBeGreaterThanOrEqual(5);
+ for (const runbook of OPERATOR_RUNBOOKS) {
+ expect(runbook.operatorPromise.length).toBeGreaterThan(30);
+ expect(runbook.phases.length).toBeGreaterThanOrEqual(3);
+ for (const phase of runbook.phases) {
+ expect(phase.riskIfSkipped.length).toBeGreaterThan(20);
+ expect(phase.exitCriteria.length).toBeGreaterThan(0);
+ }
+ }
+ });
+
+ it('should return a runbook for the core routed mission families', () => {
+ expect(runbookForFamily('ai_red_team')?.title).toContain('AI Agent Boundary');
+ expect(runbookForFamily('web_api')?.title).toContain('Web/API');
+ expect(runbookForFamily('code_supply_chain')?.title).toContain('Repository Trust');
+ });
+
+ it('should expose forefront pressure lanes with defensive conversion artifacts', () => {
+ expect(FOREFRONT_PRESSURE_LANES.length).toBeGreaterThanOrEqual(6);
+ for (const lane of FOREFRONT_PRESSURE_LANES) {
+ expect(lane.frontierSignal.length).toBeGreaterThan(40);
+ expect(lane.pressureQuestion).toMatch(/\?$/);
+ expect(lane.operatorMove.length).toBeGreaterThan(40);
+ expect(lane.defensiveArtifact.length).toBeGreaterThan(30);
+ expect(lane.containment.length).toBeGreaterThan(30);
+ expect(lane.recommendedResources.length).toBeGreaterThan(0);
+ }
+ });
+
+ it('should return forefront lanes by mission family', () => {
+ expect(forefrontPressureForFamily('ai_red_team').map(lane => lane.id)).toContain('frontier-browser-tool-privilege');
+ expect(forefrontPressureForFamily('agent_warfare').map(lane => lane.id)).toContain('frontier-agent-command-injection');
+ });
+});
+
+describe('Tool Adapter Catalog', () => {
+ it('should expose a broad command-ready and catalog-only arsenal', () => {
+ const summary = summarizeToolCatalog();
+ expect(TOOL_ADAPTERS.length).toBeGreaterThanOrEqual(30);
+ expect(summary.commandReady).toBeGreaterThanOrEqual(20);
+ expect(summary.catalogOnly).toBeGreaterThanOrEqual(2);
+ expect(summary.unmodeled).toBe(false);
+ expect(TOOL_ADAPTERS.length).toBeLessThanOrEqual(FRONTIER_ARSENAL_MILESTONE);
+ expect(SAFE_COMMANDS).toContain('nuclei');
+ expect(SAFE_COMMANDS).toContain('semgrep');
+ expect(SAFE_COMMANDS).not.toContain('msfconsole');
+ });
+
+ it('should give core mission families first-class adapter coverage', () => {
+ expect(adaptersForFamily('web_api').map(adapter => adapter.id)).toEqual(expect.arrayContaining(['nmap', 'nuclei', 'ffuf', 'katana']));
+ expect(adaptersForFamily('code_supply_chain').map(adapter => adapter.id)).toEqual(expect.arrayContaining(['semgrep', 'gitleaks', 'trivy', 'syft']));
+ expect(adaptersForFamily('ai_red_team').map(adapter => adapter.id)).toEqual(expect.arrayContaining(['garak', 'promptfoo']));
+ expect(adaptersForFamily('smart_contract').map(adapter => adapter.id)).toEqual(expect.arrayContaining(['slither', 'mythril', 'foundry-forge', 'solhint']));
+ expect(adaptersForFamily('crypto_secrets').map(adapter => adapter.id)).toEqual(expect.arrayContaining(['openssl', 'john', 'hashcat', 'foundry-cast']));
+ expect(adaptersForFamily('reverse_binary').map(adapter => adapter.id)).toEqual(expect.arrayContaining(['binwalk', 'radare2', 'apktool', 'jadx']));
+ });
+});
+
+describe('OpGeneral v2 Campaign Control', () => {
+ function generalWithResponse(payload: Record): OpGeneral {
+ const llm = {
+ prompt: async () => `\`\`\`json\n${JSON.stringify(payload)}\n\`\`\``,
+ } as unknown as LLMBackbone;
+ return new OpGeneral(llm);
+ }
+
+ it('should enrich sparse plans with hunt lanes, work orders, evidence contracts, and review gates', async () => {
+ const general = generalWithResponse({
+ codename: 'IRON CURRENT',
+ summary: 'Authorized local web/API weird-machine hunt.',
+ targets: [{ address: 'http://127.0.0.1:3333/ui/', expectedType: 'web_application', priority: 1, rationale: 'Local T3MP3ST UI' }],
+ objectives: [{ description: 'Find boundary failures in the local control plane', priority: 1, successCriteria: 'Evidence-backed hypotheses and retests', phase: 'reconnaissance' }],
+ operators: [{ archetype: 'scanner', count: 2, deployPhase: 'reconnaissance', briefing: 'Probe local UI/API edges safely' }],
+ roe: { scope: ['http://127.0.0.1:3333/ui/'], exclusions: ['external targets'], maxDetections: 1, destructiveAllowed: false, requireApproval: [] },
+ complexity: 'moderate',
+ rationale: 'Start local, preserve evidence, and require receipts before active external action.',
+ });
+
+ const plan = await general.planOperation({ objective: 'Run an authorized local web/API zero-day hunt against http://127.0.0.1:3333/ui/' });
+ const review = general.reviewPlan(plan);
+
+ expect(plan.missionFamily).toBe('web_api');
+ expect(plan.huntLanes.length).toBeGreaterThan(0);
+ expect(plan.workOrders.length).toBeGreaterThanOrEqual(2);
+ expect(plan.evidenceContract.requiredArtifacts.length).toBeGreaterThan(2);
+ expect(plan.authorityReceipts.map(receipt => receipt.action)).toContain('mission_execution');
+ expect(plan.missionGate.status).not.toBe('hold');
+ expect(review.score).toBeGreaterThanOrEqual(70);
+ });
+
+ it('should hold the gate when a directive has no explicit target or scope', async () => {
+ const general = generalWithResponse({
+ codename: 'VAGUE THUNDER',
+ summary: 'Interesting but under-scoped operation.',
+ objectives: [{ description: 'Hunt something somewhere', priority: 1, successCriteria: 'Unknown', phase: 'reconnaissance' }],
+ operators: [{ archetype: 'analyst', count: 1, deployPhase: 'reconnaissance', briefing: 'Clarify scope' }],
+ roe: { scope: [], exclusions: [], maxDetections: 0, destructiveAllowed: false, requireApproval: [] },
+ complexity: 'high',
+ rationale: 'Scope is not explicit enough for execution.',
+ });
+
+ const plan = await general.planOperation({ objective: 'Find weird machines wherever they may be' });
+ const review = general.reviewPlan(plan);
+
+ expect(review.status).toBe('hold');
+ expect(review.blockers.join(' ')).toMatch(/target|scope/i);
+ expect(plan.missionGate.status).toBe('hold');
+ });
+
+ it('should preserve specialist multiplicity and expose work-order assignments for execution', async () => {
+ const general = generalWithResponse({
+ codename: 'DOUBLE SCANNER',
+ summary: 'Authorized local scanner-heavy review.',
+ targets: [{ address: 'localhost', expectedType: 'web_application', priority: 1, rationale: 'Local lab' }],
+ objectives: [{ description: 'Compare two scanner routes', priority: 1, successCriteria: 'Two independent evidence paths', phase: 'reconnaissance' }],
+ operators: [
+ { archetype: 'scanner', count: 2, deployPhase: 'reconnaissance', briefing: 'Route A and Route B scanner comparison' },
+ { archetype: 'analyst', count: 1, deployPhase: 'actions_on_objectives', briefing: 'Synthesize proof and falsifiers' },
+ ],
+ roe: { scope: ['localhost'], exclusions: ['external targets'], maxDetections: 1, destructiveAllowed: false, requireApproval: [] },
+ complexity: 'moderate',
+ rationale: 'Multiplicity matters for independent route comparison.',
+ });
+
+ const plan = await general.planOperation({ objective: 'Authorized local route comparison against localhost' });
+ const execConfig = general.executePlan(plan);
+
+ expect(execConfig.operators.filter(archetype => archetype === 'scanner').length).toBeGreaterThanOrEqual(2);
+ expect(execConfig.operatorAssignments.some(assignment => assignment.workOrderIds.length > 0)).toBe(true);
+ expect(execConfig.workOrders.every(order => order.falsifier && order.retest)).toBe(true);
+ });
+});
+
+describe('Wedged-dispatch timeout backstop', () => {
+ // A fake AgentLoop whose run() never settles — simulates a truly-hung LLM call
+ // (the wedge symptom: operator pinned in `executing` forever).
+ const makeWedgingLoop = () =>
+ ({ run: () => new Promise(() => { /* never resolves */ }) } as unknown as import('../agent/index.js').AgentLoop);
+
+ it('OperatorAgent.abortActiveTask returns a wedged operator to idle so it can take new work', async () => {
+ const { createOperator } = await import('../operators/index.js');
+ const { LLMBackbone } = await import('../llm/index.js');
+ const { Arsenal } = await import('../arsenal/index.js');
+
+ const op = createOperator('Wedge-1', 'recon', undefined, new LLMBackbone({ provider: 'mock', model: 'mock-model' }));
+ op.attachArsenal(new Arsenal(), makeWedgingLoop());
+
+ const task = {
+ id: 't-wedge', missionId: 'm1', name: 'recon', description: 'scan example.com',
+ phase: 'reconnaissance' as any, operatorType: 'recon' as const, status: 'pending' as const,
+ priority: 1, dependencies: [], createdAt: Date.now(),
+ };
+
+ // Fire the task but DON'T await — the promise never settles (wedge).
+ void op.assignTask(task as any);
+ // Let the microtasks run so assignTask advances to 'executing'.
+ await Promise.resolve();
+ await Promise.resolve();
+ expect(op.status).toBe('executing');
+ expect(op.isAvailable()).toBe(false);
+
+ // Backstop reset.
+ op.abortActiveTask('test timeout');
+ expect(op.status).toBe('idle');
+ expect(op.isAvailable()).toBe(true);
+ expect(op.state.currentTask).toBeNull();
+ expect(op.getSummary().failedTasks).toBe(1);
+ });
+
+ it('a wedged mission reaches phase advancement once the per-dispatch backstop fires', async () => {
+ // Tiny backstop so the test is fast and deterministic.
+ const prev = process.env.T3MP3ST_TASK_TIMEOUT_MS;
+ process.env.T3MP3ST_TASK_TIMEOUT_MS = '30';
+ try {
+ // Re-import with the env override in place so resolveTaskTimeoutMs() reads it.
+ const mod = await import('../index.js');
+ const command = new mod.TempestCommand({
+ name: 'Wedge Op',
+ llm: { provider: 'mock', model: 'mock-model' },
+ });
+
+ // A target + a recon operator whose agent loop wedges forever.
+ command.targetEnv.addTarget({ name: 'example.com', address: 'example.com', type: 'web_application', zone: 'external' });
+ const op = command.spawnOperator('Recon-Wedge', 'recon');
+ op.attachArsenal(command.arsenal, makeWedgingLoop());
+
+ let phaseAdvanced = false;
+ command.on('mission:phase_changed', () => { phaseAdvanced = true; });
+
+ command.start();
+
+ // Poll (real timers): the 1s tick loop must dispatch, wedge, then reap the
+ // dispatch via the backstop and advance past RECON.
+ const deadline = Date.now() + 8000;
+ while (Date.now() < deadline && !phaseAdvanced) {
+ await new Promise(r => setTimeout(r, 100));
+ }
+ command.stop();
+
+ expect(phaseAdvanced).toBe(true);
+ // The wedged operator was reset back to idle (or re-tasked in a later phase),
+ // never left stuck in 'executing' with no current task.
+ const stuck = command.cell.getAllOperators().some(
+ o => (o.status === 'executing' || o.status === 'tasked') && !o.state.currentTask
+ );
+ expect(stuck).toBe(false);
+ } finally {
+ if (prev === undefined) delete process.env.T3MP3ST_TASK_TIMEOUT_MS;
+ else process.env.T3MP3ST_TASK_TIMEOUT_MS = prev;
+ }
+ }, 15000);
+});
+
+describe('Codex account provider', () => {
+ it('should expose Codex as an API-keyless planning backend', () => {
+ const llm = new LLMBackbone({
+ provider: 'codex',
+ model: 'codex-default',
+ });
+
+ expect(llm.getProvider()).toBe('codex');
+ expect(llm.getModel()).toBe('codex-default');
+ expect(llm.validateConfig()).toEqual({ valid: true });
+ });
+});
diff --git a/src/__tests__/ingest-limits.test.ts b/src/__tests__/ingest-limits.test.ts
new file mode 100644
index 0000000..964316c
--- /dev/null
+++ b/src/__tests__/ingest-limits.test.ts
@@ -0,0 +1,75 @@
+/**
+ * ingest ceiling tests — the bounded-ingest hardening must FIRE only on pathological input
+ * and leave a normal ingest byte-identical (no truncated flag, no behavior change).
+ */
+
+import { describe, it, expect, afterAll } from 'vitest';
+import { mkdtempSync, writeFileSync, rmSync } from 'fs';
+import { tmpdir } from 'os';
+import { join } from 'path';
+import { ingestRepository, crawl, type IngestConfig } from '../recon/code-ingest.js';
+
+function makeRepo(nFiles: number): string {
+ const dir = mkdtempSync(join(tmpdir(), 't3mp3st-ingest-limits-'));
+ for (let i = 0; i < nFiles; i++) {
+ writeFileSync(join(dir, `mod_${i}.py`), `def f_${i}(x):\n return x + ${i}\n`);
+ }
+ return dir;
+}
+
+describe('ingest ceilings (bounded ingest, non-regressing)', () => {
+ const dirs: string[] = [];
+ const cfg = (repoRoot: string, extra: Partial = {}): IngestConfig => ({
+ repoRoot, includeExts: ['.py'], excludeGlobs: [], ...extra,
+ });
+ afterAll(() => { for (const d of dirs) { try { rmSync(d, { recursive: true, force: true }); } catch { /* noop */ } } });
+
+ it('crawl caps at maxFiles and ingest flags stats.truncated', () => {
+ const dir = makeRepo(20); dirs.push(dir);
+ expect(crawl(cfg(dir, { maxFiles: 5 })).length).toBe(5);
+ const res = ingestRepository(cfg(dir, { maxFiles: 5 }));
+ expect(res.stats.files).toBe(5);
+ expect(res.stats.truncated).toBe(true);
+ });
+
+ it('ingest caps at maxTotalBytes and flags stats.truncated', () => {
+ const dir = makeRepo(20); dirs.push(dir);
+ const res = ingestRepository(cfg(dir, { maxTotalBytes: 50 })); // ~30 bytes/file → breaks after ~2
+ expect(res.stats.truncated).toBe(true);
+ expect(res.stats.blocks).toBeGreaterThan(0);
+ });
+
+ it('a NORMAL ingest under the ceilings is unchanged — no truncated key', () => {
+ const dir = makeRepo(10); dirs.push(dir);
+ const res = ingestRepository(cfg(dir, { maxFiles: 50000, maxTotalBytes: 1_000_000_000 }));
+ expect(res.stats.files).toBe(10);
+ expect(res.stats.blocks).toBe(10);
+ expect(res.stats.truncated).toBeUndefined(); // shape identical to pre-hardening
+ });
+
+ it('no ceilings configured = fully unbounded (back-compat)', () => {
+ const dir = makeRepo(8); dirs.push(dir);
+ const res = ingestRepository(cfg(dir)); // no maxFiles / maxTotalBytes at all
+ expect(res.stats.files).toBe(8);
+ expect(res.stats.truncated).toBeUndefined();
+ });
+
+ it('an empty / non-Python repo yields 0 files (the ONLY case the whitebox guard throws on)', () => {
+ const dir = mkdtempSync(join(tmpdir(), 't3mp3st-ingest-empty-')); dirs.push(dir);
+ writeFileSync(join(dir, 'README.md'), '# not python\n');
+ const res = ingestRepository(cfg(dir));
+ expect(res.stats.files).toBe(0);
+ expect(res.analysisUnits.length).toBe(0);
+ });
+
+ it('a Python repo with only module-level code has files>0 but 0 blocks (whitebox guard must NOT throw)', () => {
+ // A valid script (imports + top-level statements, no def/class) is legitimate input. The
+ // whitebox empty-guard gates on stats.files===0, so this files>0 case proceeds — it must
+ // NOT regress into a 500 (regression sweep finding #2).
+ const dir = mkdtempSync(join(tmpdir(), 't3mp3st-ingest-modlevel-')); dirs.push(dir);
+ writeFileSync(join(dir, 'app.py'), 'import os\nx = 1\nprint(os.getcwd(), x)\n');
+ const res = ingestRepository(cfg(dir));
+ expect(res.stats.files).toBe(1); // there IS source
+ expect(res.analysisUnits.length).toBe(0); // but 0 extractable def/class blocks
+ });
+});
diff --git a/src/__tests__/no-phantom-tools.test.ts b/src/__tests__/no-phantom-tools.test.ts
new file mode 100644
index 0000000..3ba264a
--- /dev/null
+++ b/src/__tests__/no-phantom-tools.test.ts
@@ -0,0 +1,55 @@
+import { describe, it, expect } from 'vitest';
+import { ARCHETYPE_PROFILES } from '../operators/index.js';
+import { BUILTIN_TOOLS, EXTERNAL_TOOLS } from '../arsenal/index.js';
+import { OPERATOR_SYSTEM_PROMPTS } from '../prompts/index.js';
+
+// ─────────────────────────────────────────────────────────────────────────────
+// NO PHANTOM TOOLS — the "advertised = wired" guard for the arsenal.
+//
+// An operator must never advertise a tool the harness can't call. Phantom names
+// (metasploit/sqlmap/mimikatz, or a bare "nmap" when the real tool is "nmap_scan")
+// burn the agent's iterations on calls that 404. This test makes that fail the
+// build by construction — the same discipline as test:no-fitting, applied to tools.
+// ─────────────────────────────────────────────────────────────────────────────
+
+const REGISTERED = new Set([...BUILTIN_TOOLS, ...EXTERNAL_TOOLS].map((t) => t.name));
+
+describe('no phantom tools (advertised = wired)', () => {
+ it('the arsenal registers a non-trivial, unique set of callable tools', () => {
+ const all = [...BUILTIN_TOOLS, ...EXTERNAL_TOOLS].map((t) => t.name);
+ expect(all.length).toBeGreaterThanOrEqual(20);
+ expect(new Set(all).size).toBe(all.length); // no duplicate tool names
+ });
+
+ it('every archetype defaultTool is a REGISTERED, callable tool', () => {
+ const phantoms: string[] = [];
+ for (const [archetype, profile] of Object.entries(ARCHETYPE_PROFILES)) {
+ for (const tool of profile.defaultTools) {
+ if (!REGISTERED.has(tool)) phantoms.push(`${archetype} → "${tool}"`);
+ }
+ }
+ expect(
+ phantoms,
+ `phantom tools advertised by archetypes but not registered in the arsenal: ${phantoms.join(', ')}`,
+ ).toEqual([]);
+ });
+
+ it('operator system prompts reference no phantom binaries (advertise only what is wired)', () => {
+ // Classic phantoms we do NOT wrap. Backtick-bare forms (`nmap`) distinguish the phantom
+ // bare binary from the real registered tool (nmap_scan, which is `nmap_scan`).
+ const PHANTOMS = ['metasploit', 'mimikatz', 'sqlmap', 'bloodhound', 'crackmapexec', 'nikto', 'wpscan', 'psexec', '`nmap`', '`nuclei`', '`burp`'];
+ const hits: string[] = [];
+ for (const [archetype, prompt] of Object.entries(OPERATOR_SYSTEM_PROMPTS)) {
+ const low = String(prompt).toLowerCase();
+ for (const p of PHANTOMS) if (low.includes(p.toLowerCase())) hits.push(`${archetype}: ${p}`);
+ }
+ expect(hits, `phantom binaries named in operator prompts: ${hits.join(', ')}`).toEqual([]);
+ });
+
+ it('every registered tool has a real handler (no metadata-only stubs)', () => {
+ const noHandler = [...BUILTIN_TOOLS, ...EXTERNAL_TOOLS]
+ .filter((t) => typeof t.handler !== 'function')
+ .map((t) => t.name);
+ expect(noHandler, `registered tools missing an executable handler: ${noHandler.join(', ')}`).toEqual([]);
+ });
+});
diff --git a/src/__tests__/redact-credential.test.ts b/src/__tests__/redact-credential.test.ts
new file mode 100644
index 0000000..951f14a
--- /dev/null
+++ b/src/__tests__/redact-credential.test.ts
@@ -0,0 +1,44 @@
+/**
+ * redactCredential — the API/output redactor that keeps raw harvested secrets from ever
+ * leaving the process (external-audit P0: /api/mission/findings previously returned raw
+ * Credential.secret). Pins the invariant so a refactor can't silently re-expose secrets.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { redactCredential } from '../evidence/index.js';
+import type { Credential } from '../types/index.js';
+
+describe('redactCredential — raw secrets never leave the process', () => {
+ const cred: Credential = {
+ id: 'cred-1',
+ type: 'password',
+ username: 'admin',
+ secret: 'sup3rs3cr3t!',
+ domain: 'example.com',
+ targetId: 't-1',
+ source: 'harvest',
+ discoveredAt: 1,
+ validatedAt: 2,
+ privilegeLevel: 'admin',
+ };
+
+ it('strips the raw secret entirely — no key, no value anywhere in the output', () => {
+ const r = redactCredential(cred);
+ expect('secret' in r).toBe(false);
+ expect(JSON.stringify(r)).not.toContain('sup3rs3cr3t');
+ });
+
+ it('keeps the non-sensitive metadata + a secretCaptured flag', () => {
+ const r = redactCredential(cred);
+ expect(r.id).toBe('cred-1');
+ expect(r.type).toBe('password');
+ expect(r.username).toBe('admin');
+ expect(r.domain).toBe('example.com');
+ expect(r.privilegeLevel).toBe('admin');
+ expect(r.secretCaptured).toBe(true);
+ });
+
+ it('secretCaptured is false when no secret was captured', () => {
+ expect(redactCredential({ ...cred, secret: '' }).secretCaptured).toBe(false);
+ });
+});
diff --git a/src/__tests__/spine-live.test.ts b/src/__tests__/spine-live.test.ts
new file mode 100644
index 0000000..9e6ae70
--- /dev/null
+++ b/src/__tests__/spine-live.test.ts
@@ -0,0 +1,68 @@
+import { describe, it, expect } from 'vitest';
+import { createOperator } from '../operators/index.js';
+import type { Finding } from '../types/index.js';
+
+// ─────────────────────────────────────────────────────────────────────────────
+// HONESTY SPINE — LOAD-BEARING IN THE LIVE PATH.
+//
+// The provenance gate (gateLiveFinding) must run where findings are CREATED, not as
+// an opt-in verifyFinding(id) call nothing invokes. This test pins that invariant to
+// the operator's recordFinding chokepoint — the same path executeTask() uses to record
+// every agent finding. A model-asserted finding (no tool evidence) must be recorded
+// UNVERIFIED; a tool-backed finding must verify. If this regresses, a model can assert a
+// "critical" into the record for free again — the exact failure the whole project prevents.
+// ─────────────────────────────────────────────────────────────────────────────
+
+function baseFinding(over: Partial): Finding {
+ return {
+ id: '',
+ title: 'x',
+ description: 'x',
+ severity: 'high',
+ targetId: 't',
+ operatorId: 'o',
+ phase: 'exploitation',
+ evidence: [],
+ discoveredAt: Date.now(),
+ ...over,
+ } as Finding;
+}
+
+describe('honesty spine is load-bearing at finding creation (operator.recordFinding)', () => {
+ it('a model-asserted finding (no tool evidence) is recorded UNVERIFIED with gate reasons', () => {
+ const op = createOperator('test-recon', 'recon');
+ let blocked: { reasons: string[] } | undefined;
+ op.on('finding:gate-blocked', (e) => { blocked = e; });
+
+ const f = baseFinding({ title: 'model says critical', severity: 'critical', evidence: [] });
+ op.recordFinding(f);
+
+ expect(f.verifyGate?.passed).toBe(false);
+ expect(f.verifiedAt).toBeUndefined();
+ expect(f.verifyGate?.provenance).toBe('none');
+ expect(blocked, 'a gate-blocked event must fire for an unbacked finding').toBeDefined();
+ expect(blocked!.reasons.join(' ')).toMatch(/provenance|evidence/i);
+ });
+
+ it('a tool-backed finding (real tool-output evidence) verifies', () => {
+ const op = createOperator('test-recon2', 'recon');
+ const f = baseFinding({
+ title: 'nmap found open port',
+ severity: 'high',
+ evidence: [{ type: 'output', content: 'PORT 22/tcp open ssh\n...real nmap output...', timestamp: Date.now() }],
+ });
+ op.recordFinding(f);
+
+ expect(f.verifyGate?.passed).toBe(true);
+ expect(f.verifyGate?.provenance).toBe('tool');
+ expect(f.verifiedAt).toBeTypeOf('number');
+ });
+
+ it('getVerified-style filter only surfaces gate-passed findings', () => {
+ const op = createOperator('test-recon3', 'recon');
+ op.recordFinding(baseFinding({ title: 'prose claim', evidence: [] }));
+ op.recordFinding(baseFinding({ title: 'tool claim', evidence: [{ type: 'output', content: 'x'.repeat(20), timestamp: Date.now() }] }));
+ const verified = op.getFindings().filter((f) => f.verifyGate?.passed);
+ expect(verified.map((f) => f.title)).toEqual(['tool claim']);
+ });
+});
diff --git a/src/__tests__/stub-honesty.test.ts b/src/__tests__/stub-honesty.test.ts
new file mode 100644
index 0000000..e4b308b
--- /dev/null
+++ b/src/__tests__/stub-honesty.test.ts
@@ -0,0 +1,108 @@
+import { describe, it, expect } from 'vitest';
+import {
+ ExploitEngine,
+ ScannerOrchestrator,
+ BrowserAutomation,
+ BenchmarkRunner,
+ PersistenceController,
+ SwarmController,
+ CloudSecurityEngine,
+ ProtocolHandler,
+ WorkflowOrchestrator,
+ WorkflowBuilder,
+} from '../stubs/index.js';
+
+// ─────────────────────────────────────────────────────────────────────────────
+// STUB HONESTY — placeholder engines must never fabricate success.
+//
+// Several "advanced modules" are interface-only stubs (see src/stubs/index.ts).
+// The launch risk is a stub that RETURNS something that looks like a real result:
+// a { success: true }, a deployed implant, an "Executed X" node result. A caller
+// (or the UI) then believes work happened that never did. This test locks in the
+// honest contract: stub methods return not-implemented / failure shapes, never a
+// fabricated success or real-looking data.
+// ─────────────────────────────────────────────────────────────────────────────
+
+describe('stub honesty (no fabricated success)', () => {
+ it('ExploitEngine.exploit reports failure, not a fake exploited target', async () => {
+ const result = await new ExploitEngine().exploit('10.0.0.1', {
+ name: 'stub',
+ targetService: 'http',
+ });
+ expect(result.success).toBe(false);
+ expect(String(result.output || '')).toMatch(/not implemented/i);
+ });
+
+ it('ScannerOrchestrator.scan returns no fabricated findings', async () => {
+ const result = await new ScannerOrchestrator().scan('10.0.0.1');
+ expect(result.findings).toEqual([]);
+ });
+
+ it('BrowserAutomation.navigate does not fake a loaded page', async () => {
+ const state = await new BrowserAutomation().navigate('http://example.com');
+ // No real navigation happened: no live URL, an honest "Not implemented" title.
+ expect(state.url).toBe('');
+ expect(state.title).toMatch(/not implemented/i);
+ });
+
+ it('BenchmarkRunner.run does not fabricate a passing score', async () => {
+ const metrics = await new BenchmarkRunner().run([]);
+ expect(metrics.score).toBe(0);
+ });
+
+ it('PersistenceController.deploy returns an honest failure, not a fake implant', async () => {
+ const result = await new PersistenceController().deploy('10.0.0.1', {
+ type: 'scheduled_task',
+ location: 'stub',
+ });
+ const asRecord = result as unknown as Record;
+ // Must NOT look like a real deployed implant (no fabricated id/implant object).
+ expect(asRecord.id).toBeUndefined();
+ expect(asRecord.implant).toBeUndefined();
+ // And it must never claim success.
+ expect(asRecord.success).not.toBe(true);
+ expect(result.success).toBe(false);
+ expect(result.error).toMatch(/not implemented/i);
+ });
+
+ it('SwarmController.initialize spawns no fabricated agents', async () => {
+ const agents = await new SwarmController().initialize({
+ agentCount: 5,
+ behavior: 'exploration',
+ });
+ expect(agents).toEqual([]);
+ });
+
+ it('CloudSecurityEngine.scan discovers no fabricated resources', async () => {
+ const resources = await new CloudSecurityEngine().scan({ provider: 'aws' });
+ expect(resources).toEqual([]);
+ });
+
+ it('ProtocolHandler.http does not fabricate a real HTTP response', async () => {
+ const response = await new ProtocolHandler().http({ method: 'GET', url: 'http://example.com' });
+ expect(response.status).toBe(0);
+ expect(response.body).toMatch(/not implemented/i);
+ });
+
+ it('WorkflowOrchestrator.execute never reports fabricated node successes', async () => {
+ const workflow = new WorkflowBuilder()
+ .addNode({ id: 'a', type: 'recon', action: { type: 'noop', params: {} } })
+ .addNode({ id: 'b', type: 'scan', action: { type: 'noop', params: {} } })
+ .addEdge({ from: 'a', to: 'b' })
+ .build();
+
+ const report = await new WorkflowOrchestrator(undefined).execute(workflow);
+
+ // The topological walk is real, but NO node actually executes.
+ expect(report.results.length).toBe(2);
+ for (const nodeResult of report.results) {
+ expect(nodeResult.success).toBe(false);
+ expect(nodeResult.notExecuted).toBe(true);
+ // No fabricated "Executed ..." claim.
+ expect(String(nodeResult.output || '')).not.toMatch(/^Executed /);
+ }
+ // The overall execution must NOT be reported as completed.
+ expect(report.execution.status).not.toBe('completed');
+ expect(report.execution.status).toBe('failed');
+ });
+});
diff --git a/src/__tests__/whitebox-containment.test.ts b/src/__tests__/whitebox-containment.test.ts
new file mode 100644
index 0000000..840bd26
--- /dev/null
+++ b/src/__tests__/whitebox-containment.test.ts
@@ -0,0 +1,112 @@
+/**
+ * whitebox path-containment tests (B-03)
+ *
+ * resolveContainedRepoPath is the security chokepoint for the white-box path: it
+ * turns an operator-supplied repoPath into a canonical, contained absolute path
+ * or throws RepoPathError. These tests pin the containment invariants so a future
+ * refactor can't silently reopen the arbitrary-directory-read hole:
+ * - accepts real dirs inside the allowed root (root itself + nested)
+ * - rejects dirs outside the root, '..' escapes, and symlinks that escape
+ * - rejects non-existent paths, files (non-dirs), and empty/non-string input
+ *
+ * The root is pinned via T3MP3ST_REPO_ROOT to a realpath'd temp dir so the test
+ * is independent of the operator's real home.
+ */
+
+import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
+import { mkdtempSync, writeFileSync, mkdirSync, rmSync, symlinkSync, realpathSync } from 'fs';
+import { statSync } from 'node:fs';
+import { tmpdir } from 'os';
+import { join } from 'path';
+import { resolveContainedRepoPath, RepoPathError } from '../recon/whitebox.js';
+
+// Mock node:fs (what whitebox.ts imports) so we can force statSync to throw AFTER realpath
+// succeeds — the TOCTOU race path. Every other export keeps its real implementation, and
+// statSync itself defaults to real (vi.fn wrapping the original), overridden per-test below.
+vi.mock('node:fs', async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, statSync: vi.fn(actual.statSync) };
+});
+
+describe('resolveContainedRepoPath (B-03 path containment)', () => {
+ let base: string; // realpath'd temp base holding both root/ and outside/
+ let root: string; // the allowed root (set as T3MP3ST_REPO_ROOT)
+ let outside: string; // a sibling dir OUTSIDE the root
+ let prevRoot: string | undefined;
+
+ beforeAll(() => {
+ prevRoot = process.env.T3MP3ST_REPO_ROOT;
+ // realpath the temp base up-front: on macOS /tmp -> /private/tmp is a symlink,
+ // which would otherwise make our own fixtures look like they escape the root.
+ base = realpathSync(mkdtempSync(join(tmpdir(), 't3mp3st-contain-')));
+ root = join(base, 'root');
+ outside = join(base, 'outside');
+ mkdirSync(join(root, 'repo', 'sub'), { recursive: true });
+ mkdirSync(outside, { recursive: true });
+ writeFileSync(join(root, 'afile.txt'), 'x');
+ process.env.T3MP3ST_REPO_ROOT = root;
+ });
+
+ afterAll(() => {
+ if (prevRoot === undefined) delete process.env.T3MP3ST_REPO_ROOT;
+ else process.env.T3MP3ST_REPO_ROOT = prevRoot;
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
+ });
+
+ it('accepts a directory inside the allowed root (returns canonical path)', () => {
+ expect(resolveContainedRepoPath(join(root, 'repo'))).toBe(realpathSync(join(root, 'repo')));
+ });
+
+ it('accepts the root itself', () => {
+ expect(resolveContainedRepoPath(root)).toBe(realpathSync(root));
+ });
+
+ it('accepts a nested subdirectory', () => {
+ expect(resolveContainedRepoPath(join(root, 'repo', 'sub'))).toBe(realpathSync(join(root, 'repo', 'sub')));
+ });
+
+ it('rejects a directory OUTSIDE the root', () => {
+ expect(() => resolveContainedRepoPath(outside)).toThrow(RepoPathError);
+ });
+
+ it('rejects a traversal that escapes the root', () => {
+ expect(() => resolveContainedRepoPath(join(root, '..', 'outside'))).toThrow(/outside the allowed root/);
+ });
+
+ it('rejects a non-existent path', () => {
+ expect(() => resolveContainedRepoPath(join(root, 'nope'))).toThrow(/does not resolve/);
+ });
+
+ it('rejects a file (not a directory)', () => {
+ expect(() => resolveContainedRepoPath(join(root, 'afile.txt'))).toThrow(/must be a directory/);
+ });
+
+ it('rejects empty / whitespace / non-string input', () => {
+ expect(() => resolveContainedRepoPath('')).toThrow(RepoPathError);
+ expect(() => resolveContainedRepoPath(' ')).toThrow(RepoPathError);
+ expect(() => resolveContainedRepoPath(undefined as unknown as string)).toThrow(RepoPathError);
+ expect(() => resolveContainedRepoPath(42 as unknown as string)).toThrow(RepoPathError);
+ });
+
+ it('rejects a symlink whose target escapes the root (realpath resolves it)', () => {
+ const link = join(root, 'escape-link');
+ try {
+ symlinkSync(outside, link);
+ } catch {
+ return; // symlink creation not permitted in this env — skip rather than fail
+ }
+ expect(() => resolveContainedRepoPath(link)).toThrow(RepoPathError);
+ });
+
+ it('normalizes a statSync failure (TOCTOU delete after realpath) to RepoPathError', () => {
+ // The dir realpath-resolves (exists) but statSync then throws — simulating a deletion in
+ // the race window between realpath and stat. The wrap must convert it to a RepoPathError
+ // ('not accessible'), which the API layer turns into a clean 400 (never an unhandled fs
+ // error / unhandled rejection). Guards the statSync-wrap catch added this session.
+ const target = join(root, 'repo'); // real, in-root dir — realpath succeeds
+ vi.mocked(statSync).mockImplementationOnce(() => {
+ throw Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
+ });
+ expect(() => resolveContainedRepoPath(target)).toThrow(/not accessible/);
+ });
+});
diff --git a/src/admiral/index.ts b/src/admiral/index.ts
new file mode 100644
index 0000000..8925cb5
--- /dev/null
+++ b/src/admiral/index.ts
@@ -0,0 +1,251 @@
+/**
+ * The ADMIRAL — t3mp3st's conversational mission-intake officer.
+ *
+ * A human describes, in plain language, the engagement they want. The Admiral
+ * holds a short conversation to elicit a precise, AUTHORIZED mission, drafts the
+ * directive, and hands off to Op General (the executor) — it never runs anything
+ * itself. This is the natural-language front door above /api/general/{plan,auto}.
+ *
+ * DESIGN INVARIANTS (read before editing):
+ * 1. PLANNER, NOT EXECUTOR. The Admiral only prepares a brief + directive. The
+ * actual launch goes through the authorization gate and Op General. The
+ * Admiral must never claim it ran, probed, or found anything.
+ * 2. DRY-RUN IS THE DEFAULT. Fidelity defaults to dry_run (plan only, no
+ * packets). live requires the operator to explicitly authorize real packets
+ * against a target they confirm they own / are permitted to test.
+ * 3. HONEST INTAKE. If a request is out-of-scope, unauthorized, or points at a
+ * live third party without permission, the Admiral says so and steers to a
+ * dry-run or a lab target. No jailbreak framing changes this.
+ */
+
+import type { LLMBackbone } from '../llm/index.js';
+import type { Directive } from '../general/index.js';
+import type { OpsecLevel } from '../types/index.js';
+
+export type MissionFamilyId =
+ | 'zero_day_hunt' | 'pentest' | 'smart_contract'
+ | 'repo_audit' | 'ctf_range' | 'ai_red_team';
+
+export type Fidelity = 'dry_run' | 'live';
+
+export interface MissionBrief {
+ objective: string;
+ target: string;
+ family: MissionFamilyId | '';
+ scope: string;
+ fidelity: Fidelity;
+}
+
+export interface AdmiralTurn {
+ reply: string;
+ brief: MissionBrief;
+ missing: string[];
+ ready: boolean;
+}
+
+export interface ChatMsg {
+ role: 'user' | 'assistant';
+ content: string;
+}
+
+const FAMILIES: MissionFamilyId[] = ['zero_day_hunt', 'pentest', 'smart_contract', 'repo_audit', 'ctf_range', 'ai_red_team'];
+
+export const ADMIRAL_SYSTEM_PROMPT = `You are the ADMIRAL — t3mp3st's mission-intake officer. A human operator describes, in plain language, a security engagement they want to run. Your job is to turn that into a precise, AUTHORIZED mission that the autonomous swarm (Op General + specialist operators) can execute.
+
+YOU ARE A PLANNER AND INTAKE OFFICER, NOT THE EXECUTOR. You never run, probe, scan, or find anything yourself — you only prepare the brief. When it is complete you hand off to the operator to confirm authorization and engage.
+
+Gather exactly these slots:
+- objective: what they want to achieve, one crisp sentence
+- target: the concrete thing to point at (repo/SDK URL, host/IP/CIDR, contract address, model endpoint, or a challenge/range)
+- family: infer ONE of [zero_day_hunt, pentest, smart_contract, repo_audit, ctf_range, ai_red_team]
+- scope: the rules of engagement — what is in-scope and who authorized it
+- fidelity: dry_run (plan only, NO packets — the safe default) or live (real packets — requires explicit authorization)
+
+RULES:
+- Ask ONE focused question at a time, for the single most important missing slot. Be brief, sharp, a touch wry — an experienced operator's voice, not a perky chatbot. 1-3 sentences.
+- Infer family from what they describe; only ask if genuinely ambiguous.
+- DEFAULT fidelity to dry_run. Set live ONLY when the user explicitly authorizes real packets against a target they confirm they own or are permitted to test. If they ask to hit a live third party without saying they're authorized, keep it dry_run and say why.
+- NEVER claim you launched, ran, scanned, or discovered anything. You prepare; the General executes after the gate.
+- If the ask is unauthorized or out of scope, say so plainly and steer to a dry-run or a lab/CTF target.
+- When every required slot (objective, target, family, scope) is filled, set ready=true, give a one-paragraph brief summary, and tell them to hit REVIEW & ENGAGE to confirm authorization and launch.
+
+SYSTEM-AUTHORITATIVE FIELDS (set your best estimate, but the system decides):
+- "fidelity" is ADVISORY — the system always pins it to dry_run out of conversation; going live is a separate, deliberate human action (the UI toggle + the launch confirm gate). Never present live as something you can enable here.
+- "ready" is RE-DERIVED by the system from whether all four required slots are filled; your value is a hint, not the gate.
+
+OUTPUT FORMAT — return ONLY a single JSON object, no prose or markdown fences outside it:
+{
+ "reply": "your conversational message to the operator",
+ "brief": {
+ "objective": "" or a string,
+ "target": "" or a string,
+ "family": "" or one of [zero_day_hunt, pentest, smart_contract, repo_audit, ctf_range, ai_red_team],
+ "scope": "" or a string,
+ "fidelity": "dry_run"
+ },
+ "missing": ["names of slots still needed"],
+ "ready": true or false
+}`;
+
+function emptyBrief(): MissionBrief {
+ return { objective: '', target: '', family: '', scope: '', fidelity: 'dry_run' };
+}
+
+function coerceTurn(raw: any): AdmiralTurn {
+ const b = (raw && raw.brief) || {};
+ const family = FAMILIES.includes(b.family) ? b.family : '';
+ // SECURITY: the conversation NEVER escalates to live. A typed "I'm authorized, go live"
+ // is a claim, not authorization — the model must not flip fidelity on it. Live is a
+ // deliberate human action: the UI fidelity toggle + the /api/admiral/launch confirm gate
+ // (409 unless confirmed). Fidelity out of converse is therefore always dry_run.
+ const fidelity: Fidelity = 'dry_run';
+ const brief: MissionBrief = {
+ objective: typeof b.objective === 'string' ? b.objective : '',
+ target: typeof b.target === 'string' ? b.target : '',
+ family,
+ scope: typeof b.scope === 'string' ? b.scope : '',
+ fidelity,
+ };
+ // required slots: objective, target, family, scope
+ const missing: string[] = [];
+ if (!brief.objective) missing.push('objective');
+ if (!brief.target) missing.push('target');
+ if (!brief.family) missing.push('family');
+ if (!brief.scope) missing.push('scope');
+ const ready = missing.length === 0;
+ return {
+ reply: typeof raw?.reply === 'string' && raw.reply.trim() ? raw.reply : "Tell me what you want to go after, and I'll set it up.",
+ brief,
+ missing: Array.isArray(raw?.missing) && raw.missing.length ? raw.missing : missing,
+ ready: typeof raw?.ready === 'boolean' ? raw.ready && ready : ready,
+ };
+}
+
+function extractJson(text: string): any {
+ // strip code fences, grab the outermost {...}
+ const cleaned = text.replace(/```(?:json)?/gi, '').trim();
+ const start = cleaned.indexOf('{');
+ const end = cleaned.lastIndexOf('}');
+ if (start === -1 || end === -1 || end < start) throw new Error('no JSON object in Admiral reply');
+ return JSON.parse(cleaned.slice(start, end + 1));
+}
+
+// ── Anti-fitting gate (mirrors scripts/lessons.mjs isAnswerLeak + scripts/test-no-fitting.mjs FORBIDDEN) ──
+// Every Admiral prompt suggestion MUST pass this. It rejects challenge-specific tells so the advisor
+// can never quietly become the harness-side fitting the project's measurement moat is built against.
+const FORBIDDEN_TELLS: Array<[RegExp, string]> = [
+ [/cve-\d{4}-\d{3,}/i, 'a specific CVE id'],
+ [/\/(opt|home|var|srv|root)\/[\w./-]*flag|flag\.(txt|php)\b/i, 'a specific flag file path'],
+ [/flag\{[^}]/i, 'a literal flag value'],
+ [/\b(haproxy|nginx|apache|mitmproxy|tomcat|jetty|gunicorn|werkzeug)\s*\/?\s*\d+\.\d+/i, 'a version-pinned target product'],
+ [/\bsku_\w+|customtemplate|backup-?heart|backup-?migration|\bbmi_\w+|internal\.router/i, 'a specific endpoint/class/host from a challenge'],
+ [/\b9(14[0-9]|15[0-9])\b.{0,30}\b9(14[0-9]|15[0-9])\b/, 'a specific port pair from a challenge'],
+ [/content-dir|backup-backup|synacktiv generator/i, 'a specific challenge artifact'],
+];
+function isAnswerLeak(text: string): { leak: boolean; why?: string } {
+ const s = String(text || '');
+ if (/\b[A-Za-z][A-Za-z0-9_]{1,19}\{[^}\n]{3,}\}/.test(s)) return { leak: true, why: 'contains a flag-shaped secret (wrapped {…}) — an answer, not a technique' };
+ if (/CANARY_SELF_FIT_[0-9a-fA-F]{4,}/.test(s)) return { leak: true, why: 'contains a self-fitting answer canary' };
+ const benchId = /\bXBEN[-_ ]?\d{2,}\b/i.test(s) || /\bcyb(?:svc|sec|los)?_[a-z][a-z0-9_]{6,}\b/i.test(s);
+ const answerStmt = /\b(?:the )?(?:flag|password|secret|admin[\s-]?password)\s*(?:is|was|=|:)\s*[`'"\w/${]/i.test(s) || /\bpayload\s*(?:is|was|=|:)\s*[`'"]/i.test(s) || /\bsolution\s*(?:is|was|=|:)\s*[`'"]/i.test(s);
+ if (benchId && answerStmt) return { leak: true, why: 'pairs a challenge id with a concrete answer — a memorized per-challenge solution (fitting)' };
+ return { leak: false };
+}
+export function isFittingTell(text: string): { tell: boolean; why?: string } {
+ const leak = isAnswerLeak(text);
+ if (leak.leak) return { tell: true, why: leak.why };
+ for (const [rx, why] of FORBIDDEN_TELLS) if (rx.test(String(text || ''))) return { tell: true, why };
+ return { tell: false };
+}
+
+export interface PromptSuggestion {
+ anchor: string; currentText: string; proposedText: string; rationale: string;
+ severity: 'high' | 'medium' | 'low'; fittingCheck: 'pass' | 'REJECTED'; fittingWhy?: string;
+}
+export interface PromptAdvice { summary: string; suggestions: PromptSuggestion[]; rejected: number; }
+
+const SUGGEST_SYSTEM_PROMPT = `You are the ADMIRAL acting as a PROMPT COACH for one of t3mp3st's operator agents. The operator ran and struggled. Given its CURRENT system prompt and a FAILURE SIGNAL (what went wrong), propose targeted edits that make it a BETTER GENERAL OPERATOR.
+
+HARD RULES — the project's integrity moat; violate them and the suggestion is discarded:
+- Propose ONLY GENERAL METHODOLOGY / TRADECRAFT improvements (e.g. "sweep the whole vuln family before refuting it", "read your own error output before mutating", "verify the flag is present in real tool output").
+- NEVER include a challenge-specific tell: no CVE id, flag path, literal flag value, version-pinned product, specific endpoint/host/port, or a challenge id paired with an answer. Those are FITTING and get rejected.
+- You improve the operator's GENERAL skill, never memorize one target's solution.
+
+Anchor each suggestion to an exact snippet of the CURRENT prompt. Reply with STRICT JSON only:
+{"summary":"<1-2 sentences>","suggestions":[{"anchor":"","currentText":"","proposedText":"","rationale":"","severity":"high|medium|low"}]}
+Propose 2-6 high-value suggestions; if the prompt is already strong, say so and return few/none.`;
+
+export class Admiral {
+ private llm: LLMBackbone;
+ constructor(llm: LLMBackbone) { this.llm = llm; }
+
+ /**
+ * One conversational turn. `messages` is the full prior dialogue (user/assistant).
+ * Returns the Admiral's reply plus the running mission brief + readiness.
+ */
+ async converse(messages: ChatMsg[]): Promise {
+ const llmMessages = [
+ { role: 'system' as const, content: ADMIRAL_SYSTEM_PROMPT },
+ ...messages.map((m) => ({ role: m.role, content: m.content })),
+ ];
+ const res = await this.llm.chat(llmMessages, { maxTokens: 1024, temperature: 0.4 });
+ try {
+ return coerceTurn(extractJson(res.content || ''));
+ } catch {
+ // graceful fallback: keep the conversation alive even if the model didn't emit clean JSON
+ return {
+ reply: (res.content || '').slice(0, 600) || "Say that again — what's the target and what are we trying to prove?",
+ brief: emptyBrief(),
+ missing: ['objective', 'target', 'family', 'scope'],
+ ready: false,
+ };
+ }
+ }
+
+ /**
+ * Prompt-coach: analyze an operator's system prompt against a failure signal and propose
+ * line-anchored GENERAL improvements. Every suggestion runs through the anti-fitting gate;
+ * any challenge-specific tell is flagged REJECTED (the UI/auto-apply must skip it).
+ */
+ async suggest(operatorPrompt: string, failureSignal?: string): Promise {
+ const user = `CURRENT OPERATOR SYSTEM PROMPT:\n"""\n${String(operatorPrompt || '').slice(0, 8000)}\n"""\n\nFAILURE SIGNAL (what went wrong on the last run):\n"""\n${(failureSignal && failureSignal.trim()) ? failureSignal.slice(0, 6000) : 'No transcript provided — review for general weaknesses: gives up too early, ignores its own error/feedback, narrow vuln-family coverage, weak provenance/verification discipline.'}\n"""`;
+ const res = await this.llm.chat(
+ [{ role: 'system' as const, content: SUGGEST_SYSTEM_PROMPT }, { role: 'user' as const, content: user }],
+ { maxTokens: 2000, temperature: 0.3 },
+ );
+ let parsed: any = {};
+ try { parsed = extractJson(res.content || ''); } catch { parsed = { summary: (res.content || '').slice(0, 400), suggestions: [] }; }
+ const raw = Array.isArray(parsed.suggestions) ? parsed.suggestions : [];
+ let rejected = 0;
+ const suggestions: PromptSuggestion[] = raw.slice(0, 8).map((s: any) => {
+ const proposedText = String(s.proposedText || '');
+ const check = isFittingTell(`${proposedText} ${String(s.rationale || '')} ${String(s.currentText || '')}`);
+ if (check.tell) rejected++;
+ return {
+ anchor: String(s.anchor || '').slice(0, 200),
+ currentText: String(s.currentText || '').slice(0, 800),
+ proposedText: proposedText.slice(0, 800),
+ rationale: String(s.rationale || '').slice(0, 400),
+ severity: (['high', 'medium', 'low'].includes(s.severity) ? s.severity : 'medium') as 'high' | 'medium' | 'low',
+ fittingCheck: check.tell ? 'REJECTED' : 'pass',
+ fittingWhy: check.why,
+ };
+ });
+ return { summary: String(parsed.summary || 'Reviewed the operator prompt.').slice(0, 600), suggestions, rejected };
+ }
+}
+
+/** Map a completed brief into the Directive shape Op General consumes. */
+export function briefToDirective(brief: MissionBrief): Directive {
+ const opsec: OpsecLevel = brief.fidelity === 'live' ? 'covert' : 'silent';
+ return {
+ objective: brief.objective || `Engagement against ${brief.target}`,
+ constraints: `mission_family=${brief.family || 'unspecified'}; fidelity=${brief.fidelity}; ` +
+ (brief.fidelity === 'dry_run' ? 'DRY-RUN: plan only, no packets, no claimed findings.' : 'LIVE: authorized real packets within scope only.'),
+ scopeHints: [brief.target, brief.scope].filter(Boolean).join(' — '),
+ urgency: 'normal',
+ opsecPreference: opsec,
+ };
+}
+
+export const ADMIRAL_FAMILIES = FAMILIES;
diff --git a/src/agent/index.ts b/src/agent/index.ts
new file mode 100644
index 0000000..1dafb52
--- /dev/null
+++ b/src/agent/index.ts
@@ -0,0 +1,505 @@
+/**
+ * T3MP3ST Agent Loop
+ *
+ * ReAct (Reason + Act) agent that connects LLM tool calling to Arsenal execution.
+ * This is the core autonomous loop: the LLM decides which tool to use,
+ * Arsenal executes it, and results are fed back for the next decision.
+ */
+
+import { EventEmitter } from 'eventemitter3';
+import type { LLMBackbone } from '../llm/index.js';
+import type { Arsenal } from '../arsenal/index.js';
+import type {
+ LLMMessage,
+ LLMToolDefinition,
+ LLMToolCall,
+ ToolResult,
+ ToolFinding,
+ Severity,
+ Target,
+ Task,
+} from '../types/index.js';
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export interface AgentLoopOptions {
+ /** Max ReAct iterations before forcing a conclusion (default: 15) */
+ maxIterations?: number;
+ /** Max total tokens to spend (default: 50000) */
+ maxTokens?: number;
+ /** Tool categories to expose (default: all) */
+ toolCategories?: string[];
+ /** Whether to include detailed tool output in context (default: true) */
+ verboseToolOutput?: boolean;
+ /** Max characters per tool result before truncation (default: 4000) */
+ maxToolOutputLength?: number;
+}
+
+export interface AgentStep {
+ iteration: number;
+ type: 'tool_call' | 'observation' | 'reasoning';
+ toolName?: string;
+ toolArgs?: Record;
+ toolResult?: ToolResult;
+ content?: string;
+ timestamp: number;
+}
+
+export interface AgentResult {
+ success: boolean;
+ /** Final summary from the LLM */
+ summary: string;
+ /** All steps taken during execution */
+ steps: AgentStep[];
+ /** Findings extracted from tool results */
+ findings: ToolFinding[];
+ /** Total iterations used */
+ iterations: number;
+ /** Total tokens consumed */
+ tokensUsed: number;
+ /** Duration in ms */
+ durationMs: number;
+ /** Whether the agent hit the iteration limit */
+ hitLimit: boolean;
+}
+
+export interface AgentEvents {
+ 'agent:step': AgentStep;
+ 'agent:tool_call': { name: string; args: Record };
+ 'agent:tool_result': { name: string; result: ToolResult };
+ 'agent:thinking': { content: string };
+ 'agent:complete': AgentResult;
+ 'agent:error': { error: Error; step: number };
+}
+
+// =============================================================================
+// AGENT LOOP
+// =============================================================================
+
+export class AgentLoop extends EventEmitter {
+ private llm: LLMBackbone;
+ private arsenal: Arsenal;
+ private options: Required;
+
+ constructor(llm: LLMBackbone, arsenal: Arsenal, options?: AgentLoopOptions) {
+ super();
+ this.llm = llm;
+ this.arsenal = arsenal;
+ this.options = {
+ maxIterations: options?.maxIterations ?? 15,
+ maxTokens: options?.maxTokens ?? 50000,
+ toolCategories: options?.toolCategories ?? [],
+ verboseToolOutput: options?.verboseToolOutput ?? true,
+ maxToolOutputLength: options?.maxToolOutputLength ?? 4000,
+ };
+ }
+
+ /**
+ * Run the ReAct loop for a task
+ */
+ async run(
+ task: Task,
+ systemPrompt: string,
+ target?: Target,
+ sourceContext?: string
+ ): Promise {
+ const startTime = Date.now();
+ const steps: AgentStep[] = [];
+ const allFindings: ToolFinding[] = [];
+ let tokensUsed = 0;
+ let hitLimit = false;
+ // anti-stall: dedup identical tool calls + detect runs of no-new-findings (ported from the hunter)
+ const seenCalls = new Map();
+ let noProgress = 0;
+
+ // Get tool definitions from Arsenal
+ const toolDefs = this.arsenal.getToolDefinitions(
+ this.options.toolCategories.length > 0 ? this.options.toolCategories : undefined
+ );
+
+ // Build initial messages
+ const messages: LLMMessage[] = [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: this.buildTaskPrompt(task, target, toolDefs, sourceContext) },
+ ];
+
+ for (let i = 0; i < this.options.maxIterations; i++) {
+ try {
+ // Ask the LLM what to do next
+ const response = await this.llm.chatWithTools(messages, toolDefs, {
+ maxTokens: 4096,
+ temperature: 0.3, // Lower temperature for tool-using tasks
+ });
+
+ tokensUsed += response.usage?.totalTokens || 0;
+
+ // If the LLM wants to call tools
+ if (response.toolCalls?.length) {
+ // Record the assistant's reasoning
+ if (response.content) {
+ const thinkStep: AgentStep = {
+ iteration: i,
+ type: 'reasoning',
+ content: response.content,
+ timestamp: Date.now(),
+ };
+ steps.push(thinkStep);
+ this.emit('agent:thinking', { content: response.content });
+ }
+
+ // Add assistant message to context (with tool calls)
+ messages.push({
+ role: 'assistant',
+ content: response.content || '',
+ toolCalls: response.toolCalls,
+ });
+
+ // Execute each tool call
+ const findingsBefore = allFindings.length;
+ for (const toolCall of response.toolCalls) {
+ // ANTI-STALL: a byte-identical repeat is almost always wasted — steer instead of re-running.
+ const callHash = `${toolCall.name}:${JSON.stringify(toolCall.arguments || {})}`;
+ let toolStep: AgentStep;
+ if (seenCalls.has(callHash)) {
+ const dup: ToolResult = {
+ success: false,
+ error: `Duplicate call — you already ran ${toolCall.name} with these exact arguments. ` +
+ `Prior result: ${seenCalls.get(callHash)}. Do NOT repeat it — change the arguments, pick a different tool, or move to your final debrief.`,
+ };
+ toolStep = { iteration: i, type: 'tool_call', toolName: toolCall.name, toolArgs: toolCall.arguments, toolResult: dup, timestamp: Date.now() };
+ } else {
+ toolStep = await this.executeTool(toolCall, target, i);
+ seenCalls.set(callHash, String(toolStep.toolResult?.output || toolStep.toolResult?.error || 'no output').replace(/\s+/g, ' ').slice(0, 160));
+ }
+ steps.push(toolStep);
+
+ // Collect findings — tag with REAL tool provenance (the output that backs them)
+ if (toolStep.toolResult?.findings) {
+ const out = String(toolStep.toolResult.output ?? '').slice(0, 4000);
+ for (const tf of toolStep.toolResult.findings) {
+ allFindings.push({ ...tf, provenance: 'tool', toolName: toolCall.name, toolOutput: out || tf.details });
+ }
+ }
+
+ // Build tool result content
+ const resultContent = this.formatToolResult(toolStep.toolResult);
+
+ // Add tool result to context
+ messages.push({
+ role: 'tool',
+ content: resultContent,
+ toolCallId: toolCall.id,
+ name: toolCall.name,
+ });
+ }
+
+ // ANTI-STALL: after a run of iterations with no new findings, steer (once) toward a new
+ // vector or a final debrief — don't let the agent grind the budget on a dead approach.
+ noProgress = allFindings.length > findingsBefore ? 0 : noProgress + 1;
+ if (noProgress >= 4 && i < this.options.maxIterations - 2) {
+ messages.push({
+ role: 'user',
+ content: '[System: 4 iterations with no new findings. Either pursue a GENUINELY different vector/tool/argument now, or produce your final debrief if the surface is exhausted. Do not keep repeating the current approach.]',
+ });
+ noProgress = 0;
+ }
+ } else {
+ // LLM finished reasoning — this is the final answer.
+ const finalStep: AgentStep = {
+ iteration: i,
+ type: 'reasoning',
+ content: response.content,
+ timestamp: Date.now(),
+ };
+ steps.push(finalStep);
+
+ // Parse the structured debrief block so the model's OWN analysis is captured —
+ // previously only tool-emitted findings were kept and the final report was dropped.
+ for (const f of this.parseFinalFindings(response.content || '')) {
+ if (!allFindings.some((x) => x.title === f.title)) allFindings.push(f);
+ }
+
+ const result: AgentResult = {
+ success: true,
+ summary: response.content,
+ steps,
+ findings: allFindings,
+ iterations: i + 1,
+ tokensUsed,
+ durationMs: Date.now() - startTime,
+ hitLimit: false,
+ };
+
+ this.emit('agent:complete', result);
+ return result;
+ }
+
+ // Check token budget
+ if (tokensUsed >= this.options.maxTokens) {
+ hitLimit = true;
+ break;
+ }
+ } catch (error) {
+ this.emit('agent:error', { error: error as Error, step: i });
+
+ // On error, try to continue with a reduced context
+ const errMsg = error instanceof Error ? error.message : String(error);
+ messages.push({
+ role: 'user',
+ content: `[System: Tool execution error: ${errMsg}. Please continue with available information or try a different approach.]`,
+ });
+ }
+ }
+
+ // Hit iteration or token limit — ask LLM for a final summary
+ hitLimit = true;
+ messages.push({
+ role: 'user',
+ content: 'You have reached the maximum number of steps. Please provide a final summary of everything you discovered, including all findings and recommendations.',
+ });
+
+ let summary = 'Agent reached iteration limit without producing a final summary.';
+ try {
+ const finalResponse = await this.llm.chat(messages, { maxTokens: 2048 });
+ summary = finalResponse.content;
+ tokensUsed += finalResponse.usage?.totalTokens || 0;
+ // capture the structured debrief from the limit-summary too (same contract as the clean finish)
+ for (const f of this.parseFinalFindings(summary)) {
+ if (!allFindings.some((x) => x.title === f.title)) allFindings.push(f);
+ }
+ } catch {
+ // Use what we have
+ }
+
+ const result: AgentResult = {
+ success: allFindings.length > 0 || steps.some(s => s.toolResult?.success),
+ summary,
+ steps,
+ findings: allFindings,
+ iterations: this.options.maxIterations,
+ tokensUsed,
+ durationMs: Date.now() - startTime,
+ hitLimit,
+ };
+
+ this.emit('agent:complete', result);
+ return result;
+ }
+
+ /**
+ * Execute a single tool call via the Arsenal
+ */
+ private async executeTool(
+ toolCall: LLMToolCall,
+ target: Target | undefined,
+ iteration: number
+ ): Promise {
+ this.emit('agent:tool_call', { name: toolCall.name, args: toolCall.arguments });
+
+ let toolResult: ToolResult;
+ try {
+ toolResult = await this.arsenal.execute(toolCall.name, {
+ target,
+ parameters: toolCall.arguments,
+ });
+ } catch (err) {
+ // A bad/hallucinated tool name must NOT crash the loop. Return the callable set so the
+ // model self-corrects in-place instead of dying or looping on a tool that doesn't exist.
+ const available = this.arsenal
+ .getToolDefinitions(this.options.toolCategories.length ? this.options.toolCategories : undefined)
+ .map((t) => t.name);
+ toolResult = {
+ success: false,
+ error: `Tool "${toolCall.name}" is not available. Callable tools this run: ${available.join(', ') || '(none)'}. ` +
+ `Use one of these EXACT names — do not invent tools; if none fit, describe the capability you need in prose.`,
+ };
+ }
+
+ this.emit('agent:tool_result', { name: toolCall.name, result: toolResult });
+
+ return {
+ iteration,
+ type: 'tool_call',
+ toolName: toolCall.name,
+ toolArgs: toolCall.arguments,
+ toolResult,
+ timestamp: Date.now(),
+ };
+ }
+
+ /**
+ * Build the initial task prompt with target context and prior intel
+ */
+ private buildTaskPrompt(task: Task, target?: Target, tools?: LLMToolDefinition[], sourceContext?: string): string {
+ const parts: string[] = [];
+
+ parts.push(`## MISSION TASK: ${task.name}`);
+ parts.push(`**Phase**: ${task.phase} | **Priority**: ${task.priority}/10`);
+ parts.push(`\n### Objective`);
+ parts.push(task.description);
+
+ // White-box source excerpt (security-prioritized) — provided by the large-repo
+ // analysis pipeline. Optional + backward-compatible: absent/empty keeps the
+ // original black-box prompt. When present, give the model the real source so it
+ // can ground findings in code rather than probing blind.
+ if (sourceContext && sourceContext.trim().length > 0) {
+ parts.push(`\n### White-box source (security-prioritized excerpt)`);
+ parts.push(`The following is a security-prioritized excerpt of the target's own source code. Use it to locate and confirm vulnerabilities against the actual implementation — but only report a finding as verified when a tool result backs it.`);
+ parts.push(sourceContext);
+ }
+
+ if (target) {
+ parts.push(`\n### Target Intelligence`);
+ parts.push(`- **Address**: ${target.address}`);
+ parts.push(`- **Type**: ${target.type} | **Zone**: ${target.zone} | **Status**: ${target.status}`);
+
+ // Prior recon intel: discovered services
+ if (target.services?.length) {
+ parts.push(`\n**Discovered Services** (from prior recon):`);
+ for (const svc of target.services) {
+ const version = svc.version ? ` v${svc.version}` : '';
+ const banner = svc.banner ? ` [${svc.banner.slice(0, 60)}]` : '';
+ parts.push(` - ${svc.port}/${svc.protocol}: ${svc.name}${version}${banner}`);
+ }
+ parts.push(`Use this intel to focus your efforts. These are confirmed services.`);
+ }
+
+ // Prior vuln intel: discovered vulnerabilities
+ if (target.vulnerabilities?.length) {
+ parts.push(`\n**Known Vulnerabilities** (from prior scanning):`);
+ for (const vuln of target.vulnerabilities.slice(0, 10)) {
+ const cves = vuln.cve?.length ? ` (${vuln.cve.join(', ')})` : '';
+ parts.push(` - [${vuln.severity.toUpperCase()}] ${vuln.name}${cves}`);
+ if (vuln.description) parts.push(` ${vuln.description.slice(0, 200)}`);
+ }
+ if (target.vulnerabilities.length > 10) {
+ parts.push(` ... and ${target.vulnerabilities.length - 10} more`);
+ }
+ parts.push(`Use this intel to prioritize your testing.`);
+ }
+
+ // Prior credential intel
+ if (target.credentials?.length) {
+ parts.push(`\n**Harvested Credentials** (from prior phases):`);
+ for (const cred of target.credentials.slice(0, 5)) {
+ parts.push(` - ${cred.username || 'unknown'}:*** (${cred.type}) via ${cred.source}`);
+ }
+ }
+ }
+
+ if (tools?.length) {
+ parts.push(`\n### Arsenal (${tools.length} tools available)`);
+ parts.push(`You have the following tools available via function calling. Select the most effective tool for each step of your assessment:`);
+ // Group tools by rough function
+ const toolList = tools.map(t => ` - **${t.name}**: ${t.description}`);
+ parts.push(toolList.join('\n'));
+ parts.push(`\n### Standing Orders`);
+ parts.push(`1. Call tools via function calling — do NOT fabricate results`);
+ parts.push(`2. Analyze each result before deciding the next action`);
+ parts.push(`3. Report findings immediately as you discover them`);
+ parts.push(`4. When finished, END your final message with a single fenced \`\`\`json block:\n` +
+ ` {"findings":[{"title":"…","severity":"critical|high|medium|low|info","details":"… cite the tool output that evidences it …","cvss":0.0,"cve":["…"],"remediation":"…"}],"abstained":false}\n` +
+ ` This block is the ONLY finding channel the harness records — anything described only in prose is dropped. Emit [] findings + "abstained":true if you found nothing real.`);
+ }
+
+ return parts.join('\n');
+ }
+
+ /**
+ * Format a tool result for the LLM context
+ */
+ private formatToolResult(result?: ToolResult): string {
+ if (!result) return 'No result returned.';
+
+ const parts: string[] = [];
+ parts.push(`Success: ${result.success}`);
+
+ if (result.output) {
+ let output = result.output;
+ // ALWAYS cap tool output (verbose just gets a larger budget). Previously this was gated on
+ // !verboseToolOutput, which defaults TRUE — so by default nothing was capped and large tool
+ // results flooded the context window. Keep HEAD+TAIL: flags/results often land at the end.
+ const cap = this.options.verboseToolOutput ? this.options.maxToolOutputLength * 4 : this.options.maxToolOutputLength;
+ if (output.length > cap) {
+ const head = Math.floor(cap * 0.7);
+ const tail = cap - head;
+ output = output.slice(0, head) + `\n… (truncated ${output.length - cap} chars) …\n` + output.slice(-tail);
+ }
+ parts.push(output);
+ }
+
+ if (result.error) {
+ parts.push(`Error: ${result.error}`);
+ }
+
+ if (result.findings?.length) {
+ parts.push('\nFindings:');
+ for (const f of result.findings) {
+ parts.push(` - [${f.severity.toUpperCase()}] ${f.title}: ${f.details}`);
+ }
+ }
+
+ return parts.join('\n');
+ }
+
+ /**
+ * Parse the model's final debrief block into structured findings. The operator is told to
+ * end its last message with a fenced ```json {findings:[…]} block; this is the ONLY prose→data
+ * channel the harness honors (no substring guessing). Returns [] if there's no valid block.
+ */
+ private parseFinalFindings(content: string): ToolFinding[] {
+ if (!content) return [];
+ const SEV = new Set(['critical', 'high', 'medium', 'low', 'info']);
+ const blocks = [...content.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)].map((m) => m[1]);
+ const candidates = blocks.length ? blocks.reverse() : [content];
+ for (const c of candidates) {
+ const start = c.indexOf('{');
+ const end = c.lastIndexOf('}');
+ if (start === -1 || end <= start) continue;
+ try {
+ const obj = JSON.parse(c.slice(start, end + 1));
+ if (!obj || !Array.isArray(obj.findings)) continue;
+ return obj.findings.filter((f: any) => f && f.title).map((f: any) => ({
+ title: String(f.title).slice(0, 200),
+ severity: (SEV.has(String(f.severity).toLowerCase()) ? String(f.severity).toLowerCase() : 'info') as Severity,
+ details: String(f.details ?? f.evidence ?? f.evidence_ref ?? '').slice(0, 4000),
+ cvss: typeof f.cvss === 'number' ? f.cvss : undefined,
+ cve: Array.isArray(f.cve) ? f.cve.map(String) : undefined,
+ remediation: f.remediation ? String(f.remediation) : undefined,
+ // Model-asserted in the debrief — NO tool provenance. The gate downgrades these.
+ provenance: 'model' as const,
+ }));
+ } catch { /* try the next candidate block */ }
+ }
+ return [];
+ }
+}
+
+// =============================================================================
+// FACTORY
+// =============================================================================
+
+export function createAgentLoop(
+ llm: LLMBackbone,
+ arsenal: Arsenal,
+ options?: AgentLoopOptions
+): AgentLoop {
+ return new AgentLoop(llm, arsenal, options);
+}
+
+/**
+ * Run a one-shot agent task — convenience function
+ */
+export async function runAgentTask(
+ llm: LLMBackbone,
+ arsenal: Arsenal,
+ task: Task,
+ systemPrompt: string,
+ target?: Target,
+ options?: AgentLoopOptions
+): Promise {
+ const agent = createAgentLoop(llm, arsenal, options);
+ return agent.run(task, systemPrompt, target);
+}
diff --git a/src/agent/local-agents.ts b/src/agent/local-agents.ts
new file mode 100644
index 0000000..50740a1
--- /dev/null
+++ b/src/agent/local-agents.ts
@@ -0,0 +1,287 @@
+// =============================================================================
+// LOCAL AGENT CONNECTORS — "bring your own already-authed agent"
+// =============================================================================
+// Detect + connect agent CLIs that already live, authenticated, on the operator's machine
+// (Claude Code, Codex, Hermes) and drive them headlessly as t3mp3st operators.
+//
+// SECURITY POSTURE (important):
+// - We NEVER read, print, log, or transmit credential contents. Auth is detected purely by the
+// PRESENCE of the CLI's own auth artifact (a file path or a macOS keychain item) — never its bytes.
+// - We do NOT enter or store any key. The CLIs are already logged in by the user; we only invoke them.
+// - A "ping"/"dispatch" spawns the user's own CLI as a child process with a prompt and captures its
+// stdout (the model's reply to OUR prompt — not secrets). Everything is local + user-initiated.
+// =============================================================================
+
+import { execFile, execFileSync, spawn } from 'child_process';
+import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs';
+import { homedir, tmpdir } from 'os';
+import { join } from 'path';
+
+// t3mp3st injects its OWN provider keys (from .env) into the server process. If we let those leak into
+// a spawned CLI, the CLI uses t3mp3st's key instead of the user's native login → 401. The entire point
+// of this feature is "use the agent you already authed", so we strip these before spawning so each CLI
+// falls back to its own auth (keychain / ~/.codex/auth.json / ~/.hermes/.env).
+const PROVIDER_ENV_TO_STRIP = [
+ 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL',
+ 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENAI_ORGANIZATION',
+ 'OPENROUTER_API_KEY',
+];
+
+const HOME = homedir();
+const expand = (p: string): string => (p.startsWith('~') ? HOME + p.slice(1) : p);
+
+export type LocalAgentId = 'claude' | 'codex' | 'hermes';
+
+interface AgentSpec {
+ id: LocalAgentId;
+ label: string;
+ vendor: string;
+ bin: string;
+ blurb: string;
+ /** how we drive it as a one-shot, non-interactive operator */
+ invokeHint: string;
+ versionArgs: string[];
+ parseVersion: (out: string) => string;
+ /** any-of: presence ⇒ authed. PRESENCE ONLY — contents are never read. */
+ authArtifacts: string[];
+ /** macOS keychain fallback service name */
+ keychainService?: string;
+ /** build the argv for a headless one-shot prompt */
+ oneShot: (prompt: string, model?: string) => string[];
+}
+
+const SPECS: AgentSpec[] = [
+ {
+ id: 'claude',
+ label: 'Claude Code',
+ vendor: 'Anthropic',
+ bin: 'claude',
+ blurb: 'Anthropic agentic CLI',
+ invokeHint: 'claude -p ""',
+ versionArgs: ['--version'],
+ parseVersion: (o) => (o.match(/[\d]+\.[\d]+(\.[\d]+)?/) || ['?'])[0],
+ authArtifacts: ['~/.claude/.credentials.json', '~/.claude.json'],
+ keychainService: 'Claude Code-credentials',
+ oneShot: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])],
+ },
+ {
+ id: 'codex',
+ label: 'Codex',
+ vendor: 'OpenAI',
+ bin: 'codex',
+ blurb: 'OpenAI Codex CLI',
+ invokeHint: 'codex exec ""',
+ versionArgs: ['--version'],
+ parseVersion: (o) => (o.match(/[\d]+\.[\d]+(\.[\d]+)?/) || ['?'])[0],
+ authArtifacts: ['~/.codex/auth.json', '~/.config/codex/auth.json'],
+ oneShot: (p, m) => ['exec', ...(m ? ['-m', m] : []), p],
+ },
+ {
+ id: 'hermes',
+ label: 'Hermes',
+ vendor: 'Hermes Agent',
+ bin: 'hermes',
+ blurb: 'Hermes Agent — tool-calling AI',
+ invokeHint: 'hermes -z "" (--yolo only if T3MP3ST_HERMES_YOLO=1)',
+ versionArgs: ['--version'],
+ parseVersion: (o) => (o.match(/v([\d]+\.[\d]+(\.[\d]+)?)/)?.[1]) || (o.match(/[\d]+\.[\d]+(\.[\d]+)?/) || ['?'])[0],
+ authArtifacts: ['~/.hermes/.env'],
+ oneShot: (p, m) => ['-z', p, ...(hermesYoloEnabled() ? ['--yolo'] : []), ...(m ? ['-m', m] : [])],
+ },
+];
+
+export function getSpec(id: string): AgentSpec | undefined {
+ return SPECS.find((s) => s.id === id);
+}
+
+/**
+ * B-06 — Hermes '--yolo' auto-approves EVERY tool call with no confirmation:
+ * powerful but dangerous (unattended command exec with no gate). It is OFF by
+ * default; the operator must explicitly opt in with T3MP3ST_HERMES_YOLO=1. Safe
+ * mode (the default) runs Hermes without the flag so it honors its own approval
+ * prompts. Applies to both the one-shot backbone call and the chat path.
+ */
+export function hermesYoloEnabled(): boolean {
+ return /^(1|true|yes|on)$/i.test((process.env.T3MP3ST_HERMES_YOLO || '').trim());
+}
+
+export interface AgentDetection {
+ id: LocalAgentId;
+ label: string;
+ vendor: string;
+ bin: string;
+ blurb: string;
+ invokeHint: string;
+ installed: boolean;
+ path?: string;
+ version?: string;
+ authed: boolean;
+ authMethod?: string; // e.g. "file" or "keychain" — never the secret itself
+ ready: boolean; // installed && authed
+}
+
+function resolvePath(bin: string): string | undefined {
+ try {
+ return execFileSync('command', ['-v', bin], { shell: '/bin/bash', encoding: 'utf8' }).trim() || undefined;
+ } catch {
+ try { return execFileSync('which', [bin], { encoding: 'utf8' }).trim() || undefined; } catch { return undefined; }
+ }
+}
+
+function authState(spec: AgentSpec): { authed: boolean; method?: string } {
+ for (const a of spec.authArtifacts) {
+ if (existsSync(expand(a))) return { authed: true, method: 'file' };
+ }
+ if (spec.keychainService) {
+ try {
+ execFileSync('security', ['find-generic-password', '-s', spec.keychainService], { stdio: 'ignore' });
+ return { authed: true, method: 'keychain' };
+ } catch { /* not in keychain */ }
+ }
+ return { authed: false };
+}
+
+function detectOne(spec: AgentSpec): Promise {
+ const base = {
+ id: spec.id, label: spec.label, vendor: spec.vendor, bin: spec.bin,
+ blurb: spec.blurb, invokeHint: spec.invokeHint,
+ };
+ return new Promise((resolve) => {
+ execFile(spec.bin, spec.versionArgs, { timeout: 8000 }, (err, stdout) => {
+ if (err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
+ resolve({ ...base, installed: false, authed: false, ready: false });
+ return;
+ }
+ // even a non-zero exit but no ENOENT means the binary exists
+ const version = spec.parseVersion(String(stdout || ''));
+ const auth = authState(spec);
+ resolve({
+ ...base,
+ installed: true,
+ path: resolvePath(spec.bin),
+ version,
+ authed: auth.authed,
+ authMethod: auth.method,
+ ready: auth.authed,
+ });
+ });
+ });
+}
+
+/** Detect every known local agent CLI (installed? authed? ready?). No tokens spent. */
+export async function detectLocalAgents(): Promise {
+ // Test/CI hook: T3MP3ST_DISABLE_LOCAL_AGENTS=1 forces a backbone-less server (skip
+ // local-agent auto-detection) so key-required / fail-closed paths can be exercised
+ // deterministically — used by scripts/arsenal-smoke.mjs for a reproducible run.
+ if (/^(1|true|yes|on)$/i.test((process.env.T3MP3ST_DISABLE_LOCAL_AGENTS || '').trim())) return [];
+ return Promise.all(SPECS.map(detectOne));
+}
+
+export interface AgentRunResult {
+ ok: boolean;
+ latencyMs: number;
+ output: string;
+ error?: string;
+}
+
+/**
+ * Drive a connected agent with a one-shot headless prompt (real round-trip — SPENDS the agent's quota).
+ * Used by /ping (liveness proof) and /dispatch (actually using the operator).
+ */
+export function runLocalAgent(
+ id: string,
+ prompt: string,
+ opts: { model?: string; timeoutMs?: number; maxChars?: number } = {},
+): Promise {
+ const spec = getSpec(id);
+ const t0 = Date.now();
+ if (!spec) return Promise.resolve({ ok: false, latencyMs: 0, output: '', error: `unknown agent: ${id}` });
+ const args = spec.oneShot(prompt, opts.model);
+ const timeoutMs = opts.timeoutMs ?? 120000;
+ const maxChars = opts.maxChars ?? 4000;
+ // child env = ours minus the injected provider keys, so the CLI uses its OWN native login.
+ const env: NodeJS.ProcessEnv = { ...process.env };
+ for (const k of PROVIDER_ENV_TO_STRIP) delete env[k];
+ return new Promise((resolve) => {
+ // stdin:'ignore' so the agent doesn't stall waiting on piped input (e.g. `claude -p`'s 3s stdin wait).
+ const child = spawn(spec.bin, args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
+ let out = '';
+ let errOut = '';
+ let done = false;
+ const finish = (r: AgentRunResult): void => { if (!done) { done = true; clearTimeout(timer); resolve(r); } };
+ const timer = setTimeout(() => {
+ try { child.kill('SIGKILL'); } catch { /* noop */ }
+ finish({ ok: false, latencyMs: Date.now() - t0, output: out.trim().slice(0, maxChars), error: `timed out after ${timeoutMs}ms` });
+ }, timeoutMs);
+ child.stdout?.on('data', (d) => { if (out.length < 1_000_000) out += String(d); });
+ child.stderr?.on('data', (d) => { if (errOut.length < 100_000) errOut += String(d); });
+ child.on('error', (e) => finish({ ok: false, latencyMs: Date.now() - t0, output: '', error: (e as Error).message }));
+ child.on('close', (code) => {
+ const latencyMs = Date.now() - t0;
+ const output = out.trim().slice(0, maxChars);
+ if (code === 0) finish({ ok: true, latencyMs, output });
+ else finish({ ok: false, latencyMs, output, error: (errOut.trim() || `exited with code ${code}`).slice(0, 300) });
+ });
+ });
+}
+
+/** A cheap liveness probe — asks the agent to echo a token. SPENDS a tiny bit of the agent's quota. */
+export function pingLocalAgent(id: string, prompt?: string, timeoutMs?: number): Promise {
+ return runLocalAgent(id, prompt || 'Reply with exactly the single word: PONG', {
+ timeoutMs: timeoutMs ?? 90000,
+ maxChars: 400,
+ });
+}
+
+/**
+ * Drive a connected agent as the LLM BACKEND for the mission/operator flow — a long-prompt one-shot
+ * that returns ONLY the model's reply text (no CLI banner). Per-agent invocation: claude/codex feed
+ * the prompt via STDIN (robust for long planning prompts), codex uses --output-last-message for a
+ * clean reply, hermes takes the prompt as an arg. Provider keys are stripped so each CLI uses its own
+ * login (no API key needed). Throws on non-zero exit / timeout so the LLMBackbone retry/fallback fires.
+ */
+export function localAgentChat(id: string, prompt: string, opts: { model?: string; timeoutMs?: number } = {}): Promise {
+ const spec = getSpec(id);
+ if (!spec) return Promise.reject(new Error(`unknown local agent: ${id}`));
+ const env: NodeJS.ProcessEnv = { ...process.env };
+ for (const k of PROVIDER_ENV_TO_STRIP) delete env[k];
+ const model = opts.model && opts.model !== 'codex-default' && opts.model !== id ? opts.model : undefined;
+ const timeoutMs = opts.timeoutMs ?? 240000;
+
+ let args: string[];
+ let viaStdin = true;
+ let outFile: string | null = null;
+ let workDir: string | null = null;
+ if (id === 'claude') {
+ args = ['-p', '--output-format', 'text', ...(model ? ['--model', model] : [])];
+ } else if (id === 'codex') {
+ workDir = mkdtempSync(join(tmpdir(), 't3mp3st-codexllm-'));
+ outFile = join(workDir, 'reply.txt');
+ args = ['exec', '--skip-git-repo-check', '--color', 'never', '--sandbox', 'read-only', '--output-last-message', outFile, ...(model ? ['-m', model] : [])];
+ } else { // hermes — takes the prompt as an arg
+ args = ['-z', prompt, ...(hermesYoloEnabled() ? ['--yolo'] : []), ...(model ? ['-m', model] : [])];
+ viaStdin = false;
+ }
+
+ const cleanup = () => { if (workDir) { try { rmSync(workDir, { recursive: true, force: true }); } catch { /* noop */ } } };
+ return new Promise((resolve, reject) => {
+ const child = spawn(spec.bin, args, { env, stdio: [viaStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
+ let out = '';
+ let errOut = '';
+ let done = false;
+ const finish = (fn: () => void) => { if (!done) { done = true; clearTimeout(timer); cleanup(); fn(); } };
+ const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* noop */ } finish(() => reject(new Error(`${id} timed out after ${timeoutMs}ms`))); }, timeoutMs);
+ child.stdout?.on('data', (d) => { if (out.length < 8_000_000) out += String(d); });
+ child.stderr?.on('data', (d) => { if (errOut.length < 200_000) errOut += String(d); });
+ child.on('error', (e) => finish(() => reject(e)));
+ child.on('close', (code) => {
+ let content = out.trim();
+ if (outFile) { try { content = (readFileSync(outFile, 'utf8').trim() || content); } catch { /* fall back to stdout */ } }
+ finish(() => {
+ if (code === 0 && content) resolve(content);
+ else reject(new Error((errOut.trim() || content || `exited with code ${code}`).slice(0, 800)));
+ });
+ });
+ if (viaStdin && child.stdin) { child.stdin.write(prompt); child.stdin.end(); }
+ });
+}
diff --git a/src/analysis/index.ts b/src/analysis/index.ts
new file mode 100644
index 0000000..462f1c7
--- /dev/null
+++ b/src/analysis/index.ts
@@ -0,0 +1,387 @@
+/**
+ * T3MP3ST Analysis Engine
+ *
+ * Report generation and findings analysis.
+ */
+
+import type {
+ Report,
+ Finding,
+ ExecutiveSummary,
+ AttackPath,
+ Recommendation,
+ Severity,
+} from '../types/index.js';
+import type { EvidenceVault } from '../evidence/index.js';
+import type { TargetEnvironment } from '../target/index.js';
+import type { MissionControl } from '../mission/index.js';
+import type { OpsecController } from '../opsec/index.js';
+import { randomUUID } from 'crypto';
+import { SEVERITY_SCORES } from '../evidence/index.js';
+
+// =============================================================================
+// ANALYSIS ENGINE
+// =============================================================================
+
+export class AnalysisEngine {
+ private vault: EvidenceVault;
+ private targetEnv: TargetEnvironment;
+
+ constructor(
+ vault: EvidenceVault,
+ targetEnv: TargetEnvironment,
+ _mission: MissionControl,
+ _opsec: OpsecController
+ ) {
+ this.vault = vault;
+ this.targetEnv = targetEnv;
+ // mission and opsec are available for future threat modeling and risk analysis
+ }
+
+ /**
+ * Generate a report
+ */
+ generateReport(
+ missionId: string,
+ type: Report['type'] = 'full_report'
+ ): Report {
+ const findings = this.vault.getAllFindings();
+ const targetStats = this.targetEnv.getStats();
+ const vaultStats = this.vault.getStats();
+
+ const summary = this.generateExecutiveSummary(findings, targetStats, vaultStats);
+ const attackPaths = this.generateAttackPaths(findings);
+ const recommendations = this.generateRecommendations(findings);
+
+ return {
+ id: randomUUID(),
+ missionId,
+ type,
+ generatedAt: Date.now(),
+ summary,
+ findings,
+ attackPaths,
+ recommendations,
+ };
+ }
+
+ /**
+ * Generate executive summary
+ */
+ private generateExecutiveSummary(
+ findings: Finding[],
+ targetStats: ReturnType,
+ vaultStats: ReturnType
+ ): ExecutiveSummary {
+ // Determine overall risk rating
+ let riskRating: Severity = 'info';
+ if (vaultStats.bySeverity.critical > 0) riskRating = 'critical';
+ else if (vaultStats.bySeverity.high > 0) riskRating = 'high';
+ else if (vaultStats.bySeverity.medium > 0) riskRating = 'medium';
+ else if (vaultStats.bySeverity.low > 0) riskRating = 'low';
+
+ const overview = this.generateOverviewText(findings, targetStats, riskRating);
+
+ return {
+ overview,
+ riskRating,
+ criticalFindings: vaultStats.bySeverity.critical,
+ highFindings: vaultStats.bySeverity.high,
+ mediumFindings: vaultStats.bySeverity.medium,
+ lowFindings: vaultStats.bySeverity.low,
+ infoFindings: vaultStats.bySeverity.info,
+ successfulExploits: findings.filter(f => f.exploitedAt).length,
+ credentialsHarvested: vaultStats.totalCredentials,
+ systemsCompromised: targetStats.owned,
+ };
+ }
+
+ /**
+ * Generate overview text
+ */
+ private generateOverviewText(
+ findings: Finding[],
+ targetStats: ReturnType,
+ riskRating: Severity
+ ): string {
+ const totalFindings = findings.length;
+ const criticalHigh = findings.filter(
+ f => f.severity === 'critical' || f.severity === 'high'
+ ).length;
+
+ if (totalFindings === 0) {
+ return 'No security vulnerabilities were identified during this assessment.';
+ }
+
+ let overview = `During the security assessment, ${totalFindings} finding${totalFindings === 1 ? ' was' : 's were'} identified`;
+
+ if (criticalHigh > 0) {
+ overview += `, including ${criticalHigh} critical or high-severity issue${criticalHigh === 1 ? '' : 's'} that require immediate attention`;
+ }
+
+ overview += `. The overall risk rating for this assessment is ${riskRating.toUpperCase()}.`;
+
+ if (targetStats.owned > 0) {
+ overview += ` ${targetStats.owned} system${targetStats.owned === 1 ? ' was' : 's were'} successfully compromised during testing.`;
+ }
+
+ return overview;
+ }
+
+ /**
+ * Generate attack paths from findings
+ */
+ private generateAttackPaths(findings: Finding[]): AttackPath[] {
+ const paths: AttackPath[] = [];
+
+ // Group findings by target
+ const byTarget = new Map();
+ for (const finding of findings) {
+ const existing = byTarget.get(finding.targetId) || [];
+ existing.push(finding);
+ byTarget.set(finding.targetId, existing);
+ }
+
+ // Create attack paths for targets with multiple findings
+ for (const [targetId, targetFindings] of byTarget) {
+ if (targetFindings.length >= 2) {
+ const sortedFindings = targetFindings.sort(
+ (a, b) => a.discoveredAt - b.discoveredAt
+ );
+
+ const maxSeverity = sortedFindings.reduce((max, f) => {
+ return SEVERITY_SCORES[f.severity] > SEVERITY_SCORES[max]
+ ? f.severity
+ : max;
+ }, 'info' as Severity);
+
+ paths.push({
+ id: randomUUID(),
+ name: `Attack Chain - ${targetId.substring(0, 8)}`,
+ description: `Attack path through ${targetFindings.length} findings on target`,
+ steps: sortedFindings.map(
+ f => `${f.title} (${f.severity})`
+ ),
+ findings: sortedFindings.map(f => f.id),
+ impactLevel: maxSeverity,
+ });
+ }
+ }
+
+ return paths;
+ }
+
+ /**
+ * Generate recommendations from findings
+ */
+ private generateRecommendations(findings: Finding[]): Recommendation[] {
+ const recommendations: Recommendation[] = [];
+
+ // Group by severity for prioritization
+ const criticalFindings = findings.filter(f => f.severity === 'critical');
+ const highFindings = findings.filter(f => f.severity === 'high');
+ const mediumFindings = findings.filter(f => f.severity === 'medium');
+
+ // Critical findings get immediate priority
+ for (const finding of criticalFindings) {
+ recommendations.push({
+ id: randomUUID(),
+ findingId: finding.id,
+ priority: 'immediate',
+ title: `Remediate: ${finding.title}`,
+ description: finding.remediation || `Address the critical vulnerability: ${finding.description}`,
+ effort: this.estimateEffort(finding),
+ impact: 'high',
+ });
+ }
+
+ // High findings get short-term priority
+ for (const finding of highFindings) {
+ recommendations.push({
+ id: randomUUID(),
+ findingId: finding.id,
+ priority: 'short_term',
+ title: `Remediate: ${finding.title}`,
+ description: finding.remediation || `Address the high-severity vulnerability: ${finding.description}`,
+ effort: this.estimateEffort(finding),
+ impact: 'high',
+ });
+ }
+
+ // Medium findings get long-term priority
+ for (const finding of mediumFindings) {
+ recommendations.push({
+ id: randomUUID(),
+ findingId: finding.id,
+ priority: 'long_term',
+ title: `Address: ${finding.title}`,
+ description: finding.remediation || `Mitigate the medium-severity issue: ${finding.description}`,
+ effort: this.estimateEffort(finding),
+ impact: 'medium',
+ });
+ }
+
+ return recommendations;
+ }
+
+ /**
+ * Estimate remediation effort
+ */
+ private estimateEffort(finding: Finding): 'low' | 'medium' | 'high' {
+ // Simple heuristic based on severity and evidence complexity
+ if (finding.severity === 'critical') return 'high';
+ if (finding.severity === 'high') return 'medium';
+ if (finding.evidence.length > 3) return 'medium';
+ return 'low';
+ }
+
+ /**
+ * Export report to Markdown
+ */
+ exportToMarkdown(report: Report): string {
+ const lines: string[] = [];
+
+ // Header
+ lines.push('# Security Assessment Report');
+ lines.push('');
+ lines.push(`**Generated:** ${new Date(report.generatedAt).toISOString()}`);
+ lines.push(`**Report Type:** ${report.type}`);
+ lines.push('');
+
+ // Executive Summary
+ lines.push('## Executive Summary');
+ lines.push('');
+ lines.push(report.summary.overview);
+ lines.push('');
+ lines.push('### Risk Overview');
+ lines.push('');
+ lines.push(`| Severity | Count |`);
+ lines.push(`|----------|-------|`);
+ lines.push(`| Critical | ${report.summary.criticalFindings} |`);
+ lines.push(`| High | ${report.summary.highFindings} |`);
+ lines.push(`| Medium | ${report.summary.mediumFindings} |`);
+ lines.push(`| Low | ${report.summary.lowFindings} |`);
+ lines.push(`| Info | ${report.summary.infoFindings} |`);
+ lines.push('');
+
+ // Key Metrics
+ lines.push('### Key Metrics');
+ lines.push('');
+ lines.push(`- **Overall Risk Rating:** ${report.summary.riskRating.toUpperCase()}`);
+ lines.push(`- **Successful Exploits:** ${report.summary.successfulExploits}`);
+ lines.push(`- **Credentials Harvested:** ${report.summary.credentialsHarvested}`);
+ lines.push(`- **Systems Compromised:** ${report.summary.systemsCompromised}`);
+ lines.push('');
+
+ // Findings
+ lines.push('## Findings');
+ lines.push('');
+
+ const sortedFindings = [...report.findings].sort(
+ (a, b) => SEVERITY_SCORES[b.severity] - SEVERITY_SCORES[a.severity]
+ );
+
+ for (const finding of sortedFindings) {
+ lines.push(`### ${finding.title}`);
+ lines.push('');
+ lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
+ if (finding.cvss) lines.push(`**CVSS:** ${finding.cvss}`);
+ if (finding.cve?.length) lines.push(`**CVE:** ${finding.cve.join(', ')}`);
+ lines.push('');
+ lines.push('**Description:**');
+ lines.push(finding.description);
+ lines.push('');
+
+ if (finding.remediation) {
+ lines.push('**Remediation:**');
+ lines.push(finding.remediation);
+ lines.push('');
+ }
+
+ if (finding.evidence.length > 0) {
+ lines.push('**Evidence:**');
+ for (const evidence of finding.evidence) {
+ lines.push(`- ${evidence.type}: \`${evidence.content.substring(0, 100)}${evidence.content.length > 100 ? '...' : ''}\``);
+ }
+ lines.push('');
+ }
+ }
+
+ // Attack Paths
+ if (report.attackPaths.length > 0) {
+ lines.push('## Attack Paths');
+ lines.push('');
+
+ for (const path of report.attackPaths) {
+ lines.push(`### ${path.name}`);
+ lines.push('');
+ lines.push(`**Impact Level:** ${path.impactLevel.toUpperCase()}`);
+ lines.push('');
+ lines.push(path.description);
+ lines.push('');
+ lines.push('**Steps:**');
+ for (let i = 0; i < path.steps.length; i++) {
+ lines.push(`${i + 1}. ${path.steps[i]}`);
+ }
+ lines.push('');
+ }
+ }
+
+ // Recommendations
+ if (report.recommendations.length > 0) {
+ lines.push('## Recommendations');
+ lines.push('');
+
+ const byPriority = {
+ immediate: report.recommendations.filter(r => r.priority === 'immediate'),
+ short_term: report.recommendations.filter(r => r.priority === 'short_term'),
+ long_term: report.recommendations.filter(r => r.priority === 'long_term'),
+ };
+
+ if (byPriority.immediate.length > 0) {
+ lines.push('### Immediate Priority');
+ lines.push('');
+ for (const rec of byPriority.immediate) {
+ lines.push(`- **${rec.title}** (Effort: ${rec.effort})`);
+ lines.push(` ${rec.description}`);
+ }
+ lines.push('');
+ }
+
+ if (byPriority.short_term.length > 0) {
+ lines.push('### Short-Term Priority');
+ lines.push('');
+ for (const rec of byPriority.short_term) {
+ lines.push(`- **${rec.title}** (Effort: ${rec.effort})`);
+ lines.push(` ${rec.description}`);
+ }
+ lines.push('');
+ }
+
+ if (byPriority.long_term.length > 0) {
+ lines.push('### Long-Term Priority');
+ lines.push('');
+ for (const rec of byPriority.long_term) {
+ lines.push(`- **${rec.title}** (Effort: ${rec.effort})`);
+ lines.push(` ${rec.description}`);
+ }
+ lines.push('');
+ }
+ }
+
+ return lines.join('\n');
+ }
+}
+
+// =============================================================================
+// FACTORY FUNCTIONS
+// =============================================================================
+
+export function createAnalysisEngine(
+ vault: EvidenceVault,
+ targetEnv: TargetEnvironment,
+ mission: MissionControl,
+ opsec: OpsecController
+): AnalysisEngine {
+ return new AnalysisEngine(vault, targetEnv, mission, opsec);
+}
diff --git a/src/arsenal/catalog.ts b/src/arsenal/catalog.ts
new file mode 100644
index 0000000..e14da0b
--- /dev/null
+++ b/src/arsenal/catalog.ts
@@ -0,0 +1,875 @@
+import type { MissionFamily } from '../resources/index.js';
+
+export type ToolCategory =
+ | 'core'
+ | 'osint'
+ | 'dns'
+ | 'network'
+ | 'web'
+ | 'api'
+ | 'vulnerability'
+ | 'supply_chain'
+ | 'cloud'
+ | 'container'
+ | 'ai_agent'
+ | 'smart_contract'
+ | 'crypto'
+ | 'fuzzing'
+ | 'mobile'
+ | 'secrets'
+ | 'reverse'
+ | 'forensics'
+ | 'credentials'
+ | 'post_exploitation'
+ | 'reporting';
+
+export type ToolRisk = 'local_read' | 'passive' | 'active' | 'intrusive' | 'credential' | 'dangerous';
+export type ToolExecutionMode = 'safe_command' | 'receipt_required' | 'import_only' | 'catalog_only';
+
+export interface ToolAdapter {
+ id: string;
+ binary: string;
+ name: string;
+ category: ToolCategory;
+ families: MissionFamily[];
+ risk: ToolRisk;
+ execution: ToolExecutionMode;
+ networked: boolean;
+ evidenceKinds: string[];
+ outputFormats: string[];
+ installHint: string;
+ commandHint: string;
+ parserStatus: 'structured' | 'text' | 'planned';
+ notes: string;
+}
+
+const WEB_FAMILIES: MissionFamily[] = ['web_api', 'reporting_remediation'];
+const SUPPLY_FAMILIES: MissionFamily[] = ['code_supply_chain', 'reporting_remediation'];
+const CLOUD_FAMILIES: MissionFamily[] = ['cloud_infra', 'reporting_remediation'];
+const AI_FAMILIES: MissionFamily[] = ['ai_red_team', 'agent_warfare', 'reporting_remediation'];
+const SMART_CONTRACT_FAMILIES: MissionFamily[] = ['smart_contract', 'crypto_secrets', 'code_supply_chain', 'reporting_remediation'];
+const CRYPTO_FAMILIES: MissionFamily[] = ['crypto_secrets', 'code_supply_chain', 'reporting_remediation'];
+const REVERSE_FAMILIES: MissionFamily[] = ['reverse_binary', 'code_supply_chain', 'reporting_remediation'];
+
+export const FRONTIER_ARSENAL_MILESTONE = 104;
+
+export const TOOL_ADAPTERS: ToolAdapter[] = [
+ {
+ id: 'file',
+ binary: 'file',
+ name: 'file',
+ category: 'core',
+ families: ['code_supply_chain', 'reverse_binary', 'reporting_remediation'],
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['artifact_fingerprint'],
+ outputFormats: ['text'],
+ installHint: 'Usually preinstalled on Unix-like systems.',
+ commandHint: 'file package.json',
+ parserStatus: 'text',
+ notes: 'Harmless local artifact identification used by smoke tests and ledgers.',
+ },
+ {
+ id: 'curl',
+ binary: 'curl',
+ name: 'curl',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['http_request', 'http_response', 'header_probe'],
+ outputFormats: ['text', 'headers'],
+ installHint: 'Usually preinstalled on macOS and Linux.',
+ commandHint: 'curl -I https://example.com',
+ parserStatus: 'text',
+ notes: 'General HTTP probe. External targets require ScopeGuard receipts.',
+ },
+ {
+ id: 'dig',
+ binary: 'dig',
+ name: 'dig',
+ category: 'dns',
+ families: ['web_api', 'cloud_infra', 'social_osint', 'reporting_remediation'],
+ risk: 'passive',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['dns_record'],
+ outputFormats: ['text'],
+ installHint: 'Install bind utilities, e.g. `brew install bind` or OS package equivalent.',
+ commandHint: 'dig +short example.com',
+ parserStatus: 'text',
+ notes: 'DNS record collection for scoped targets.',
+ },
+ {
+ id: 'host',
+ binary: 'host',
+ name: 'host',
+ category: 'dns',
+ families: ['web_api', 'cloud_infra', 'social_osint'],
+ risk: 'passive',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['dns_record'],
+ outputFormats: ['text'],
+ installHint: 'Install bind utilities, e.g. `brew install bind` or OS package equivalent.',
+ commandHint: 'host example.com',
+ parserStatus: 'text',
+ notes: 'Simple DNS lookup companion to dig.',
+ },
+ {
+ id: 'whois',
+ binary: 'whois',
+ name: 'whois',
+ category: 'osint',
+ families: ['web_api', 'cloud_infra', 'social_osint'],
+ risk: 'passive',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['registration_record'],
+ outputFormats: ['text'],
+ installHint: 'Usually preinstalled, or install via OS package manager.',
+ commandHint: 'whois example.com',
+ parserStatus: 'text',
+ notes: 'Passive registration context for scoped assets.',
+ },
+ {
+ id: 'nmap',
+ binary: 'nmap',
+ name: 'Nmap',
+ category: 'network',
+ families: ['web_api', 'cloud_infra', 'reporting_remediation'],
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['open_port', 'service_fingerprint'],
+ outputFormats: ['text', 'xml'],
+ installHint: 'brew install nmap',
+ commandHint: 'nmap -sV --top-ports 100 --open 127.0.0.1',
+ parserStatus: 'text',
+ notes: 'Primary scoped port and service discovery engine.',
+ },
+ {
+ id: 'subfinder',
+ binary: 'subfinder',
+ name: 'Subfinder',
+ category: 'osint',
+ families: ['web_api', 'cloud_infra', 'social_osint'],
+ risk: 'passive',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['subdomain'],
+ outputFormats: ['text', 'jsonl'],
+ installHint: 'brew install projectdiscovery/tap/subfinder',
+ commandHint: 'subfinder -d example.com -silent',
+ parserStatus: 'planned',
+ notes: 'High-value passive asset discovery adapter.',
+ },
+ {
+ id: 'httpx',
+ binary: 'httpx',
+ name: 'httpx',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['http_service', 'technology_fingerprint'],
+ outputFormats: ['jsonl', 'text'],
+ installHint: 'brew install projectdiscovery/tap/httpx',
+ commandHint: 'httpx -u https://example.com -status-code -title -tech-detect -json',
+ parserStatus: 'planned',
+ notes: 'HTTP probing and technology detection for discovered assets.',
+ },
+ {
+ id: 'naabu',
+ binary: 'naabu',
+ name: 'Naabu',
+ category: 'network',
+ families: ['web_api', 'cloud_infra'],
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['open_port'],
+ outputFormats: ['jsonl', 'text'],
+ installHint: 'brew install projectdiscovery/tap/naabu',
+ commandHint: 'naabu -host 127.0.0.1 -top-ports 100',
+ parserStatus: 'planned',
+ notes: 'Fast scoped port discovery; pair with nmap for service verification.',
+ },
+ {
+ id: 'katana',
+ binary: 'katana',
+ name: 'Katana',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['endpoint', 'crawl_result'],
+ outputFormats: ['jsonl', 'text'],
+ installHint: 'brew install projectdiscovery/tap/katana',
+ commandHint: 'katana -u https://example.com -jsonl -silent',
+ parserStatus: 'planned',
+ notes: 'Crawler for endpoint discovery and attack-surface mapping.',
+ },
+ {
+ id: 'nuclei',
+ binary: 'nuclei',
+ name: 'Nuclei',
+ category: 'vulnerability',
+ families: ['web_api', 'cloud_infra', 'code_supply_chain', 'reporting_remediation'],
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['scanner_finding', 'template_match'],
+ outputFormats: ['jsonl'],
+ installHint: 'brew install projectdiscovery/tap/nuclei',
+ commandHint: 'nuclei -u https://example.com -severity medium,high,critical -jsonl',
+ parserStatus: 'structured',
+ notes: 'Template-driven scanner; findings should flow into validation and retest.',
+ },
+ {
+ id: 'ffuf',
+ binary: 'ffuf',
+ name: 'FFUF',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['content_discovery'],
+ outputFormats: ['json'],
+ installHint: 'brew install ffuf',
+ commandHint: 'ffuf -u https://example.com/FUZZ -w wordlist.txt -of json',
+ parserStatus: 'structured',
+ notes: 'Content and parameter discovery. Rate-limit on real targets.',
+ },
+ {
+ id: 'gobuster',
+ binary: 'gobuster',
+ name: 'Gobuster',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['content_discovery', 'dns_bruteforce'],
+ outputFormats: ['text'],
+ installHint: 'brew install gobuster',
+ commandHint: 'gobuster dir -u https://example.com -w wordlist.txt',
+ parserStatus: 'text',
+ notes: 'Directory and DNS brute-force companion to ffuf.',
+ },
+ {
+ id: 'feroxbuster',
+ binary: 'feroxbuster',
+ name: 'Feroxbuster',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['content_discovery'],
+ outputFormats: ['json', 'text'],
+ installHint: 'brew install feroxbuster',
+ commandHint: 'feroxbuster -u https://example.com --json',
+ parserStatus: 'planned',
+ notes: 'Recursive content discovery; useful when ffuf finds nested paths.',
+ },
+ {
+ id: 'nikto',
+ binary: 'nikto',
+ name: 'Nikto',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['scanner_finding'],
+ outputFormats: ['text', 'json'],
+ installHint: 'brew install nikto',
+ commandHint: 'nikto -h https://example.com',
+ parserStatus: 'text',
+ notes: 'Legacy web server checks; validate findings before reporting.',
+ },
+ {
+ id: 'dalfox',
+ binary: 'dalfox',
+ name: 'Dalfox',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['xss_probe', 'scanner_finding'],
+ outputFormats: ['json', 'text'],
+ installHint: 'brew install dalfox',
+ commandHint: 'dalfox url https://example.com?q=test -o result.json --format json',
+ parserStatus: 'planned',
+ notes: 'XSS validation adapter for scoped endpoints.',
+ },
+ {
+ id: 'sqlmap',
+ binary: 'sqlmap',
+ name: 'SQLMap',
+ category: 'web',
+ families: WEB_FAMILIES,
+ risk: 'intrusive',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['sqli_validation'],
+ outputFormats: ['text'],
+ installHint: 'brew install sqlmap',
+ commandHint: 'sqlmap -u "https://example.com/item?id=1" --batch --level=1 --risk=1',
+ parserStatus: 'text',
+ notes: 'Keep level/risk low unless the mission receipt explicitly permits intrusive testing.',
+ },
+ {
+ id: 'semgrep',
+ binary: 'semgrep',
+ name: 'Semgrep',
+ category: 'supply_chain',
+ families: SUPPLY_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['static_analysis_finding', 'sarif'],
+ outputFormats: ['json', 'sarif'],
+ installHint: 'brew install semgrep or pipx install semgrep',
+ commandHint: 'semgrep scan --config auto --json .',
+ parserStatus: 'planned',
+ notes: 'Static analysis backbone for repository hardening.',
+ },
+ {
+ id: 'gitleaks',
+ binary: 'gitleaks',
+ name: 'Gitleaks',
+ category: 'secrets',
+ families: SUPPLY_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['secret_finding'],
+ outputFormats: ['json', 'sarif'],
+ installHint: 'brew install gitleaks',
+ commandHint: 'gitleaks detect --source . --report-format json --redact',
+ parserStatus: 'planned',
+ notes: 'Secret detection must redact values before ledger insertion.',
+ },
+ {
+ id: 'trufflehog',
+ binary: 'trufflehog',
+ name: 'TruffleHog',
+ category: 'secrets',
+ families: SUPPLY_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['secret_finding'],
+ outputFormats: ['json'],
+ installHint: 'brew install trufflehog',
+ commandHint: 'trufflehog filesystem . --json --no-update',
+ parserStatus: 'planned',
+ notes: 'Deep secret scanner. Keep verification/network modes explicit.',
+ },
+ {
+ id: 'trivy',
+ binary: 'trivy',
+ name: 'Trivy',
+ category: 'container',
+ families: ['code_supply_chain', 'cloud_infra', 'reporting_remediation'],
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['dependency_vulnerability', 'container_vulnerability', 'iac_misconfig'],
+ outputFormats: ['json', 'sarif'],
+ installHint: 'brew install aquasecurity/trivy/trivy',
+ commandHint: 'trivy fs --format json .',
+ parserStatus: 'planned',
+ notes: 'Vulnerability and IaC scanner for repos and images.',
+ },
+ {
+ id: 'syft',
+ binary: 'syft',
+ name: 'Syft',
+ category: 'supply_chain',
+ families: SUPPLY_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['sbom'],
+ outputFormats: ['spdx-json', 'cyclonedx-json'],
+ installHint: 'brew install syft',
+ commandHint: 'syft dir:. -o cyclonedx-json',
+ parserStatus: 'planned',
+ notes: 'SBOM generator feeding vulnerability and provenance workflows.',
+ },
+ {
+ id: 'grype',
+ binary: 'grype',
+ name: 'Grype',
+ category: 'supply_chain',
+ families: SUPPLY_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['dependency_vulnerability'],
+ outputFormats: ['json'],
+ installHint: 'brew install grype',
+ commandHint: 'grype dir:. -o json',
+ parserStatus: 'planned',
+ notes: 'Pairs well with Syft SBOMs for dependency vulnerability triage.',
+ },
+ {
+ id: 'osv-scanner',
+ binary: 'osv-scanner',
+ name: 'OSV Scanner',
+ category: 'supply_chain',
+ families: SUPPLY_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: true,
+ evidenceKinds: ['dependency_vulnerability'],
+ outputFormats: ['json'],
+ installHint: 'brew install osv-scanner',
+ commandHint: 'osv-scanner --format json --recursive .',
+ parserStatus: 'planned',
+ notes: 'Uses OSV vulnerability data; note network dependency in evidence.',
+ },
+ {
+ id: 'checkov',
+ binary: 'checkov',
+ name: 'Checkov',
+ category: 'cloud',
+ families: CLOUD_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['iac_misconfig'],
+ outputFormats: ['json', 'sarif'],
+ installHint: 'brew install checkov or pipx install checkov',
+ commandHint: 'checkov -d . -o json',
+ parserStatus: 'planned',
+ notes: 'IaC policy scanner for Terraform, Kubernetes, and cloud templates.',
+ },
+ {
+ id: 'prowler',
+ binary: 'prowler',
+ name: 'Prowler',
+ category: 'cloud',
+ families: CLOUD_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['cloud_control_finding'],
+ outputFormats: ['json', 'csv'],
+ installHint: 'pipx install prowler',
+ commandHint: 'prowler aws --output-formats json',
+ parserStatus: 'planned',
+ notes: 'Cloud account assessment requires explicit account scope and credentials handling.',
+ },
+ {
+ id: 'garak',
+ binary: 'garak',
+ name: 'Garak',
+ category: 'ai_agent',
+ families: AI_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['model_probe', 'prompt_test_result'],
+ outputFormats: ['jsonl', 'report'],
+ installHint: 'pipx install garak',
+ commandHint: 'garak --model_type rest --model_name scoped-model',
+ parserStatus: 'planned',
+ notes: 'Model/agent probing must use scoped models and redacted transcripts.',
+ },
+ {
+ id: 'promptfoo',
+ binary: 'promptfoo',
+ name: 'promptfoo',
+ category: 'ai_agent',
+ families: AI_FAMILIES,
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['prompt_eval', 'regression_result'],
+ outputFormats: ['json', 'junit'],
+ installHint: 'npm install -g promptfoo',
+ commandHint: 'promptfoo eval --output results.json',
+ parserStatus: 'planned',
+ notes: 'Best fit for repeatable prompt/tool/memory regression packs.',
+ },
+ {
+ id: 'slither',
+ binary: 'slither',
+ name: 'Slither',
+ category: 'smart_contract',
+ families: SMART_CONTRACT_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['smart_contract_static_finding', 'solidity_call_graph'],
+ outputFormats: ['json', 'sarif', 'text'],
+ installHint: 'pipx install slither-analyzer',
+ commandHint: 'slither . --json slither-report.json',
+ parserStatus: 'planned',
+ notes: 'Static-analysis backbone for Solidity and EVM contract review.',
+ },
+ {
+ id: 'mythril',
+ binary: 'myth',
+ name: 'Mythril',
+ category: 'smart_contract',
+ families: SMART_CONTRACT_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['symbolic_execution_finding', 'smart_contract_trace'],
+ outputFormats: ['json', 'text'],
+ installHint: 'pipx install mythril',
+ commandHint: 'myth analyze contracts/Target.sol -o json',
+ parserStatus: 'planned',
+ notes: 'Symbolic execution for scoped local contract artifacts.',
+ },
+ {
+ id: 'echidna',
+ binary: 'echidna',
+ name: 'Echidna',
+ category: 'smart_contract',
+ families: SMART_CONTRACT_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['property_test_result', 'fuzz_counterexample'],
+ outputFormats: ['json', 'text'],
+ installHint: 'brew install crytic/tap/echidna',
+ commandHint: 'echidna . --config echidna.yaml --format json',
+ parserStatus: 'planned',
+ notes: 'Property-based fuzzing for local smart-contract harnesses.',
+ },
+ {
+ id: 'foundry-forge',
+ binary: 'forge',
+ name: 'Foundry Forge',
+ category: 'smart_contract',
+ families: SMART_CONTRACT_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['unit_test_result', 'fuzz_test_result', 'coverage_signal'],
+ outputFormats: ['json', 'text'],
+ installHint: 'Install Foundry with foundryup from https://book.getfoundry.sh/getting-started/installation',
+ commandHint: 'forge test --json',
+ parserStatus: 'planned',
+ notes: 'Local EVM tests, invariant checks, and replay harnesses.',
+ },
+ {
+ id: 'foundry-cast',
+ binary: 'cast',
+ name: 'Foundry Cast',
+ category: 'smart_contract',
+ families: ['smart_contract', 'crypto_secrets', 'reporting_remediation'],
+ risk: 'active',
+ execution: 'receipt_required',
+ networked: true,
+ evidenceKinds: ['chain_query', 'transaction_dry_run', 'abi_decode'],
+ outputFormats: ['json', 'text'],
+ installHint: 'Install Foundry with foundryup from https://book.getfoundry.sh/getting-started/installation',
+ commandHint: 'cast call --rpc-url scoped-rpc.example 0x0000000000000000000000000000000000000000 "symbol()(string)"',
+ parserStatus: 'planned',
+ notes: 'Chain interaction stays receipt-gated; prefer dry-run and read-only calls.',
+ },
+ {
+ id: 'solhint',
+ binary: 'solhint',
+ name: 'Solhint',
+ category: 'smart_contract',
+ families: SMART_CONTRACT_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['solidity_lint_finding'],
+ outputFormats: ['json', 'stylish', 'text'],
+ installHint: 'npm install -g solhint',
+ commandHint: 'solhint "contracts/**/*.sol" --formatter json',
+ parserStatus: 'planned',
+ notes: 'Fast lint layer for Solidity hygiene before heavier analysis.',
+ },
+ {
+ id: 'openssl',
+ binary: 'openssl',
+ name: 'OpenSSL',
+ category: 'crypto',
+ families: CRYPTO_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['certificate_metadata', 'crypto_material_fingerprint'],
+ outputFormats: ['text'],
+ installHint: 'Usually preinstalled, or install via `brew install openssl@3`.',
+ commandHint: 'openssl x509 -in certificate.pem -noout -text',
+ parserStatus: 'text',
+ notes: 'Local certificate, key, and crypto-material inspection. Do not log private key values.',
+ },
+ {
+ id: 'john',
+ binary: 'john',
+ name: 'John the Ripper',
+ category: 'crypto',
+ families: CRYPTO_FAMILIES,
+ risk: 'credential',
+ execution: 'receipt_required',
+ networked: false,
+ evidenceKinds: ['hash_audit_result', 'password_policy_signal'],
+ outputFormats: ['text'],
+ installHint: 'brew install john',
+ commandHint: 'john --test',
+ parserStatus: 'planned',
+ notes: 'Local hash-strength auditing only; cracking runs require explicit mission receipts and redacted evidence.',
+ },
+ {
+ id: 'hashcat',
+ binary: 'hashcat',
+ name: 'Hashcat',
+ category: 'crypto',
+ families: CRYPTO_FAMILIES,
+ risk: 'credential',
+ execution: 'receipt_required',
+ networked: false,
+ evidenceKinds: ['hash_audit_result', 'gpu_cracking_benchmark'],
+ outputFormats: ['text', 'json'],
+ installHint: 'brew install hashcat',
+ commandHint: 'hashcat --benchmark',
+ parserStatus: 'planned',
+ notes: 'Receipt-gated local hash auditing and benchmark evidence; never store recovered secrets.',
+ },
+ {
+ id: 'radamsa',
+ binary: 'radamsa',
+ name: 'Radamsa',
+ category: 'fuzzing',
+ families: ['reverse_binary', 'web_api', 'code_supply_chain', 'reporting_remediation'],
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['fuzz_case', 'mutation_seed'],
+ outputFormats: ['binary', 'text'],
+ installHint: 'brew install radamsa',
+ commandHint: 'radamsa sample.txt -o fuzz-case.txt',
+ parserStatus: 'planned',
+ notes: 'Local mutation engine for controlled parser and protocol harnesses.',
+ },
+ {
+ id: 'afl-fuzz',
+ binary: 'afl-fuzz',
+ name: 'AFL++',
+ category: 'fuzzing',
+ families: REVERSE_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['crash_case', 'coverage_signal', 'fuzz_corpus'],
+ outputFormats: ['directory', 'text'],
+ installHint: 'brew install afl++',
+ commandHint: 'afl-fuzz -i seeds -o findings -- ./target @@',
+ parserStatus: 'planned',
+ notes: 'Local binary fuzzing harness; production targets require a lab fixture.',
+ },
+ {
+ id: 'radare2',
+ binary: 'r2',
+ name: 'radare2',
+ category: 'reverse',
+ families: REVERSE_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['binary_analysis', 'function_graph', 'string_reference'],
+ outputFormats: ['json', 'text'],
+ installHint: 'brew install radare2',
+ commandHint: 'r2 -q -c "iIj" artifact.bin',
+ parserStatus: 'planned',
+ notes: 'Scriptable local reverse-engineering adapter for artifacts and firmware.',
+ },
+ {
+ id: 'apktool',
+ binary: 'apktool',
+ name: 'Apktool',
+ category: 'mobile',
+ families: REVERSE_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['mobile_manifest', 'apk_resource_tree'],
+ outputFormats: ['directory', 'text'],
+ installHint: 'brew install apktool',
+ commandHint: 'apktool d app.apk -o apk-out',
+ parserStatus: 'planned',
+ notes: 'Mobile app unpacking for owned APKs and lab samples.',
+ },
+ {
+ id: 'jadx',
+ binary: 'jadx',
+ name: 'JADX',
+ category: 'mobile',
+ families: REVERSE_FAMILIES,
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['decompiled_source', 'mobile_static_finding'],
+ outputFormats: ['directory', 'text'],
+ installHint: 'brew install jadx',
+ commandHint: 'jadx --output-dir jadx-out app.apk',
+ parserStatus: 'planned',
+ notes: 'Android decompilation for scoped local APK review.',
+ },
+ {
+ id: 'exiftool',
+ binary: 'exiftool',
+ name: 'ExifTool',
+ category: 'forensics',
+ families: ['code_supply_chain', 'reverse_binary', 'reporting_remediation'],
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['metadata'],
+ outputFormats: ['json', 'text'],
+ installHint: 'brew install exiftool',
+ commandHint: 'exiftool -json artifact.bin',
+ parserStatus: 'planned',
+ notes: 'Metadata extraction for artifacts and reports.',
+ },
+ {
+ id: 'binwalk',
+ binary: 'binwalk',
+ name: 'Binwalk',
+ category: 'reverse',
+ families: ['reverse_binary', 'reporting_remediation'],
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['firmware_signature', 'embedded_file'],
+ outputFormats: ['text'],
+ installHint: 'brew install binwalk',
+ commandHint: 'binwalk firmware.bin',
+ parserStatus: 'text',
+ notes: 'Firmware and embedded-file triage.',
+ },
+ {
+ id: 'metasploit',
+ binary: 'msfconsole',
+ name: 'Metasploit',
+ category: 'post_exploitation',
+ families: ['web_api', 'cloud_infra', 'reporting_remediation'],
+ risk: 'dangerous',
+ execution: 'catalog_only',
+ networked: true,
+ evidenceKinds: ['exploit_validation'],
+ outputFormats: ['workspace_export'],
+ installHint: 'brew install metasploit',
+ commandHint: 'Catalog-only until a narrow module runner and receipt gate exist.',
+ parserStatus: 'planned',
+ notes: 'Do not expose through generic command execution; needs per-module adapters.',
+ },
+ {
+ id: 'hydra',
+ binary: 'hydra',
+ name: 'Hydra',
+ category: 'credentials',
+ families: ['web_api', 'cloud_infra', 'reporting_remediation'],
+ risk: 'credential',
+ execution: 'catalog_only',
+ networked: true,
+ evidenceKinds: ['auth_control_test'],
+ outputFormats: ['text'],
+ installHint: 'brew install hydra',
+ commandHint: 'Catalog-only until account-lockout and written authorization gates exist.',
+ parserStatus: 'planned',
+ notes: 'Credential attack tooling should be modeled, not generically executable.',
+ },
+ {
+ id: 'bloodhound',
+ binary: 'bloodhound',
+ name: 'BloodHound',
+ category: 'credentials',
+ families: ['cloud_infra', 'reporting_remediation'],
+ risk: 'credential',
+ execution: 'import_only',
+ networked: false,
+ evidenceKinds: ['attack_path_graph'],
+ outputFormats: ['json', 'graph'],
+ installHint: 'Install BloodHound CE or compatible collectors.',
+ commandHint: 'Import collector output; do not run collectors without AD scope receipts.',
+ parserStatus: 'planned',
+ notes: 'Useful as graph evidence once collector output is explicitly authorized.',
+ },
+ {
+ id: 'yara',
+ binary: 'yara',
+ name: 'YARA',
+ category: 'forensics',
+ families: ['reverse_binary', 'code_supply_chain', 'reporting_remediation'],
+ risk: 'local_read',
+ execution: 'safe_command',
+ networked: false,
+ evidenceKinds: ['artifact_signature'],
+ outputFormats: ['text', 'json'],
+ installHint: 'brew install yara',
+ commandHint: 'yara rules.yar artifact.bin',
+ parserStatus: 'planned',
+ notes: 'Local artifact classification and malware-family matching.',
+ },
+];
+
+export const SAFE_COMMANDS = [
+ ...new Set(
+ TOOL_ADAPTERS
+ .filter(adapter => adapter.execution !== 'catalog_only' && adapter.execution !== 'import_only')
+ .map(adapter => adapter.binary)
+ ),
+].sort();
+
+export const NETWORK_COMMANDS = new Set(
+ TOOL_ADAPTERS
+ .filter(adapter => adapter.networked && adapter.execution !== 'catalog_only' && adapter.execution !== 'import_only')
+ .map(adapter => adapter.binary)
+);
+
+export function adaptersForFamily(family?: string): ToolAdapter[] {
+ return family
+ ? TOOL_ADAPTERS.filter(adapter => adapter.families.includes(family as MissionFamily))
+ : TOOL_ADAPTERS;
+}
+
+export function adapterForBinary(binary: string): ToolAdapter | undefined {
+ return TOOL_ADAPTERS.find(adapter => adapter.binary === binary || adapter.id === binary);
+}
+
+export function summarizeToolCatalog(adapters = TOOL_ADAPTERS): Record {
+ const commandReady = adapters.filter(adapter => adapter.execution === 'safe_command' || adapter.execution === 'receipt_required').length;
+ const byExecution = adapters.reduce>((acc, adapter) => {
+ acc[adapter.execution] = (acc[adapter.execution] || 0) + 1;
+ return acc;
+ }, {});
+ const byRisk = adapters.reduce>((acc, adapter) => {
+ acc[adapter.risk] = (acc[adapter.risk] || 0) + 1;
+ return acc;
+ }, {});
+ const byCategory = adapters.reduce>((acc, adapter) => {
+ acc[adapter.category] = (acc[adapter.category] || 0) + 1;
+ return acc;
+ }, {});
+ return {
+ total: adapters.length,
+ commandReady,
+ catalogOnly: adapters.filter(adapter => adapter.execution === 'catalog_only').length,
+ importOnly: adapters.filter(adapter => adapter.execution === 'import_only').length,
+ unmodeled: adapters.length === 0 || commandReady === 0,
+ byExecution,
+ byRisk,
+ byCategory,
+ };
+}
diff --git a/src/arsenal/index.ts b/src/arsenal/index.ts
new file mode 100644
index 0000000..252e9ce
--- /dev/null
+++ b/src/arsenal/index.ts
@@ -0,0 +1,3063 @@
+/**
+ * T3MP3ST Arsenal
+ *
+ * Tool registry and execution management.
+ */
+
+import { EventEmitter } from 'eventemitter3';
+import { randomUUID } from 'crypto';
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+import * as net from 'net';
+import * as dns from 'dns';
+import * as tls from 'tls';
+
+const execFileAsync = promisify(execFile);
+import type {
+ CustomTool,
+ ToolContext,
+ ToolResult,
+ Target,
+ LLMToolDefinition,
+} from '../types/index.js';
+
+const dnsResolve = promisify(dns.resolve);
+const dnsResolve4 = promisify(dns.resolve4);
+const dnsResolveMx = promisify(dns.resolveMx);
+const dnsResolveTxt = promisify(dns.resolveTxt);
+const dnsResolveNs = promisify(dns.resolveNs);
+const dnsReverse = promisify(dns.reverse);
+
+import { CVE_DATABASE } from '../stubs/index.js';
+import type { CVEEntry } from '../stubs/index.js';
+
+// =============================================================================
+// PORT SCANNING UTILITY
+// =============================================================================
+
+async function checkPort(host: string, port: number, timeout: number = 2000): Promise {
+ return new Promise((resolve) => {
+ const socket = new net.Socket();
+ socket.setTimeout(timeout);
+
+ socket.on('connect', () => {
+ socket.destroy();
+ resolve(true);
+ });
+
+ socket.on('timeout', () => {
+ socket.destroy();
+ resolve(false);
+ });
+
+ socket.on('error', () => {
+ socket.destroy();
+ resolve(false);
+ });
+
+ socket.connect(port, host);
+ });
+}
+
+// =============================================================================
+// EVENTS
+// =============================================================================
+
+export interface ArsenalEvents {
+ 'tool:registered': CustomTool;
+ 'tool:executed': { tool: CustomTool; result: ToolResult; durationMs: number };
+ 'tool:error': { tool: CustomTool; error: Error };
+}
+
+export interface ToolExecution {
+ id: string;
+ toolName: string;
+ startedAt: number;
+ completedAt?: number;
+ result?: ToolResult;
+ error?: string;
+}
+
+// =============================================================================
+// ARSENAL
+// =============================================================================
+
+export class Arsenal extends EventEmitter {
+ private tools: Map = new Map();
+ private executions: ToolExecution[] = [];
+
+ /**
+ * Register a tool
+ */
+ register(tool: CustomTool): void {
+ this.tools.set(tool.name, tool);
+ this.emit('tool:registered', tool);
+ }
+
+ /**
+ * Register multiple tools
+ */
+ registerMany(tools: CustomTool[]): void {
+ for (const tool of tools) {
+ this.register(tool);
+ }
+ }
+
+ /**
+ * Get a tool by name
+ */
+ getTool(name: string): CustomTool | undefined {
+ return this.tools.get(name);
+ }
+
+ /**
+ * Get all tools
+ */
+ getAllTools(): CustomTool[] {
+ return Array.from(this.tools.values());
+ }
+
+ /**
+ * Get tools by category
+ */
+ getToolsByCategory(category: string): CustomTool[] {
+ return this.getAllTools().filter(t => t.category === category);
+ }
+
+ /**
+ * Execute a tool
+ */
+ async execute(
+ toolName: string,
+ context: ToolContext
+ ): Promise {
+ const tool = this.tools.get(toolName);
+ if (!tool) {
+ throw new Error(`Tool "${toolName}" not found`);
+ }
+
+ const execution: ToolExecution = {
+ id: randomUUID(),
+ toolName,
+ startedAt: Date.now(),
+ };
+ this.executions.push(execution);
+
+ const startTime = Date.now();
+
+ try {
+ const result = await tool.handler(context);
+ const durationMs = Date.now() - startTime;
+
+ execution.completedAt = Date.now();
+ execution.result = result;
+
+ this.emit('tool:executed', { tool, result, durationMs });
+
+ return result;
+ } catch (error) {
+ execution.completedAt = Date.now();
+ execution.error = error instanceof Error ? error.message : String(error);
+
+ this.emit('tool:error', { tool, error: error as Error });
+
+ return {
+ success: false,
+ error: execution.error,
+ };
+ }
+ }
+
+ /**
+ * Get execution history
+ */
+ getExecutions(): ToolExecution[] {
+ return [...this.executions];
+ }
+
+ /**
+ * Get tool categories
+ */
+ getCategories(): string[] {
+ const categories = new Set();
+ for (const tool of this.tools.values()) {
+ categories.add(tool.category);
+ }
+ return Array.from(categories);
+ }
+
+ /**
+ * Convert registered tools to LLM tool definitions for function calling
+ */
+ getToolDefinitions(categories?: string[]): LLMToolDefinition[] {
+ let tools = this.getAllTools();
+ if (categories?.length) {
+ tools = tools.filter(t => categories.includes(t.category));
+ }
+ return tools.map(tool => {
+ const properties: Record = {};
+ const required: string[] = [];
+
+ for (const param of tool.parameters || []) {
+ properties[param.name] = {
+ type: param.type,
+ description: param.description,
+ };
+ if (param.default !== undefined) {
+ properties[param.name].default = param.default;
+ }
+ if (param.required) {
+ required.push(param.name);
+ }
+ }
+
+ return {
+ name: tool.name,
+ description: tool.description,
+ parameters: {
+ type: 'object' as const,
+ properties,
+ required: required.length > 0 ? required : undefined,
+ },
+ };
+ });
+ }
+
+ /**
+ * Unregister a tool
+ */
+ unregister(name: string): boolean {
+ return this.tools.delete(name);
+ }
+
+ /**
+ * Clear all tools
+ */
+ clear(): void {
+ this.tools.clear();
+ this.executions = [];
+ }
+}
+
+// =============================================================================
+// HELPER FUNCTIONS
+// =============================================================================
+
+export function successResult(output: string, findings?: ToolResult['findings']): ToolResult {
+ return {
+ success: true,
+ output,
+ findings,
+ };
+}
+
+export function failResult(error: string): ToolResult {
+ return {
+ success: false,
+ error,
+ };
+}
+
+export function createToolContext(
+ target?: Target,
+ parameters?: Record
+): ToolContext {
+ return {
+ target,
+ parameters: parameters || {},
+ };
+}
+
+// =============================================================================
+// BUILT-IN TOOLS
+// =============================================================================
+
+export const BUILTIN_TOOLS: CustomTool[] = [
+ // =============================================================================
+ // RECONNAISSANCE TOOLS
+ // =============================================================================
+ {
+ name: 'dns_lookup',
+ description: 'Perform DNS lookup for a target',
+ category: 'recon',
+ parameters: [
+ { name: 'domain', type: 'string', description: 'Domain to lookup', required: true },
+ { name: 'type', type: 'string', description: 'Record type (A, AAAA, MX, TXT, NS)', required: false, default: 'A' },
+ ],
+ handler: async (context) => {
+ const domain = context.parameters.domain as string;
+ const recordType = (context.parameters.type as string || 'A').toUpperCase();
+
+ try {
+ let records: string[] = [];
+
+ switch (recordType) {
+ case 'A':
+ records = await dnsResolve4(domain);
+ break;
+ case 'MX': {
+ const mxRecords = await dnsResolveMx(domain);
+ records = mxRecords.map(r => `${r.priority} ${r.exchange}`);
+ break;
+ }
+ case 'TXT': {
+ const txtRecords = await dnsResolveTxt(domain);
+ records = txtRecords.map(r => r.join(''));
+ break;
+ }
+ case 'NS':
+ records = await dnsResolveNs(domain);
+ break;
+ default:
+ records = await dnsResolve(domain, recordType) as string[];
+ }
+
+ return {
+ success: true,
+ output: `DNS ${recordType} lookup for ${domain}:\n${records.join('\n')}`,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `DNS lookup failed: ${error instanceof Error ? error.message : String(error)}`,
+ };
+ }
+ },
+ },
+ {
+ name: 'port_scan',
+ description: 'Scan ports on a target (real TCP connect scan)',
+ category: 'recon',
+ parameters: [
+ { name: 'target', type: 'string', description: 'Target IP or hostname', required: true },
+ { name: 'ports', type: 'string', description: 'Ports to scan (e.g., "22,80,443")', required: false, default: '22,80,443,8080' },
+ { name: 'timeout', type: 'number', description: 'Timeout per port in ms', required: false, default: 2000 },
+ ],
+ handler: async (context) => {
+ const target = context.parameters.target as string || context.target?.address;
+ if (!target) {
+ return { success: false, error: 'No target specified' };
+ }
+
+ const portsStr = context.parameters.ports as string || '22,80,443,8080';
+ const ports = portsStr.split(',').map(p => parseInt(p.trim(), 10)).filter(p => !isNaN(p));
+ const timeout = (context.parameters.timeout as number) || 2000;
+
+ const portServices: Record = {
+ 21: 'ftp', 22: 'ssh', 23: 'telnet', 25: 'smtp', 53: 'dns',
+ 80: 'http', 110: 'pop3', 143: 'imap', 443: 'https', 445: 'smb',
+ 3306: 'mysql', 3389: 'rdp', 5432: 'postgresql', 5900: 'vnc',
+ 6379: 'redis', 8080: 'http-proxy', 8443: 'https-alt', 27017: 'mongodb',
+ };
+
+ const results: string[] = [];
+ const openPorts: number[] = [];
+
+ // Scan ports in parallel with concurrency limit
+ const concurrency = 10;
+ for (let i = 0; i < ports.length; i += concurrency) {
+ const batch = ports.slice(i, i + concurrency);
+ const checks = await Promise.all(
+ batch.map(async (port) => {
+ const isOpen = await checkPort(target, port, timeout);
+ return { port, isOpen };
+ })
+ );
+
+ for (const { port, isOpen } of checks) {
+ if (isOpen) {
+ openPorts.push(port);
+ results.push(`${port}/tcp open ${portServices[port] || 'unknown'}`);
+ } else {
+ results.push(`${port}/tcp closed`);
+ }
+ }
+ }
+
+ return {
+ success: true,
+ output: `Port scan of ${target} (${ports.length} ports scanned):\n${results.join('\n')}\n\nOpen ports: ${openPorts.length}`,
+ findings: openPorts.length > 0 ? [{
+ title: 'Open Ports Detected',
+ severity: 'info',
+ details: `Found ${openPorts.length} open ports: ${openPorts.join(', ')}`,
+ }] : undefined,
+ };
+ },
+ },
+ {
+ name: 'subdomain_enum',
+ description: 'Enumerate subdomains for a domain (real DNS resolution)',
+ category: 'recon',
+ parameters: [
+ { name: 'domain', type: 'string', description: 'Domain to enumerate', required: true },
+ { name: 'wordlist', type: 'string', description: 'Wordlist: common, extended', required: false, default: 'common' },
+ ],
+ handler: async (context) => {
+ const domain = context.parameters.domain as string;
+ const wordlistType = context.parameters.wordlist as string || 'common';
+
+ const wordlists: Record = {
+ common: ['www', 'mail', 'ftp', 'admin', 'dev', 'staging', 'api', 'app', 'cdn', 'static', 'test', 'blog', 'shop', 'store', 'portal', 'secure', 'vpn', 'remote', 'webmail', 'ns1', 'ns2'],
+ extended: ['www', 'mail', 'ftp', 'admin', 'dev', 'staging', 'api', 'app', 'cdn', 'static', 'test', 'blog', 'shop', 'store', 'portal', 'secure', 'vpn', 'remote', 'webmail', 'ns1', 'ns2', 'mx', 'smtp', 'pop', 'imap', 'cpanel', 'whm', 'webdisk', 'autodiscover', 'autoconfig', 'git', 'gitlab', 'jenkins', 'ci', 'jira', 'confluence', 'wiki', 'docs', 'status', 'monitor', 'grafana', 'prometheus', 'elastic', 'kibana', 'redis', 'mysql', 'postgres', 'mongo', 'db', 'database', 'backup', 'files', 'media', 'assets', 'images', 'img', 'video', 'download', 'upload'],
+ };
+
+ const prefixes = wordlists[wordlistType] || wordlists.common;
+ const found: { subdomain: string; ip: string }[] = [];
+
+ // Check subdomains in parallel with concurrency limit
+ const concurrency = 10;
+ for (let i = 0; i < prefixes.length; i += concurrency) {
+ const batch = prefixes.slice(i, i + concurrency);
+ const checks = await Promise.all(
+ batch.map(async (prefix) => {
+ const subdomain = `${prefix}.${domain}`;
+ try {
+ const addresses = await dnsResolve4(subdomain);
+ return { subdomain, ip: addresses[0], found: true };
+ } catch {
+ return { subdomain, ip: '', found: false };
+ }
+ })
+ );
+
+ for (const result of checks) {
+ if (result.found) {
+ found.push({ subdomain: result.subdomain, ip: result.ip });
+ }
+ }
+ }
+
+ if (found.length === 0) {
+ return {
+ success: true,
+ output: `Subdomain enumeration for ${domain}:\nNo subdomains found from ${prefixes.length} candidates tested.`,
+ };
+ }
+
+ const output = found.map(f => `${f.subdomain} -> ${f.ip}`).join('\n');
+ return {
+ success: true,
+ output: `Subdomain enumeration for ${domain}:\nFound ${found.length} subdomains (tested ${prefixes.length}):\n${output}`,
+ findings: [{
+ title: 'Subdomains Discovered',
+ severity: 'info',
+ details: `Found ${found.length} active subdomains`,
+ }],
+ };
+ },
+ },
+ {
+ name: 'whois_lookup',
+ description: 'Perform WHOIS lookup for a domain (real WHOIS query)',
+ category: 'recon',
+ parameters: [
+ { name: 'domain', type: 'string', description: 'Domain to lookup', required: true },
+ ],
+ handler: async (context) => {
+ const domain = context.parameters.domain as string;
+
+ // Determine WHOIS server based on TLD
+ const tld = domain.split('.').pop()?.toLowerCase() || '';
+ const whoisServers: Record = {
+ 'com': 'whois.verisign-grs.com',
+ 'net': 'whois.verisign-grs.com',
+ 'org': 'whois.pir.org',
+ 'io': 'whois.nic.io',
+ 'co': 'whois.nic.co',
+ 'info': 'whois.afilias.net',
+ 'biz': 'whois.biz',
+ 'me': 'whois.nic.me',
+ 'dev': 'whois.nic.google',
+ 'app': 'whois.nic.google',
+ 'uk': 'whois.nic.uk',
+ 'de': 'whois.denic.de',
+ 'fr': 'whois.nic.fr',
+ 'eu': 'whois.eu',
+ 'nl': 'whois.domain-registry.nl',
+ 'au': 'whois.auda.org.au',
+ };
+
+ const whoisServer = whoisServers[tld] || `whois.nic.${tld}`;
+
+ return new Promise((resolve) => {
+ const socket = new net.Socket();
+ let data = '';
+
+ socket.setTimeout(10000);
+
+ socket.on('connect', () => {
+ socket.write(`${domain}\r\n`);
+ });
+
+ socket.on('data', (chunk) => {
+ data += chunk.toString();
+ });
+
+ socket.on('close', () => {
+ if (data.length === 0) {
+ resolve({
+ success: false,
+ error: `No WHOIS data received for ${domain}`,
+ });
+ return;
+ }
+
+ // Extract key fields from WHOIS response
+ const lines = data.split('\n');
+ const extracted: string[] = [];
+ const importantFields = [
+ 'domain name', 'registrar', 'creation date', 'updated date',
+ 'expiration date', 'registry expiry', 'registrant',
+ 'name server', 'status', 'dnssec', 'registrar abuse',
+ ];
+
+ for (const line of lines) {
+ const lineLower = line.toLowerCase();
+ if (importantFields.some(field => lineLower.includes(field))) {
+ extracted.push(line.trim());
+ }
+ }
+
+ // Truncate if too long
+ const output = extracted.length > 0
+ ? extracted.slice(0, 20).join('\n')
+ : data.slice(0, 2000);
+
+ resolve({
+ success: true,
+ output: `WHOIS for ${domain} (via ${whoisServer}):\n\n${output}${extracted.length > 20 ? '\n... (truncated)' : ''}`,
+ });
+ });
+
+ socket.on('error', (err) => {
+ resolve({
+ success: false,
+ error: `WHOIS lookup failed: ${err.message}`,
+ });
+ });
+
+ socket.on('timeout', () => {
+ socket.destroy();
+ resolve({
+ success: false,
+ error: `WHOIS lookup timed out for ${domain}`,
+ });
+ });
+
+ socket.connect(43, whoisServer);
+ });
+ },
+ },
+
+ // =============================================================================
+ // WEB TESTING TOOLS
+ // =============================================================================
+ {
+ name: 'http_request',
+ description: 'Make an HTTP request to a target',
+ category: 'web',
+ parameters: [
+ { name: 'url', type: 'string', description: 'URL to request', required: true },
+ { name: 'method', type: 'string', description: 'HTTP method', required: false, default: 'GET' },
+ { name: 'headers', type: 'object', description: 'Request headers', required: false },
+ { name: 'body', type: 'string', description: 'Request body', required: false },
+ ],
+ handler: async (context) => {
+ const url = context.parameters.url as string;
+ const method = (context.parameters.method as string) || 'GET';
+ const headers = (context.parameters.headers as Record) || {};
+ const body = context.parameters.body as string | undefined;
+
+ try {
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body && method !== 'GET' ? body : undefined,
+ signal: AbortSignal.timeout(10000),
+ });
+
+ const responseHeaders = Object.fromEntries(response.headers.entries());
+ return {
+ success: true,
+ output: `HTTP ${method} ${url}\nStatus: ${response.status} ${response.statusText}\nHeaders: ${JSON.stringify(responseHeaders, null, 2)}`,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to fetch: ${error instanceof Error ? error.message : String(error)}`,
+ };
+ }
+ },
+ },
+ {
+ name: 'header_analysis',
+ description: 'Analyze security headers of a URL',
+ category: 'web',
+ parameters: [
+ { name: 'url', type: 'string', description: 'URL to analyze', required: true },
+ ],
+ handler: async (context) => {
+ const url = context.parameters.url as string;
+ const securityHeaders = [
+ 'Strict-Transport-Security', 'Content-Security-Policy', 'X-Frame-Options',
+ 'X-Content-Type-Options', 'X-XSS-Protection', 'Referrer-Policy',
+ 'Permissions-Policy', 'Cross-Origin-Opener-Policy', 'Cross-Origin-Resource-Policy',
+ ];
+ try {
+ const response = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(10000) });
+ const analysis = securityHeaders.map(h => {
+ const value = response.headers.get(h);
+ return `${h}: ${value ? `✓ ${value}` : '✗ Missing'}`;
+ });
+ return {
+ success: true,
+ output: `Security Header Analysis for ${url}:\n${analysis.join('\n')}`,
+ };
+ } catch (error) {
+ return { success: false, error: `Failed: ${error instanceof Error ? error.message : String(error)}` };
+ }
+ },
+ },
+ {
+ name: 'dir_bruteforce',
+ description: 'Bruteforce directories on a web server (real HTTP requests)',
+ category: 'web',
+ parameters: [
+ { name: 'url', type: 'string', description: 'Base URL to scan', required: true },
+ { name: 'wordlist', type: 'string', description: 'Wordlist type: common, admin, api, files', required: false, default: 'common' },
+ { name: 'timeout', type: 'number', description: 'Timeout per request in ms', required: false, default: 5000 },
+ ],
+ handler: async (context) => {
+ const baseUrl = (context.parameters.url as string).replace(/\/$/, '');
+ const wordlistType = context.parameters.wordlist as string || 'common';
+ const timeout = (context.parameters.timeout as number) || 5000;
+
+ const wordlists: Record = {
+ common: ['admin', 'login', 'dashboard', 'api', 'backup', 'config', 'test', 'dev', 'staging', '.git', '.git/config', '.env', 'robots.txt', 'sitemap.xml', '.htaccess', 'wp-config.php', 'web.config'],
+ admin: ['admin', 'administrator', 'wp-admin', 'phpmyadmin', 'cpanel', 'manager', 'console', 'panel', 'admin.php', 'login.php', 'controlpanel', 'adminpanel'],
+ api: ['api', 'api/v1', 'api/v2', 'api/v3', 'graphql', 'rest', 'swagger', 'swagger-ui', 'docs', 'openapi', 'api-docs', 'health', 'status', 'metrics'],
+ files: ['backup.zip', 'backup.tar.gz', 'db.sql', 'database.sql', 'dump.sql', '.git/HEAD', '.svn/entries', 'composer.json', 'package.json', '.DS_Store', 'Thumbs.db', 'debug.log', 'error.log'],
+ };
+
+ const words = wordlists[wordlistType] || wordlists.common;
+ const found: { path: string; status: number; size?: number }[] = [];
+ const interesting = [200, 201, 301, 302, 307, 308, 401, 403];
+
+ // Scan paths in parallel with concurrency limit
+ const concurrency = 5;
+ for (let i = 0; i < words.length; i += concurrency) {
+ const batch = words.slice(i, i + concurrency);
+ const checks = await Promise.all(
+ batch.map(async (path) => {
+ const fullUrl = `${baseUrl}/${path}`;
+ try {
+ const response = await fetch(fullUrl, {
+ method: 'GET',
+ signal: AbortSignal.timeout(timeout),
+ redirect: 'manual',
+ });
+ const contentLength = response.headers.get('content-length');
+ return {
+ path: `/${path}`,
+ status: response.status,
+ size: contentLength ? parseInt(contentLength, 10) : undefined,
+ interesting: interesting.includes(response.status),
+ };
+ } catch {
+ return { path: `/${path}`, status: 0, interesting: false };
+ }
+ })
+ );
+
+ for (const result of checks) {
+ if (result.interesting) {
+ found.push({ path: result.path, status: result.status, size: result.size });
+ }
+ }
+ }
+
+ if (found.length === 0) {
+ return {
+ success: true,
+ output: `Directory bruteforce on ${baseUrl}:\nNo interesting paths found from ${words.length} tested.`,
+ };
+ }
+
+ const output = found.map(f => `${f.path} -> ${f.status}${f.size ? ` (${f.size} bytes)` : ''}`).join('\n');
+ const critical = found.filter(f => f.path.includes('.git') || f.path.includes('.env') || f.path.includes('backup'));
+
+ return {
+ success: true,
+ output: `Directory bruteforce on ${baseUrl}:\nFound ${found.length} interesting paths (tested ${words.length}):\n${output}`,
+ findings: critical.length > 0 ? [{
+ title: 'Sensitive Paths Exposed',
+ severity: 'high',
+ details: `Found potentially sensitive paths: ${critical.map(c => c.path).join(', ')}`,
+ }] : found.length > 0 ? [{
+ title: 'Directories Discovered',
+ severity: 'info',
+ details: `Found ${found.length} accessible paths`,
+ }] : undefined,
+ };
+ },
+ },
+ {
+ name: 'technology_detect',
+ description: 'Detect technologies used by a website (real analysis)',
+ category: 'web',
+ parameters: [
+ { name: 'url', type: 'string', description: 'URL to analyze', required: true },
+ ],
+ handler: async (context) => {
+ const url = context.parameters.url as string;
+
+ try {
+ const response = await fetch(url, {
+ signal: AbortSignal.timeout(10000),
+ });
+
+ const headers = Object.fromEntries(response.headers.entries());
+ const body = await response.text();
+ const detected: { tech: string; evidence: string }[] = [];
+
+ // Server detection from headers
+ if (headers['server']) {
+ detected.push({ tech: headers['server'], evidence: 'Server header' });
+ }
+ if (headers['x-powered-by']) {
+ detected.push({ tech: headers['x-powered-by'], evidence: 'X-Powered-By header' });
+ }
+
+ // CDN/Proxy detection
+ if (headers['cf-ray']) detected.push({ tech: 'Cloudflare', evidence: 'CF-Ray header' });
+ if (headers['x-amz-cf-id']) detected.push({ tech: 'AWS CloudFront', evidence: 'x-amz-cf-id header' });
+ if (headers['x-vercel-id']) detected.push({ tech: 'Vercel', evidence: 'x-vercel-id header' });
+ if (headers['x-netlify-id']) detected.push({ tech: 'Netlify', evidence: 'x-netlify-id header' });
+
+ // Framework detection from HTML
+ if (body.includes('wp-content') || body.includes('wp-includes')) {
+ detected.push({ tech: 'WordPress', evidence: 'wp-content/wp-includes paths' });
+ }
+ if (body.includes('_next/static') || body.includes('__NEXT_DATA__')) {
+ detected.push({ tech: 'Next.js', evidence: '_next paths or __NEXT_DATA__' });
+ }
+ if (body.includes('/_nuxt/')) {
+ detected.push({ tech: 'Nuxt.js', evidence: '_nuxt paths' });
+ }
+ if (body.includes('ng-version') || body.includes('ng-app')) {
+ detected.push({ tech: 'Angular', evidence: 'ng-version/ng-app attributes' });
+ }
+ if (body.includes('data-reactroot') || body.includes('__REACT_DEVTOOLS')) {
+ detected.push({ tech: 'React', evidence: 'React markers in HTML' });
+ }
+ if (body.includes('data-v-') || body.includes('Vue.js')) {
+ detected.push({ tech: 'Vue.js', evidence: 'Vue.js markers' });
+ }
+ if (body.includes('jquery') || body.includes('jQuery')) {
+ detected.push({ tech: 'jQuery', evidence: 'jQuery references' });
+ }
+ if (body.includes('bootstrap')) {
+ detected.push({ tech: 'Bootstrap', evidence: 'Bootstrap CSS/JS' });
+ }
+ if (body.includes('tailwind')) {
+ detected.push({ tech: 'Tailwind CSS', evidence: 'Tailwind references' });
+ }
+
+ // CMS detection
+ if (body.includes('Drupal') || body.includes('drupal.js')) {
+ detected.push({ tech: 'Drupal', evidence: 'Drupal markers' });
+ }
+ if (body.includes('Joomla') || body.includes('/media/jui/')) {
+ detected.push({ tech: 'Joomla', evidence: 'Joomla markers' });
+ }
+ if (body.includes('Shopify') || body.includes('cdn.shopify.com')) {
+ detected.push({ tech: 'Shopify', evidence: 'Shopify CDN' });
+ }
+
+ // Meta generator tag
+ const generatorMatch = body.match(/]*name=["']generator["'][^>]*content=["']([^"']+)["']/i);
+ if (generatorMatch) {
+ detected.push({ tech: generatorMatch[1], evidence: 'Meta generator tag' });
+ }
+
+ if (detected.length === 0) {
+ return {
+ success: true,
+ output: `Technology detection for ${url}:\nNo technologies detected from headers or content.`,
+ };
+ }
+
+ const output = detected.map(d => `• ${d.tech} (${d.evidence})`).join('\n');
+ return {
+ success: true,
+ output: `Technology detection for ${url}:\n${output}`,
+ findings: [{
+ title: 'Technologies Detected',
+ severity: 'info',
+ details: `Detected ${detected.length} technologies: ${detected.map(d => d.tech).join(', ')}`,
+ }],
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Technology detection failed: ${error instanceof Error ? error.message : String(error)}`,
+ };
+ }
+ },
+ },
+
+ // =============================================================================
+ // VULNERABILITY SCANNING TOOLS
+ // =============================================================================
+ {
+ name: 'xss_scan',
+ description: 'Test for XSS vulnerabilities (real requests with payload reflection check)',
+ category: 'vuln',
+ parameters: [
+ { name: 'url', type: 'string', description: 'URL with parameter to test', required: true },
+ { name: 'param', type: 'string', description: 'Parameter name to test', required: true },
+ ],
+ handler: async (context) => {
+ const baseUrl = context.parameters.url as string;
+ const param = context.parameters.param as string;
+
+ const payloads = [
+ { payload: '', name: 'Basic script tag' },
+ { payload: '
', name: 'IMG onerror' },
+ { payload: '">