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).

text
┌──────────────────────────────────────────────────────────┐
│  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

text
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 mapping

Module 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.

go
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.

go
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

  1. Create internal/<module>/domain/domain.go — result types, no external imports
  2. Create internal/<module>/port/port.go — one interface
  3. Create internal/<module>/<adapter>/ — implements the port, use internal/shared/errors for all returned errors
  4. Create cmd/annave/cmd/<module>.go — parent command: var myCmd = &cobra.Command{Use: "mymodule"}
  5. Create cmd/annave/cmd/<module>_<subcommand>.go — parse --format, call the port, switch on format and render
  6. Add configurable thresholds to internal/shared/config/limits.yaml
  7. Add any new error codes to internal/shared/config/messages.yaml

How to add a security rule

Secret pattern

go
// 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

go
// 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:

go
shared.InvalidInput("--since must be a duration or date")
shared.IOFailure("cannot read file")
shared.New(shared.ErrCodeParseFailed, shared.StageAnalysis, "invalid YAML")