Architecture
Hexagonal design, full package map, module anatomy, and how to add a new module or rule.
Design pattern
ANNÁVE CLI uses hexagonal architecture (ports and adapters). The domain core of each module — its data models and logic — has no knowledge of Cobra, terminal output, or any specific external API. All delivery and infrastructure concerns live in adapters that connect to the core through formal interfaces (ports).
┌──────────────────────────────────────────────────────────┐
│ CLI layer — cmd/annave/cmd/ │
│ flag parsing, output formatting, error display │
└─────────────────────┬────────────────────────────────────┘
│ calls port interface
▼
┌──────────────────────────────────────────────────────────┐
│ Domain — internal/<module>/domain/ │
│ data models, value types, no external dependencies │
│ │
│ Port — internal/<module>/port/ │
│ one interface per module, implemented by the adapter │
└─────────────────────┬────────────────────────────────────┘
│ implemented by
▼
┌──────────────────────────────────────────────────────────┐
│ Adapter — internal/<module>/<adapter>/ │
│ external calls: k8s API, AWS SDK, filesystem, shell │
└──────────────────────────────────────────────────────────┘Package map
annave.tech/cli
├── cmd/annave/
│ ├── main.go
│ └── cmd/
│ ├── root.go ← root command, version subcommand
│ ├── log.go / log_analyze.go
│ ├── health.go / health_check.go
│ ├── doc.go / doc_search.go
│ ├── cleanup.go / cleanup_scan.go
│ ├── security.go / security_audit.go
│ ├── infra.go / infra_validate.go
│ └── cost.go / cost_scan.go
├── internal/
│ ├── shared/
│ │ ├── errors/ ← AnnaveError, error codes, stage constants
│ │ ├── output/ ← Printer, Format, plain/json/table rendering
│ │ └── config/ ← embedded limits.yaml and messages.yaml
│ ├── log/
│ │ ├── domain/ ← LogEntry, Anomaly, Finding, AnalysisReport
│ │ ├── port/ ← Analyzer interface
│ │ └── analyzer/ ← parser, patterns, spikes, clusters, ranker
│ ├── health/
│ │ ├── domain/ ← ServiceTarget, HealthResult, HealthReport
│ │ ├── port/ ← Checker interface
│ │ └── checker/ ← http, tcp, dns, chain, runner
│ ├── doc/
│ │ ├── domain/ ← DocFile, SearchResult, Index, SearchQuery
│ │ ├── port/ ← Searcher interface
│ │ └── searcher/ ← indexer, search, open, formats
│ ├── cleanup/
│ │ ├── domain/ ← K8sResource, IdleResource, CleanupPlan
│ │ ├── port/ ← Scanner interface
│ │ └── scanner/ ← client, pods, pvcs, configmaps, namespaces, dryrun
│ ├── security/
│ │ ├── domain/ ← Finding, Severity, AuditType, AuditReport
│ │ ├── port/ ← Auditor interface
│ │ └── scanner/
│ │ ├── rules/ ← shared Rule type, severity constants
│ │ ├── secrets.go ← 12 secret patterns
│ │ ├── sast_go.go ← 10 Go SAST rules
│ │ ├── sast_ts.go ← 5 TypeScript SAST rules
│ │ ├── k8s_live.go ← live cluster checks via client-go
│ │ ├── k8s_local.go ← local YAML manifest checks
│ │ └── scanner.go ← orchestrator
│ ├── infra/
│ │ ├── domain/ ← ValidationIssue, ValidationResult
│ │ ├── port/ ← Validator interface
│ │ └── validator/ ← terraform, helm, k8s_yaml, validator
│ └── cost/
│ ├── domain/ ← CostRecord, CostAnomaly, CostReport
│ ├── port/ ← CostAnalyzer interface
│ └── analyzer/ ← aws (full), gcp (stub), azure (stub), analyzer (router)
└── config/
├── limits.yaml ← per-module limits, embedded at build time
└── messages.yaml ← error code → message mappingModule anatomy
Every module has the same three-layer structure.
1. Domain
Pure Go types. No imports from external packages or other internal modules (except internal/shared for primitives). This is the data contract between the port and the adapter.
type AnalysisReport struct {
File string
Format LogFormat
TotalLines int
ParsedLines int
TimeRange TimeRange
Findings []Finding
}2. Port
A single interface per module. The CLI calls this; the adapter implements it.
type Analyzer interface {
Analyze(r io.Reader, filename string, opts AnalyzeOptions) (*domain.AnalysisReport, error)
}3. Adapter
Implements the port interface. All external API calls, shell invocations, and filesystem access live here.
How to add a new module
- Create
internal/<module>/domain/domain.go— result types, no external imports - Create
internal/<module>/port/port.go— one interface - Create
internal/<module>/<adapter>/— implements the port, useinternal/shared/errorsfor all returned errors - Create
cmd/annave/cmd/<module>.go— parent command:var myCmd = &cobra.Command{Use: "mymodule"} - Create
cmd/annave/cmd/<module>_<subcommand>.go— parse--format, call the port, switch on format and render - Add configurable thresholds to
internal/shared/config/limits.yaml - Add any new error codes to
internal/shared/config/messages.yaml
How to add a security rule
Secret pattern
// internal/security/scanner/secrets.go — append to secretRules
{
ID: "SECRET013",
Title: "My Token",
Severity: domain.SeverityHigh,
Remediation: "Move to environment variable or secrets manager",
Pattern: rules.MustCompile(`mytoken_[A-Za-z0-9]{32}`),
},SAST rule
// internal/security/scanner/sast_go.go — append to goRules
{
ID: "GO011",
Title: "Insecure TLS skip verify",
Severity: domain.SeverityHigh,
Pattern: rules.MustCompile(`InsecureSkipVerify:\s*true`),
FileExts: []string{".go"},
},Error handling
All errors from adapters and the CLI layer use AnnaveError. Create them with the helpers in internal/shared/errors:
shared.InvalidInput("--since must be a duration or date")
shared.IOFailure("cannot read file")
shared.New(shared.ErrCodeParseFailed, shared.StageAnalysis, "invalid YAML")