Skip to content

dasokkk/CL1201

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CL-1201

A command-line pentest orchestrator and reporting tool. It sits on top of your existing offensive stack — BloodHound, Nmap, Certipy, Nuclei, NetExec, Hashcat, enum4linux-ng — ingests their output into a unified state model, runs detection rules over the state, and produces client-deliverable PDF reports with or without an AI writer.

CL-1201 is not a C2, an implant, or a loader. Operators bring their own offensive stack; CL-1201 captures, correlates, and reports.


Why CL-1201

Operator workflow on a typical AD engagement spreads across BloodHound, NetExec, Hashcat, Certipy, Nuclei, and a handful of one-off scripts. Findings live in screenshots and ad-hoc notes. Reports get stitched together by hand at the end. The crown-jewel cost on most engagements isn't recon or exploitation — it's the 2-to-5 days the report eats up after the active work is done. That's the billable hours you can't bill, and the weekends you don't get back.

CL-1201 closes that gap. Three things separate it from existing reporting tools:

1. Report time cut 60–70%

Findings are typed objects with structured boilerplate prose — description, evidence, remediation, references, MITRE techniques. Jinja2 + WeasyPrint render the final PDF directly from the state DB; the boilerplate library handles the long-form prose that gets retyped per engagement today. The first engagement is faster than your current process. The tenth is dramatically faster, because the library compounds.

2. NDA-safe AI — the LLM never sees client data

Every red team shop wants AI help on reports. Most can't use it because client hostnames, IPs, SIDs, and usernames crossing the wire to a cloud LLM is a contract violation. CL-1201 solves this at the wire.

Real entity values — FQDNs, IPs, SIDs, usernames, SPNs — are swapped for engagement-scoped opaque IDs (HOST_017, USER_042) before the HTTPS request to Anthropic or OpenAI. The LLM writes the report against anonymised data; CL-1201 rehydrates the real values locally for operator display. The AI provably never touches client data.

cl report ai produces a full engagement deliverable — executive summary, attack narrative, per-finding detail, remediation, root cause — written against a fixed blueprint derived from three real red-team reports. Not AI slop; grounded in your actual findings data.

No API key required to evaluate. --dry-run writes the redacted prompt to a file. Paste it into any chat UI on a personal account, save the reply, feed it back with --from-response. End-to-end run without touching a paid API.

3. Bring your own offensive stack

CL-1201 is not a C2, not an implant, not a loader, and not a replacement for the tools in your kit. It ingests output from BloodHound, Nmap, Certipy, Nuclei, NetExec, Hashcat, and enum4linux-ng, correlates findings across them, and produces the report. Keep every tool you already use. CL-1201 is the layer that turns their JSON into a deliverable.


Re-test in hours, not days

Re-running an adapter against a remediated environment surfaces which rules still fire. Per-finding verified-fix proof blocks carry the evidence forward into the deliverable automatically.

Junior operators contribute safely

Approval gates hold destructive actions behind a senior-approver flow without blocking read-only triage. The session-leader model prevents conflicting concurrent writes without needing a server in the loop.


Sample report

Generated end-to-end by cl report ai against the synthetic INITECH engagement bundled in examples/initech/. No manual editing.

Screenshot of page 3 Sample report — page 3


Install

Requires Python 3.11+. Not yet on PyPI — install from source:

git clone https://github.com/dasokkk/cl-1201.git
cd cl-1201
pip install .

Or with uv:

uv pip install .

For C2 integrations (extras defined in pyproject.toml):

pip install ".[c2-sliver]"
pip install ".[c2-mythic]"

Editable install for development:

pip install -e ".[dev]"

Quick start — end-to-end PoC against the bundled demo

examples/initech/ ships a self-consistent fake engagement: one domain, one set of users and hosts, seven tool outputs (BloodHound, Nmap, Nuclei, NetExec, Certipy, Hashcat, enum4linux-ng). Same identifiers across every file, so the rule engine surfaces a coherent attack chain rather than disconnected items.

# 1. Create an engagement
cl init demo --client "Initech, Inc."
cd demo

# 2. Ingest the bundled synthetic outputs (set EX to your local checkout path)
EX=../examples/initech
cl ingest run bloodhound       $EX/bloodhound
cl ingest run nmap             $EX/nmap_internal_scan.xml
cl ingest run nuclei           $EX/nuclei_web_scan.jsonl
cl ingest run netexec          $EX/netexec_smb_sweep.jsonl
cl ingest run enum4linux-ng    $EX/enum4linux_dc01.json
cl ingest run certipy          $EX/certipy_dump.json
cl ingest run hashcat          $EX/hashcat.potfile

# 3. Run detection rules + render the deterministic report
cl findings run
cl report build --format html

Open the generated HTML under reports/. You should see kerberoastable accounts, Log4j RCE, ESC1 / ESC6 ADCS misconfig, SMB null session, cracked-password reuse, and stale privileged accounts — all linked back to the seed Log4j foothold on WEB01.

PDF note. --format pdf needs WeasyPrint's native deps (GTK / Pango / Cairo). On Linux: sudo apt install python3-weasyprint. On Windows, see the WeasyPrint first-steps docs. HTML output works everywhere.

AI report (manual path — no API key needed)

# 1. Dump the redacted prompt to disk
cl report ai --all-findings --all-files --dry-run prompt.txt

# 2. Paste prompt.txt into Claude / ChatGPT / your LLM of choice, save the reply

# 3. Feed the reply back — render straight to PDF
cl report ai --from-response reply.md --format pdf -o report.pdf

AI report (automated path — API key configured)

# engagement.yaml
ai:
  mode: anonymized
  task_backends:
    report-narrative: anthropic:claude-opus-4-7
cl report ai --all-findings --all-files --format pdf -o report.pdf

Architecture

                 ┌─────────────────────────┐
   tool output   │   ingest adapters       │   normalize → entities + relationships
   (JSON/XML)    │   bloodhound, nmap,     │
          ─────► │   certipy, nuclei,      │
                 │   netexec, hashcat,     │
                 │   enum4linux-ng         │
                 └────────────┬────────────┘
                              │
                              ▼
                 ┌─────────────────────────┐
                 │   per-engagement state  │
                 │   state.db  (SQLite)    │  ◄── append-only telemetry.jsonl
                 │   loot/     (AES-GCM)   │       round IDs, actor IDs, payloads
                 └────────────┬────────────┘
                              │
                 ┌────────────┴────────────┐
                 │                         │
                 ▼                         ▼
    ┌─────────────────────────┐  ┌─────────────────────────┐
    │   rule engine           │  │   AI report engine      │
    │   → ProposedFinding[]   │  │   anonymizer proxy      │
    │   AD / ADCS / web /     │  │   blueprint x (fixed)   │
    │   network / ops rules   │  │   grounding assembler   │
    └────────────┬────────────┘  └────────────┬────────────┘
                 │                             │
                 └──────────────┬──────────────┘
                                ▼
                 ┌─────────────────────────┐
                 │   report builder        │   Jinja2 / Markdown → WeasyPrint PDF
                 │   deterministic PDF     │   3-tier override chain:
                 │   AI narrative PDF      │   engagement › firm theme › builtin
                 └─────────────────────────┘

Per-engagement directory:

engagements/<name>/
├── engagement.yaml      config (scope, adapters, profile, AI mode, operators)
├── state.db             SQLite, WAL mode, FK on
├── telemetry.jsonl      append-only event log
├── loot/                AES-256-GCM blobs + salt.bin (Argon2id KDF)
├── reports/             generated PDFs / HTML
└── templates/           per-engagement boilerplate overrides

Commands

Command What it does
init Create a new engagement directory
ingest Run tool adapters that parse and store tool output
findings Evaluate rules against state, manage proposed findings
report build Render the deterministic boilerplate report (PDF or HTML)
report preview Alias for report build --format html
report ai Generate a full report via cloud LLM, modelled on the real-world blueprint
loot Encrypted credential store — unlock, add, list, show, export
boilerplate Manage reusable report boilerplate snippets
state Query the engagement SQLite state DB directly
verify Re-test mode: flag confirmed findings for re-check
approval Approval workflow for findings before they hit the report
leader Transfer session-leader role between operators
run Invoke a raw tool action
ask LLM-backed planner — describe a task, get structured next steps
listen Subscribe to C2 events (Sliver / Mythic)
serve Read-only HTTPS mirror of engagement state for the team
dash Live operator dashboard
snapshot Pre-migration state snapshots
log Query the engagement telemetry log
migrate Run DB schema migrations
completion Generate shell completions (bash, zsh, fish, PowerShell)

Global options (work on every command):

-e, --engagement PATH   Engagement directory. Overrides CL_ENGAGEMENT and cwd walk-up.
-a, --actor TEXT        Your handle for telemetry. Defaults to OS user.
-v, --verbose           Full Python tracebacks on errors.
    --no-color          Disable ANSI (also honours NO_COLOR env).
    --json              Machine-readable JSON output.
    --version           Print version and exit.

report ai options

--findings TEXT        Comma-separated finding IDs. Interactive picker if omitted.
--files TEXT           Comma-separated loot IDs. Interactive picker if omitted.
--all-findings         Include every finding without prompting.
--all-files            Include every loot item (metadata only) without prompting.
--archetype TEXT       Shape hint: narrative, findings, or technical.
--dry-run PATH         Write the redacted prompt to disk; skip the backend entirely.
                       In anonymized mode also writes <path>.mapping.json.
--from-response PATH   Skip prompt building; rehydrate this LLM reply file and render.
--mapping PATH         Entity mapping from --dry-run. Only valid with --from-response.
--format / -f TEXT     Output format: md (default), html, or pdf.
                       Inferred from --output extension if omitted.
-o, --output PATH      Output path. Defaults to reports/<name>-ai-<ts>.<ext>.

Supported adapters

Tier 1 — default enabled, metadata only:

Adapter Input format What lands in state
bloodhound SharpHound JSON (BH CE v4+) Domains, users, hosts, groups, OUs, GPOs and edges
nmap XML Hosts with port/service attributes; NSE script results
certipy Certipy JSON Certificate templates and CAs for ADCS rules
nuclei Nuclei JSON Per-template findings tagged by severity + CVE/CWE

Tier 2 — opt-in, handle secrets:

Adapter Input format What lands in state
netexec Normalised JSON Auth results, lifted credentials and hashes (loot)
hashcat Normalised JSON Cracked passwords; updates linked hash validation status
enum4linux_ng Normalised JSON SMB signing, anonymous session flags, share/user enumeration

Tier 2 adapters require --force-tier2 and either an unlocked loot session or --loot-passphrase-stdin. Plaintext credentials never touch the telemetry log.


Multi-operator

# Operator with write access shares a read-only view
cl serve --port 8443 --cert cert.pem --key key.pem

# Transfer write access to another operator
cl leader transfer --to teammate-handle

cl listen subscribes to a live Sliver or Mythic event stream and feeds new data into engagement state in real time.


Repo layout

cl-1201/
├── cl1201/
│   ├── cli/             typer subcommands
│   ├── core/            engagement context, errors
│   ├── config/          pydantic schemas for engagement.yaml
│   ├── ingest/          adapter registry and 7 built-in adapters
│   ├── state/           SQLite store, migrations, telemetry writer
│   ├── findings/        rule engine and rules
│   ├── loot/            encrypted blob store and session
│   ├── anonymizer/      entity redaction + rehydration proxy
│   ├── planner/         LLM task dispatch, including report-narrative
│   ├── report/          IR, builder, templates, boilerplate, AI renderer
│   └── telemetry/       append-only JSONL writer
├── SampleReports/x/     fixed real-world blueprint (style-spec + exemplars)
├── examples/initech/    self-consistent synthetic engagement (7 tool outputs + sample-report.pdf)
├── docs/                getting-started + adapter / rule / boilerplate authoring guides
├── tests/               mirrors the package layout (1149 tests)
│   └── fixtures/        hand-crafted JSON/XML for adapter tests
├── pyproject.toml
├── README.md
└── LICENSE              MIT

License

MIT — see LICENSE.



How it works

This section is for people who want to understand the internals — whether to extend the tool, contribute, or just satisfy curiosity. You don't need any of this to use CL-1201.


The engagement directory

cl init creates a directory that acts as the single source of truth for one engagement. Everything — state, loot, telemetry, templates — lives inside it. You can zip it up, share it, or archive it and nothing is lost.

engagement.yaml holds the operator-editable config: client name, scope (IP ranges, domains), which adapters are enabled, which report profile to use, AI mode and backends, which operators are registered. Most commands read it at startup to build an EngagementContext object that flows through the entire call stack.


State DB

state.db is a plain SQLite database opened in WAL mode with foreign keys enabled. The schema is versioned — cl migrate applies pending migrations and cl snapshot creates a checkpoint before any breaking change.

The entity model is flat: hosts, services, users, groups, computers, certificate templates, and so on are all rows in typed tables. Relationships (edges) are separate rows with from_id, to_id, and a type column. This mirrors the BloodHound graph model but stays relational so rules can use SQL rather than graph traversal for most queries.

cl state query runs raw SQL against the DB. Useful for ad-hoc exploration during an engagement.


Ingest pipeline

When you run cl ingest run <adapter> <file>:

  1. Lookup — the adapter name is matched against the registry (built-ins + any YAML manifests in the engagement's adapters/ directory).
  2. Parse — the adapter reads the file and extracts IncomingEntity objects with a type, attributes, and optional relationships.
  3. Normalise — entities map to the internal schema via attribute mappings defined in the adapter.
  4. Upsert — entities and relationships go into state.db via EngagementContext. Duplicates are merged; telemetry records the round ID and actor.
  5. Result — counts of created / updated / skipped records come back to the CLI.

Built-in adapters are Python classes subclassing ToolAdapter. For cases where Python isn't needed, the DeclarativeAdapter lets you define the same thing in YAML:

adapter: my_tool
input_format: json
entities:
  - type: Host
    source: $.hosts[*]
    attrs:
      ip: $.address
      hostname: $.name
relationships:
  - from: Host
    to: Service
    via: $.open_ports[*]
    attrs:
      port: $.port
      protocol: $.protocol

Drop this file in adapters/ and cl ingest run my_tool output.json picks it up automatically. The YAML manifest is compiled once on startup and runs through the same code path as the built-ins.


Findings engine

cl findings run evaluates every registered Rule class against the current engagement state. Each rule is a self-contained class that:

  1. Reads from state.db via EngagementContext (some rules also read from the loot store if unlocked).
  2. Applies its detection logic.
  3. Returns zero or more ProposedFinding objects.

Rules are organised by category under cl1201/findings/rules/:

Category Examples
ad/ Kerberoastable accounts, AS-REP roastable, unconstrained delegation, stale DA accounts, weak ACLs / DCSync
adcs/ ESC1 (enrollee supplies subject), privilege lift via certificate
network/ Weak SMB signing, NFS exports, anonymous LDAP, RPC anonymous enumeration, out-of-scope services
web/ Nuclei vulnerability lift
ops/ Classifier fallback rate

After cl findings run, proposals sit in state.db with status proposed. You triage them with cl findings triage <id> --status confirmed|dismissed|needs-evidence, run them through cl approval, and they flow into the report.

Each confirmed finding links to a boilerplate YAML that carries the long-form prose — description, background, remediation, references, MITRE ATT&CK technique. This is what cuts the report writing time.


Loot store

The loot store is a separate encrypted SQLite database. The encryption uses:

  • Argon2id (t=4, m=64 MiB, p=2) for key derivation from the operator passphrase
  • AES-256-GCM with a per-blob 96-bit nonce for encryption at rest

The loot DB is unlocked with cl loot unlock at the start of a session. It auto-locks after a configurable idle timeout. SIGINT / SIGTERM trigger an immediate scrub of the in-memory key.

Tier 2 adapters (NetExec, Hashcat) write lifted credentials directly into the loot store during ingest. Plaintext passwords and hashes never appear in telemetry.jsonl or the main state DB.


Report pipeline

Deterministic path — cl report build

  1. Builds an IR — loads confirmed findings, boilerplate prose, engagement metadata, and evidence blobs from the DB into a Python object tree (the intermediate representation).
  2. Renders HTML — passes the IR through a Jinja2 template. Templates follow a 3-tier override chain: engagement-local templates/ wins over a firm-wide theme directory, which wins over the builtin default.
  3. Converts to PDF — pipes the HTML through WeasyPrint. The --format html flag stops after step 2.

Two report profiles are available out of the box: concise (executive-friendly, shorter) and exhaustive (full technical detail, appendices, evidence). You can define custom profiles in engagement.yaml.

AI path — cl report ai

  1. Selection — operator picks which findings and loot items to send (interactive tick-box picker, --all-findings, or --findings ID,ID). Loot metadata only — blob contents are never sent.
  2. Grounding — selected findings and entity attributes are assembled into a structured Markdown block: finding titles, severities, MITRE techniques, affected entities, and file labels.
  3. Anonymization — in anonymized mode, an AnonymizingProxy wraps the LLM backend. redact() swaps real values (hostnames, IPs, usernames, SPNs) for opaque IDs outbound. rehydrate() reverses them on the response.
  4. Blueprint — the system prompt is built from the fixed blueprint x (style-spec + five real-world exemplar sections derived from professional red-team reports). This is what stops the LLM from writing generic slop.
  5. Generation — the LLM writes a full report: executive summary, attack narrative, per-finding sections, remediation, root cause.
  6. Render — the Markdown response is piped through markdown-it-py → WeasyPrint and written as .md, .html, or .pdf.

The --dry-run / --from-response flags let operators run this entire pipeline manually through any chat UI — no API key required.


Anonymizer

The anonymizer is a thin proxy around any LLM backend. It maintains an EntityMapping — a two-way dict between real values and stable IDs (HOST_017, USER_042, SPN_003). The mapping is built from the engagement state DB at dry-run or generation time and saved alongside the prompt.

Sensitive attribute keys covered: fqdn, ip, hostname, sid, samaccountname, spn, dns_name, url.

In raw mode the proxy is not installed and real values flow through. In offline mode no backend is called; --dry-run still works in offline mode because it never reaches the network.


Multi-operator and session state

Session leader. One operator owns the write-capable DB connection. This isn't a lock — it's a coordination primitive. cl leader transfer --to <handle> hands ownership to another registered operator. The intent is to avoid conflicting concurrent writes without needing a server in the loop.

Read-only mirror. cl serve starts a local HTTPS server (self-signed or supplied cert) that exposes read-only engagement state over a simple API. Team members query it without needing a local copy of state.db. cl dash consumes the same API for its live view.

C2 event stream. cl listen subscribes to a Sliver or Mythic event stream. New implant callbacks, credential material, and host data feed directly into engagement state in real time, skipping the manual ingest step for live operations.

LLM planner. cl ask takes a free-text task description, serialises relevant engagement context (findings so far, scope, discovered hosts), and sends it to a configured LLM endpoint. The response comes back as structured next steps. Useful for "what should I check given these BloodHound results" during an engagement.


Telemetry

Every significant action appends a JSON event to telemetry.jsonl. Each event includes a timestamp, the actor handle, the command, and relevant metadata (entities written, findings proposed, rules run, etc.).

This log is local — nothing phones home. Its purpose is the "who did what and when" section of the final report and post-engagement review. cl log queries it with filters for actor, time range, command type, and event kind.


CLI internals

The CLI is built with Typer (which wraps Click). Entry point: cl1201.cli.main:app. Each command group is a separate typer.Typer() instance registered as a subapp on the root app.

Global state — engagement path, actor handle, verbose/color/JSON flags — is collected by the root() callback and stored in a CliContext object on ctx.obj. Every subcommand receives it via the Typer/Click context chain.

Error handling is centralised in main(). CL1201Error subclasses render a clean one-line user_message to stderr rather than a traceback. --verbose adds the full traceback for debugging. The separation means operators get actionable hints by default and developers get full detail when they need it.


Writing a custom adapter

If the YAML DSL covers your tool's output format, drop a manifest in adapters/ — no further registration needed.

If you need custom logic, subclass ToolAdapter:

from cl1201.ingest.base import ToolAdapter, IngestResult
from cl1201.core.engagement import EngagementContext

class MyToolAdapter(ToolAdapter):
    name = "my_tool"

    def ingest(self, path: str, ctx: EngagementContext) -> IngestResult:
        # parse path, write entities via ctx, return counts
        ...

Register it by calling register_adapter(MyToolAdapter) before the CLI starts, or put it in a plugin package that declares the cl1201.adapters entry point.


Writing a custom rule

Subclass Rule in cl1201/findings/rules/ (or in a plugin):

from cl1201.findings import Rule, ProposedFinding
from cl1201.core.engagement import EngagementContext

class MyRule(Rule):
    id = "my_rule"
    title = "My custom finding"

    def evaluate(self, ctx: EngagementContext) -> list[ProposedFinding]:
        rows = ctx.state.execute("SELECT ...").fetchall()
        return [ProposedFinding(rule_id=self.id, ...) for row in rows if ...]

Rules are auto-discovered — any Rule subclass in the rules package is registered at startup. Pair the rule with a boilerplate YAML in cl1201/report/boilerplate/findings/ carrying the prose (severity, MITRE ID, description, remediation, references) and the finding renders correctly in the report without any additional wiring.

Releases

No releases published

Packages

 
 
 

Contributors