Root context file. Discovered & written 2026-07-19. This is an exploration report for the repository plus working guidance for future sessions.
Companion docs (local working notes under
.claude/, gitignored — not published):NAVIGATION.md— codebase map (directory roles + "where is X?") ·STATE.md— current state & what's broken ·SCAN.md— scan model & scanner-plug substrate ·ROADMAP.md— re-entry plan.✅ Build status: the whole tree builds and the
aimsbinary runs. A plaingo build ./...compiles every domain, the generatedpblayer, all per-domain gRPC servers,server/transport, theclient, and all CLI packages includingcmd/aims. Themaltego/gondorblocker is gated behind amaltegobuild tag and the heavy Tailscale transport (gvisor, breaks on Go 1.26) behind atailscaletag; both are opt-in. See STATE.md → Build status for how it was unblocked.
AIMS is a shared data-model and object store for offensive-security tooling. It is specification-first, not logic-first: the repo declares the objects an attacker cares about (hosts, networks, services, credentials, scans, C2 agents/channels) and gives them first-class facilities so that many different tools can contribute to and consume the same database of the same objects.
Think of it as "MISP/STIX, but for the attacker's side" — except the emphasis is on being easy to move around, easy to store in SQL, and interoperable across languages and tools.
The README states it directly: "There is no functional logic code in the project: just types and their own facilities." (That is the aspiration; in practice there is now a thin client/server/CLI layer built on top — see State below.)
-
Battle-tested, ubiquitous data models. The schemas deliberately mirror the object models of tools people already trust:
- nmap for network/host architecture —
Host,Port,ExtraPort,OS/OSMatch,Trace/Hop,TCPSequence,Uptime,Script, etc. Many proto fields carryxml:"…"tags that map directly onto nmap's XML output so nmap results unmarshal straight into the types (seehost/pb/host.proto). - Metasploit for credentials — the
credential.Coremodel (Private / Public / Realm / Origin / Login) is lifted from Metasploit's Credential API (seecredential/pb/core.proto). - Other tools contribute their own idioms where relevant.
- nmap for network/host architecture —
-
One shared database, many contributors. Any tool can push objects in and read objects out over a common gRPC API + SQL store, working with the same object instances.
-
Two orthogonal query-scoping axes. Because many tools share one store, a query needs to be narrowable along two independent dimensions, both optional, both no-ops when empty (so callers thread them through unconditionally), and both freely composable — "services contributed by nmap, on this host" is one round trip:
- Provenance / tool — "give me only my objects".
provenance.WhereContributedByjoins through each object's*_sourcesm2m; wrapped asdb.ScopeBySource(query, joinTable, objectFK, tool)and threaded through every domain Read as aSourcefilter. - Host / subnet — "give me only the objects on this host".
db.ScopeByHost(query, joinTable, objectFK, hostFK, hostIDs)joins through a table carrying both FKs (portsfor services);host.IDsMatchingresolves a host filter value to the host ids it denotes (Id, else addresses, else hostnames), keepinginternal/dbfree of domain types. Credentials express the same axis through their own path (credential.WhereLoggedInHost, viasources.service_id -> ports.host_id). Subnet/CIDR is deliberately not implemented — addresses are free text, so containment needs a typed column or a Go-side scan; see the TODO inhost/scope.go.
A third, narrower filter rides the same "no-op when empty" convention:
Prefix, the server-side completion pushdown (HostFilters.Prefix,ReadCredentialRequest.Prefix,ReadServiceRequest.Prefix). Its invariant: the SQL filter must return a superset of what the completer renders as its candidate, so carapace's local filter narrows to exactly the right set and the pushdown never drops a valid completion — hence the extraidleg on each. - Provenance / tool — "give me only my objects".
-
One set of CLI/code utilities around these objects — to consult them, and to use them as "targets" of other tools (the
scan/target.gonotion, hosts-as-targets, etc.). -
Interoperable technology-wise. Protobuf is the source of truth (good multi-language codegen); Go is the first generated/implemented target; GORM makes the objects portable across SQL backends; struct tags make them ingest tool-native formats (nmap XML, Maltego).
The whole repo is organized around one pipeline: .proto → generated Go PB types →
generated GORM ORM types → hand-written user-facing helpers + gRPC services + CLI.
proto definitions generated code hand-written layers
───────────────── ────────────── ───────────────────
<domain>/pb/*.proto ─► *.pb.go (protoc-gen-go)
*.pb.gorm.go (protoc-gen-gorm, infoblox) ─► server/<domain>/ (gRPC CRUD services)
<domain>/pb/rpc/*.proto ─► *_grpc.pb.go (gRPC services) ─► client/ (gRPC client wrappers)
─► <domain>/*.go (native-type wrappers,
display, dedup helpers)
─► cmd/<domain>/ (cobra CLI subcommands)
Key mechanisms:
- Two representations per object, produced by
infobloxopen/protoc-gen-gorm:pb.Host— the user-facing Protobuf Go type.pb.HostORM— the GORM-storable type, withToPB(ctx)/ToORM(ctx)converters.- Services convert PB→ORM to query/write, then ORM→PB to return. See
server/host/host.gofor the canonical Read/Create pattern.
- Struct tags drive interoperability. Proto files use
// @gotags:comments (applied byprotoc-go-inject-tag) to attachxml:"…"(nmap),display:"…"(CLI columns),readonly,stricttags to generated fields. GORM relations (belongs_to,many_to_many,primary_key,type:uuid) are expressed via(gorm.field)proto options fromproto/options/gorm.proto. - DB schema =
db/schema.goMigrate(db)— one bigAutoMigrate(...)registering every*ORMtype across all domains. IDs are UUID strings; relations cascade. - Native wrapper types. Each domain root file does
type Host pb.Hostto hang Go-idiomatic helpers (display formatting, OS/CPU guessing, dedup) off the generated types without polluting the generated code. - Dedup on insert.
internal/dbprovides genericFilterNew[T]+ per-domainAreHostsIdentical/identical.gocomparators so re-importing the same scan doesn't duplicate rows.Preloadbuilds association preload clauses from a filter map.
| Domain | Dir | Core objects | Model heritage |
|---|---|---|---|
| Host | host/ |
Host, Hostname, Port, ExtraPort, OS/OSMatch/OSFingerprint, User, Group, Process, FileSystem/File, Status, Uptime |
nmap |
| Network | network/ |
Address, Service, Trace/Hop, Distance, Times, TCPSequence/IPIDSequence, packets |
nmap |
| Credential | credential/ |
Core (Private/Public/Realm/Origin), Login, passwords, hashes (NTLM/replayable/nonreplayable), keys (public/private), certificates |
Metasploit |
| Scan | scan/ |
Run, Info, Stats, Target, ScanTask, TaskProgress; nmap-specific under scan/nmap (Script, Table, Element) |
nmap et al. |
| C2 | c2/ |
Agent, Channel, Task |
Sliver-like |
Each domain follows the same layout: pb/*.proto (defs) + pb/*.pb.gorm.go (generated) +
pb/rpc/*.proto (gRPC services) + <name>.go (native helpers) at the domain root.
Built on reeflective/team (a teamserver/teamclient framework extracted from Sliver) —
gives multi-user auth, transports, and RPC plumbing for free.
cmd/aims/— theaimsbinary. Boots a teamserver, an in-process AIMS gRPC client, migrates the DB, and binds the cobra command tree (cmd/aims/root.go).server/—server.New(grpcServer, WithDatabase(db))registers a gRPC service per domain (server/host,server/credential,server/network,server/scan,server/c2). Each service is a straight PB↔ORM CRUD shim over GORM.client/—Clientstruct holds one typed gRPC client per service; connects via the teamclient.client/transporthandles the dialer/middleware.server/transport/— mTLS and Tailscale listeners (tailscale.comdep) plus middleware.cmd/<domain>/— cobra subcommands (hosts list/add,services,credentials,scan,c2 agents/channels).cmd/display/is a shared table/detail/completion/color renderer driven by thedisplay:"…"field tags and per-typeDisplayFields/DisplayHeadersmaps.cmd/export/— export objects out.
- Canonical module path is
github.com/d3c3ptive/aims. Module path, GitHub remote, and the local checkout (.../d3c3ptive/aims) are all ond3c3ptivenow — always used3c3ptiveimport paths. (One dependency,github.com/maxlandon/gondorused for the Maltego integration, is stillmaxlandon-namespaced; it is a separate repo and would need its own migration/replacement decision — the last remainingmaxlandontrace.) - Codegen config lives at the repo ROOT (not in
proto/):buf.yaml,buf.lock,buf.work.yaml,buf.gen-gorm.yaml,buf.gen-grpc.yaml,maltego-tags.sh, plus the gotemplate underproto/template/{{.File.Name|dir}}/{{.File.Name|base}}.gorm.go.tmpl(URL-encoded on disk) that emits the*.proto.gorm.goDB-helper files. Makefile:make deps— installsprotoc-gen-goandprotoc-go-inject-tag.make gen— runsbuf generate --template buf.gen-gorm.yaml(go + gorm + gotemplate plugins) thenbuf generate --template buf.gen-grpc.yaml(go-grpc), then./maltego-tags.shwhich runsprotoc-go-inject-tagover every*.pb.goto apply the// @gotags:comments (xml/display/etc.).managed.go_package_prefixis pinned togithub.com/d3c3ptive/aims.
- Building: a plain
go build ./...(orgo vet) works — all deps are pinned to published versions ingo.mod(no localreplacedirectives). If a developer has an ancestorgo.workthat excludes this module, a git-ignored localgo.work(use .) at the repo root shadows it. First build pulls a large tree (tailscale, gvisor, gRPC) — expect a slow initialgo mod download. - Go 1.24. Deps resolve from the module cache (no
vendor/present despite the README).
Solo project (Maxime Landon), 92 commits over three distinct work bursts — it has been paused for ~1 year:
| Period | Focus | Commits |
|---|---|---|
| Nov 2021 | Foundation: all proto data models + generated code (host, network, credential à la Metasploit, scan/nmap), Makefile/buf codegen, Maltego tag script | 26 |
| Jun–Aug 2023 | Client/server/gRPC layer, reeflective/team teamserver transport (mTLS + Tailscale), the generic cmd/display engine, cobra command tree |
34 |
| Aug 2024 (last) | scan RPC, host/port dedup on insert, JSON/XML import-export, c2 agents/channels, display table/detail polish | 32 |
2026-07 session (current): taking one domain at a time to full depth as a "guinea pig" — identity/dedup, merge, rich display, styled completions, CLI slice. credential and services slices are done; the scan slice is in progress. See STATE.md for the live detail.
Maturity: the model + generated layer is solid; the service/CLI layer is a vertical slice that is filling out domain by domain. Read paths work broadly; mutation (Update/Delete/Upsert) is still stubbed on most services. Per-service gRPC status:
| Service | Read/List | Create | Update/Delete/Upsert | Notes |
|---|---|---|---|---|
| host (Hosts) | ✅ | ✅ (dedup) | Upsert ✅ | reference impl. Ingest wired to the shared host.MergeHost/SameHost fold: Create is additive+idempotent (skip-if-identical), Upsert merges by field-class. Deep in-place child enrichment is DONE (saveMergedHost/saveMergedPorts write back a new NSE script / filled Service.Product / new reason inside an already-persisted port). Delete still stubbed (server/host/host.go) |
| host Users | ❌ | ❌ | ❌ | all methods stubbed |
| network Services | ✅ | ❌ stub | ❌ stub | display/CLI slice done; Read/List share one body and carry all three filters (Source, Host, Prefix); ReadHost/ListHost implemented = same services plus their hosts. Create/Upsert/Delete still stubbed (services are only ever written through host ingest) |
| credential Credentials | ✅ | ✅ | Upsert ✅ · Delete ✅ | full slice done (merge, display, completions, CLI); Delete resolves by identity when no ID given — the worked Delete example |
| credential Logins | ❌ | ❌ | ❌ | all methods stubbed |
| scan Scans | ✅ | ✅ | Upsert/Delete/List ✅ | Full CRUD. DB-level host fold (via host.IngestHosts + run_hosts join, cross-run host unification). Delete clears run_hosts so shared hosts survive; Upsert is idempotent insert-or-return-existing. CLI: list/show (new Detail renderer, runState live axis) + rm (running-scan guard via scan.IsRunning) |
| c2 Agents/Channels | ✅ | ✅ | ❌ stub | see type-name note below |
- c2 server type-name asymmetry (minor): filenames now match contents —
server/c2/agent.goimplements the Agents server (type server,UnimplementedAgentsServer,CreateAgentRequest) andserver/c2/channel.gothe Channels server (type channelServer,UnimplementedChannelsServer,CreateChannelRequest). The only residual wart is that the Agents type is the genericserverwhile the Channels type is the specificchannelServer; an optionalserver→agentServerrename would make them symmetric. (The old "file↔content swap" gotcha was stale and has been removed.) - Empty CLI handlers: some command
RunEs are still stubs (e.g.hosts add,hosts rm); the command tree/completions exist but the action does nothing yet. credential/core.goscope helpers are implemented, but onlyWhereLoggedInHostis reachable from the wire (viaReadCredentialRequest.Host);WhereOriginIs/WhereOriginServiceForHost/WhereOriginSessionForHostare still code-API-only.- Maltego
AsEntity()is inconsistent: some real (host/group.go→maltego.NewEntity), some stubbed (network/service.go→return maltego.Entity{}). - README mentions a
vendor/dir and aproto/gen/layout that don't match reality (deps come from the module cache; generated code sits next to each.proto,paths=source_relative).
Fixed since the original survey (no longer issues): the display-path debug leftovers (
println/fmt.Println/emptyif head == "Purpose") and the crossedstdoutTerm/stdinTerm/stderrTerminit()incmd/display/defaults.go.
- Finish the Update/Delete/Upsert gRPC methods across the still-stubbed services. Worked
examples to copy: credential Create/Upsert/Delete (full CRUD), scan Create/Read/List/Upsert/
Delete (full CRUD as of this session), and host Create/Upsert. Still stubs: host Delete
(scaffolding present, ends in Unimplemented at
server/host/host.go:480), network Create/Upsert/ Delete, and both c2 Upsert/Delete. The DB-level ingest fold — including deep in-place child enrichment (saveMergedHost/saveMergedPorts) — is DONE and is the shared primitive these should reuse. For scan Delete, note the run_hosts-shared-host invariant (unlink, don't delete). - Wire the remaining CLI
rmhandlers to theirDeleteRPCs (scan rmis done — reference for the ID-prefix + running-scan-guard pattern;hosts rmRunEis still a stub). - The scanner-plug substrate (SCAN.md Part C) — all genuinely absent: live/streaming scans
(
Scansis unary-only,scan run nmapblocks to completion), theIngestor/Scannerplug interfaces, the stored-Host/Service→Targetbridge, and run-to-run diff. - Optionally rename the c2 Agents server
type server→agentServerfor symmetry withchannelServer(filenames already match contents; this is cosmetic, not a blocker). - Complete the Users/Logins services (both fully stubbed).
- Decide the
maxlandon/gondordependency's fate as part of the org migration.
When extending: prefer changing the .proto and regenerating (make gen) over editing
generated *.pb.go/*.pb.gorm.go by hand; put Go-idiomatic behavior in the domain root
<name>.go files; wire new CRUD in server/<domain> + client + cmd/<domain> following
the host domain as the reference implementation.
The user-facing consumption tooling is the second big theme of the repo (alongside the data model). It is built around cobra (commands) + carapace (rich, described completions)
- jedib0t/go-pretty (tables), with one shared generic display package driving all of it.
Everything renders through one type-parameterized pattern: a map[string]func(T) string
that maps a column/field name → a value-generator for an object of type T. The same map
feeds tables, detail views, and completions — you define an object's presentation once.
Table[T](values, fields, opts...)(table.go) — builds a go-pretty table. Columns come fromoptsheaders; each cell =fields[column](value). Post-processing pipeline:removeEmptyColumns(drop columns empty on every row) → weight filtering → terminal-size adaptation (term.GetSize,getMaximumWeight,adaptTableSize) so wide tables shed low-priority columns on narrow terminals.Details[T](value, fields, opts...)(details.go) — vertical "key: value" detail view for a single object. Headers are grouped by weight, and groups are separated by blank lines, so weight doubles as a section/priority grouping mechanism.Completions[T](values, fields, opts...)(complete.go) — turns objects into carapacevalue\ndescriptionpairs. One column is the candidate (the value inserted, e.g.IDorHostnames) viaWithCandidateValue(header, fallback); the rest become the aligned description.WithSplitCandidate(sep)explodes list-valued fields (e.g. multiple hostnames) into separate candidates with a shared description.
settings.go defines the functional-options opts struct. Weight is the core layout
primitive — WithHeader(name, weight) assigns each column a weight 1–4; lower = higher
priority / shown first / shown on narrower terminals (thresholds in terminalWeightSizes:
1→80 cols, 2→160, 3→240, 4→320). In tables weight controls responsive column dropping; in
details it controls section grouping. Other options: WithStyle, WithAutoSmallID /
FormatSmallID (truncate UUIDs to 8 chars), WithCandidateValue, WithSplitCandidate.
Each domain root file (e.g. host/host.go) owns its presentation contract:
DisplayFields— themap[string]func(*pb.Host) stringvalue-generators. This is where domain display logic lives: OS/CPU guessing from nmap matches, colored/up-state IDs, route/hop rendering, joining repeated fields with newlines.DisplayHeaders()/DisplayDetails()/Completions()— return the weighted[]Optionsheader sets for table / detail / completion contexts respectively.
AIMSDefaultis the default table style: borderless, no row separators, header underlined with=,FormatTitleheaders — a clean minimal look.AIMSBordersDefaultis a bordered+/-/|alternative; several go-pretty styles are also registered by name.- Raw ANSI SGR escape constants (
Bold,Dim,FgYellow, 256-colorFmt(Fg+"214"), …) are defined directly rather than only viafatih/color. Detail field names get a gray-bg/orange-fg chip (colorDetailFieldName), values are bold.
Each domain exposes a Commands(client) *cobra.Command returning a subtree
(list / add / rm / show / import / export). Reference impl: cmd/hosts/hosts.go.
list→client.<Svc>.Read(...)thendisplay.Table(res, host.DisplayFields, host.DisplayHeaders()...).show→ filters by ID prefix, thendisplay.Details(...); a--tracerouteflag appends aRoutecolumn at weight 4.- Completions (
CompleteByID,CompleteByHostnameOrIP) are carapaceActionCallbacks that connect viaclient.ConnectComplete(),Readfrom the server, and feed the objects throughdisplay.Completions(...)— i.e. completions are live DB queries, and reuse the exact sameDisplayFieldsmap as the tables. - Helper plumbing in
cmd/commands.go:BindGroup(attach a domain's commands under a help group),BindFlags,CompleteFlags,CheckError(unwrap gRPC status → plain error). cmd/export/provides reusableimport/exportsubcommands that marshal objects via JSON/XML using protobuf reflection, hooked into each domain command.
Top-level assembly: cmd/aims/commands.go bindCommands groups everything into two help
groups — "database" (hosts, credentials, services, scan) and "command & control"
(agents, channels). bindRunners walks the tree and attaches the client-connect pre-run to
leaf commands (so completions/commands lazily connect to the teamserver only when needed).
CLI-layer state note: the design/engine is solid and reusable, and the credential/services slices exercise it fully (grouped tables,
Banner+Columns+KVLinesdetail views, styled completions). Some per-command handlers are still stubs (hosts add/rmRunEreturnnil).
Design intent — sub-categorized completions (wanted, not yet built). When completing some objects, we want the candidate list to convey sub-categories rather than one flat set — e.g. "close" objects vs. atypical ones (local targets/private IPs, loopback), recently seen vs. stale, on-subnet vs. off-subnet. Convey this either via carapace tag groups (
carapace.ActionValues(...).Tag("local targets"), so candidates render under labelled headings) or via deliberate list ordering (most-relevant first). This is a standing preference: whenever you touch or are asked to write a completion function (theCompleteBy*ActionCallbacks that feeddisplay.Completions), consider whether its candidates split into meaningful sub-groups and reflect that in tags/order. Not a task to chase down proactively — apply it opportunistically when a completion is already in hand.
- Every source file carries the GPLv3 header block.
- UUID string primary keys;
CreatedAt/UpdatedAttimestamps on most objects. display:tags +cmd/display= the single source of truth for CLI rendering.xml:tags = nmap-XML ingestion contract; keep them accurate when touching host/network.