discovery

package
v0.8.4 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package discovery owns the one-shot SSH OS-fingerprint flow that captures os_family, os_version, kernel, architecture, hostname / FQDN, SELinux + AppArmor + firewall posture, and a hardware summary for each host on first contact + on-demand.

Spec: specs/system/host-discovery.spec.yaml (status: approved).

Architectural notes:

  • Credential-aware boundary. Unlike internal/liveness — which deliberately holds NO credential material and only opens a plain TCP banner read — the discovery package IS credential-aware. It imports internal/credential and golang.org/x/crypto/ssh, resolves the host's stored auth, and runs an SSH session to execute its probe commands. Spec C-11 + AC-15 source-inspect to guarantee this boundary is documented (so a future reader does not move credential-free probes here by accident).

  • One SSH session per Discover. The probe is a closed set of commands batched on a single session. Spec C-10 + AC-06 enforce exactly one ssh.Dial per Discover call.

  • Sudo failure is partial success. Firewall introspection requires root on some distros. If sudo is unavailable for the host credential, the firewall fields stay empty and the rest of the fingerprint persists. Spec C-03 + AC-05.

  • One transaction for two writes. host_system_info UPSERT + the denormalized hosts.os_* columns are persisted within a single BEGIN / COMMIT so list-page filters and the wide row never disagree. Spec C-02 + AC-07.

  • UPSERT-only on host_system_info (no history). A second Discover UPDATEs the existing row keyed by host_id. Time-series of changes belongs to the future host_intelligence_events Phase 3 spec, not here. Spec C-08 + AC-14.

  • Eventbus + audit on success. Discover publishes eventbus.HostDiscovered and emits audit.HostDiscoveryCompleted exactly once per successful run. Failures emit neither. Spec C-06 / C-07 / AC-11 / AC-12.

Index

Constants

View Source
const DefaultProbeTimeout = 10 * time.Second

DefaultProbeTimeout is the per-command budget on the SSH session. Commands that exceed it return an error and the field stays empty — a host with one hung command should not block the whole probe batch.

View Source
const JobKindHostDiscovery = "host.discovery"

JobKindHostDiscovery is the queue job kind for asynchronous Discovery runs. POST /api/v1/hosts auto-enqueues a job of this kind so the 201 response returns immediately. Spec C-05 / AC-13.

Variables

View Source
var ErrHostNotFound = errors.New("discovery: host not found")

ErrHostNotFound is returned by HostLookup.GetForDiscovery when the host is unknown or soft-deleted. The handler maps this to HTTP 404.

Functions

This section is empty.

Types

type Addr

type Addr struct {
	Host string
	Port int
}

Addr holds the minimal host-connection tuple Discovery needs.

type AuditEmitFunc

type AuditEmitFunc func(ctx context.Context, code audit.Code, ev audit.Event)

AuditEmitFunc is the audit emission seam. Production wires it to audit.Emit; tests use a recorder that counts emissions.

type ConnProfileStore

type ConnProfileStore interface {
	Get(ctx context.Context, hostID uuid.UUID) (connprofile.Profile, error)
	RecordSSHAuth(ctx context.Context, hostID uuid.UUID, m connprofile.SSHAuthMethod) error
}

ConnProfileStore is the subset of connprofile the transport uses to lead with the host's known-good SSH auth method and record what authenticated. nil disables learning (dial in the default key-first order). The host id is read from the context via connprofile.HostIDFrom.

type FactCategory added in v0.6.0

type FactCategory string

FactCategory groups host_system_info columns by the probe that collects them, so persist() can merge at category granularity: an unobserved category retains its prior stored value rather than being overwritten with an empty read.

const (
	CatOSRelease FactCategory = "os_release"
	CatUname     FactCategory = "uname"
	CatMemory    FactCategory = "memory"
	CatDisk      FactCategory = "disk"
	CatHostname  FactCategory = "hostname"
	CatFQDN      FactCategory = "fqdn"
	CatSELinux   FactCategory = "selinux"
	CatAppArmor  FactCategory = "apparmor"
	CatFirewall  FactCategory = "firewall"
)

type HostDiscoveryJobPayload

type HostDiscoveryJobPayload struct {
	HostID uuid.UUID `json:"host_id"`
}

HostDiscoveryJobPayload is the JSON shape of the host.discovery job payload the worker reads. Carries the host id so the worker can invoke Service.Discover. Exported so the worker package can decode it without importing service internals.

type HostLookup

type HostLookup interface {
	GetForDiscovery(ctx context.Context, hostID uuid.UUID) (Addr, error)
}

HostLookup is the seam Discovery uses to read host connection facts (addr, port) without coupling to the hosts package's full repository API. Production: a small adapter over pgxpool. Tests pre-build the hostFacts and use discoverWithTransport directly.

type PolicyLoader

type PolicyLoader interface {
	LoadSecurity(ctx context.Context) (systemconfig.SecurityConfig, error)
}

Service runs Discovery for a host. Construct via NewService. PolicyLoader returns the current SecurityConfig — the sudo-password fallback (system-ssh-connectivity v1.2.0 C-09 / AC-20) consults AllowCredentialSudoPassword via this seam. Production wires systemconfig.Store.LoadSecurity; tests pass a closure or leave it nil (in which case the fallback path is OFF by default).

type PoolHostLookup

type PoolHostLookup struct {
	Pool *pgxpool.Pool
}

PoolHostLookup adapts a *pgxpool.Pool into the HostLookup interface expected by Service. Production wires this; tests stub the interface directly.

func (PoolHostLookup) GetForDiscovery

func (p PoolHostLookup) GetForDiscovery(ctx context.Context, hostID uuid.UUID) (Addr, error)

GetForDiscovery reads the host's address + port. Returns pgx.ErrNoRows-like sentinel via a separate ErrHostNotFound if the host is missing or soft-deleted.

type Publisher

type Publisher interface {
	Publish(ctx context.Context, event eventbus.Event)
}

Publisher is the event-bus subset Discovery uses. The real *eventbus.Bus satisfies it; tests inject a recorder.

type SSHClientSession

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

SSHClientSession is the per-host live SSH client. Each Run opens a fresh ssh.Session (one channel per command) atop the single client. crypto/ssh sessions are not reusable across commands, so this is the idiomatic shape.

func (*SSHClientSession) Close

func (s *SSHClientSession) Close() error

func (*SSHClientSession) Run

func (s *SSHClientSession) Run(ctx context.Context, cmd string) ([]byte, int, error)

func (*SSHClientSession) RunWithStdin

func (s *SSHClientSession) RunWithStdin(ctx context.Context, cmd string, stdin []byte) ([]byte, int, error)

RunWithStdin runs cmd with the given bytes piped to the remote process's stdin. Used by ssh.RunSudo for the `sudo -S` password fallback path. Same channel-per-command pattern as Run.

Spec system-ssh-connectivity v1.1.0 C-10.

type SSHSession

type SSHSession interface {
	Run(ctx context.Context, cmd string) (stdout []byte, exitCode int, err error)
	// RunWithStdin is the sudo-password-fallback hook (system-ssh-
	// connectivity v1.1.0 C-10). ssh.RunSudo pipes the credential
	// password through here when the policy allows.
	RunWithStdin(ctx context.Context, cmd string, stdin []byte) (stdout []byte, exitCode int, err error)
	Close() error
}

SSHSession is one live session against a remote host. Run executes a single command and returns stdout + exit code; the same session can be reused for every command the probe needs. Close releases the underlying transport.

type SSHTransport

type SSHTransport interface {
	Dial(ctx context.Context, host string, port int, cred *credential.Credential) (SSHSession, error)
}

SSHTransport is the seam between the discovery service and the actual SSH path. Production uses sshTransport (wraps owssh.Dial + ssh.Session); tests use stubSSHTransport.

Dial MUST be called at most once per Discover (spec C-10).

type SSHTransportProd

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

SSHTransportProd is the production SSHTransport. Wraps internal/ssh.Dial to honor the project's host-key + auth policy. Exported so cmd/openwatch and tests that want a real (not stubbed) transport can construct one without internal package boundaries.

func NewSSHTransport

func NewSSHTransport(mode owssh.Mode, store owssh.KnownHostsStore) *SSHTransportProd

NewSSHTransport returns a production SSHTransport with the given host-key policy. NewService() calls this with TOFU + an in-memory known-hosts store by default; cmd/openwatch can override with a strict + persistent store later.

func (*SSHTransportProd) Dial

func (t *SSHTransportProd) Dial(ctx context.Context, host string, port int, cred *credential.Credential) (SSHSession, error)

Dial opens one SSH client connection and returns it as an SSHSession that multiplexes ssh.Session per Run call. When a profile store is wired and the ctx carries a host id, the dial leads with the host's recorded auth method and records the one that authenticated (a hint, not a lock: both methods are still offered, and a stale hint self-heals).

func (*SSHTransportProd) WithProfiles

WithProfiles enables per-host SSH auth-method learning: the transport leads the dial with the host's recorded method and records which method authenticated. The host id comes from connprofile.WithHostID on the ctx. nil (the default) keeps the historical key-first, no-learning behavior.

type Service

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

func NewService

func NewService(pool *pgxpool.Pool, emit AuditEmitFunc, bus Publisher) *Service

NewService constructs a Service. emit + bus may be nil — Discover degrades gracefully (audit + publish skip). credSvc may be nil for tests that only exercise the discoverWithTransport seam.

A production SSH transport (TOFU host-key policy, in-memory known- hosts store) is installed by default. Tests override via WithSSHTransport; cmd/openwatch can swap in a strict / persistent store via NewSSHTransport + WithSSHTransport before Run.

func (*Service) Discover

func (s *Service) Discover(ctx context.Context, hostID uuid.UUID) (SystemFacts, error)

Discover runs one full Discovery for hostID. Resolves credential, opens ONE SSH session, runs the probe batch, parses, persists in a single transaction, publishes the bus event, emits the audit event.

Returns SystemFacts on success (including the partial-success path where sudo was unavailable for firewall introspection — facts.Firewall* stays empty in that case). Errors come from credential resolution, SSH dial, or persistence — never from a sub-command's non-zero exit.

func (*Service) RunDiscovery

func (s *Service) RunDiscovery(ctx context.Context, hostID uuid.UUID) error

RunDiscovery is the error-only adapter the worker uses (matches worker.HostDiscoveryRunner interface signature). Discards the SystemFacts return — the worker only cares about success / failure, since persist + bus + audit are already done inside Discover.

func (*Service) WithCredentialService

func (s *Service) WithCredentialService(c *credential.Service) *Service

WithCredentialService wires the credential resolver. Required for Discover; not required for discoverWithTransport.

func (*Service) WithHostLookup

func (s *Service) WithHostLookup(h HostLookup) *Service

WithHostLookup wires the host-row reader. Required for Discover.

func (*Service) WithPolicyLoader

func (s *Service) WithPolicyLoader(p PolicyLoader) *Service

WithPolicyLoader wires the SecurityConfig reader. When unset, the sudo-password fallback (v1.2.0 AC-20) stays OFF — probeFirewall behaves exactly as in v1.1.0.

func (*Service) WithProfiles

func (s *Service) WithProfiles(p SudoProfileStore) *Service

WithProfiles enables per-host sudo-mode learning for the firewall probe: lead with the host's recorded sudo mode and record the mode a sudo firewall command confirms. nil (the default) keeps the historical sudo -n-first probing. Spec system-connection-profile v1.2.0 C-07.

func (*Service) WithSSHTransport

func (s *Service) WithSSHTransport(t SSHTransport) *Service

WithSSHTransport overrides the SSH transport (tests).

type SudoProfileStore

type SudoProfileStore interface {
	Get(ctx context.Context, hostID uuid.UUID) (connprofile.Profile, error)
	RecordSudoMode(ctx context.Context, hostID uuid.UUID, m connprofile.SudoMode) error
}

SudoProfileStore is the subset of connprofile the discovery service uses to learn the host's SUDO mode for the firewall probe: lead with the recorded mode and record the mode a sudo firewall command confirms. nil disables sudo-mode learning. (SSH auth-method learning is handled separately by the profile-aware transport.) Spec system-connection- profile v1.2.0.

type SystemFacts

type SystemFacts struct {
	// /etc/os-release
	OSName             string
	OSVersion          string
	OSVersionFull      string
	OSID               string
	OSIDLike           string
	OSPrettyName       string
	PlatformIdentifier string
	OSFamily           string // derived from OSID + OSIDLike

	// uname -srvm
	KernelName    string
	KernelRelease string
	KernelVersion string
	Architecture  string

	// /proc/meminfo (MB)
	MemTotalMB     int
	MemAvailableMB int
	SwapTotalMB    int

	// df -BG /
	DiskTotalGB int
	DiskUsedGB  int
	DiskFreeGB  int

	// hostname
	Hostname string
	FQDN     string

	// security posture
	SELinuxStatus   string
	AppArmorEnabled bool

	// firewall (may be empty when sudo unavailable)
	FirewallService string
	FirewallStatus  string

	CollectedAt time.Time

	// Observed records which fact CATEGORIES this run actually collected (the
	// probe ran and returned usable output). persist() carries forward the
	// prior stored value for any category NOT observed, so a failed or denied
	// probe never blanks previously-good data (spec C-08, v1.5.0). An observed
	// category keeps the run's values even when genuinely empty/zero — that is
	// a real observation, not a missing one.
	Observed map[FactCategory]bool

	// Attempts records WHY a non-observed category was not collected this run
	// (denied | failed | timeout), so the persisted freshness can tell an
	// operator whether a stale value is a fixable sudo denial or a transient
	// connectivity failure. Only categories that were attempted-but-not-observed
	// appear here; an observed category is absent from this map. Spec C-14,
	// v1.7.0.
	Attempts map[FactCategory]string
}

SystemFacts is the typed bundle of every fact one Discover run collected. Mirrors the host_system_info column layout one-for-one so persist() is a straight value-to-column map.

Directories

Path Synopsis
Package scheduler is the recurring driver for OS discovery — the loop that finds hosts whose hosts.os_discovered_at column is stale (NULL or older than the policy interval) and enqueues host.discovery jobs through internal/queue so the worker pool picks them up and runs discovery.Service.Discover on them.
Package scheduler is the recurring driver for OS discovery — the loop that finds hosts whose hosts.os_discovered_at column is stale (NULL or older than the policy interval) and enqueues host.discovery jobs through internal/queue so the worker pool picks them up and runs discovery.Service.Discover on them.

Jump to

Keyboard shortcuts

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