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
- Variables
- type Addr
- type AuditEmitFunc
- type ConnProfileStore
- type FactCategory
- type HostDiscoveryJobPayload
- type HostLookup
- type PolicyLoader
- type PoolHostLookup
- type Publisher
- type SSHClientSession
- type SSHSession
- type SSHTransport
- type SSHTransportProd
- type Service
- func (s *Service) Discover(ctx context.Context, hostID uuid.UUID) (SystemFacts, error)
- func (s *Service) RunDiscovery(ctx context.Context, hostID uuid.UUID) error
- func (s *Service) WithCredentialService(c *credential.Service) *Service
- func (s *Service) WithHostLookup(h HostLookup) *Service
- func (s *Service) WithPolicyLoader(p PolicyLoader) *Service
- func (s *Service) WithProfiles(p SudoProfileStore) *Service
- func (s *Service) WithSSHTransport(t SSHTransport) *Service
- type SudoProfileStore
- type SystemFacts
Constants ¶
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.
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 ¶
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 AuditEmitFunc ¶
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 ¶
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 ¶
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 ¶
PoolHostLookup adapts a *pgxpool.Pool into the HostLookup interface expected by Service. Production wires this; tests stub the interface directly.
func (PoolHostLookup) GetForDiscovery ¶
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 ¶
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) 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 ¶
func (t *SSHTransportProd) WithProfiles(p ConnProfileStore) *SSHTransportProd
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 ¶
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 ¶
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. |