netdev

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 60 Imported by: 0

Documentation

Overview

Package netdev is the network-device operations capability (NETDEV_SPEC): connection transport (internal/netdev/transport), device inventory, and the credential plumbing. P0 ships the transport layer and the security seams (profile tool seal, secret namespace); drivers, tools, and UI land in P1+.

State history (NETDEV_SPEC_V2 §状态历史): pre-mutation snapshots of the ops state files, modeled after the coding-mode checkpoints (Claude Code / ZCode alignment). Every state transition (proposal approve/execute/rollback, job lifecycle, cutover lifecycle, template saves, finding verdicts, inventory and config edits) snapshots the entity files it is about to overwrite; a suffix rewind then restores every file touched since a chosen event to its pre-event content.

Layer contract — restoring here reverts LOCAL RECORDS ONLY. Device-side rollback stays on the proposal/cutover path; the UI must say so. The audit log remains the immutable what-happened trail; state history is the restorable what-it-looked-like-before trail. Excluded by design: append-only journals (audit.jsonl, inspections, port events), telemetry (series, metrics.db), add-only libraries (backups, srvconf), and secrets.enc.json.

Index

Constants

View Source
const (
	FindingActive   = "active"
	FindingAck      = "ack"
	FindingResolved = "resolved"
	FindingFalsePos = "false-positive"
)

Finding lifecycle states (beyond "" for human/AI findings).

View Source
const (
	WeakTierBasic = "basic"      // fixed vendor-default/empty/username set
	WeakTierDict  = "dictionary" // user-supplied dictionary file

)

Weak-credential tiers.

View Source
const (
	AuditOK          = "ok"
	AuditDeviceError = "device-error"
	AuditRefused     = "refused"
	AuditFailure     = "failure"
)

Audit statuses.

View Source
const (
	CutoverRunning = "running"
	CutoverHold    = "hold" // stopped at a decision point / failed gate / countdown spent
	CutoverDone    = "done"
	CutoverFailed  = "failed"
	CutoverAborted = "aborted"
)

Cutover statuses.

View Source
const (
	CutoverStepPending  = "pending"
	CutoverStepRunning  = "running"
	CutoverStepGating   = "gating"
	CutoverStepDone     = "done"
	CutoverStepFailed   = "failed"
	CutoverStepSkipped  = "skipped"
	CutoverStepRolled   = "rolled-back"
	CutoverStepApproved = "approved" // a proposal step that finished its change
)

Cutover step statuses.

View Source
const (
	ChainKindEvent        = "event"
	ChainKindAction       = "action"
	ChainKindEvidence     = "evidence"
	ChainKindConclusion   = "conclusion"
	ChainKindRemediation  = "remediation"
	ChainKindVerification = "verification"
)

Chain kinds — the six semantic columns.

View Source
const (
	SourceDiscover  = "discover"
	SourceNmap      = "nmap-import"
	SourceNetprobe  = "netprobe"
	SourceTopo      = "topo-neighbor"
	SourceLocateARP = "locate-arp"
)

Discovery sources (spec §4.2.2): where a lead came from. layer-discover arrives with F4.

View Source
const (
	SeverityInfo     = "info"
	SeverityWarning  = "warning"
	SeverityCritical = "critical"
)

Severities.

View Source
const (
	JobRunning = "running"
	JobPaused  = "paused"
	JobDone    = "done"
	JobFailed  = "failed"  // a step failed with on-fail=abort — later steps untouched
	JobAborted = "aborted" // human pressed stop
)

Job statuses.

View Source
const (
	JobStepPending = "pending"
	JobStepRunning = "running"
	JobStepOK      = "ok"
	JobStepFailed  = "failed"
	JobStepSkipped = "skipped"
)

Step statuses.

View Source
const (
	JobOnFailPause    = "pause"    // default — freeze for a human, resumable
	JobOnFailAbort    = "abort"    // mark the job failed, skip the rest
	JobOnFailContinue = "continue" // log and move on (fail streak still counts)
)

OnFail policies.

View Source
const (
	LiveConn       = "conn"        // connection/session state change
	LiveCmdStart   = "cmd_start"   // a command began (classified, about to run)
	LiveCmdOutput  = "cmd_output"  // incremental cleaned+redacted output text
	LiveCmdEnd     = "cmd_end"     // command finished (ok / device-error / failure)
	LiveCmdRefused = "cmd_refused" // a guardrail/classifier refusal (visible!)
	LiveTurnBegin  = "turn"        // a new user turn: per-turn budget counters reset
)

Live event kinds.

View Source
const (
	LiveConnConnected  = "connected"
	LiveConnConnecting = "connecting"
	LiveConnReconnect  = "reconnecting"
	LiveConnStopped    = "stopped"
	LiveConnIdleClosed = "idle-closed" // reaper closed the idle session
)

Connection states for LiveConn events.

View Source
const (
	ProposalDraft     = "draft"
	ProposalApproved  = "approved"
	ProposalExecuting = "executing"
	ProposalDone      = "done"
	ProposalPartial   = "partial"  // some steps applied, later ones skipped — frozen for a human
	ProposalFailed    = "failed"   // rollback attempted and failed — alert
	ProposalRejected  = "rejected" // human vetoed (completion-spec §4.1): agent sees the reason next turn
)

Proposal statuses.

View Source
const (
	StepCLI           = "cli"
	StepK8sApply      = "k8s-apply"
	StepSQLMigration  = "sql-migration"
	StepFileUpload    = "file-upload"
	StepCertReplace   = "cert-replace"
	StepRestoreVerify = "restore-verify" // §7.3 备份恢复演练
)

Structured step types (§7.1): the proposal step grew from a CLI command string into a discriminated union. Type "" behaves as cli everywhere — old proposals deserialize unchanged.

View Source
const (
	ProposalWatching = "watching"
	ProposalClosed   = "closed"
)

Proposal is one change proposal.

View Source
const (
	RoleRouter   = "router"
	RoleSwitch   = "switch"
	RoleFirewall = "firewall"
	RoleIPS      = "ips"
	RoleVPN      = "vpn"
	RoleBastion  = "bastion"
	RoleServer   = "server"
	RoleAP       = "ap"
	RoleCloud    = "cloud"
	RoleUnknown  = ""
)

Role values (NETDEV_IMPORT_AND_FINGERPRINT_SPEC §2.1). Keep in sync with the frontend TopoIcon set and the §2.3 word table below.

View Source
const (
	RoleSourceConfig   = "config"
	RoleSourceKind     = "kind"
	RoleSourceGroup    = "group"
	RoleSourceModel    = "model"
	RoleSourceVendor   = "vendor"
	RoleSourceNeighbor = "neighbor"
	RoleSourceLabel    = "label"
	RoleSourceNone     = "none"
)

RoleSource records how a role was derived, coarse-grained for the UI's confidence hints: config > kind > group > model > vendor > neighbor/label (import/remote) > none.

View Source
const (
	SecretKindPassword   = "password"   // device / hop login passwords
	SecretKindPassphrase = "passphrase" // private-key passphrases
	SecretKindSNMPAuth   = "snmp-auth"  // SNMPv3 auth passwords
	SecretKindSNMPPriv   = "snmp-priv"  // SNMPv3 privacy passwords
	SecretKindKubeconfig = "kubeconfig" // kind=k8s targets: the whole kubeconfig YAML
	SecretKindAPIToken   = "api-token"  // kind=firewall targets: vendor REST API tokens
)

Secret kinds.

View Source
const (
	StateActorUser   = "user"   // desktop bridge (a human clicked)
	StateActorAgent  = "agent"  // LLM tool call (netdev_propose, netdev_finding…)
	StateActorIM     = "im"     // bot gateway approval / ack
	StateActorSystem = "system" // runners, schedulers, watchers
)

Actors for event attribution.

View Source
const (
	StateEventPropose      = "propose"
	StateEventApprove      = "approve"
	StateEventReject       = "reject"
	StateEventExecute      = "execute"
	StateEventRollback     = "rollback"
	StateEventDelete       = "delete"
	StateEventCloseWatch   = "close-watch"
	StateEventJobStart     = "job-start"
	StateEventJobPause     = "job-pause"
	StateEventJobResume    = "job-resume"
	StateEventJobAbort     = "job-abort"
	StateEventJobFreeze    = "job-freeze"
	StateEventJobFinish    = "job-finish"
	StateEventCutoverStart = "cutover-start"
	StateEventCutoverGo    = "cutover-continue"
	StateEventCutoverBack  = "cutover-rollback"
	StateEventCutoverAbort = "cutover-abort"
	StateEventCutoverHold  = "cutover-hold"
	StateEventCutoverDone  = "cutover-finish"
	StateEventTplSave      = "template-save"
	StateEventTplDelete    = "template-delete"
	StateEventTplApply     = "template-apply"
	StateEventFindAck      = "finding-ack"
	StateEventFindFP       = "finding-false-positive"
	StateEventFindResolve  = "finding-resolve"
	StateEventFindDismiss  = "finding-dismiss"
	StateEventFindClear    = "findings-clear"
	StateEventSettings     = "settings-save"
	StateEventPromote      = "hosts-promote"
	StateEventLeadDismiss  = "lead-dismiss"
	StateEventImport       = "import-apply"
	StateEventTopo         = "topo-apply"
	StateEventGolden       = "golden-set"
	StateEventCaseSave     = "case-save"
	StateEventCaseDelete   = "case-delete"
	// StateEventRestoreKeep marks the synthetic reverse event written just
	// before a restore; restoring back to it replays (redoes) the rewound
	// changes.
	StateEventRestoreKeep = "restore-keep"
)

Event kinds — structured values stored per event and localized in the frontend via ndv.hist.kind.*.

View Source
const (
	TestOK             = "ok"
	TestUnknownHostKey = "unknown-host-key"
	TestAuthFailed     = "auth-failed"
	TestError          = "error"
)

Test statuses.

View Source
const EscalationTimeout = 15 * time.Minute

EscalationTimeout: critical finding unhandled for this long → escalate.

View Source
const SecretNamespace = "netdev"

SecretNamespace is the key prefix under the secret store where every netdev credential lives: "netdev/<kind>/<name>" (e.g. "netdev/password/NETDEV_PWD_L1"). Grouping under one prefix lets sandbox deny rules, audits, and future export tools address device credentials as a set, separate from provider keys and cowork mail passwords.

View Source
const SourceLayer = "layer-discover"

SourceLayer is F4's store source constant.

Variables

View Source
var HostKeyPrompt transport.HostKeyPrompt

HostKeyPrompt is the interactive TOFU hook. nil (default) = strict mode: unknown host keys are rejected. The desktop settings panel (P1 UI) injects a dialog-backed prompt here at startup.

Functions

func AckFindingByID

func AckFindingByID(id string) error

AckFindingByID marks one finding acknowledged (seen, being worked).

func AppendAudit

func AppendAudit(e Audit) error

AppendAudit writes one entry. Failures are returned (the caller logs); an audit failure must not silently vanish — but it also must not block the diagnostic hand (the command already ran; the audit gap is itself observable in ops review).

func AppendInspectionRow

func AppendInspectionRow(row InspectionJournalRow) error

AppendInspectionRow appends one R1 line (best-effort; also kicks the once-a-day compaction check).

func AppendPortEvent

func AppendPortEvent(ip string, port int, kind string)

AppendPortEvent appends one R2 line (best-effort, called from discovered.go).

func ApplyExtraRead

func ApplyExtraRead(cfg *config.Config)

ApplyExtraRead wires the [netdev.extra_read] vendor tables into the drivers' runtime read extensions — the B-1 knowledge-growth path: users teach the read table (via settings or the one-click refusal chip), never the model. boot's RegisterTools and the desktop settings bridge call it after every config change so the tables go live without a restart.

func AssessmentActive

func AssessmentActive(nd config.NetDevConfig) error

AssessmentActive validates the engagement envelope. nil = active.

func AuditChainHead

func AuditChainHead() string

AuditChainHead returns the local audit chain head ("" when nothing is chained yet) — the value cross-anchored to the trust domain (spec §八).

func AuditPath

func AuditPath() string

AuditPath returns the audit file location (test override included).

func CancelDiscoverRuns

func CancelDiscoverRuns() int

CancelDiscoverRuns cancels every in-flight discovery run; returns how many.

func CaseBundle

func CaseBundle(caseID string) (string, error)

CaseBundle writes the 复盘报告 (case + related findings + audit excerpt) into the state dir and returns the path — the FDE/蓝队交接件 v1.

func CaseReport

func CaseReport(c *IncidentCase) string

CaseReport renders the case as a 复盘 markdown page.

func CasesDir

func CasesDir() string

CasesDir stores one JSON per case.

func CleanupSeries

func CleanupSeries()

CleanupSeries drops points older than the retention (called opportunistically on app start; a rewrite-in-place under the lock).

func ClearFindings

func ClearFindings() (int, error)

ClearFindings deletes every persisted finding and returns how many — the queue's "clear all" (double-confirmed in the UI). Dev/test droppings are the main payload; real queues rebuild from the next inspection run.

func CloseProposalWatch

func CloseProposalWatch(id string) error

CloseProposalWatch manually ends the watching period.

func CountPromotions

func CountPromotions() int

CountPromotions returns the historical total of 待确认区→纳管 promotions.

func CtxStateActor

func CtxStateActor(ctx context.Context, actor string) context.Context

CtxStateActor tags a context with the state-history actor for transitions invoked through it — the cutover runner executes proposals as "system" while the desktop/IM bridges keep the "user" default.

func DeleteCase

func DeleteCase(id string) error

DeleteCase removes one case by id.

func DeleteDiscoveredHost

func DeleteDiscoveredHost(ip string) error

DeleteDiscoveredHost removes one lead (dismissed by the user, or consumed by promotion).

func DeleteSecret

func DeleteSecret(kind, name string) error

DeleteSecret removes one netdev credential.

func DeleteTemplate

func DeleteTemplate(id string) error

DeleteTemplate removes one template.

func DeviceLayersPath

func DeviceLayersPath() string

DeviceLayersPath exposes the layer ledger's file location for the state-history snapshot (promotions rewrite it).

func DiffBackups

func DiffBackups(device, idA, idB string) (string, error)

DiffBackups returns the unified diff between two of a device's versions (a = old, b = new), reusing the repo's myers diff.

func DiscoveredDir

func DiscoveredDir() string

DiscoveredDir stores leads as one JSON per IP.

func DismissFinding

func DismissFinding(id string) error

DismissFinding deletes one finding by id — the findings queue's per-item ×. An unknown id is an error so the UI can tell "already gone" from "deleted".

func EnsureNotifier

func EnsureNotifier(cfg *config.Config)

EnsureNotifier wires the outlets from config (idempotent; SharedManager calls it on every config load so settings changes go live without restart).

func EnsureSyslogReceiver

func EnsureSyslogReceiver(cfg *config.Config)

EnsureSyslogReceiver starts (or stops on port 0 / config change) the UDP listener. Idempotent; called from the desktop bridge on settings load/save. Every call also refreshes the receiver's view of the inventory, so devices added after startup still map by address.

func EnsureTrapReceiver

func EnsureTrapReceiver(cfg *config.Config)

EnsureTrapReceiver starts (port > 0) or stops (port 0) the trap listener. Idempotent; called from the desktop bridge with every settings load.

func ExtendScopesCandidates

func ExtendScopesCandidates(existing, candidates []string) ([]string, error)

ExtendScopesCandidates validates candidate CIDRs (from a precheck plan the user confirmed on the plan card) against the existing scopes and returns only those not already covered (PENLAB_CAPABILITY_GAPS P0-1). The caller persists the extension and audits it — the never-off scope guardrail is EXTENDED by an explicit human decision, never bypassed.

func FalsePositiveFindingByID

func FalsePositiveFindingByID(id string) error

FalsePositiveFindingByID marks one finding a false positive AND learns the Source key into the suppression table.

func FindingsDir

func FindingsDir() string

FindingsDir stores findings as one JSON per finding.

func FlapCount

func FlapCount(device string, window time.Duration) int

FlapCount counts reachability up↔down transitions within the window. The row limit assumes a poll interval ≥10s (window/10 rows covers the window even at the densest supported cadence).

func FlushSyslogCounts

func FlushSyslogCounts()

FlushSyslogCounts appends pending buckets to the day file (best-effort, drains the buffer). Called by the receiver's 30s flusher and by tests.

func GetBackupText

func GetBackupText(device, id string) (string, error)

GetBackupText returns a version's redacted config text.

func GetSecret

func GetSecret(kind, name string) (string, bool, error)

func GoldenDir

func GoldenDir() string

GoldenDir stores one baseline per device (<name>.conf + <name>.meta).

func HumanTTYKillAll

func HumanTTYKillAll() int

HumanTTYKillAll is the emergency-stop hook (§6.1 紧急停止同样杀人工终端).

func HumanTTYResize

func HumanTTYResize(deviceName string, cols, rows int) error

HumanTTYResize forwards the frontend terminal's size changes to the PTY.

func HumanTTYStop

func HumanTTYStop(deviceName string)

HumanTTYStop closes the human terminal session (idempotent).

func ImportCVEFeed

func ImportCVEFeed(raw string) (int, error)

ImportCVEFeed validates and caches a user-supplied feed (JSON string). Accepts the simplified schema or a raw NVD JSON export (both converted to the simplified cache format). Returns the imported count.

func ImportStageFile

func ImportStageFile(content string) (string, error)

ImportStageFile persists an in-browser-read export payload to the state dir and returns the staged path — Wails' file dialog gives the frontend the file CONTENT (not an OS path the Go side can reopen), so the wizard stages content first, then previews/applies by path.

func InferDeviceRole

func InferDeviceRole(d config.NetDevDevice) (role, source string)

InferDeviceRole is the §2.3 priority chain for an inventory device: explicit config role > data-plane kind > group words > model/name words > vendor default > unknown. The neighbor-platform rung arrives with F2.

func InitAuditAnchoring

func InitAuditAnchoring(cfg *config.Config)

InitAuditAnchoring arms the hook with the shared embedded node. Called wherever the trust-domain runtime starts (RegisterTools, the CLI daemon's executor branch); hosts without trustdomain never arm — the hook is a no-op costing one mutex trip per audit entry.

func JobPause

func JobPause(id string) error

JobPause asks a running job to freeze at the next boundary (between steps or retries). The in-flight attempt finishes — commands are short and read-only.

func ListConsolePorts

func ListConsolePorts() []string

ListConsolePorts enumerates the machine's present serial ports (platform implementations in console_windows.go / console_other.go).

func LoadDeviceLayers

func LoadDeviceLayers() map[string]int

LoadDeviceLayers returns device → recursion depth (nil map when none).

func LogAllowedRoots

func LogAllowedRoots(d config.NetDevDevice) []string

LogAllowedRoots returns the device's log whitelist: /var/log (the classic system-log root, always allowed) plus the device's log_paths entries.

func NeighborCommand

func NeighborCommand(driverKey string) (string, bool)

NeighborCommand returns the read command that yields neighbor information for the driver key (huawei-vrp → LLDP brief, cisco-ios → CDP detail).

func NotifyConfigured

func NotifyConfigured() bool

NotifyConfigured reports whether at least one outlet is live (the test button's guard: clicking with nothing configured must explain, not no-op).

func NotifyPushText

func NotifyPushText(kind, title, text string)

NotifyPushText pushes one outbound text (the daily briefing, future digests) through every configured outlet. The webhook variant carries it as JSON {kind, title, text}.

func OpenFindingTallies

func OpenFindingTallies() (critical, warning, info int)

OpenFindingTallies counts open findings by severity (R1 rows carry the run-end tallies — the risk-score trend source). Status "resolved" is out.

func P90IfDown

func P90IfDown(device string, window time.Duration) (int, bool)

P90IfDown is the 90th percentile of ifDown over the window; ok=false when history is too thin to judge "usual" (callers treat that as no signal).

func PauseDiscoverRun

func PauseDiscoverRun(id string) bool

PauseDiscoverRun cancels ONE run by id (the bridge's 暂停 button); false when the run already finished.

func ProjectForDevices

func ProjectForDevices(cfg *config.Config, devices []string) string

ProjectForDevices maps a device list to the owning project by group membership: the project whose groups contain the most of the listed devices wins; ties go to config order. Ungrouped devices count as the "未分组" group (mirroring the frontend's inScope bucket). Pseudo-devices ("(all)", "(unknown)", "(cve-feed)") and empty lists match nothing → "".

func ProposalsDir

func ProposalsDir() string

ProposalsDir stores one JSON per proposal under the netdev state dir.

func ReadHumanTTYRecording

func ReadHumanTTYRecording(path string) (string, error)

ReadHumanTTYRecording returns one recording's (already redacted) text. The path is restricted to the recordings dir — evidence files elsewhere are not readable through this surface.

func RecordDeviceLayer

func RecordDeviceLayer(device string, layer int) error

RecordDeviceLayer stamps one device's layer (idempotent).

func RecordDiscovered

func RecordDiscovered(source string, hosts []DiscoverHostResult) error

RecordDiscovered merges one scan batch into the store (upsert per IP: sources dedupe, ports merge by number, hints only strengthen). Errors are returned but callers treat recording as best-effort — a store hiccup must never fail the discovery run itself. New ports fire R2 newly-opened events.

func RecordDiscoveredHTTP

func RecordDiscoveredHTTP(ip string, port int, fp *HTTPFingerprint) error

RecordDiscoveredHTTP files F3's application fingerprint for one ip:port (upsert; the port row is created if the lead only ever had table sources).

func RecordDiscoveredHints

func RecordDiscoveredHints(source, ip, vendor, role string) error

RecordDiscoveredHints strengthens one lead's vendor/role hints (F2: SNMP sysDescr or neighbor platform). Existing hints win — hints never downgrade.

func RecordDiscoveredPorts

func RecordDiscoveredPorts(source, ip, hostname string, ports []DiscoveredPort) error

RecordDiscoveredPorts records a lead whose ports came from a non-tunnel source (nmap import: service name instead of banner).

func RecordDiscoveredSwept

func RecordDiscoveredSwept(source string, hosts []DiscoverHostResult, swept []int) error

RecordDiscoveredSwept is RecordDiscovered for a FULL port sweep: the caller probed exactly the swept list, so a stored port in that list that no longer answers is newly-closed (R2 event; the row drops). Hosts whose ports all closed never appear in results — their rows age out via LastSeen, a documented limitation (the partial-close signal is what matters).

func RecordMetricPoint

func RecordMetricPoint(device string, p MetricPoint) error

RecordMetricPoint appends one poll rollup and trims the ring.

func RecordPromotion

func RecordPromotion(device, ip string)

RecordPromotion appends one promotion line (best-effort).

func RecordSeries

func RecordSeries(device, metric string, v float64)

RecordSeries appends one point (best-effort; failures are silent — the timeline is a convenience layer, never a blocker).

func Redact

func Redact(s string) string

Redact applies every pattern; later patterns see earlier replacements, so a line can only lose more, never regain, secret material.

func RedactCounted

func RedactCounted(s string) (string, int)

RedactCounted is Redact plus the number of substitutions it made. The count powers the transparency reminder appended to tool output ("N 处敏感字段已 脱敏") and the audit entry's redacted field — the user can see redaction happened without ever seeing what it hid.

func RegisterTools

func RegisterTools(reg *tool.Registry, cfg *config.Config)

RegisterTools adds the netdev tool family to the registry. boot calls this ONLY inside the netdev profile branch, so dev/cowork sessions never see these tools (the reverse half of the hard seal; NETDEV_SPEC §7.1).

func RenderAttackPaths

func RenderAttackPaths(r *AttackPathReport) string

RenderAttackPaths turns a report into the OpsResult-style text block the UI and the daily briefing both carry. 推演 watermark first line, always.

func ResetEscalationStateForTest

func ResetEscalationStateForTest()

ResetEscalationStateForTest clears the once-map (tests only).

func ResolveFindingByID

func ResolveFindingByID(id string) error

ResolveFindingByID manually resolves one finding (the 发现 card's button).

func RoleFromName

func RoleFromName(name string) (role, source string)

RoleFromName classifies a node that exists only as a label — an unmanaged LLDP/CDP neighbor or an imported design shape. Weaker than the inventory chain (no vendor/config to lean on): keyword hit or unknown.

func RunningConfigCommand

func RunningConfigCommand(driverKey string) (string, bool)

RunningConfigCommand returns the read command that dumps the running configuration for a driver key.

func SanitizeForLive

func SanitizeForLive(s string) string

SanitizeForLive is the exported seam used by tests and the desktop bridge to apply the same chunk cleanup the session tap applies.

func SaveCase

func SaveCase(c *IncidentCase) error

SaveCase persists one case (id/时间戳在空时生成).

func SaveDiscoveryRun

func SaveDiscoveryRun(r *DiscoveryRunState) error

SaveDiscoveryRun persists the run state (idempotent overwrite).

func SaveFinding

func SaveFinding(f *Finding) error

SaveFinding validates and persists one finding.

func SaveLastBaseline

func SaveLastBaseline(s BaselineSummary)

SaveLastBaseline persists the latest baseline summary (best-effort).

func SaveProposal

func SaveProposal(p *Proposal) error

SaveProposal persists p (create or update).

func SaveRollingFinding

func SaveRollingFinding(f *Finding) error

SaveRollingFinding saves f as the SINGLE rolling entry for its Source: an existing finding with the same Source (any status) is updated in place (ID and original raise time preserved) instead of piling one copy per run. Used by the per-run info summaries (inspection/baseline) — their history lives in the inspection journal; the findings queue keeps one live card.

func SaveScheduleStamp

func SaveScheduleStamp(st ScheduleStamp)

SaveScheduleStamp persists the latest stamp (best-effort, single file).

func SaveTemplate

func SaveTemplate(t *Template) error

SaveTemplate persists t (create or update; ID assigned on create).

func SaveTopologyDesign

func SaveTopologyDesign(d *TopologyDesign) error

SaveTopologyDesign persists the confirmed design snapshot (idempotent overwrite; audited by the bridge caller).

func SecretHealth

func SecretHealth() (count int, lastChanged time.Time)

GetSecret reads one netdev credential.

netdev credentials are ALWAYS fetched explicitly through this accessor by the transport layer inside a netdev session — they are never loaded into the process environment (secret.Store.LoadIntoEnv), which coding/office bash children would inherit. That is the data-plane half of the profile isolation (NETDEV_SPEC §7.2). SecretHealth reports the netdev-namespace credential count and the vault file's last-change time (age granularity is the whole store — per- entry timestamps would need a schema bump; honest, coarse, cheap).

func SecretKey

func SecretKey(kind, name string) (string, error)

SecretKey builds the namespaced store key for one credential. name is the config-declared env-style identifier (the password_env value), so the key is stable across hosts that share a credential name.

func SeriesRead

func SeriesRead(device string, window time.Duration) map[string][]SeriesPoint

SeriesRead returns one device's points (all metrics) inside the window.

func SetAuditPath

func SetAuditPath(p string)

SetAuditPath overrides the audit file location (tests). Switching files invalidates the cached chain head — chaining a fresh file off the previous file's head would break verification at its very first line.

func SetBackupsDir

func SetBackupsDir(p string)

func SetFindingObserver

func SetFindingObserver(fn func(*Finding))

SetFindingObserver installs the saved-finding callback (replaces any previous one). Same pattern as SetHealthObserver.

func SetGoldenFromBackup

func SetGoldenFromBackup(device, versionID string) error

SetGoldenFromBackup copies one backup version's (already redacted) text as the device's baseline. Human-triggered from the 备份时间线.

func SetHealthObserver

func SetHealthObserver(fn func(DeviceHealth))

func SetNotifyPusher

func SetNotifyPusher(p NotifyPusher)

func SetSecret

func SetSecret(kind, name, value string) error

SetSecret stores one netdev credential.

func SharedRemoteNode

func SharedRemoteNode(cfg *config.Config) (*trustdomain.Node, error)

SharedRemoteNode lazily opens this host's embedded trust-domain node: identity + persisted ledger + peers from trustdomain.bootstrap_peers. One instance per process (the CLI daemon and agent tools must share it).

func SrvConfDiff

func SrvConfDiff(idA, idB string) (string, error)

SrvConfDiff returns the unified diff between two snapshots (a = old).

func SrvConfText

func SrvConfText(id string) (string, error)

SrvConfText returns one snapshot's content.

func StampDiscoveredLayer

func StampDiscoveredLayer(layer int, ips []string) error

StampDiscoveredLayer stamps the recursion depth on a batch of leads.

func StampDiscoveredVantage

func StampDiscoveredVantage(vantage string, ips []string) error

StampDiscoveredVantage marks which vantage produced a batch of leads (best-effort provenance stamp after RecordDiscovered).

func StartEscalationWatcher

func StartEscalationWatcher()

StartEscalationWatcher launches the escalation sweep goroutine once per process. Sweep cadence is half the timeout so the worst-case detection lag is timeout + half — acceptable for the minimal version.

func StateEventDiff

func StateEventDiff(id int) []diff.Change

StateEventDiff renders what a rewind to before this event would revert, reusing the checkpoint store's preview (old = current on-disk, new = snapshotted pre-event).

func StateEventSnap

func StateEventSnap(kind, entity, actor string, absPaths ...string) int

StateEventSnap records one restorable event: the current (pre-mutation) content of the given absolute paths. Call it immediately before the mutation, co-located with the transition's audit entry. Best-effort — a snapshot failure logs and never blocks the mutation itself. Returns the event id, or -1 when nothing was snapshotted.

func SyslogReceiverStatus

func SyslogReceiverStatus() (listening bool, port int, buffered int)

SyslogReceiverStatus reports the listener state for the UI.

func SyslogTail

func SyslogTail(device string, tailN int, grep string) []string

SyslogTail reads a device's ring buffer (newest last), optional grep filter (regex, literal fallback), bounded to tailN.

func TrapReceiverStatus

func TrapReceiverStatus() (listening bool, port int, buffered int)

TrapReceiverStatus reports the listener state for the UI.

func TrapTail

func TrapTail(device string, tailN int) []string

TrapTail reads a device's trap ring (newest last).

func TrustHostKey

func TrustHostKey(fingerprint string) error

TrustHostKey durably trusts a first-seen key the human confirmed. The fingerprint must match one captured by a recent TestConnection — trusting an arbitrary fingerprint without the presented key would record nothing real.

func ValidateRestoreFrom

func ValidateRestoreFrom(id string, steps []ProposalStep) error

ValidateRestoreFrom checks a restore proposal's source version (备份→恢复 闭环): the id must exist in the vault, belong to its own device prefix, and that device must carry a step in the proposal — a restore proposal touches the device the version came from.

Types

type AggregatedFinding

type AggregatedFinding struct {
	Key        string     `json:"key"` // Source or title
	Count      int        `json:"count"`
	Open       int        `json:"open"`     // active+ack
	Severity   string     `json:"severity"` // highest among members
	Devices    []string   `json:"devices"`
	Title      string     `json:"title"` // representative title
	Newest     time.Time  `json:"newest"`
	Members    []*Finding `json:"members,omitempty"` // newest first, capped
	Suppressed int        `json:"suppressed"`        // ≥2 = 此键已被误报学习降级
}

AggregatedFinding is the queue's collapsed view: one row per 根因键 (Source;人工/AI Finding 用 Title), with the member findings hidden inside.

func AggregateFindings

func AggregateFindings() []AggregatedFinding

AggregateFindings collapses the finding list by 根因键 (§4.10 同类聚合).

type AttackPath

type AttackPath struct {
	ExposureDevice string           `json:"exposure_device"`
	Reason         string           `json:"reason"` // the finding that marks the exposure
	FindingID      string           `json:"finding_id,omitempty"`
	Steps          []AttackPathStep `json:"steps"`
	EndDevice      string           `json:"end_device"`
	EndRole        string           `json:"end_role,omitempty"`
	EndManaged     bool             `json:"end_managed"`
	Hops           int              `json:"hops"`
	Score          int              `json:"score"`
}

AttackPath is one simulated route from an exposure point to a reachable asset.

type AttackPathReport

type AttackPathReport struct {
	GeneratedAt    string          `json:"generated_at"`
	Simulated      bool            `json:"simulated"` // constant true — 推演, not 实测
	EdgeSources    []string        `json:"edge_sources"`
	Nodes          int             `json:"nodes"`
	Edges          int             `json:"edges"`
	ExposurePoints []ExposurePoint `json:"exposure_points"`
	Paths          []AttackPath    `json:"paths"`
	Cuts           []CutSuggestion `json:"cut_suggestions"`
}

AttackPathReport is the whole simulation output.

func BuildAttackPaths

func BuildAttackPaths(g TopologyGraph, findings []*Finding) *AttackPathReport

BuildAttackPaths runs the simulation. Pure function: graph + findings in, report out. Callers assemble the graph from zero-session sources (IP-plan inference, design imports, persisted snapshots) — the sim itself connects to nothing.

type AttackPathStep

type AttackPathStep struct {
	From string `json:"from"`
	To   string `json:"to"`
	Via  string `json:"via"` // edge source: bastion | design | lldp | cdp
}

AttackPathStep is one hop with the edge that supports it.

type Audit

type Audit struct {
	Time        time.Time `json:"time"`
	Device      string    `json:"device"`
	Via         []string  `json:"via,omitempty"`
	Command     string    `json:"command"`
	Class       string    `json:"class"`  // read | write | dangerous | unknown (classifier verdict)
	Status      string    `json:"status"` // ok | device-error | refused | failure
	OutputBytes int       `json:"output_bytes,omitempty"`
	Error       string    `json:"error,omitempty"`
	// Hash chains entries (B-batch): sha256 over the previous entry's hash and
	// this entry's canonical JSON (hash field excluded). Any tampering with an
	// old line — or a line removed — breaks every later hash. Entries written
	// before the chain landed have empty hashes; the chain starts at the first
	// hashed entry.
	Hash string `json:"hash,omitempty"`
}

Audit records every device interaction in an append-only JSONL file under <user config>/fairpeer/netdev/audit.jsonl — one of the never-off guardrails (NETDEV_SPEC invariant 4). Command text and outcome are recorded; raw output is deliberately NOT stored (device output can carry secrets — the redactor and evidence integration land in P2; until then only the size and a hash are kept).

type AuditChainStatus

type AuditChainStatus struct {
	Total       int    `json:"total"`
	Chained     int    `json:"chained"`
	OK          bool   `json:"ok"`
	FirstBroken string `json:"firstBroken,omitempty"`
}

AuditChainStatus is the verification verdict for the audit tab's badge.

func VerifyAuditChain

func VerifyAuditChain() AuditChainStatus

VerifyAuditChain re-computes every hashed entry's hash in sequence. Legacy entries without hashes are skipped (the chain starts at the first hashed entry); a mismatch marks the chain broken from that entry on.

type AuditWindowStats

type AuditWindowStats struct {
	ByClass          map[string]int
	Read24h          int
	Write24h         int // write + proposal-write
	Guardrail24h     int
	ProposalWrites   int
	ProposalRollback int
	Entries          int // window size (honest denominator for the mix)
}

AuditWindowStats is the aggregate the overview stats bar and the cutover board read. Read24h/Write24h/Guardrail24h mirror the briefing counters.

func AuditWindow

func AuditWindow(days int) *AuditWindowStats

AuditWindow aggregates the audit chain inside the window (days, default 30, cap 90). Cached by (file size, mtime) — the chain is append-only, so the cache key is a single comparison.

type BackupVersion

type BackupVersion struct {
	ID     string `json:"id"` // <device>@<unixnano>
	Device string `json:"device"`
	At     string `json:"at"`    // 01-02 15:04:05
	Bytes  int    `json:"bytes"` // redacted text length
	Lines  int    `json:"lines"`
}

func ListBackups

func ListBackups(device string) []BackupVersion

ListBackups returns a device's versions, newest first ("" = every device).

type BannerInfo

type BannerInfo struct {
	// Kind: ssh | http | ftp | smtp | "" (other/unparsed).
	Kind string `json:"kind,omitempty"`
	// Product is the software token from the banner ("Huawei-1.0",
	// "OpenSSH_9.6", "nginx/1.24.0", "vsftpd 3.0.3").
	Product string `json:"product,omitempty"`
	// Version is the product's version when it splits cleanly.
	Version string `json:"version,omitempty"`
	// VendorHint: huawei | cisco | zte | hillstone | vmware | linux | "".
	VendorHint string `json:"vendor_hint,omitempty"`
	// RoleHint is a topology-role suggestion; only set when the banner
	// strongly implies a class (VMware → server). Network-vendor SSH banners
	// deliberately stay empty — Huawei ships the same SSH line on switches,
	// routers and firewalls, and a wrong icon is worse than none.
	RoleHint string `json:"role_hint,omitempty"`
}

BannerInfo is the parsed form of one port banner.

func ParseBanner

func ParseBanner(raw string) BannerInfo

ParseBanner classifies a captured banner. Empty/unprintable input returns the zero value — the caller treats absence of hints as absence of data, never as a negative result.

type BaselineAggView

type BaselineAggView struct {
	Devices int    `json:"devices"`
	Checked int    `json:"checked"`
	Rules   int    `json:"rules"`
	Hits    int    `json:"hits"`
	At      string `json:"at"`
}

BaselineAggView is the JSON-persisted last-run summary.

func LoadLastBaseline

func LoadLastBaseline() *BaselineAggView

LoadLastBaseline reads it back; nil when never run (引导态,不是 0).

type BaselineSummary

type BaselineSummary struct {
	Devices int    `json:"devices"`
	Checked int    `json:"checked"` // devices whose config was actually read
	Rules   int    `json:"rules"`   // applicable rule count across devices
	Hits    int    `json:"hits"`    // total violations
	At      string `json:"at"`
}

BaselineSummary is the aggregate the UI and the agent tool see.

type BaselineViolation

type BaselineViolation struct {
	Rule       string   `json:"rule"`
	Title      string   `json:"title"`
	Severity   string   `json:"severity"`
	Suggestion string   `json:"suggestion,omitempty"`
	Evidence   []string `json:"evidence"`
}

BaselineViolation is one rule hit on one device.

func CheckBaseline

func CheckBaseline(driverKey, config string) []BaselineViolation

CheckBaseline runs the rule battery over one config text and returns the violated rules with their evidence lines (already-redacted text). Pure function — unit-testable without any device.

type CVEEntry

type CVEEntry struct {
	ID       string   `json:"id"` // CVE-2024-xxxx
	Desc     string   `json:"desc"`
	Products []string `json:"products"` // lowercase vendor/product substrings
	Severity string   `json:"severity"` // critical | high | medium | low
}

CVEEntry is one simplified feed item.

type CVEMatch

type CVEMatch struct {
	Device   string `json:"device"`
	CVEID    string `json:"cve_id"`
	Desc     string `json:"desc"`
	Severity string `json:"severity"`
	Product  string `json:"product"` // matched product substring
}

CVEMatch is one device ↔ CVE hit.

type CaseEntry

type CaseEntry struct {
	Time   time.Time `json:"time"`
	Kind   string    `json:"kind"` // finding | log | audit | triage | note
	Device string    `json:"device,omitempty"`
	Text   string    `json:"text"`
	Ref    string    `json:"ref,omitempty"` // e.g. finding id
}

CaseEntry is one reference card pinned on the case timeline.

type CaseIOC

type CaseIOC struct {
	Value   string    `json:"value"`
	Type    string    `json:"type"` // ip | hash | domain | keyword
	Note    string    `json:"note,omitempty"`
	AddedAt time.Time `json:"added_at"`
}

CaseIOC is one indicator in the case ledger.

type ChainEdge

type ChainEdge struct {
	From  string `json:"from"`
	To    string `json:"to"`
	Label string `json:"label"`
}

ChainEdge is one typed relation (触发/执行/产生/证实/处置/验证/闭环).

type ChainNode

type ChainNode struct {
	ID     string `json:"id"`
	Kind   string `json:"kind"`
	Label  string `json:"label"`
	Device string `json:"device,omitempty"`
	At     string `json:"at,omitempty"`
	// RefType/RefID deep-link the card to its source row (finding/audit/
	// proposal/case-entry); the dash never carries the full text.
	RefType string `json:"ref_type,omitempty"`
	RefID   string `json:"ref_id,omitempty"`
	Status  string `json:"status,omitempty"` // finding severity | proposal status
	// Group>0 marks a collapsed sibling group ("N× 命令采集").
	Group int `json:"group,omitempty"`
}

ChainNode is one card on the investigation chain (title-level only).

type CutSuggestion

type CutSuggestion struct {
	From         string `json:"from"`
	To           string `json:"to"`
	Via          string `json:"via"`
	PathsRemoved int    `json:"paths_removed"`
}

CutSuggestion ranks edges by how many simulated paths they carry — cutting the top edges removes the most reach (a prioritization hint for proposals, never an executed change).

type CutoverBoard

type CutoverBoard struct {
	ID            string               `json:"id"`
	Name          string               `json:"name"`
	Status        string               `json:"status"`
	Deadline      string               `json:"deadline,omitempty"`
	RemainingSec  int64                `json:"remaining_sec"` // >0 running; <=0 window over
	Frozen        int                  `json:"frozen"`        // affected device count
	Steps         []CutoverBoardStep   `json:"steps"`
	Devices       []CutoverBoardDevice `json:"devices"`
	RollbackReady bool                 `json:"rollback_ready"`
	RollbackNote  string               `json:"rollback_note,omitempty"`
	Jobs          []CutoverBoardJob    `json:"jobs"`
	Audit         []CutoverBoardAudit  `json:"audit"`
	Report        string               `json:"report,omitempty"`
	HasActive     bool                 `json:"has_active"` // any running/hold cutover exists
	Found         bool                 `json:"found"`      // false = no cutover at all → empty state
}

CutoverBoard is the whole cutover screen payload.

func BuildCutoverBoard

func BuildCutoverBoard(id string) *CutoverBoard

BuildCutoverBoard assembles the cutover screen. id=="" picks the active cutover, else the newest one (终态复盘).

type CutoverBoardAudit

type CutoverBoardAudit struct {
	Time    string `json:"time"`
	Device  string `json:"device"`
	Command string `json:"command"`
	Status  string `json:"status"`
}

CutoverBoardAudit is one command-stream row for this cutover.

type CutoverBoardDevice

type CutoverBoardDevice struct {
	Device        string `json:"device"`
	Status        string `json:"status"` // done | running | pending | mixed
	RollbackReady bool   `json:"rollback_ready"`
}

CutoverBoardDevice is one affected device's own progress + rollback point.

type CutoverBoardJob

type CutoverBoardJob struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Status      string `json:"status"`
	ActiveMS    int64  `json:"active_ms"`
	Commands    int    `json:"commands"`
	MaxWallSec  int    `json:"max_wall_sec,omitempty"`
	MaxCommands int    `json:"max_commands,omitempty"`
}

CutoverBoardJob is a live job row (budget burn view; jobs are fleet-wide, not tied to the cutover — labeled as such in the UI).

type CutoverBoardStep

type CutoverBoardStep struct {
	Label      string `json:"label"`
	Status     string `json:"status"`
	Device     string `json:"device,omitempty"`
	ProposalID string `json:"proposal_id,omitempty"`
	Gate       bool   `json:"gate,omitempty"`
	Decision   bool   `json:"decision_point,omitempty"`
	EstSec     int    `json:"est_sec,omitempty"`
	StartedAt  string `json:"started_at,omitempty"`
	EndedAt    string `json:"ended_at,omitempty"`
}

CutoverBoardStep is one pipeline node (proposal steps + gates + reads).

type CutoverGate

type CutoverGate struct {
	Device     string `json:"device"`
	Command    string `json:"command"`
	Expect     string `json:"expect"`
	SustainSec int    `json:"sustain_sec,omitempty"` // default 30
	TimeoutSec int    `json:"timeout_sec,omitempty"` // default 2×sustain+90
}

CutoverGate is a semantic verification gate: Command's output must match Expect CONTINUOUSLY for SustainSec (e.g. "OSPF 邻居 Full 且持续 60s").

type CutoverRun

type CutoverRun struct {
	ID       string        `json:"id"`
	Name     string        `json:"name"`
	Deadline time.Time     `json:"deadline"` // 总倒计时(割接窗口结束)
	Steps    []CutoverStep `json:"steps"`
	Status   string        `json:"status"`
	HoldNote string        `json:"hold_note,omitempty"`
	Cursor   int           `json:"cursor"`

	// 割接前后基线快照(device → backup id)+ 对比报告。
	PreSnapshot  map[string]string `json:"pre_snapshot,omitempty"`
	PostSnapshot map[string]string `json:"post_snapshot,omitempty"`
	Report       string            `json:"report,omitempty"`

	CreatedAt time.Time  `json:"created_at"`
	StartedAt *time.Time `json:"started_at,omitempty"`
	EndedAt   *time.Time `json:"ended_at,omitempty"`
}

CutoverRun is one cutover execution.

func GetCutover

func GetCutover(id string) (*CutoverRun, error)

GetCutover loads one run.

func ListCutovers

func ListCutovers() ([]*CutoverRun, error)

ListCutovers returns runs newest-first.

type CutoverStep

type CutoverStep struct {
	Label         string       `json:"label"`
	EstSec        int          `json:"est_sec,omitempty"`     // 预计耗时(倒计时排程显示)
	ProposalID    string       `json:"proposal_id,omitempty"` // execute an APPROVED proposal
	Device        string       `json:"device,omitempty"`      // …or one sealed read command
	Command       string       `json:"command,omitempty"`
	Gate          *CutoverGate `json:"gate,omitempty"`           // post-step verification gate
	DecisionPoint bool         `json:"decision_point,omitempty"` // 回退决策点:完成后 hold 等人
	Impact        string       `json:"impact,omitempty"`         // 影响描述(决策点并列展示)

	Status    string     `json:"status"`
	StartedAt *time.Time `json:"started_at,omitempty"`
	EndedAt   *time.Time `json:"ended_at,omitempty"`
	Output    string     `json:"output,omitempty"`
	Error     string     `json:"error,omitempty"`
}

CutoverStep is one runbook entry.

type DeviceHealth

type DeviceHealth struct {
	Device     string     `json:"device"`
	Time       time.Time  `json:"time"`
	Reachable  bool       `json:"reachable"`
	UptimeSec  int64      `json:"uptimeSec"`
	Interfaces []IfHealth `json:"interfaces"`
	LastError  string     `json:"lastError,omitempty"`
	// 水位(SNMP OID 扩展):0 = 未采集到(厂商 OID 不适用或设备不答)。
	CpuPct int `json:"cpuPct,omitempty"`
	MemPct int `json:"memPct,omitempty"`
	// 接口 octets 计数器累计和(读取端差分成 bps;0 = 未采集)。
	InOct  uint64 `json:"inOct,omitempty"`
	OutOct uint64 `json:"outOct,omitempty"`
}

DeviceHealth is one device's latest poll result.

func (DeviceHealth) IfDown

func (h DeviceHealth) IfDown() int

func (DeviceHealth) IfUp

func (h DeviceHealth) IfUp() int

UpCount/DownCount helpers for the UI (oper-down among admin-up = real alarms).

type DiscoverHostResult

type DiscoverHostResult struct {
	IP    string              `json:"ip"`
	Ports []DiscoverPortProbe `json:"ports"`
}

DiscoverHostResult is one live host.

type DiscoverPlan

type DiscoverPlan struct {
	Vantage  string     `json:"vantage"`
	Steps    []PlanStep `json:"steps"`
	ArpKnown int        `json:"arp_known"`
	Warnings []string   `json:"warnings"`
}

DiscoverPlan is the confirm-before-probe object (spec §4.5 precheck flow).

type DiscoverPortProbe

type DiscoverPortProbe struct {
	Port   int    `json:"port"`
	Open   bool   `json:"open"`
	Banner string `json:"banner,omitempty"` // first bytes, e.g. "SSH-2.0-..."
}

DiscoverPortProbe is one port's outcome on one host.

type DiscoveredHost

type DiscoveredHost struct {
	IP         string           `json:"ip"`
	Hostname   string           `json:"hostname,omitempty"`
	VendorHint string           `json:"vendor_hint,omitempty"`
	RoleHint   string           `json:"role_hint,omitempty"`
	FirstSeen  time.Time        `json:"first_seen"`
	LastSeen   time.Time        `json:"last_seen"`
	Sources    []string         `json:"sources"`
	Ports      []DiscoveredPort `json:"ports"`
	// Vantage records which managed device saw this lead (F4 provenance —
	// the human-driven recursion's audit trail); Layer is the recursion depth
	// it was found at (inventory vantage → 1).
	Vantage string `json:"vantage,omitempty"`
	Layer   int    `json:"layer,omitempty"`
}

DiscoveredHost is one unmanaged asset lead.

func ListDiscoveredHosts

func ListDiscoveredHosts() ([]*DiscoveredHost, error)

ListDiscoveredHosts returns leads newest-first (LastSeen).

type DiscoveredPort

type DiscoveredPort struct {
	Port   int        `json:"port"`
	Banner string     `json:"banner,omitempty"`
	Parsed BannerInfo `json:"parsed,omitempty"`
	// HTTP carries F3's opt-in application fingerprint (title/Server/cert)
	// when http_probe is enabled and the port answered one standard GET.
	HTTP *HTTPFingerprint `json:"http,omitempty"`
	At   time.Time        `json:"at"`
	// FirstSeen stamps the port's first observation (R2 journal basis:
	// newly-opened/newly-closed events are the diff between sweeps).
	FirstSeen time.Time `json:"first_seen,omitempty"`
}

DiscoveredPort is one port observation on one host.

type DiscoveryBoard

type DiscoveryBoard struct {
	Leads         int `json:"leads"`
	Fingerprinted int `json:"fingerprinted"`
	Pending       int `json:"pending"`
	Promoted      int `json:"promoted"`
	Managed       int `json:"managed"`

	SubnetsDone  int `json:"subnets_done"`
	SubnetsTotal int `json:"subnets_total"`
	LayerDepth   int `json:"layer_depth"`
	MaxHops      int `json:"max_hops"`

	RunStatus    string `json:"run_status,omitempty"`
	RunVantage   string `json:"run_vantage,omitempty"`
	RunUpdatedAt string `json:"run_updated_at,omitempty"`

	Funnel     []DiscoveryFunnelStep `json:"funnel"`
	Layers     []DiscoveryLayerRow   `json:"layers"`
	TriSource  TriSourceAgg          `json:"tri_source"`
	PortEvents []PortEvent           `json:"port_events"`
}

DiscoveryBoard is the whole discovery screen payload.

type DiscoveryFunnelStep

type DiscoveryFunnelStep struct {
	Key   string `json:"key"`
	Count int    `json:"count"`
}

DiscoveryFunnelStep is one funnel stage (Key: leads|fingerprinted|pending| promoted|managed — labels live in the frontend i18n).

type DiscoveryLayerRow

type DiscoveryLayerRow struct {
	Layer int    `json:"layer"`
	Label string `json:"label"`
	Note  string `json:"note,omitempty"`
}

DiscoveryLayerRow is one layer-ledger summary row.

type DiscoveryRunState

type DiscoveryRunState struct {
	ID        string   `json:"id"`
	Vantage   string   `json:"vantage"`
	Ports     []int    `json:"ports,omitempty"`
	Cidrs     []string `json:"cidrs"`
	DoneCidrs []string `json:"done_cidrs"`
	// Status: running | paused | done.
	Status     string `json:"status"`
	StartedAt  string `json:"started_at"`
	UpdatedAt  string `json:"updated_at"`
	FoundSoFar int    `json:"found_so_far"`
}

DiscoveryRunState is the persisted progress of one layered scan.

func LoadDiscoveryRun

func LoadDiscoveryRun() (*DiscoveryRunState, error)

LoadDiscoveryRun returns the current run (nil when none/corrupt).

func (*DiscoveryRunState) Remaining

func (r *DiscoveryRunState) Remaining() []string

Remaining returns the not-yet-probed nets in plan order.

type Evidence

type Evidence struct {
	Device  string `json:"device"`
	Command string `json:"command"`
	Output  string `json:"output"` // redacted excerpt
}

Evidence is one command-output pair backing a finding.

type ExecResult

type ExecResult struct {
	Device  string `json:"device"`
	Command string `json:"command"`
	Class   string `json:"class"`
	Output  string `json:"output"`
	IsError bool   `json:"is_error"`
	Refused bool   `json:"refused,omitempty"`
	Refusal string `json:"refusal,omitempty"`
}

ExecResult is the tool-facing outcome of one command.

type ExpectedStateView

type ExpectedStateView struct {
	Total     int            `json:"total"` // 有采集面的设备数
	Reachable int            `json:"reachable"`
	Missing   []DeviceHealth `json:"missing"` // 期望在线但不可达
	NoProbe   []string       `json:"noProbe"` // 清单里暂无健康采集面的设备
}

ExpectedStateView — 期望状态对比(§5.4 第三件):清单声明 vs 健康采集。

type ExposureBoard

type ExposureBoard struct {
	Simulated      bool                `json:"simulated"` // constant true — 推演角标
	GeneratedAt    string              `json:"generated_at"`
	Critical       int                 `json:"critical"`
	Warning        int                 `json:"warning"`
	Paths          []AttackPath        `json:"paths"`           // top 50 by score
	Cuts           []CutSuggestion     `json:"cut_suggestions"` // top 10
	ExposurePoints []ExposurePoint     `json:"exposure_points"`
	Matrix         []ExposureMatrixRow `json:"matrix"`
	CVEBySeverity  map[string]int      `json:"cve_by_severity,omitempty"`
	CVENeedsFeed   bool                `json:"cve_needs_feed"`
	UnmanagedEnds  int                 `json:"unmanaged_ends"`
	MaxHops        int                 `json:"max_hops"`
}

ExposureBoard is the whole exposure screen payload.

type ExposureMatrixRow

type ExposureMatrixRow struct {
	Device      string `json:"device"`
	Critical    int    `json:"critical"`
	Warning     int    `json:"warning"`
	Info        int    `json:"info"`
	CveCritical int    `json:"cve_critical"`
	CveHigh     int    `json:"cve_high"`
	Managed     bool   `json:"managed"`
}

ExposureMatrixRow is one device's exposure tally.

type ExposurePoint

type ExposurePoint struct {
	Device    string `json:"device"`
	FindingID string `json:"finding_id,omitempty"`
	Reason    string `json:"reason"`
	Paths     int    `json:"paths"`
}

ExposurePoint is one 起点 with its simulated blast radius.

type Finding

type Finding struct {
	ID         string     `json:"id"`
	Title      string     `json:"title"`
	Severity   string     `json:"severity"`
	Devices    []string   `json:"devices"`
	Detail     string     `json:"detail"`
	Evidence   []Evidence `json:"evidence"`
	Suggestion string     `json:"suggestion,omitempty"` // what change to draft (netdev_propose), if any
	CreatedAt  time.Time  `json:"created_at"`
	// Source marks the automatic origin ("alert:<rule>:<device>",
	// "syslog:<device>:<class>") — auto-findings dedup and auto-resolve by it;
	// human/AI findings leave it empty.
	Source string `json:"source,omitempty"`
	// Status is the alert lifecycle: "" (human/AI finding) | active | resolved.
	Status     string     `json:"status,omitempty"`
	ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
	// Project is the site-scope snapshot stamped at save time: the project
	// whose groups own the most of this finding's devices (ProjectForDevices).
	// "" = 未分组 — such findings (unknown-source syslog, "(all)" sweeps,
	// pre-Project legacy files) stay visible in EVERY project view so the
	// blind-spot rule holds: alerts that match no project are never hidden.
	// Stamped only when empty — a re-save keeps the original stamp; group
	// membership is a view concern, the stamp is audit history.
	Project string `json:"project,omitempty"`
}

Finding is one diagnosis conclusion.

func ImportNmapForConfig

func ImportNmapForConfig(xmlText string, deviceAddrs []string) (*Finding, error)

ImportNmapForConfig adapts ImportNmapXML to the config type (bridge seam).

func ImportNmapXML

func ImportNmapXML(xmlText string, cfg inventory) (*Finding, error)

ImportNmapXML parses an nmap -oX dump and files one Finding: every live host with its open ports; hosts missing from the inventory are flagged 待确认 (they may be joinable — the user decides, nothing dials).

func ListFindings

func ListFindings() ([]*Finding, error)

ListFindings returns findings newest-first.

type GoldenDrift

type GoldenDrift struct {
	Device  string
	Extra   []string // in running, not in golden (意外配置)
	Missing []string // in golden, not in running (丢失配置)
	Note    string   // e.g. offline fallback marker
}

GoldenDrift is one check's outcome for one device.

type GoldenInfo

type GoldenInfo struct {
	Set   bool   `json:"set"`
	At    string `json:"at"`
	Lines int    `json:"lines"`
}

GoldenInfo describes one device's baseline for the UI.

func GoldenInfoOf

func GoldenInfoOf(device string) GoldenInfo

GoldenInfoOf reports the baseline's presence for the timeline header.

type HTTPFingerprint

type HTTPFingerprint struct {
	Title   string `json:"title,omitempty"`
	Server  string `json:"server,omitempty"`
	CertCN  string `json:"cert_cn,omitempty"`
	CertSAN string `json:"cert_san,omitempty"`
}

HTTPFingerprint is what one polite request learned.

type HealthSnapshot

type HealthSnapshot struct {
	PollIntervalSeconds int            `json:"pollIntervalSeconds"`
	Devices             []DeviceHealth `json:"devices"`
}

HealthSnapshot is the whole fleet's latest state.

type HumanTTYRecording

type HumanTTYRecording struct {
	Device string `json:"device"`
	Path   string `json:"path"`
	At     string `json:"at"` // yyyymmdd-hhmmss
	Bytes  int64  `json:"bytes"`
}

HumanTTYRecording is one saved session recording (completion-spec §6 #5: 回放查看——文件已在落盘时 ANSI 剥离 + 脱敏,此处只做只读罗列/读取).

func ListHumanTTYRecordings

func ListHumanTTYRecordings() ([]HumanTTYRecording, error)

ListHumanTTYRecordings scans the recording dir, newest first.

type HumanTTYState

type HumanTTYState struct {
	Device    string    `json:"device"`
	Connected bool      `json:"connected"`
	StartedAt time.Time `json:"started_at"`
	Bytes     int64     `json:"bytes"` // total output bytes (for audit)
	Error     string    `json:"error,omitempty"`
}

HumanTTYState is the terminal session lifecycle.

func HumanTTYStatus

func HumanTTYStatus() []HumanTTYState

HumanTTYStatus reports all active sessions (for the live panel / audit tab).

type IfBriefCounts

type IfBriefCounts struct {
	Up   int `json:"up"`
	Down int `json:"down"`
}

IfBriefCounts is the per-device interface tally lifted from the inspection battery's interface-brief output (line-count heuristic, not a per-interface ledger — good enough for a trend, labeled as such).

func SummarizeIfBrief

func SummarizeIfBrief(output string) IfBriefCounts

SummarizeIfBrief counts interface rows by link state from a brief-style output (huawei `display interface brief` / cisco `show interfaces status`). Heuristic line counting — the R1 row is a trend input, not an interface DB.

type IfHealth

type IfHealth struct {
	Name    string `json:"name"`
	AdminUp bool   `json:"adminUp"`
	OperUp  bool   `json:"operUp"`
}

IfHealth is one interface row (ifDescr/ifAdminStatus/ifOperStatus).

type ImportDBSource

type ImportDBSource struct {
	Name string   `json:"name"`
	Type string   `json:"type"`
	Host string   `json:"host"`
	Port int      `json:"port"`
	Via  []string `json:"via"`
}

ImportDBSource is a db_sources skeleton.

type ImportDevice

type ImportDevice struct {
	Name    string   `json:"name"`
	Vendor  string   `json:"vendor"`
	Kind    string   `json:"kind"`
	Address string   `json:"address"`
	Group   string   `json:"group"`
	Via     []string `json:"via"`
}

ImportDevice is the skeleton from the export file.

type ImportDeviceConflict

type ImportDeviceConflict struct {
	Imported ImportDevice `json:"imported"`
	Local    ImportDevice `json:"local"`
}

ImportDeviceConflict adds the local view for side-by-side comparison.

type ImportPreview

type ImportPreview struct {
	// NewDevices would be ADDED (name not in current inventory).
	NewDevices []ImportDevice `json:"new_devices"`
	// ConflictDevices exist by name — their address/vendor may differ; the
	// user decides keep-local vs take-imported per device.
	ConflictDevices []ImportDeviceConflict `json:"conflict_devices"`
	// DBNew / DBOverride mirror the same split for db_sources.
	DBNew     []ImportDBSource `json:"db_new"`
	DBOverlap []string         `json:"db_overlap"`
	// SkippedCounts are informational: findings/series/audit tails are
	// import-read-only (the local audit chain stays append-only).
	FindingsSeen int    `json:"findings_seen"`
	ExportedAt   string `json:"exported_at"`
	Source       string `json:"source"`
}

ImportPreview is the human-review diff for one import run.

type ImportTopoPreview

type ImportTopoPreview struct {
	Graph    TopologyGraph   `json:"graph"`
	Stats    TopoImportStats `json:"stats"`
	Warnings []string        `json:"warnings"`
}

ImportTopoPreview is the no-side-effect parse result the UI confirms.

func ImportDrawio

func ImportDrawio(xmlText string, deviceNames []string, deviceAddrs map[string]string) (*ImportTopoPreview, error)

ImportDrawio parses a .drawio/.xml design into a preview. deviceNames and deviceAddrs drive the L3 fusion (managed markers); empty slices are fine.

func ImportVsdx

func ImportVsdx(data []byte, deviceNames []string, deviceAddrs map[string]string) (*ImportTopoPreview, error)

ImportVsdx parses a .vsdx file into the standard preview. deviceNames / deviceAddrs drive the L3 fusion, exactly like ImportDrawio.

type IncidentCase

type IncidentCase struct {
	ID        string      `json:"id"`
	Title     string      `json:"title"`
	Status    string      `json:"status"` // open | closed
	Devices   []string    `json:"devices,omitempty"`
	Entries   []CaseEntry `json:"entries,omitempty"`
	IOCs      []CaseIOC   `json:"iocs,omitempty"`
	CreatedAt time.Time   `json:"created_at"`
	UpdatedAt time.Time   `json:"updated_at"`
}

IncidentCase is one investigation.

func ListCases

func ListCases() ([]*IncidentCase, error)

ListCases returns all cases, newest-updated first.

type InspectionJournalRow

type InspectionJournalRow struct {
	At           string                   `json:"at"`   // 2006-01-02T15:04:05
	Kind         string                   `json:"kind"` // inspection | baseline
	Devices      int                      `json:"devices"`
	Checked      int                      `json:"checked"`
	Critical     int                      `json:"critical"` // open tallies at run end — the risk-score trend source
	Warning      int                      `json:"warning"`
	Info         int                      `json:"info"`
	BaselineHits int                      `json:"baseline_hits,omitempty"`
	IfBrief      map[string]IfBriefCounts `json:"if_brief,omitempty"`
}

InspectionJournalRow is one run's summary line (R1).

func ReadInspectionRows

func ReadInspectionRows(limit int) []InspectionJournalRow

ReadInspectionRows returns up to limit newest R1 rows (oldest→newest order preserved for sparklines).

type InvestigationChain

type InvestigationChain struct {
	CaseID    string `json:"case_id,omitempty"`
	CaseTitle string `json:"case_title,omitempty"`
	HasCase   bool   `json:"has_case"`
	FindingID string `json:"finding_id,omitempty"`
	// Counts carries per-kind node tallies — the inline stats row; the
	// denominators are the node counts themselves (可查 via deep links).
	Counts    map[string]int  `json:"counts"`
	Nodes     []ChainNode     `json:"nodes"`
	Edges     []ChainEdge     `json:"edges"`
	Timeline  []TimelineEvent `json:"timeline,omitempty"`
	Truncated bool            `json:"truncated"`
}

InvestigationChain is the whole chain screen payload.

type Job

type Job struct {
	ID        string         `json:"id"`
	Name      string         `json:"name"`
	Steps     []JobStep      `json:"steps"`
	StepState []JobStepState `json:"step_state"`
	Status    string         `json:"status"`
	Budget    JobBudget      `json:"budget"`

	CreatedAt time.Time  `json:"created_at"`
	StartedAt *time.Time `json:"started_at,omitempty"`
	EndedAt   *time.Time `json:"ended_at,omitempty"`

	ActiveMS     int64  `json:"active_ms"`            // wall clock actually spent running (pauses excluded)
	Commands     int    `json:"commands"`             // attempts executed against the budget
	Cursor       int    `json:"cursor"`               // next step index (断点续跑 anchor)
	BreakpointOK int    `json:"breakpoint_ok"`        // cursor whose PauseBefore was human-confirmed
	PauseNote    string `json:"pause_note,omitempty"` // why it paused — shown to the human
	CreatedBy    string `json:"created_by,omitempty"`
}

Job is one runbook run.

func GetJob

func GetJob(id string) (*Job, error)

GetJob loads one job.

func JobAbort

func JobAbort(id string) (*Job, error)

JobAbort stops a running/paused job for good; remaining steps are skipped.

func ListJobs

func ListJobs() ([]*Job, error)

ListJobs returns jobs newest-first.

type JobBudget

type JobBudget struct {
	MaxWallSec  int `json:"max_wall_sec,omitempty"` // default 1800
	MaxCommands int `json:"max_commands,omitempty"` // default 200
	FailStreak  int `json:"fail_streak,omitempty"`  // default 3 consecutive failures → circuit-break pause
}

Job watchdog budgets (v1 C 批). Zero fields take the defaults; a paused job does not burn wall clock.

type JobStep

type JobStep struct {
	Name        string `json:"name"`
	Device      string `json:"device"`
	Command     string `json:"command"`
	Expect      string `json:"expect,omitempty"`       // regex that must appear in the output within the timeout
	TimeoutSec  int    `json:"timeout_sec,omitempty"`  // per attempt (default 60)
	Retries     int    `json:"retries,omitempty"`      // extra attempts after the first failure (≤4)
	OnFail      string `json:"on_fail,omitempty"`      // pause | abort | continue (default pause)
	PauseBefore bool   `json:"pause_before,omitempty"` // 断点:human confirms before this step runs
}

JobStep is one runbook entry: a single sealed read command with its own success criterion (Expect), timing, and failure policy.

type JobStepState

type JobStepState struct {
	Status    string     `json:"status"`
	Attempts  int        `json:"attempts,omitempty"`
	Output    string     `json:"output,omitempty"` // last attempt tail, already redacted by Exec
	Error     string     `json:"error,omitempty"`
	StartedAt *time.Time `json:"started_at,omitempty"`
	EndedAt   *time.Time `json:"ended_at,omitempty"`
}

JobStepState is the step's execution trail.

type LiveDeviceState

type LiveDeviceState struct {
	Device    string `json:"device"`
	Vendor    string `json:"vendor"`
	OS        string `json:"os,omitempty"`
	Group     string `json:"group,omitempty"`
	Connected bool   `json:"connected"`
	VTYUse    int    `json:"vtyUse"`
	VTYCap    int    `json:"vtyCap"`
}

LiveDeviceState is one device's snapshot entry for the panel's initial paint.

type LiveEvent

type LiveEvent struct {
	Kind   string `json:"kind"`
	Device string `json:"device"`
	Time   int64  `json:"time"` // unix milliseconds

	// LiveConn
	State  string `json:"state,omitempty"`
	VTYUse int    `json:"vtyUse,omitempty"` // sessions currently held on the device
	VTYCap int    `json:"vtyCap,omitempty"` // max_sessions_per_device (0 = unset)

	// LiveCmdStart / LiveCmdEnd / LiveCmdRefused
	Command string `json:"command,omitempty"`
	Class   string `json:"class,omitempty"` // read | write | dangerous | unknown | guardrail

	// LiveCmdOutput
	Chunk string `json:"chunk,omitempty"`

	// LiveCmdEnd / LiveCmdRefused
	Status string `json:"status,omitempty"` // ok | device-error | refused | failure
	MS     int64  `json:"ms,omitempty"`     // wall-clock duration of the command
	Bytes  int    `json:"bytes,omitempty"`  // redacted output size
	Reason string `json:"reason,omitempty"` // refusal / failure reason
}

LiveEvent is one observable moment. Fields are grouped by kind; only the relevant subset is populated.

type LiveSnapshot

type LiveSnapshot struct {
	Devices []LiveDeviceState `json:"devices"`
	Spent   int               `json:"spent"`  // commands spent this turn
	Budget  int               `json:"budget"` // turn_command_budget (0 = unlimited)
}

LiveSnapshot is the panel's mount-time state: per-device connection/VTY state plus the per-turn budget counters.

type LocateHit

type LocateHit struct {
	Device    string `json:"device"`
	Interface string `json:"interface,omitempty"`
	Line      string `json:"line"`
}

LocateHit is one device's answer for the target.

type LocateResult

type LocateResult struct {
	Target     string      `json:"target"`
	Hits       []LocateHit `json:"hits"`
	Devices    []string    `json:"searched"`
	Skipped    []string    `json:"skipped,omitempty"`
	Covered    int         `json:"covered_devices"`
	Total      int         `json:"total_devices"`
	BudgetStop bool        `json:"budget_stopped"`
	Note       string      `json:"note,omitempty"`
}

LocateResult is the fan-out outcome.

type LogFollowEvent

type LogFollowEvent struct {
	Device string `json:"device"`
	Source string `json:"source"`
	Chunk  string `json:"chunk,omitempty"`
	Done   bool   `json:"done,omitempty"`
	Reason string `json:"reason,omitempty"`
}

LogFollowEvent is one streaming callback: a chunk of (redacted, line-aligned) text, or the terminal done event carrying the stop reason.

type LogSearchHit

type LogSearchHit struct {
	Device  string   `json:"device"`
	Source  string   `json:"source"`
	Line    string   `json:"line"`
	Context []string `json:"context,omitempty"`
}

LogSearchHit is one matching line with its ±context.

type LogSearchResult

type LogSearchResult struct {
	Pattern    string         `json:"pattern"`
	Hits       []LogSearchHit `json:"hits"`
	Devices    []string       `json:"devices_searched"`
	Skipped    []string       `json:"skipped"` // device — reason strings
	Covered    int            `json:"covered_devices"`
	Total      int            `json:"total_devices"`
	HitDevice  int            `json:"devices_with_hits"`
	BudgetStop bool           `json:"budget_stopped"`
	Note       string         `json:"note,omitempty"`
}

LogSearchResult is the sweep outcome. Covered/Total + Skipped make partial coverage explicit — a budget-stopped sweep reports exactly what it missed.

type LogSourceProbe

type LogSourceProbe struct {
	Device     string      `json:"device"`
	Services   []string    `json:"services"`
	Containers []string    `json:"containers"`
	Files      []ProbeFile `json:"files"`
	Errors     []string    `json:"errors"`
}

LogSourceProbe is the probe result: running services (journal: candidates), containers (docker:), and /var/log files (file:). Errors carries per-leg notes (e.g. "docker: not available") — a missing docker is the common case, surfaced as a hint rather than a failure.

type Manager

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

Manager owns per-device connections and enforces the structural read-only seal: every netdev_exec command passes the driver classifier, and anything that is not ClassRead is refused and audited — never executed. There is no code path from the agent to a write; writes arrive with the proposal pipeline (P4), which a human approves.

func NewManager

func NewManager(cfg *config.Config) *Manager

func SharedManager

func SharedManager(cfg *config.Config) *Manager

SharedManager returns the process-wide Manager, refreshing its config. The desktop bridge and the scheduler call this instead of NewManager so reaper goroutines and per-device session caches exist exactly once (a Manager per call would leak a reaper and re-dial VTYs needlessly).

func (*Manager) ApproveProposal

func (m *Manager) ApproveProposal(id string, confirm2 bool) (*Proposal, error)

ApproveProposal is the human gate. It enforces: current status draft, group policies (confirm2 when demanded), and the change window of every involved group. The agent has NO path here — approval arrives only from the desktop bridge with the human's click. The load→check→set→save runs as one critical section so a racing reject/execute can't interleave (last-write-wins used to silently drop a transition).

func (*Manager) BackupDiffCurrent

func (m *Manager) BackupDiffCurrent(ctx context.Context, device, id string) (string, error)

BackupDiffCurrent diffs a stored version (old) against the device's CURRENT running-config (new) — the restore-drafting backbone: "what must change to return to this version". The current side is a fresh sealed read (classifier / budget / redaction / audit all apply, same as any netdev_exec).

func (*Manager) BuildDiscoveryBoard

func (m *Manager) BuildDiscoveryBoard() *DiscoveryBoard

BuildDiscoveryBoard assembles the discovery screen (needs Manager only for the config inventory).

func (*Manager) BuildExposureBoard

func (m *Manager) BuildExposureBoard() *ExposureBoard

BuildExposureBoard assembles the exposure screen (推演 + 事实矩阵).

func (*Manager) BuildInvestigationChain

func (m *Manager) BuildInvestigationChain(caseID, findingID string, hours int) *InvestigationChain

BuildInvestigationChain assembles the six-column chain (Manager method only to reuse Timeline; everything else reads package-level stores).

func (*Manager) Cfg

func (m *Manager) Cfg() *config.Config

Cfg exposes the manager's live config (the desktop import-apply bridge mirrors the merged inventory into the config being persisted).

func (*Manager) Close

func (m *Manager) Close()

Close tears down every cached connection.

func (*Manager) CredentialInventory

func (m *Manager) CredentialInventory() string

CredentialInventory builds the credential-health page (markdown): every device's auth surface + which secrets are actually set.

func (*Manager) CutoverAbort

func (m *Manager) CutoverAbort(id string) (*CutoverRun, error)

CutoverAbort stops everything for good (running or held).

func (*Manager) CutoverContinue

func (m *Manager) CutoverContinue(id string) (*CutoverRun, error)

CutoverContinue presses 继续 at a hold.

func (*Manager) CutoverRollback

func (m *Manager) CutoverRollback(ctx context.Context, id string) (*CutoverRun, error)

CutoverRollback presses 回退 at a hold: the run's executed proposals unwind newest-first (each still audited); the run ends aborted.

func (*Manager) CutoverStart

func (m *Manager) CutoverStart(def *CutoverRun) (*CutoverRun, error)

CutoverStart validates the runbook, snapshots the network baseline, and launches the runner. Every proposal a step references must already be approved — the cutover executes the approved change, it never approves.

func (*Manager) DBQuery

func (m *Manager) DBQuery(ctx context.Context, sourceName, query string) (string, error)

DBQuery runs ONE allowlisted read-only statement against a configured [[netdev.db_sources]] entry. Output is JSON lines, redacted, row-capped.

func (*Manager) DeleteProposal

func (m *Manager) DeleteProposal(id string) error

DeleteProposal removes a proposal file. Only draft and terminal states (rejected/done/failed/closed) may be deleted — the live pipeline (approved/executing/partial/watching) must stay auditable. partial keeps its record because the applied steps are still on the devices.

func (*Manager) DiscoverLayer

func (m *Manager) DiscoverLayer(ctx context.Context, vantage string, cidrs []string, ports []int) ([]DiscoverHostResult, error)

DiscoverLayer probes the CONFIRMED subnets through the vantage device's own SSH tunnel (direct-tcpip from its network position). Scope whitelist applies per CIDR at the dial boundary; results record as layer-discover.

func (*Manager) DiscoverPrecheck

func (m *Manager) DiscoverPrecheck(ctx context.Context, vantage string) (*DiscoverPlan, error)

DiscoverPrecheck runs the three read-only table commands on a managed vantage through the sealed Exec path (audited by Exec itself) and folds them into a plan. Zero probe traffic — the polite-first layer of F4.

func (*Manager) DiscoverResume

func (m *Manager) DiscoverResume(ctx context.Context) ([]DiscoverHostResult, error)

DiscoverResume continues a paused layered scan from its last checkpoint. Results accumulate in the 待确认区 either way; the return value carries only this continuation's finds.

func (*Manager) DiscoverTCP

func (m *Manager) DiscoverTCP(ctx context.Context, via, cidr string, ports []int) ([]DiscoverHostResult, error)

DiscoverTCP probes ip:port combinations across a CIDR. via names a configured hop ("" = dial directly from this machine). The requested CIDR must sit INSIDE the configured [netdev.discovery] scopes — enforced here at the dial boundary, not trusted to the caller (guardrail invariant 3). Results land in the 待确认区 store (F1): parse passively, keep forever.

func (*Manager) DockerGet

func (m *Manager) DockerGet(ctx context.Context, deviceName, what, arg string, tailN int) (string, error)

DockerGet answers ONE whitelisted GET against the device's Docker Engine. what ∈ ping | version | info | ps | inspect | logs | images. Sealed by sealAPIGet (guardrails + live + audit + redaction).

func (*Manager) EnsureHealthPoller

func (m *Manager) EnsureHealthPoller()

EnsureHealthPoller starts the singleton sweep loop (idempotent); it reads the interval from the Manager's live config each tick, so a settings change to poll_interval_seconds applies within one interval without a restart.

func (*Manager) Exec

func (m *Manager) Exec(ctx context.Context, deviceName, command string) ExecResult

Exec runs one command on one configured device under the read-only seal.

func (*Manager) ExecuteProposal

func (m *Manager) ExecuteProposal(ctx context.Context, id string) (*Proposal, error)

ExecuteProposal rolls the approved change device-by-device: backup → apply → mark. The FIRST failure freezes the proposal as partial (later steps untouched) — a human then decides rollback (which runs the authored plan over the already-applied steps) or keep. There is no automatic rollback.

func (*Manager) ExpectedState

func (m *Manager) ExpectedState() ExpectedStateView

ExpectedState diffs the inventory against the latest SNMP health sweep.

func (*Manager) ExportState

func (m *Manager) ExportState() (string, error)

ExportState writes one JSON snapshot into the netdev state dir and returns its path. Devices carry skeleton fields ONLY — credentials never leave the secret store.

func (*Manager) FirewallGet

func (m *Manager) FirewallGet(ctx context.Context, deviceName, what string) (string, error)

FirewallGet answers ONE whitelisted GET against the device's REST API.

func (*Manager) HandoffReport

func (m *Manager) HandoffReport(since time.Time) string

HandoffReport builds the shift-handoff page (markdown).

func (*Manager) HealthSnapshot

func (m *Manager) HealthSnapshot() HealthSnapshot

HealthSnapshot returns the fleet's latest state.

func (*Manager) HumanTTYStart

func (m *Manager) HumanTTYStart(deviceName string, onData func(chunk string)) (*HumanTTYState, error)

HumanTTYStart opens a human terminal session to a device: its own transport client + a PTY shell channel. The onData callback receives decoded output chunks (the desktop layer forwards them as "netdev:humantty" Wails events).

func (*Manager) HumanTTYWrite

func (m *Manager) HumanTTYWrite(deviceName, input string) error

HumanTTYWrite sends user keystrokes to the device PTY.

func (*Manager) ImportApply

func (m *Manager) ImportApply(path string, addNames, takeOvernames []string) (int, error)

ImportApply performs the reviewed merge: adds the named new devices and takes the imported side of the named conflicts. Credentials are NOT migrated — the skeleton lands with empty credential fields and the user re-enters them in settings (spec: 凭证不迁移).

func (*Manager) ImportPreview

func (m *Manager) ImportPreview(path string) (*ImportPreview, error)

ImportPreview reads an export file and builds the confirm-area diff. No mutation happens here — preview only.

func (*Manager) JobResume

func (m *Manager) JobResume(id string) (*Job, error)

JobResume continues a paused job from its cursor.

func (*Manager) JobStart

func (m *Manager) JobStart(def *Job) (*Job, error)

JobStart validates the definition, persists it as running, and launches the runner goroutine. The definition arrives from the desktop bridge (UI / runbook presets) — the agent has no tool that creates jobs.

func (*Manager) KillAllConnections

func (m *Manager) KillAllConnections() int

KillAllConnections is the emergency stop: every device connection and CLI session is closed immediately (freeing all VTY lines), audited as such. The Manager stays usable — the next diagnostic command reconnects on demand. Human terminals die with it too (§6.1 紧急停止同样杀人工终端).

func (*Manager) KubeGet

func (m *Manager) KubeGet(ctx context.Context, deviceName, what, namespace, name string, tailN int) (string, error)

KubeGet answers ONE whitelisted GET against the device's pinned context. what ∈ version | pods | pod | podlog | events | deployments | nodes. Sealed by sealAPIGet (guardrails + live + audit + redaction).

func (*Manager) LiveState

func (m *Manager) LiveState() LiveSnapshot

LiveState builds the snapshot over the configured inventory.

func (*Manager) Locate

func (m *Manager) Locate(ctx context.Context, target string) LocateResult

Locate fans one IP/MAC across the inventory's ARP surfaces.

func (*Manager) LogFollow

func (m *Manager) LogFollow(deviceName, source string, onEvent func(LogFollowEvent)) error

LogFollow starts (or replaces) the streaming follow of one log source on one device. Events stream to onEvent until a cap trips, the device drops, or LogFollowStop is called; the final event is always Done with a reason.

func (*Manager) LogFollowActive

func (m *Manager) LogFollowActive(deviceName string) string

LogFollowActive reports the device's active follow source ("" = none).

func (*Manager) LogFollowStop

func (m *Manager) LogFollowStop(deviceName string)

LogFollowStop kills the device's active follow, if any.

func (*Manager) LogRead

func (m *Manager) LogRead(ctx context.Context, deviceName, source string, tailN int, since, grep string) ExecResult

LogRead fetches one log source on one configured device. The composed command goes through the SAME sealed Exec path as netdev_exec (budget, classifier, redaction, audit, live events all apply); since/grep filtering happens client-side on the fetched lines.

func (*Manager) LogSearch

func (m *Manager) LogSearch(ctx context.Context, pattern string, devices, sources []string, since string) LogSearchResult

LogSearch fans pattern across devices (empty = every linux host in the inventory). Custom sources override the default candidates; they ride the same per-device whitelist as netdev_log_read.

func (*Manager) MatchCVEs

func (m *Manager) MatchCVEs() ([]CVEMatch, error)

MatchCVEs runs the inventory against the cached feed.

func (*Manager) MatchCVEsToFindings

func (m *Manager) MatchCVEsToFindings() (*Finding, error)

MatchCVEsToFindings runs the match and files each device's hits as ONE Finding (dedup key: "cve:" + device — re-runs update rather than pile up).

func (*Manager) NetconfRPC

func (m *Manager) NetconfRPC(ctx context.Context, deviceName, inner string) (string, error)

NetconfRPC runs one read-only NETCONF RPC on the device and returns the <rpc-reply>…</rpc-reply> document (raw XML text, redacted upstream of the model by the exec-path caller if surfaced there).

func (*Manager) NetprobeSweep

func (m *Manager) NetprobeSweep(ctx context.Context, cidr string, doICMP bool) (*NetprobeSweepResult, error)

NetprobeSweep orchestrates cmd/netprobe over one in-scope CIDR. Leads land in the 待确认区 (source netprobe) — ICMP-only hosts become port-less alive rows, port hits merge like any other discovery source.

func (*Manager) NmapSweep

func (m *Manager) NmapSweep(ctx context.Context, cidr string) (*NmapSweepResult, error)

NmapSweep orchestrates one user-supplied nmap service sweep over a CIDR. Gates (in order): engagement envelope → scope whitelist → host cap → binary presence. Results land in the 待确认区 store (source nmap-import) so the promote → 指纹回填 → CVE 匹配 loop consumes them like any lead.

func (*Manager) PollHealthOnce

func (m *Manager) PollHealthOnce(ctx context.Context)

PollHealthOnce sweeps every SNMP-configured device once, then evaluates the alert rules over the fresh results.

func (*Manager) ProbeLogSources

func (m *Manager) ProbeLogSources(ctx context.Context, deviceName string) LogSourceProbe

ProbeLogSources probes one configured device. Every command is a read-table line composed here (never model- or user-supplied), so a refusal can only mean the device's driver refused it — reported per leg, never fatal.

func (*Manager) ProposalNeedsConfirm2

func (m *Manager) ProposalNeedsConfirm2(p *Proposal) bool

ProposalNeedsConfirm2 reports whether any step demands the secondary confirmation: a proposal+confirm2 group (§6.3) OR a step whose verbs scanned as destructive (§7.1 — delete/scale-down 类动词落 dangerous + confirm2).

func (*Manager) RedfishGet

func (m *Manager) RedfishGet(ctx context.Context, deviceName, path string) (string, error)

RedfishGet performs ONE read-only GET against a device's BMC and returns the JSON body (redacted). vendor=redfish devices carry the BMC address and a passwordEnv-resolved credential; the TLS skip is deliberate — BMC certificates are almost universally self-signed, and the channel carries read-only inventory data.

func (*Manager) RejectProposal

func (m *Manager) RejectProposal(id, reason string) (*Proposal, error)

RejectProposal is the human veto (completion-spec §4.1): the write path's gate could only say yes — this is the "no". draft/approved may be rejected (approved-but-not-executed vetoes cost nothing; executing states are past the point of veto and must ride rollback instead). The reason is persisted on the proposal so the agent's next turn sees WHY it was turned down.

func (*Manager) RemoteWorkHandler

func (m *Manager) RemoteWorkHandler() nettrans.WorkHandler

RemoteWorkHandler adapts netdev's read-only diagnostics onto the trust domain's delegated-work surface (TRUSTDOMAIN_SPEC §7.3 as-built) — the first real WorkHandler consumer. A fleet member holding a scoped token can pull this host's health board or run a host-triage battery on one of this host's OWN managed devices; everything flows the same read-only pipeline (guardrails, audit, refusal ledger) as local agent use.

能力隔离 (spec §1.4 #7): there is simply no write operation on this path — the vocabulary below cannot express one, and Serve's §7.3 gate runs before any of this code.

Resource/operation vocabulary:

netdev/health  read                      → HealthSnapshot JSON
netdev/triage  read  {"device":"name"}   → TriageReport JSON

func (*Manager) RollbackProposal

func (m *Manager) RollbackProposal(ctx context.Context, id string) (*Proposal, error)

RollbackProposal runs the authored rollback plan over the APPLIED steps only, oldest-last so state unwinds in reverse. Frozen/failed proposals only. A rollback failure marks the proposal failed (alert) and stops.

func (*Manager) RunBackup

func (m *Manager) RunBackup(ctx context.Context, device string) ([]BackupVersion, error)

RunBackup reads one device's (or every device's, device == "") running config through the sealed Exec path and files each as a new version. Returns the versions written, in device order.

func (*Manager) RunBaseline

func (m *Manager) RunBaseline(ctx context.Context) (*Finding, error)

RunBaseline reads every device's running-config through the sealed path (full audit, redaction before rules run) and files one Finding per violated rule plus a summary Finding. Mirror of RunInspection's flow.

func (*Manager) RunBaselineFor

func (m *Manager) RunBaselineFor(ctx context.Context, devices []string) (*Finding, error)

RunBaselineFor is RunBaseline scoped to a device set (nil/empty = all) — the P2-3 proposal-driven re-check runs only the devices a finished proposal actually touched, so an in-flight fix elsewhere can't be auto-resolved out from under its own alert.

func (*Manager) RunGoldenCheck

func (m *Manager) RunGoldenCheck(ctx context.Context, device string) ([]GoldenDrift, error)

RunGoldenCheck snapshots the device's running-config (sealed read) and diffs it against the baseline. Fires/resolves the golden Finding by source, like the alert engine. device="" sweeps every device that has a baseline; returns the per-device drifts (empty drifts included so callers can report "clean").

func (*Manager) RunInspection

func (m *Manager) RunInspection(ctx context.Context) (*Finding, error)

RunInspection sweeps every inventory device with a fixed read battery and files one Finding with the evidence — the manual form of 定时巡检 (the scheduler wiring lands with the jobs integration). Everything runs through the sealed read path (classifier + redaction + audit per command).

func (*Manager) RunJobSync

func (m *Manager) RunJobSync(ctx context.Context, def *Job) (*Job, error)

RunJobSync starts a job and BLOCKS until its runner exits (terminal status, pause, or ctx end — on ctx end the job is aborted). This is how synchronous callers ride the engine (§4.1 体检电池 runbook): same step semantics, budgets, and persisted job trail, minus the async interaction.

func (*Manager) SFTPBrowse

func (m *Manager) SFTPBrowse(ctx context.Context, deviceName, dirPath string) ([]string, error)

SFTPBrowse lists a whitelisted directory (ls, sealed).

func (*Manager) SFTPDownload

func (m *Manager) SFTPDownload(ctx context.Context, deviceName, remotePath string) ([]byte, error)

SFTPDownload reads one file from a device over the existing SSH transport. Returns the content (bounded). The path must sit inside the device's log_paths whitelist or /var/log (§6.2: 路径浏览限白名单).

func (*Manager) SetLiveObserver

func (m *Manager) SetLiveObserver(fn func(LiveEvent))

SetLiveObserver installs the live-event sink (replaces any previous one). The callback must be fast and non-blocking — the desktop bridge coalesces and forwards on its own goroutine.

func (*Manager) SnmpQuery

func (m *Manager) SnmpQuery(ctx context.Context, deviceName, oid, mode string) (string, error)

SnmpQuery runs ONE GET (oid exact) or a bounded WALK (prefix) against a vendor=snmp device. Results render as `OID = value` lines, redacted.

func (*Manager) SrvConfDrift

func (m *Manager) SrvConfDrift(ctx context.Context, path string, devices []string) ([]SrvConfDriftRow, error)

SrvConfDrift compares the CURRENT content of one path across devices (the first device is the baseline): the 「同一份 nginx 配置三个环境差在哪」view (§7.3). Devices must share the path inside their own config_paths whitelist.

func (*Manager) SrvConfSnapshot

func (m *Manager) SrvConfSnapshot(ctx context.Context, deviceName, path string) (*SrvConfVersion, error)

SrvConfSnapshot reads one whitelisted config file and stores a version.

func (*Manager) TemplateApply

func (m *Manager) TemplateApply(t *Template, userVars map[string]string) (*Proposal, error)

TemplateApply renders + validates + saves ONE draft proposal whose steps are per-device cli steps (rolling execution, first-failure freeze, per-device rollback matrix — the existing proposal pipeline unchanged, §7.2).

func (*Manager) TemplateRender

func (m *Manager) TemplateRender(t *Template, userVars map[string]string) ([]TemplatePreviewDevice, error)

TemplateRender is the 逐台 dry-run(§7.2):renders every target's commands, classifies each rendered product (渲染产物仍逐条过分类器), and flags destructive verbs — with NO side effects on any device.

func (*Manager) TestConnection

func (m *Manager) TestConnection(ctx context.Context, deviceName string) TestResult

TestConnection dials one configured device and opens (then closes) a CLI session. It never prompts interactively.

func (*Manager) Timeline

func (m *Manager) Timeline(device string, hours int) []TimelineEvent

Timeline assembles the correlation stream for one device ("" = all) inside the window (hours, default 24, cap 168).

func (*Manager) TopologySnapshot

func (m *Manager) TopologySnapshot(ctx context.Context) (*TopologyGraph, error)

func (*Manager) Triage

func (m *Manager) Triage(ctx context.Context, deviceName string) TriageReport

Triage runs the battery on one host and returns the structured report. §4.1 runbook 升级:电池作为 Job 引擎 runbook 执行——每步 30s 超时、 on-fail=continue(诊断电池要尽量收全)、预算封顶(墙钟 10 分钟 / 命令数 = 电池长度),全程留 job 轨迹(jobs 目录可见、可审计);步骤语义(expect /retry/断点)对电池默认关闭但引擎已具备——R5 入侵排查向导直接复用。

func (*Manager) TurnBegin

func (m *Manager) TurnBegin()

TurnBegin resets the per-turn command budget. The desktop bridge calls it on every user submit (netdev mode), making turn_command_budget a true per-ask control: each question the user asks buys a fresh budget of read commands, and nothing carries over.

func (*Manager) ValidateProposal

func (m *Manager) ValidateProposal(p *Proposal) error

ValidateProposal checks a draft against the inventory policies: every step's device must exist, no step may target a read-only group, and each structured type's own contract must hold (§7.1 — e.g. sql-migration without a down script is not submittable). Group policy `proposal+confirm2` is legal but flags ApproveProposal for the secondary confirmation.

func (*Manager) WeakCredCheck

func (m *Manager) WeakCredCheck(ctx context.Context, deviceName, tier, dictPath string) (WeakCredResult, error)

WeakCredCheck tests a device's login against the tier's candidate set. Gated on the engagement envelope; every attempt audited (password NEVER logged — only the attempt ordinal).

func (*Manager) WeeklyReport

func (m *Manager) WeeklyReport() string

WeeklyReport builds the weekly ops digest (markdown).

type MetricPoint

type MetricPoint struct {
	Time      time.Time `json:"time"`
	Reachable bool      `json:"up"`
	UptimeSec int64     `json:"us"`
	IfUp      int       `json:"iu"`
	IfDown    int       `json:"id"`
	// 水位(DASHBOARD spec §7.3 SNMP OID 扩展):cpu/mem 为百分比,octets
	// 为 ifTable 计数器累计和——速率(bps)由读取端按相邻点差分计算。
	Cpu    int    `json:"cu,omitempty"`
	Mem    int    `json:"me,omitempty"`
	InOct  uint64 `json:"io,omitempty"`
	OutOct uint64 `json:"oo,omitempty"`
}

MetricPoint is one poll's rollup for one device.

func MetricHistory

func MetricHistory(device string, limit int) []MetricPoint

MetricHistory returns the device's newest-first points (bounded).

type NetDevEvent

type NetDevEvent struct {
	Time time.Time
	Text string
}

NetDevEvent is one structured ring entry (syslog/trap).

func SyslogEventsSince

func SyslogEventsSince(device string, since time.Time) []NetDevEvent

SyslogEventsSince returns one device's ring entries (structured) since t.

func TrapEventsSince

func TrapEventsSince(device string, since time.Time) []NetDevEvent

TrapEventsSince returns one device's trap ring entries since t.

type NetprobeHost

type NetprobeHost struct {
	IP    string `json:"ip"`
	ICMP  bool   `json:"icmp"`
	Ports []struct {
		Port   int    `json:"port"`
		Open   bool   `json:"open"`
		Banner string `json:"banner,omitempty"`
	} `json:"ports"`
}

NetprobeHost mirrors cmd/netprobe's hostResult JSON.

type NetprobeSweepResult

type NetprobeSweepResult struct {
	CIDR      string         `json:"cidr"`
	ICMP      bool           `json:"icmp"`
	Alive     int            `json:"alive"`
	WithPorts int            `json:"with_ports"`
	Results   []NetprobeHost `json:"results"`
	Command   string         `json:"command"`
	Duration  string         `json:"duration"`
}

NetprobeSweepResult summarizes one orchestrated sweep.

type NmapHost

type NmapHost struct {
	IP       string            `json:"ip"`
	Hostname string            `json:"hostname,omitempty"`
	Services []NmapHostService `json:"services"`
}

NmapHost is one host with its open services.

type NmapHostService

type NmapHostService struct {
	Port    int    `json:"port"`
	Proto   string `json:"proto,omitempty"`
	Service string `json:"service,omitempty"` // name, e.g. ssh/http/redis
	Product string `json:"product,omitempty"` // e.g. OpenSSH / nginx
	Version string `json:"version,omitempty"`
}

NmapHostService is one open service on one host.

type NmapSweepResult

type NmapSweepResult struct {
	CIDR      string     `json:"cidr"`
	Hosts     int        `json:"hosts"`
	OpenPorts int        `json:"open_ports"`
	Results   []NmapHost `json:"results"`
	Command   string     `json:"command"`
	Duration  string     `json:"duration"`
}

NmapSweepResult summarizes one orchestrated sweep.

type NotifyPusher

type NotifyPusher interface {
	Push(ctx context.Context, dest, text string) error
}

NotifyPusher is the embedded IM gateway's push seam (desktop injects the live *bot.BotGateway at start/stop — interface to avoid the import cycle, mirroring builtin.SetIMPusher).

type PlanStep

type PlanStep struct {
	CIDR      string `json:"cidr"`
	Class     string `json:"class"`
	Hosts     int    `json:"hosts"`
	DefaultOn bool   `json:"default_on"`
}

PlanStep mirrors one planned probe for the UI's plan card.

type PortEvent

type PortEvent struct {
	At   string `json:"at"`
	IP   string `json:"ip"`
	Port int    `json:"port"`
	Kind string `json:"kind"` // newly-opened | newly-closed
}

PortEvent is one observed port state change on an unmanaged lead.

func ListPortEvents

func ListPortEvents(limit int) []PortEvent

ListPortEvents returns up to limit newest port-change events.

type PrecheckResult

type PrecheckResult struct {
	Vantage       string        `json:"vantage"`
	Vendor        string        `json:"vendor,omitempty"`
	Interfaces    []string      `json:"interfaces"` // name/ip/mask rows (display)
	DirectSubnets []SubnetClass `json:"direct_subnets"`
	RoutedSubnets []SubnetClass `json:"routed_subnets"`
	ArpKnownIPs   []string      `json:"arp_known_ips"`
	Uncovered     string        `json:"uncovered_note,omitempty"` // aggregates beyond coverage
	Warnings      []string      `json:"warnings"`
	At            string        `json:"at"`
}

PrecheckResult is what one vantage's three read-only tables yielded.

type ProbeFile

type ProbeFile struct {
	Name    string `json:"name"`    // messages
	Path    string `json:"path"`    // /var/log/messages
	Size    string `json:"size"`    // 2.1M (from ls -lh; "" = unknown)
	Allowed bool   `json:"allowed"` // readable via file: as-is
}

ProbeFile is one candidate log file. Allowed marks whether it sits inside the device's log whitelist (/var/log + log_paths): a false file needs its directory registered (one click in the UI) before file: reads work.

type Proposal

type Proposal struct {
	ID     string         `json:"id"`
	Intent string         `json:"intent"`
	Status string         `json:"status"`
	Steps  []ProposalStep `json:"steps"`
	// 恢复提案来源(备份→恢复闭环):从备份版本起草的恢复提案记录来源
	// 版本 ID(device@nanos);审计与前端据此回放「恢复自哪一版」。
	RestoreFrom string    `json:"restore_from,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	ApprovedAt  time.Time `json:"approved_at,omitempty"`
	ExecutedAt  time.Time `json:"executed_at,omitempty"`
	Approver    string    `json:"approver,omitempty"`
	Confirm2    bool      `json:"confirm2"`       // secondary confirmation for proposal+confirm2 groups
	Note        string    `json:"note,omitempty"` // freeze/rollback reason trail
	// 驳回(completion-spec §4.1):draft/approved 可被人否决;Reason 随变更
	// 持久化,agent 下一轮读到变更即见被拒原因。
	RejectedAt   time.Time `json:"rejected_at,omitempty"`
	RejectReason string    `json:"reject_reason,omitempty"`
	// 观察期(§7.1):done → watching(默认 30 分钟)→ closed;劣化触发 Finding。
	WatchUntil *time.Time `json:"watch_until,omitempty"`
	WatchNote  string     `json:"watch_note,omitempty"`
	// HealthBase is the ifDown count per target device at watch start(-1 =
	// 当时不可达)——观察期劣化检测(健康轮询对比)的基线。仅 SNMP 配置
	// 设备有信号;其余设备跳过对比。
	HealthBase map[string]int `json:"health_base,omitempty"`
}

func GetProposal

func GetProposal(id string) (*Proposal, error)

GetProposal loads one proposal.

func ListProposals

func ListProposals() ([]*Proposal, error)

ListProposals returns proposals newest-first.

type ProposalStep

type ProposalStep struct {
	Device   string   `json:"device"`
	Type     string   `json:"type,omitempty"`     // "" | cli | k8s-apply | sql-migration | file-upload | cert-replace
	Commands []string `json:"commands"`           // cli: the change, in order
	Rollback []string `json:"rollback,omitempty"` // cli: reverse commands, authored with the change

	// Structured payloads (§7.1) — only the fields of the step's type matter.
	YAML          string `json:"yaml,omitempty"`           // k8s-apply: full manifest
	UpSQL         string `json:"up_sql,omitempty"`         // sql-migration
	DownSQL       string `json:"down_sql,omitempty"`       // sql-migration — REQUIRED (missing ⇒ not submittable)
	LocalPath     string `json:"local_path,omitempty"`     // file-upload / cert-replace: local cert file
	RemotePath    string `json:"remote_path,omitempty"`    // file-upload / cert-replace: absolute target path
	KeyLocalPath  string `json:"key_local_path,omitempty"` // cert-replace: local private key file
	KeyRemotePath string `json:"key_remote_path,omitempty"`
	Checksum      string `json:"checksum,omitempty"`   // optional sha256 the uploaded bytes must match
	ReloadCmd     string `json:"reload_cmd,omitempty"` // cert-replace: service reload after the swap
	// restore-verify(§7.3 备份恢复演练): restore a config snapshot to a
	// STAGING target and run the verify read. Device = receiver.
	RestoreDevice  string `json:"restore_device,omitempty"`  // snapshot source device
	RestoreVersion string `json:"restore_version,omitempty"` // snapshot id; "" = latest at execute time
	VerifyCmd      string `json:"verify_cmd,omitempty"`      // e.g. `nginx -t`

	Backup    string `json:"backup,omitempty"` // captured pre-change state (redacted)
	Applied   bool   `json:"applied"`
	Error     string `json:"error,omitempty"`
	Dangerous bool   `json:"dangerous,omitempty"` // destructive verb scan ⇒ forces confirm2 (§7.1)
}

ProposalStep is one device's slice of a change.

type Result

type Result struct {
	Command string
	Output  string // cleaned text: echo and trailing prompt removed
	IsError bool   // a driver error pattern matched
}

Result is one command's outcome.

type SSHClient

type SSHClient interface {
	NewSession() (*ssh.Session, error)
}

SSHClient is the subset netconf needs (avoids importing ssh types further).

type ScheduleStamp

type ScheduleStamp struct {
	At    string `json:"at"`   // 2006-01-02T15:04:05
	Kind  string `json:"kind"` // inspection
	Ok    bool   `json:"ok"`
	Title string `json:"title,omitempty"` // the filed Finding's title
	Note  string `json:"note,omitempty"`  // failure note
}

ScheduleStamp records the last SCHEDULED run's outcome (manual runs do not stamp — the compliance card answers "did the scheduler fire").

func LoadScheduleStamp

func LoadScheduleStamp() *ScheduleStamp

LoadScheduleStamp reads it back; nil = the scheduler never fired.

type SeriesPoint

type SeriesPoint struct {
	T      int64   `json:"t"` // unix seconds
	Device string  `json:"d"`
	Metric string  `json:"m"`
	Value  float64 `json:"v"`
}

type Session

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

Session is one PTY-driven interactive CLI session with a network device, built on a transport.Client's SSH connection. It exists because network devices generally do not support one-shot exec channels: the CLI must be driven interactively (paging off, prompt detection, echo stripping) — the driver layer supplies the vendor knowledge.

func OpenConsoleSession

func OpenConsoleSession(ctx context.Context, portName string, baud int, drv driver.Driver, encoding string) (*Session, error)

OpenConsoleSession opens the line, runs the driver's paging-off sequence, and waits for the first driver prompt — then hands back a Session whose stdin IS the line and whose output buffer is fed by a reader goroutine. Session.Run (echo strip, prompt match, pager advance, clean) works unchanged; Close is nil-ssh-safe.

func OpenSession

func OpenSession(ctx context.Context, client *transport.Client, drv driver.Driver, encoding string) (*Session, error)

OpenSession establishes the interactive CLI over an already-connected transport.Client: requests a PTY, starts the shell, disables paging per the driver, and waits for the first prompt.

func (*Session) Close

func (s *Session) Close() error

Close ends the session (the transport Client stays connected). A console session has no ssh.Session behind it (nil) — only its serial line closes.

func (*Session) Run

func (s *Session) Run(ctx context.Context, cmd string) (Result, error)

Run sends one command and returns when the device prompt reappears. The command must already be classified Read by the caller — the exec tool enforces that; the session layer stays classification-agnostic.

func (*Session) SetOutputObserver

func (s *Session) SetOutputObserver(fn func(string))

SetOutputObserver installs the live output tap (nil removes it). Callers own the callback's thread-safety; Manager's observer coalesces off-thread.

type SrvConfDriftRow

type SrvConfDriftRow struct {
	Device string `json:"device"`
	Group  string `json:"group"`
	Status string `json:"status"` // same | drift | error | absent
	Diff   string `json:"diff,omitempty"`
	Error  string `json:"error,omitempty"`
}

SrvConfDriftRow is one device's drift verdict against the baseline device.

type SrvConfVersion

type SrvConfVersion struct {
	ID     string `json:"id"`
	Device string `json:"device"`
	Path   string `json:"path"`
	At     string `json:"at"`
	Bytes  int    `json:"bytes"`
	Lines  int    `json:"lines"`
}

SrvConfVersion is one config-file snapshot.

func SrvConfVersions

func SrvConfVersions(device, path string) []SrvConfVersion

SrvConfVersions lists a (device, path)'s snapshots, newest first.

type StateEventMeta

type StateEventMeta struct {
	ID         int               `json:"id"`
	Time       time.Time         `json:"time"`
	Kind       string            `json:"kind"`
	Entity     string            `json:"entity,omitempty"`
	Actor      string            `json:"actor"`
	Paths      []string          `json:"paths"`
	Live       []StateLiveEntity `json:"live,omitempty"`
	CanRestore bool              `json:"canRestore"`
	CanRedo    bool              `json:"canRedo"`
}

StateEventMeta is the timeline-facing summary of one state event.

func StateEventMetas

func StateEventMetas() []StateEventMeta

StateEventMetas lists events newest-first with the ZCode-style safety classification: an event is restorable unless the suffix it would revert touches an entity that is currently live (a proposal executing/watching, a job running/paused, a cutover running/hold). Redo is offered only on the newest event when it is a restore-keep — a newer event would be collateral of the redo's own suffix restore.

type StateLiveEntity

type StateLiveEntity struct {
	Type   string `json:"type"`   // proposal | job | cutover
	ID     string `json:"id"`     // entity id
	Status string `json:"status"` // why it is live (executing / running / hold…)
}

StateLiveEntity names an entity the suffix of a candidate restore touches that is currently mid-flight; restoring under it would fight its runner.

type StateRestoreResult

type StateRestoreResult struct {
	Written        []string `json:"written"`
	Deleted        []string `json:"deleted"`
	ReverseEventID int      `json:"reverseEventId"`
}

StateRestoreResult reports what a suffix restore wrote back / deleted and the synthetic reverse event that enables redo.

func StateRestore

func StateRestore(id int, actor string) (StateRestoreResult, error)

StateRestore rewinds local state to just before event id: every path touched by id or any later event is restored to its earliest post-id snapshot (deleted when it did not exist). Before touching anything it re-checks the live-entity classification (the preview may be stale) and writes a restore-keep reverse event so the rewind itself is redoable. Local records only — devices are never contacted.

type SubnetClass

type SubnetClass struct {
	CIDR      string `json:"cidr"`
	Class     string `json:"class"` // direct-small | routed-small | medium | large
	Hosts     int    `json:"hosts"`
	DefaultOn bool   `json:"default_on"`
}

SubnetClass is one classified network with its plan default.

type SyslogCountRow

type SyslogCountRow struct {
	Hour   string `json:"hour"` // YYYY-MM-DDTHH
	Device string `json:"device"`
	Class  string `json:"class"`
	N      int    `json:"n"`
}

SyslogCountRow is one {hour, device, class} bucket.

func SyslogCountTail

func SyslogCountTail(limit int) []SyslogCountRow

SyslogCountTail returns up to limit newest R3 rows across day files.

type SyslogStatusView

type SyslogStatusView struct {
	Listening bool `json:"listening"`
	Port      int  `json:"port"`
	Buffered  int  `json:"buffered"`
}

SyslogStatusView is the bridge payload for the UI.

type Template

type Template struct {
	ID        string         `json:"id"`
	Name      string         `json:"name"`
	Intent    string         `json:"intent"`
	Vars      []string       `json:"vars,omitempty"` // user-supplied variable names
	Steps     []TemplateStep `json:"steps"`          // per-device command templates
	Targets   []string       `json:"targets"`        // explicit device names
	CreatedAt time.Time      `json:"created_at"`
}

Template is one batch change template. Vars carries omitempty so a variable-less template serializes without the key (the frontend's `?? []` guards the absent case) instead of as JSON null.

func GetTemplate

func GetTemplate(id string) (*Template, error)

GetTemplate loads one template.

func ListTemplates

func ListTemplates() ([]*Template, error)

ListTemplates returns templates newest-first.

type TemplatePreviewDevice

type TemplatePreviewDevice struct {
	Device    string                `json:"device"`
	Available bool                  `json:"available"` // device exists & not read-only-group
	Reason    string                `json:"reason,omitempty"`
	Steps     []TemplatePreviewStep `json:"steps"`
}

TemplatePreviewDevice is one target's rendered result (dry-run, §7.2).

type TemplatePreviewStep

type TemplatePreviewStep struct {
	Commands  []string `json:"commands"`
	Rollback  []string `json:"rollback"`
	Classes   []string `json:"classes"`   // per rendered command: read|write|dangerous|unknown
	Dangerous bool     `json:"dangerous"` // verb scan hit — confirm2 forced on apply
}

TemplatePreviewStep is one rendered command with its classifier verdict.

type TemplateStep

type TemplateStep struct {
	Commands []string `json:"commands"`
	Rollback []string `json:"rollback"`
}

TemplateStep is one per-device step's COMMAND TEMPLATE ({{var}} placeholders).

type TestResult

type TestResult struct {
	Device      string `json:"device"`
	Status      string `json:"status"` // ok | unknown-host-key | auth-failed | refused-by-classifier | error
	Detail      string `json:"detail,omitempty"`
	Host        string `json:"host,omitempty"`
	KeyType     string `json:"keyType,omitempty"`
	Fingerprint string `json:"fingerprint,omitempty"`
}

TestConnection runs the first-device flow: connect → TOFU (captured, not interactive) → CLI session (paging-off exercises the prompt state machine). The result tells the UI exactly what to do next: trust the host key, fix credentials, or celebrate.

Two-step TOFU: the strict prompt captures the presented key and rejects, so no code path can silently trust anything — the human confirms the fingerprint in the UI and calls TrustHostKey with the SAME fingerprint.

type TimelineEvent

type TimelineEvent struct {
	Time   time.Time `json:"time"`
	Kind   string    `json:"kind"` // change | finding | event
	Device string    `json:"device"`
	Title  string    `json:"title"`
	Detail string    `json:"detail,omitempty"`
}

TimelineEvent is one correlation entry on the unified axis.

type TopoImportStats

type TopoImportStats struct {
	Total          int    `json:"total"`
	Managed        int    `json:"managed"`
	NewNodes       int    `json:"new_nodes"`
	UnresolvedRole int    `json:"unresolved_role"`
	Pages          int    `json:"pages"`
	UsedPage       string `json:"used_page"`
}

TopoImportStats summarizes one preview.

type TopoReconcile

type TopoReconcile struct {
	Tri       TriSourceAgg   `json:"tri"`
	Platforms map[string]int `json:"platforms"` // edge source → count (lldp/cdp/design/bastion)
}

TopoReconcile is the topology tab's reconciliation card: the offline design↔plan diff plus neighbor-platform coverage (edges by source).

func BuildTopoReconcile

func BuildTopoReconcile(cfg *config.Config) *TopoReconcile

BuildTopoReconcile is pure over the two offline stores (design import + IP-plan inference); zero probes — a view, never a scan.

type TopologyDesign

type TopologyDesign struct {
	ImportedAt string        `json:"imported_at"`
	SourceFile string        `json:"source_file"`
	Graph      TopologyGraph `json:"graph"`
}

TopologyDesign is the persisted third source (plan | design | snapshot).

func LoadTopologyDesign

func LoadTopologyDesign() (*TopologyDesign, error)

LoadTopologyDesign returns the stored design, or nil when none exists.

type TopologyEdge

type TopologyEdge struct {
	LocalDevice  string `json:"local_device"`
	LocalPort    string `json:"local_port"`
	RemoteDevice string `json:"remote_device"`
	RemotePort   string `json:"remote_port,omitempty"`
	RemoteIP     string `json:"remote_ip,omitempty"`
	Source       string `json:"source"` // lldp | cdp
	// Platform is the neighbor's self-described platform/model (F2): CDP
	// "Platform:" line or LLDP "System description" — vendor/model words that
	// feed role inference and the 待确认区 hints. Free text from the wire,
	// display-only.
	Platform string `json:"platform,omitempty"`
}

TopologyEdge is one adjacency.

type TopologyGraph

type TopologyGraph struct {
	Nodes []TopologyNode `json:"nodes"`
	Edges []TopologyEdge `json:"edges"`
	At    string         `json:"at"`
}

TopologyGraph is the merged snapshot for the layout's mini-map.

func InferTopology

func InferTopology(cfg *config.Config) TopologyGraph

iptopo.go — the LOCAL topology view (the user's rendering doctrine): the program computes the network's architecture itself, mostly from the intranet IP plan, with ZERO device sessions and ZERO model calls. The map shows on click; real link data (LLDP/CDP) only arrives when the user explicitly asks for the measured sweep — the two never mix silently.

Inference order per device (first hit wins):

  1. the user's own group assignment (核心/汇聚/接入… — their words are the ground truth, the view is just another surface for it);
  2. name conventions (CORE/AGG/ACC/FW prefixes common in IP plans);
  3. subnet ordering: with nothing else to go on, devices in the numerically lowest /24 read as closer to the core (10.0.0.x under 10.0.2.x in a planned network) — marked inferred, correctable by assigning a group.

Links are NEVER fabricated from IPs: two devices sharing a management subnet proves nothing about their physical wiring. The plan view draws structure (bands + subnet clusters); edges exist only from real neighbor data or an explicit bastion (via) chain.

func TopologyFused

func TopologyFused(cfg *config.Config) TopologyGraph

TopologyFused is the zero-session graph the attack-path sim and the exposure board share: IP-plan inference + persisted design import merged.

type TopologyNode

type TopologyNode struct {
	Name     string `json:"name"`
	Managed  bool   `json:"managed"`
	DeviceIP string `json:"device_ip,omitempty"`
	// Subnet annotates the local IP-plan view (the /24 the address lives in).
	Subnet string `json:"subnet,omitempty"`
	// Tier is the LOCAL inference's band (0 core / 1 agg / 2 access / 3
	// unmanaged) from group words, name conventions, or subnet ordering —
	// present only in the IP-plan view; the LLDP snapshot leaves it at -1
	// and the map falls back to its degree heuristic.
	Tier int `json:"tier"`
	// Role is the device-class icon key (router/switch/firewall/ips/vpn/
	// bastion/server/ap/cloud; "" = unknown), ORTHOGONAL to tier: tier picks
	// the band, role picks the icon, health keeps stroke color, managed keeps
	// solid/dashed. RoleSource records the provenance (config|kind|group|
	// model|vendor|label|none) for the tooltip's confidence hint.
	Role       string `json:"role,omitempty"`
	RoleSource string `json:"role_source,omitempty"`
}

TopologySnapshot runs the neighbor query on every inventory device (the sealed read path, redacted) and merges the edges. Nodes not in the inventory carry Unmanaged=true — visible, never connectable.

type TriSourceAgg

type TriSourceAgg struct {
	Design     int `json:"design"`
	Plan       int `json:"plan"`
	Matched    int `json:"matched"`
	OnlyDesign int `json:"only_design"`
	OnlyPlan   int `json:"only_plan"`
}

TriSourceAgg is the design↔plan reconciliation (图实一致性, honest counts).

type TriageReport

type TriageReport struct {
	Device    string          `json:"device"`
	Vendor    string          `json:"vendor"`
	Sections  []TriageSection `json:"sections"`
	Anomalies []string        `json:"anomalies,omitempty"`
	Summary   string          `json:"summary"`
	CreatedAt time.Time       `json:"created_at"`
}

type TriageSection

type TriageSection struct {
	Name    string   `json:"name"`
	Command string   `json:"command"`
	Ok      bool     `json:"ok"`
	Refused string   `json:"refused,omitempty"`
	Lines   []string `json:"lines,omitempty"`
}

type WeakCredResult

type WeakCredResult struct {
	Device   string `json:"device"`
	Tier     string `json:"tier"`
	Weak     bool   `json:"weak"`
	Attempts int    `json:"attempts"`
	Budget   int    `json:"budget"`
	Detail   string `json:"detail,omitempty"`
}

WeakCredResult reports one device's check.

Directories

Path Synopsis
Package driver holds the per-vendor network-device CLI knowledge: how to classify a command (the structural read-only seal), how to disable paging, what the prompt looks like, and what error output looks like.
Package driver holds the per-vendor network-device CLI knowledge: how to classify a command (the structural read-only seal), how to disable paging, what the prompt looks like, and what error output looks like.
Package transport is the netdev SSH connection layer: host resolution (configured entries + ~/.ssh/config layering), authentication, host-key verification (system known_hosts read-only + a netdev-managed TOFU file), and a supervised connection with keepalive and exponential-backoff reconnect.
Package transport is the netdev SSH connection layer: host resolution (configured entries + ~/.ssh/config layering), authentication, host-key verification (system known_hosts read-only + a netdev-managed TOFU file), and a supervised connection with keepalive and exponential-backoff reconnect.

Jump to

Keyboard shortcuts

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