01 / THE CONSOLE
Using the console
uv run kiyooo serve starts the FastAPI backend on :8000; cd web && npm run dev
starts the Next.js console on :3000, proxying /api/* to it — one origin for the whole
product. There's no auth yet, so run it behind a trusted network. All 17 pages live in one flat sidebar nav,
grouped here only for the tour:
Recon & scope
Change feed (the root page) — ASSET_NEW/PORT_OPENED/TAKEOVER_RISK and friends, newest first.
Scope — orgs, seeds, exclusions; the DB-backed twin of hand-editing scope.yaml.
Ingest — a config-driven connector for any REST/JSON ASM/VM tool you already pay for, plus the unmapped-vendor-type backlog.
Attack surface
Cloud posture · Containers · Repos · Mobile — each pairs an account/scan-target form with a real scanner (Prowler, Trivy, TruffleHog, apktool+TruffleHog).
AI attack surface — the native fingerprint categories: MCP servers, inference endpoints, vector stores, model registries, notebooks, shadow AI tenants.
Attack paths — correlated chains from a weakness to a sensitive resource, computed on demand, not stored.
Triage & assets
Triage queue — verdict + evidence + Agree/Disagree in one click; select rows to bulk-assign an owner or resolve several at once.
Asset explorer — searchable inventory; click through to an asset's ownership and graph edges.
Coverage · Scan runs · Categories — orphan-asset reporting, a per-run audit trail, and a live "would this match" predicate tester.
Console
Model configuration — pin bulk/escalation/embedding roles to Ollama, Anthropic, or any OpenAI-compatible endpoint; test a connection before pinning.
Settings — turn optional modules on/off; never touches data already ingested.
Dashboard — the triage funnel, model/human agreement by category, and LLM spend.
Try it without running anything
The product page has the real thing embedded, live-clickable — same nav, same on-page copy, same CSS tokens as the real app, populated with example data — no server, no Docker, no network calls, and nothing shown is live. Open it full-page if you'd rather not scroll.
02 / THE CLI
Using the CLI
Every command below actually ran against the real database, LLM, and detection engine while writing this page — not copied from a spec. First run:
# setup + a health check that touches your own infra only, no target uv sync cp .env.example .env make dev # docker compose up -d + alembic upgrade head uv run kiyooo doctor # DB / redis / minio / LLM provider / org-context — all green before anything else
Then the full loop, one org, start to a drafted ticket:
kiyooo org create acme --name "Acme Corporation" --created-by you@acme.example kiyooo seed add --org acme --domain acme.example --added-by you@acme.example kiyooo seed add --org acme --wildcard "*.acme.example" --added-by you@acme.example echo acme.example > seeds.txt kiyooo scan --seeds seeds.txt --org acme --profile standard # -> prints a scan_run_id, call it $RUN kiyooo detect --scan-run $RUN # category + control predicates — pure logic, no LLM call kiyooo triage --scan-run $RUN # LLM adjudication over every NEW finding, evidence-cited kiyooo route run --scan-run $RUN # resolves owner + SLA, drafts/files per the autonomy ladder kiyooo route approvals list # see what's drafted before anything sends kiyooo route approvals send <id> --reviewer you@acme.example
Confirmed live in this exact run: on a freshly created org with no --active flag, the
httpx/tlsx probe stage still executes but every check comes back
DENY from ScopeGuard — safely denied, not silently skipped. That's invariant #3,
not a documentation claim.
A human can skip the CLI for the review step entirely — it's the same call the console's Agree/Disagree buttons make:
kiyooo review <finding-id> --verdict true_positive \ --rationale "confirmed via console access" --reviewer you@acme.example
And see exactly what a scan would touch before it touches anything:
kiyooo scan --explain # every adapter, what it reads, its rate cap — no network touched kiyooo scan --seeds seeds.txt --dry-run # prints the exact commands, runs none of them kiyooo categories test # every category's should/should-not-match fixtures — 95 passed, 0 failed
The full reference — every command group (scope, scan, triage, route, ingest, enrich, categories, eval, feedback, metrics, audit) with every flag — is CLI.md in the repository, kept in sync with --help by policy.
03 / FOUNDATION
Architecture
An org-context repository loads at the start of every run. It is PR-reviewed and contains scope.yaml, categories/*.yaml, controls.yaml, and teams.yaml. That context governs every downstream decision — a malformed category fails loudly, never silently skipped.
A scheduler triggers the recon orchestrator, a staged DAG that shells out to existing open-source tools — subfinder, amass, dnsx, httpx, naabu, katana, nuclei, tlsx — plus Shodan, Censys, and CT-log APIs for passive discovery. Five further domain modules wrap Prowler, Trivy, TruffleHog, and apktool for cloud, containers, source code, and mobile. Discovery is orchestrated, not reimplemented.
Raw output lands in an evidence store (MinIO), then becomes an asset graph in Postgres. Immutable per-run snapshots feed a diff engine that emits changes such as ASSET_NEW, PORT_OPENED, TAKEOVER_RISK, and DECOMMISSION_CANDIDATE. A change feed is more useful operationally than a static inventory.
A parallel external finding import path — a generic, config-driven REST/JSON connector, plus hand-written Tenable/Mandiant-style adapters — maps output from tools an org already pays for onto the same schema. Their severity is stored as an attribute, never inherited; kiyooo re-derives its own verdict through rules and triage.
04 / BUILD
Tech stack
| Layer | Choice | Reason |
|---|---|---|
| Core | Python 3.12 + asyncio | Workers are subprocesses anyway — orchestration benefits most from async I/O. |
| Data | Postgres 16 + pgvector | Graph relationships and embeddings without a second datastore. |
| Queue | arq + Redis | Small, async-native job execution. |
| Object store | MinIO (S3-compatible) | Raw evidence, response bodies, screenshots. |
| API | FastAPI + Pydantic v2 | Schema types stay aligned with structured model output. |
| Console | Next.js 15 + React 19 + TypeScript | Plain CSS, no framework dependency — one custom stylesheet, no build-step surprises. |
| Models | Ollama / Anthropic / any OpenAI-compatible endpoint | Local by default, with an explicit bring-your-own-model escalation path. |
| Config | YAML in git, Pydantic-validated | Reviewable, diffable, and auditable. |
05 / DECISIONING
Model routing policy
Bulk pass uses a small local model for the majority of findings with a clear verdict.
Escalation pass uses a stronger model when confidence is low, severity is high or critical, or a category is marked always_escalate.
Prompts and model digests are pinned artifacts — a pin is always a new row, never an edit. A model change is treated as a change to detection logic.
Every version must clear an eval harness measuring precision, recall, and critical-miss rate on a labelled corpus before it ships.
06 / BOUNDARIES
Design commitments
- The model never invents facts. Every claim cites an evidence ID present in the bundle; uncited claims are rejected by the validator.
- Evidence is untrusted input. Response bodies and certificate fields are data, never instructions — an instruction found inside evidence is a finding to report, not a command to follow.
- The model never sends packets. It requests verification through a typed call; a scope-enforcing executor performs it, passive-only unless explicitly authorized.
- Scope is a hard boundary. Out-of-scope targets are blocked at the network executor layer, not a config hint.
- Suppression is explicit. False-positive reasoning is stored with the finding and is reversible, never a silent drop.
- Nothing reaches an owner unread. Outbound routing has an approval gate that relaxes only as agreement earns it, per category.
07 / RECORDS
Data model, briefly
findingStable fingerprint survives rescans; a status that only ever moves forward through review, never closed by a model.
verdictModel, confidence, reasoning, a business_impact_hypothesis, and citations — one row per adjudication pass.
human_reviewReviewer, agreement, rationale — this is where a business justification for accepting or overriding a call actually lives.
ownershipMulti-source, confidence-scored — cloud tag, IaC, CODEOWNERS, DNS audit, or a manual override.
approvalThe human gate on every outbound ticket, email, or Slack draft before it sends.
model_pinA pinned model + prompt version, changed only through an eval run and a changelog entry.
08 / OPERATIONS
Autonomy is earned, not granted
Every category begins held for a human. A critical miss demotes that category one step and files an incident.
| Level | Behaviour |
|---|---|
| Shadow | Verdicts computed and stored; nothing sent. |
| Draft for approval | Every outbound message drafted and held for a named human to send. |
| Auto-file | Files automatically once a category has earned it through sustained agreement — sampled audit continues. |
09 / SCOPE
Non-goals
- Not a pentest tool: no exploitation, writes, DoS, or credential brute force.
- Not a scanner rewrite: it orchestrates nuclei, Prowler, Trivy, TruffleHog and others rather than replacing them.
- Not a compliance report generator, although the data can support one later.
- Not multi-tenant SaaS in v1: single organisation, self-hosted.
Current status
The repository is public and MIT-licensed. This page and the product page reflect the same code, tests, and labs referenced throughout — not a separate demo build.