mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-10 12:02:36 +02:00
initial: Syntrex extraction from sentinel-community (615 files)
This commit is contained in:
commit
2c50c993b1
175 changed files with 32396 additions and 0 deletions
198
internal/infrastructure/audit/decisions.go
Normal file
198
internal/infrastructure/audit/decisions.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package audit
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const decisionsFileName = "decisions.log"
|
||||
|
||||
// DecisionModule identifies the subsystem that made a decision.
|
||||
type DecisionModule string
|
||||
|
||||
const (
|
||||
ModuleSynapse DecisionModule = "SYNAPSE"
|
||||
ModulePeer DecisionModule = "PEER"
|
||||
ModuleMode DecisionModule = "MODE"
|
||||
ModuleDIPWatcher DecisionModule = "DIP-WATCHER"
|
||||
ModuleOracle DecisionModule = "ORACLE"
|
||||
ModuleGenome DecisionModule = "GENOME"
|
||||
ModuleDoctor DecisionModule = "DOCTOR"
|
||||
ModuleSOC DecisionModule = "SOC" // AI SOC event pipeline decisions
|
||||
ModuleCorrelation DecisionModule = "CORRELATION" // SOC correlation engine decisions
|
||||
)
|
||||
|
||||
// Decision represents a tamper-evident decision record (v3.7).
|
||||
// Each record includes a hash of the previous record, forming an
|
||||
// append-only chain that detects any attempt to alter history.
|
||||
type Decision struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Module DecisionModule `json:"module"`
|
||||
Decision string `json:"decision"`
|
||||
Reason string `json:"reason"`
|
||||
PrevHash string `json:"prev_hash"`
|
||||
}
|
||||
|
||||
// String formats the decision for file output.
|
||||
func (d Decision) String() string {
|
||||
return fmt.Sprintf("[%s] | %s | %s | %s | %s",
|
||||
d.Timestamp.Format("2006-01-02T15:04:05.000Z07:00"),
|
||||
d.Module,
|
||||
d.Decision,
|
||||
d.Reason,
|
||||
d.PrevHash,
|
||||
)
|
||||
}
|
||||
|
||||
// Hash computes SHA-256 of this decision record for chain linking.
|
||||
func (d Decision) Hash() string {
|
||||
h := sha256.Sum256([]byte(d.String()))
|
||||
return fmt.Sprintf("%x", h)
|
||||
}
|
||||
|
||||
// DecisionLogger is a tamper-evident decision trace (v3.7 Cerebro).
|
||||
// Each Record() call appends to decisions.log with a SHA-256 chain
|
||||
// linking each entry to the previous one. Any modification to a line
|
||||
// breaks the chain, making tampering detectable.
|
||||
type DecisionLogger struct {
|
||||
mu sync.Mutex
|
||||
file *os.File
|
||||
path string
|
||||
prevHash string // Hash of last written record
|
||||
count int
|
||||
}
|
||||
|
||||
// NewDecisionLogger creates a tamper-evident decision logger.
|
||||
func NewDecisionLogger(rlmDir string) (*DecisionLogger, error) {
|
||||
if err := os.MkdirAll(rlmDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("decisions: mkdir %s: %w", rlmDir, err)
|
||||
}
|
||||
path := filepath.Join(rlmDir, decisionsFileName)
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decisions: open %s: %w", path, err)
|
||||
}
|
||||
return &DecisionLogger{
|
||||
file: f,
|
||||
path: path,
|
||||
prevHash: "GENESIS", // First record links to GENESIS sentinel.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Record writes a tamper-evident decision entry.
|
||||
// Thread-safe, append-only, hash-chained.
|
||||
func (l *DecisionLogger) Record(module DecisionModule, decision, reason string) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
d := Decision{
|
||||
Timestamp: time.Now(),
|
||||
Module: module,
|
||||
Decision: decision,
|
||||
Reason: reason,
|
||||
PrevHash: l.prevHash,
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintln(l.file, d.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("decisions: write: %w", err)
|
||||
}
|
||||
|
||||
l.prevHash = d.Hash()
|
||||
l.count++
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns decisions recorded this session.
|
||||
func (l *DecisionLogger) Count() int {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.count
|
||||
}
|
||||
|
||||
// Path returns the log file path.
|
||||
func (l *DecisionLogger) Path() string { return l.path }
|
||||
|
||||
// PrevHash returns the current chain head hash.
|
||||
func (l *DecisionLogger) PrevHash() string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.prevHash
|
||||
}
|
||||
|
||||
// RecordDecision satisfies the tools.DecisionRecorder interface.
|
||||
// Converts string module to DecisionModule for type safety.
|
||||
func (l *DecisionLogger) RecordDecision(module, decision, reason string) {
|
||||
l.Record(DecisionModule(module), decision, reason)
|
||||
}
|
||||
|
||||
// Close closes the decisions file.
|
||||
func (l *DecisionLogger) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.file != nil {
|
||||
return l.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyChainFromFile reads a decisions.log and verifies hash chain integrity.
|
||||
// Returns the number of valid records and the first broken line (0 if all valid).
|
||||
func VerifyChainFromFile(path string) (validCount int, brokenLine int, err error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
lines := splitLines(string(data))
|
||||
prevHash := "GENESIS"
|
||||
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// Each line should end with | PREV_HASH.
|
||||
// Compute expected hash of previous line.
|
||||
if i > 0 && prevHash != extractPrevHash(line) {
|
||||
return validCount, i + 1, nil
|
||||
}
|
||||
// Compute hash of this line for next iteration.
|
||||
h := sha256.Sum256([]byte(line))
|
||||
prevHash = fmt.Sprintf("%x", h)
|
||||
validCount++
|
||||
}
|
||||
return validCount, 0, nil
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
var lines []string
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\n' {
|
||||
line := s[start:i]
|
||||
if len(line) > 0 && line[len(line)-1] == '\r' {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
lines = append(lines, line)
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(s) {
|
||||
lines = append(lines, s[start:])
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func extractPrevHash(line string) string {
|
||||
// Format: [timestamp] | MODULE | DECISION | REASON | PREV_HASH
|
||||
// Extract last field after the last " | ".
|
||||
for i := len(line) - 1; i >= 0; i-- {
|
||||
if i >= 2 && line[i-2:i+1] == " | " {
|
||||
return line[i+1:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue