canary

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package canary implements graphi's hermetic egress-denied canary and zero-telemetry CI gate (SW-008).

The package provides two independent mechanisms behind one acceptance contract:

  1. Runtime egress canary — exercises the graphi surface stack and asserts that ZERO non-loopback network dials are attempted. The assertion is made at dial-attempt time via an injected interceptor (not libpcap), so even a blocked attempt is caught. On Linux it can additionally run inside a loopback-only network namespace; when isolation is unavailable the canary HARD-FAILS rather than silently passing.

  2. Static zero-telemetry gate — scans the default CGo-free build graph for telemetry/analytics SDK imports and non-allowlisted outbound dial constructors, failing CI with the offending symbol + import path.

Both mechanisms build with CGO_ENABLED=0 and produce machine-readable JSON artifacts. They live OUTSIDE the surfaces/engine production code (this is a CI/test concern, not a runtime surface concern).

Layering: internal/canary imports only the standard library, the graphi query operation vocabulary (for surface-union derivation), and go/packages for the static gate. It must not import surfaces/* or engine/* production runtime code.

Package canary — static zero-telemetry gate (SW-008 Slice 3).

The gate scans the DEFAULT CGo-free build graph (CGO_ENABLED=0, default build tags, module root) for two classes of trust-breaking code:

  1. Telemetry/analytics SDK imports — a curated denylist of import paths whose presence in the default graph fails CI with the offending import.
  2. Non-allowlisted outbound dial constructors — source-level AST scan for net.Dial / net.DialUDP / http.Client that would reach non-loopback destinations, against an explicit allowlist. The loopback-only surfaces (the daemon Unix socket, local HTTP/SSE) are allowlisted.

It invokes the Go toolchain via `go list`/`go vet`-style inspection (no golang.org/x/tools dependency, keeping the build graph lean) and is itself CGo-free. The graphi-broad CGo flavor is excluded: the gate only scans the default build graph, which is the artifact the local-first contract applies to.

Package canary — netns isolation harness.

On Linux, Run sets up a loopback-only network namespace and executes the isolated function inside it, then tears it down. On non-Linux (or when the runner lacks the required capabilities), IsAvailable returns false and Run HARD-FAILS — a misconfigured environment must never silently mask egress (SW-008 AC: "the job hard-fails rather than silently passing").

This file declares the portable interface; platform implementations live in netns_linux.go (real isolation) and netns_other.go (hard-fail stub).

Package canary — Linux loopback-only network-namespace isolator.

Run creates a fresh network namespace, brings up loopback (so in-process local servers / IPC on 127.0.0.0/8 and ::1 keep working), and leaves every other interface DOWN — so non-loopback dials cannot reach the wire. The calling thread must have CAP_SYS_ADMIN (root or appropriate caps); if not, IsAvailable() reports false and the canary hard-fails rather than silently passing.

This is pure-Go + golang.org/x/sys/unix (already an indirect dependency via modernc/sqlite) — no CGo, no libpcap, fully consistent with the CGo-free default build.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MarshalArtifact

func MarshalArtifact(a Artifact) ([]byte, error)

MarshalArtifact serializes a canary artifact to stable JSON for the CI evidence artifact.

Types

type Artifact

type Artifact struct {
	Verdict      string        `json:"verdict"` // "pass" | "fail" | "no-isolation"
	CoveredTools []string      `json:"covered_tools"`
	DialAttempts []DialAttempt `json:"dial_attempts"`
	Violations   []DialAttempt `json:"violations"` // non-loopback subset
	Isolation    string        `json:"isolation"`  // isolator kind / "unavailable"
	StartedAt    time.Time     `json:"started_at"`
	DurationMS   int64         `json:"duration_ms"`
	FailReason   string        `json:"fail_reason,omitempty"`
}

Artifact is the machine-readable canary result (SW-008 AC: "emits a machine-readable artifact (covered-tool list + packet-capture summary) proving the whole surface was exercised under denial"). Stable JSON schema for CI consumers and audit.

func Run

func Run(ctx context.Context, cfg RunConfig) (Artifact, error)

Run executes the hermetic egress canary:

  1. Preflight — require that isolation is actually available; if not, HARD-FAIL (no-isolation verdict) rather than silently passing (SW-008 AC + S2).
  2. Drive — exercise every tool/command in the union inside isolation, recording any dial attempts.
  3. Verdict — pass iff zero non-loopback dial attempts were observed; on any violation, fail naming the offending tool + destination.

It returns the Artifact and a non-nil error only for HARD-FAIL conditions (no isolation available). A "fail" verdict (violations observed) is returned as an Artifact with Verdict="fail" AND a wrapping error so CI gates naturally fail; callers wanting the structured detail should read the Artifact.

type DialAttempt

type DialAttempt struct {
	// Tool identifies the surface tool/command that triggered the dial.
	Tool string `json:"tool"`
	// Network is the dial network ("tcp", "udp", …).
	Network string `json:"network"`
	// Address is the dial destination (host:port or host).
	Address string `json:"address"`
}

DialAttempt captures a single outbound dial attempt observed during a canary run. The assertion is on ATTEMPT (the destination the code tried to reach), not on packets that made it onto the wire — so even a dial that would be blocked by netns is caught (SW-008 refinement finding D2/S1).

func (DialAttempt) IsLoopback

func (d DialAttempt) IsLoopback() bool

IsLoopback reports whether the dial destination is loopback (127.0.0.0/8 or ::1). Only loopback is permitted by the canary; in-process local servers and the daemon's Unix socket / local HTTP+SSE are the legitimate loopback users (SW-008 refinement finding S3).

type DialRecorder

type DialRecorder struct {
	// contains filtered or unexported fields
}

DialRecorder collects dial attempts observed during a canary run. The default recorder is passive (records what a driver reports). Tests and the in-process driver hook real dial attempts into it via Record.

func NewDialRecorder

func NewDialRecorder() *DialRecorder

NewDialRecorder returns an empty recorder.

func (*DialRecorder) All

func (r *DialRecorder) All() []DialAttempt

All returns a copy of all recorded attempts.

func (*DialRecorder) NonLoopback

func (r *DialRecorder) NonLoopback() []DialAttempt

NonLoopback returns only the attempts that are NOT loopback — the violations the canary must fail on.

func (*DialRecorder) Record

func (r *DialRecorder) Record(a DialAttempt)

Record appends an observed dial attempt.

type GateConfig

type GateConfig struct {
	ModuleDir string
	// GraphCommand returns the default-graph dependency list. Defaults to
	// `go list -deps -test=false ./...` under CGO_ENABLED=0; injectable for
	// hermetic tests.
	GraphCommand func(dir string) ([]string, error)
}

GateConfig configures the static gate. ModuleDir defaults to the graphi module root (detected via `go env GOMOD` when empty).

type GateResult

type GateResult struct {
	Verdict  string             `json:"verdict"` // "pass" | "fail"
	Findings []TelemetryFinding `json:"findings"`
}

GateResult is the static-gate verdict.

func RunGate

func RunGate(cfg GateConfig) (GateResult, error)

RunGate executes the static zero-telemetry gate over the default graph.

type IsolationError

type IsolationError struct {
	Reason string
}

IsolationError reports that runner-level network isolation is unavailable. The canary treats this as fatal: it MUST NOT proceed without isolation.

func (*IsolationError) Error

func (e *IsolationError) Error() string

type Isolator

type Isolator interface {
	// IsAvailable reports whether this runner can actually deny non-loopback
	// egress. A false result means the canary must hard-fail.
	IsAvailable() bool
	// Run executes fn under loopback-only isolation. It MUST be callable only
	// when IsAvailable() is true; callers gate on IsAvailable first.
	Run(fn func() error) error
}

Isolator abstracts the runner's ability to provide loopback-only network isolation. The production implementation is the Linux netns harness; tests inject a fake to exercise the preflight pass/fail branches without root.

func DefaultIsolator

func DefaultIsolator() Isolator

DefaultIsolator returns the best available isolator for the current platform (the Linux netns harness, or the hard-failing stub elsewhere). Exported so other packages — e.g. internal/audit's privacy-audit live exercise — can run a representative operation under the same loopback-only isolation as the canary.

type RunConfig

type RunConfig struct {
	Isolator Isolator
	Driver   SurfaceDriver
	Union    SurfaceUnion
}

RunConfig parameterizes a canary run. Fields are injectable so the preflight, isolation, and verdict logic are all unit-testable without netns.

type SurfaceDriver

type SurfaceDriver interface {
	// Drive runs every tool/command in the union once, recording any dial
	// attempts. It returns an error only if the surface itself fails to run;
	// dial attempts are reported via the recorder, not as an error.
	Drive(ctx context.Context, union SurfaceUnion, rec *DialRecorder) error
}

SurfaceDriver exercises a slice of the graphi surface and reports any dial attempts it observes to the recorder. The default driver runs the REAL in-process surface functions over an in-memory store, so the canary exercises genuine graphi code paths (query dispatch, search, CLI runner) rather than stubs. A test may inject a fake driver to assert verdict logic in isolation.

func DefaultDriver

func DefaultDriver(out io.Writer) SurfaceDriver

DefaultDriver returns the canonical in-process surface driver over a fresh in-memory store, exercising the real graphi surfaces (query / search / CLI). It is the representative operation used by both the canary and the privacy-audit live exercise. out receives surface stdout; pass io.Discard unless debugging.

func NewInProcessDriver

func NewInProcessDriver(store graphstore.Graphstore, out io.Writer) SurfaceDriver

NewInProcessDriver builds a driver over the given store (use an in-memory store for the hermetic canary). out receives surface stdout; pass io.Discard unless debugging.

type SurfaceUnion

type SurfaceUnion struct {
	// CLICommands are the graphi subcommands (cmd/graphi/main.go dispatch).
	CLICommands []string `json:"cli_commands"`
	// QueryOperations are the structural query operations (engine/query.Operations).
	QueryOperations []string `json:"query_operations"`
	// SearchTool is the search capability name advertised over MCP/CLI.
	SearchTool string `json:"search_tool"`
}

SurfaceUnion is the canonical set of graphi commands/tools the canary must exercise. It is derived programmatically (see NewSurfaceUnion) so the canary cannot silently miss a new tool as graphi grows — fulfilling the "drive every tool/command at least once" acceptance criterion without a hand-maintained list.

func NewSurfaceUnion

func NewSurfaceUnion() SurfaceUnion

NewSurfaceUnion derives the canonical surface union programmatically: CLI subcommands + the engine's canonical query operation list + search. Adding a new query operation to engine/query.Operations automatically extends the canary coverage — no hand-maintained list to drift (SW-008 AC + refinement A4).

func (SurfaceUnion) CoveredTools

func (su SurfaceUnion) CoveredTools() []string

CoveredTools returns the flattened list of covered tool/command identifiers, stable-sorted, for the machine-readable canary artifact.

type TelemetryFinding

type TelemetryFinding struct {
	Kind   string `json:"kind"`   // "telemetry-import" | "outbound-dial"
	Import string `json:"import"` // offending import path (telemetry) or package containing the call
	Symbol string `json:"symbol"` // offending symbol/call (outbound-dial) or ""
	File   string `json:"file"`   // source file, when known
	Reason string `json:"reason"`
}

TelemetryFinding names an import path that the gate has rejected.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL