feat(audit): 1.1 — project scaffold, schema types, collector stub, updater trust

- go.mod: module bee/audit
- schema/hardware.go: HardwareIngestRequest types (compatible with core)
- collector/collector.go: Run() stub, logs start/finish, returns empty snapshot
- updater/trust.go: Ed25519 multi-key verification via ldflags injection
- updater/trust_test.go: valid sig, tampered, multi-key any-match, dev build
- cmd/audit/main.go: --output stdout|file:<path>|usb, --version flag
- Version = "dev" by default, injected via ldflags at release

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 10:32:12 +03:00
commit a4f70b17f0
8 changed files with 362 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
// Package collector runs all hardware collectors and merges results
// into a single HardwareSnapshot. Each sub-collector is independent:
// a failure in one does not abort the others.
package collector
import (
"bee/audit/internal/schema"
"log/slog"
"time"
)
// Run executes all collectors and returns the combined snapshot.
// Partial failures are logged as warnings; collection always completes.
func Run() schema.HardwareIngestRequest {
start := time.Now()
slog.Info("audit started")
snap := schema.HardwareSnapshot{}
// collectors are added here in subsequent steps (1.2 1.10)
slog.Info("audit completed", "duration", time.Since(start).Round(time.Millisecond))
sourceType := "livcd"
protocol := "os-direct"
return schema.HardwareIngestRequest{
SourceType: &sourceType,
Protocol: &protocol,
CollectedAt: time.Now().UTC().Format(time.RFC3339),
Hardware: snap,
}
}

View File

@@ -0,0 +1,113 @@
// Package schema defines the HardwareIngestRequest types compatible with
// core/internal/ingest/parser_hardware.go. No import dependency on core.
package schema
// HardwareIngestRequest is the top-level output document produced by the audit binary.
// It is accepted as-is by the core /api/ingest/hardware endpoint.
type HardwareIngestRequest struct {
Filename *string `json:"filename"`
SourceType *string `json:"source_type"`
Protocol *string `json:"protocol"`
TargetHost string `json:"target_host"`
CollectedAt string `json:"collected_at"`
Hardware HardwareSnapshot `json:"hardware"`
}
type HardwareSnapshot struct {
Board HardwareBoard `json:"board"`
Firmware []HardwareFirmwareRecord `json:"firmware,omitempty"`
CPUs []HardwareCPU `json:"cpus,omitempty"`
Memory []HardwareMemory `json:"memory,omitempty"`
Storage []HardwareStorage `json:"storage,omitempty"`
PCIeDevices []HardwarePCIeDevice `json:"pcie_devices,omitempty"`
PowerSupplies []HardwarePowerSupply `json:"power_supplies,omitempty"`
}
type HardwareBoard struct {
Manufacturer *string `json:"manufacturer"`
ProductName *string `json:"product_name"`
SerialNumber string `json:"serial_number"`
PartNumber *string `json:"part_number"`
UUID *string `json:"uuid"`
}
type HardwareFirmwareRecord struct {
DeviceName string `json:"device_name"`
Version string `json:"version"`
}
type HardwareCPU struct {
Socket *int `json:"socket"`
Model *string `json:"model"`
Manufacturer *string `json:"manufacturer"`
Status *string `json:"status"`
SerialNumber *string `json:"serial_number"`
Firmware *string `json:"firmware"`
Cores *int `json:"cores"`
Threads *int `json:"threads"`
FrequencyMHz *int `json:"frequency_mhz"`
MaxFrequencyMHz *int `json:"max_frequency_mhz"`
}
type HardwareMemory struct {
Slot *string `json:"slot"`
Location *string `json:"location"`
Present *bool `json:"present"`
SizeMB *int `json:"size_mb"`
Type *string `json:"type"`
MaxSpeedMHz *int `json:"max_speed_mhz"`
CurrentSpeedMHz *int `json:"current_speed_mhz"`
Manufacturer *string `json:"manufacturer"`
SerialNumber *string `json:"serial_number"`
PartNumber *string `json:"part_number"`
Status *string `json:"status"`
}
type HardwareStorage struct {
Slot *string `json:"slot"`
Type *string `json:"type"`
Model *string `json:"model"`
SizeGB *int `json:"size_gb"`
SerialNumber *string `json:"serial_number"`
Manufacturer *string `json:"manufacturer"`
Firmware *string `json:"firmware"`
Interface *string `json:"interface"`
Present *bool `json:"present"`
Status *string `json:"status"`
Telemetry map[string]any `json:"telemetry,omitempty"`
}
type HardwarePCIeDevice struct {
Slot *string `json:"slot"`
VendorID *int `json:"vendor_id"`
DeviceID *int `json:"device_id"`
BDF *string `json:"bdf"`
DeviceClass *string `json:"device_class"`
Manufacturer *string `json:"manufacturer"`
Model *string `json:"model"`
LinkWidth *int `json:"link_width"`
LinkSpeed *string `json:"link_speed"`
MaxLinkWidth *int `json:"max_link_width"`
MaxLinkSpeed *string `json:"max_link_speed"`
SerialNumber *string `json:"serial_number"`
Firmware *string `json:"firmware"`
Present *bool `json:"present"`
Status *string `json:"status"`
Telemetry map[string]any `json:"telemetry,omitempty"`
}
type HardwarePowerSupply struct {
Slot *string `json:"slot"`
Present *bool `json:"present"`
Model *string `json:"model"`
Vendor *string `json:"vendor"`
WattageW *int `json:"wattage_w"`
SerialNumber *string `json:"serial_number"`
PartNumber *string `json:"part_number"`
Firmware *string `json:"firmware"`
Status *string `json:"status"`
InputType *string `json:"input_type"`
InputPowerW *float64 `json:"input_power_w"`
OutputPowerW *float64 `json:"output_power_w"`
InputVoltage *float64 `json:"input_voltage"`
}

View File

@@ -0,0 +1,59 @@
// Package updater handles binary self-update with Ed25519 signature verification.
// See bible/rules/patterns/release-signing/contract.md for the full contract.
package updater
import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"strings"
)
// trustedKeysRaw is injected at release build time via:
//
// -ldflags "-X bee/audit/internal/updater.trustedKeysRaw=base64key1:base64key2"
//
// Empty in dev builds — updates are disabled when empty.
var trustedKeysRaw string
// TrustedKeys decodes the embedded public keys.
// Returns an error if the binary was built without key injection (dev build).
func TrustedKeys() ([]ed25519.PublicKey, error) {
if strings.TrimSpace(trustedKeysRaw) == "" {
return nil, fmt.Errorf("dev build: trusted keys not embedded, updates disabled")
}
var keys []ed25519.PublicKey
for _, enc := range strings.Split(trustedKeysRaw, ":") {
enc = strings.TrimSpace(enc)
if enc == "" {
continue
}
b, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
return nil, fmt.Errorf("decode trusted key: %w", err)
}
if len(b) != ed25519.PublicKeySize {
return nil, fmt.Errorf("trusted key has wrong size: got %d, want %d", len(b), ed25519.PublicKeySize)
}
keys = append(keys, ed25519.PublicKey(b))
}
if len(keys) == 0 {
return nil, fmt.Errorf("no valid trusted keys found in build")
}
return keys, nil
}
// VerifySignature returns nil if sig was produced by any trusted key over data.
// Returns an error (never panics) on any failure — caller logs and continues.
func VerifySignature(data, sig []byte) error {
keys, err := TrustedKeys()
if err != nil {
return err
}
for _, key := range keys {
if ed25519.Verify(key, data, sig) {
return nil
}
}
return fmt.Errorf("signature verification failed: no trusted key matched")
}

View File

@@ -0,0 +1,72 @@
package updater
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"testing"
)
func TestVerifySignature_valid(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
data := []byte("release binary content")
sig := ed25519.Sign(priv, data)
trustedKeysRaw = base64.StdEncoding.EncodeToString(pub)
t.Cleanup(func() { trustedKeysRaw = "" })
if err := VerifySignature(data, sig); err != nil {
t.Fatalf("expected valid signature to pass: %v", err)
}
}
func TestVerifySignature_tampered(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
_ = pub
data := []byte("original content")
sig := ed25519.Sign(priv, data)
// different key embedded
pub2, _, _ := ed25519.GenerateKey(rand.Reader)
trustedKeysRaw = base64.StdEncoding.EncodeToString(pub2)
t.Cleanup(func() { trustedKeysRaw = "" })
if err := VerifySignature(data, sig); err == nil {
t.Fatal("expected tampered signature to fail")
}
}
func TestVerifySignature_multiKey_anyMatches(t *testing.T) {
pub1, _, _ := ed25519.GenerateKey(rand.Reader)
pub2, priv2, _ := ed25519.GenerateKey(rand.Reader)
data := []byte("signed by developer 2")
sig := ed25519.Sign(priv2, data)
trustedKeysRaw = base64.StdEncoding.EncodeToString(pub1) + ":" +
base64.StdEncoding.EncodeToString(pub2)
t.Cleanup(func() { trustedKeysRaw = "" })
if err := VerifySignature(data, sig); err != nil {
t.Fatalf("expected any-key match to pass: %v", err)
}
}
func TestVerifySignature_devBuild(t *testing.T) {
trustedKeysRaw = ""
_, priv, _ := ed25519.GenerateKey(rand.Reader)
data := []byte("content")
sig := ed25519.Sign(priv, data)
if err := VerifySignature(data, sig); err == nil {
t.Fatal("expected dev build (no keys) to fail")
}
}