baseline: P1 契约、agent 核心与中继投递(既有工作首次入库)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entry is one line of the audit log.
|
||||
//
|
||||
// Metadata only, and that is the whole design: the request and response bodies are the actual
|
||||
// credentials and SMS contents, and copying them into a second file on the same box adds a place to
|
||||
// lose them without adding any investigative value. What is kept is enough to answer "who asked for
|
||||
// what, when, and was it allowed" from the relay request id.
|
||||
type Entry struct {
|
||||
At time.Time `json:"ts"`
|
||||
Node string `json:"node,omitempty"`
|
||||
Req string `json:"req,omitempty"`
|
||||
App string `json:"app,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Status int `json:"status"`
|
||||
Outcome string `json:"outcome"`
|
||||
}
|
||||
|
||||
// Audit is an append-only JSONL writer. One line per request, flushed as it is written: an audit log
|
||||
// that loses its tail on SIGKILL cannot explain the incident that killed the process.
|
||||
type Audit struct {
|
||||
mu sync.Mutex
|
||||
sink *os.File
|
||||
enc *json.Encoder
|
||||
}
|
||||
|
||||
// OpenAudit appends to path, creating it 0600 and its parent directories if needed.
|
||||
//
|
||||
// The file is opened append-only and never rotated in-process. Rotation is logrotate's job: an
|
||||
// agent that renames its own log is an agent that can lose an entry during the rename.
|
||||
func OpenAudit(path string) (*Audit, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("runtime: audit path is empty")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sink, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Audit{sink: sink, enc: json.NewEncoder(sink)}, nil
|
||||
}
|
||||
|
||||
// NullAudit returns a writer that keeps nothing. Used by tests and by `msa-agent --audit=-`, because
|
||||
// the executor is simpler when it can assume a non-nil audit target.
|
||||
func NullAudit() *Audit { return &Audit{} }
|
||||
|
||||
// Write appends one entry. A write failure is returned to the caller rather than panicking: the
|
||||
// audit log is evidence, not a control, and a full disk must not become a remote denial of service.
|
||||
func (a *Audit) Write(entry Entry) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.sink == nil {
|
||||
return nil
|
||||
}
|
||||
if entry.At.IsZero() {
|
||||
entry.At = time.Now().UTC()
|
||||
}
|
||||
entry.At = entry.At.UTC()
|
||||
if err := a.enc.Encode(entry); err != nil {
|
||||
return fmt.Errorf("runtime: audit write: %w", err)
|
||||
}
|
||||
return a.sink.Sync()
|
||||
}
|
||||
|
||||
// Close releases the file. Subsequent writes are silently dropped, which is the right behaviour for
|
||||
// a shutdown path: failing the request that happens to race with a clean exit buys nothing.
|
||||
func (a *Audit) Close() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.sink == nil {
|
||||
return nil
|
||||
}
|
||||
err := a.sink.Close()
|
||||
a.sink = nil
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user