Files
CHORUS/docs/comprehensive/packages/shhh.md
anthonyrawlins f9c0395e03 docs: Add Phase 2 core package documentation (Execution, Config, Runtime, Security)
Comprehensive documentation for 7 critical packages covering execution engine,
configuration management, runtime infrastructure, and security layers.

Package Documentation Added:
- pkg/execution - Complete task execution engine API (Docker sandboxing, image selection)
- pkg/config - Configuration management (80+ env vars, dynamic assignments, SIGHUP reload)
- internal/runtime - Shared P2P runtime (initialization, lifecycle, agent mode)
- pkg/dht - Distributed hash table (LibP2P DHT, encrypted storage, bootstrap)
- pkg/crypto - Cryptography (age encryption, key derivation, secure random)
- pkg/ucxl - UCXL validation (decision publishing, content addressing, immutable audit)
- pkg/shhh - Secrets management (sentinel, pattern matching, redaction, audit logging)

Documentation Statistics (Phase 2):
- 7 package files created (~12,000 lines total)
- Complete API reference for all exported symbols
- Line-by-line source code analysis
- 30+ usage examples across packages
- Implementation status tracking (Production/Beta/Alpha/TODO)
- Cross-references to 20+ related documents

Key Features Documented:
- Docker Exec API usage (not SSH) for sandboxed execution
- 4-tier language detection priority system
- RuntimeConfig vs static Config with merge semantics
- SIGHUP signal handling for dynamic reconfiguration
- Graceful shutdown with dependency ordering
- Age encryption integration (filippo.io/age)
- DHT cache management and cleanup
- UCXL address format (ucxl://) and decision schema
- SHHH pattern matching and severity levels
- Bootstrap peer priority (assignment > config > env)
- Join stagger for thundering herd prevention

Progress Tracking:
- PROGRESS.md added with detailed completion status
- Phase 1: 5 files complete (Foundation)
- Phase 2: 7 files complete (Core Packages)
- Total: 12 files, ~16,000 lines documented
- Overall: 15% complete (12/62 planned files)

Next Phase: Coordination & AI packages (pkg/slurp, pkg/election, pkg/ai, pkg/providers)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 18:08:59 +10:00

1461 lines
36 KiB
Markdown

# SHHH Package Documentation
## Overview
The SHHH (Secrets Handler for Hidden Hazards) package provides CHORUS with a comprehensive secrets detection and redaction system. SHHH prevents sensitive data (API keys, tokens, private keys, passwords) from being leaked through logs, telemetry, request forwarding, or other output channels. It operates as a runtime sentinel with composable rules, audit logging, and operational metrics.
## Table of Contents
- [Architecture](#architecture)
- [Sentinel Engine](#sentinel-engine)
- [Pattern Matching](#pattern-matching)
- [Redaction Mechanisms](#redaction-mechanisms)
- [Audit Logging](#audit-logging)
- [Finding Severity Levels](#finding-severity-levels)
- [Configuration](#configuration)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
## Architecture
### Design Principles
1. **Defense in Depth**: Multiple detection rules covering various secret types
2. **Minimal False Positives**: High-signal patterns focused on real credentials
3. **Performance**: Efficient regex compilation and concurrent scanning
4. **Composability**: Custom rules, audit sinks, and finding observers
5. **Operational Visibility**: Comprehensive metrics and statistics
### Core Components
```
┌─────────────────────────────────────────────────────────────┐
│ Sentinel │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Compiled Rules │ │
│ │ • Bearer Tokens • Private Keys │ │
│ │ • API Keys • OAuth Tokens │ │
│ │ • OpenAI Secrets • Custom Rules │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┴────────────────────────────┐ │
│ │ Redaction Engine │ │
│ │ • Pattern Matching • Content Hashing │ │
│ │ • Text Replacement • Finding Aggregation │ │
│ └────────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┴────────────────────────────┐ │
│ │ Audit & Observability │ │
│ │ • Audit Sink • Finding Observers │ │
│ │ • Statistics • Metrics │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Sentinel Engine
### Creating a Sentinel
```go
// Default configuration with built-in rules
sentinel, err := shhh.NewSentinel(shhh.Config{})
if err != nil {
log.Fatal(err)
}
// Custom configuration
sentinel, err := shhh.NewSentinel(shhh.Config{
Disabled: false,
RedactionPlaceholder: "[***REDACTED***]",
DisableDefaultRules: false,
CustomRules: []shhh.RuleConfig{
{
Name: "internal-api-key",
Pattern: `(?i)(internal[_-]key\s*[:=]\s*["']?)([A-Za-z0-9]{16,})(["']?)`,
ReplacementTemplate: "$1[REDACTED]$3",
Severity: shhh.SeverityHigh,
Tags: []string{"api", "internal"},
},
},
})
```
### Sentinel Options
Configure sentinel behavior with functional options:
```go
// With audit sink
auditSink := NewAuditLogger()
sentinel, err := shhh.NewSentinel(cfg,
shhh.WithAuditSink(auditSink),
)
// With custom stats collector
stats := shhh.NewStats()
sentinel, err := shhh.NewSentinel(cfg,
shhh.WithStats(stats),
)
// With finding observer
sentinel, err := shhh.NewSentinel(cfg,
shhh.WithFindingObserver(func(ctx context.Context, findings []shhh.Finding) {
for _, f := range findings {
log.Printf("Found %d instances of %s", f.Count, f.Rule)
}
}),
)
// Combine multiple options
sentinel, err := shhh.NewSentinel(cfg,
shhh.WithAuditSink(auditSink),
shhh.WithStats(stats),
shhh.WithFindingObserver(observer),
)
```
### Runtime Control
Enable, disable, or modify sentinel behavior at runtime:
```go
// Check if enabled
if sentinel.Enabled() {
fmt.Println("Sentinel is active")
}
// Toggle sentinel on/off
sentinel.Toggle(false) // Disable
sentinel.Toggle(true) // Enable
// Update audit sink at runtime
newAuditSink := NewDatabaseAuditSink()
sentinel.SetAuditSink(newAuditSink)
// Add finding observer after creation
sentinel.AddFindingObserver(func(ctx context.Context, findings []shhh.Finding) {
// Process findings
})
```
## Pattern Matching
### Built-in Rules
SHHH includes carefully curated default rules for common secrets:
#### 1. Bearer Tokens
**Pattern**: `(?i)(authorization\s*:\s*bearer\s+)([A-Za-z0-9\-._~+/]+=*)`
**Example**:
```
Input: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.secret
Output: Authorization: Bearer [REDACTED]
```
**Severity**: Medium
**Tags**: `token`, `http`
#### 2. API Keys
**Pattern**: `(?i)((?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?)([A-Za-z0-9\-._~+/]{8,})(["']?)`
**Example**:
```
Input: API_KEY=sk_live_1234567890abcdef
Output: API_KEY=[REDACTED]
```
**Severity**: High
**Tags**: `credentials`
#### 3. OpenAI Secrets
**Pattern**: `(sk-[A-Za-z0-9]{20,})`
**Example**:
```
Input: OPENAI_KEY=sk-proj-1234567890abcdefghij
Output: OPENAI_KEY=[REDACTED]
```
**Severity**: High
**Tags**: `llm`, `api`
#### 4. OAuth Refresh Tokens
**Pattern**: `(?i)(refresh_token"?\s*[:=]\s*["']?)([A-Za-z0-9\-._~+/]{8,})(["']?)`
**Example**:
```
Input: refresh_token="1/abc123def456ghi789"
Output: refresh_token="[REDACTED]"
```
**Severity**: Medium
**Tags**: `oauth`
#### 5. Private Key Blocks
**Pattern**: `(?s)(-----BEGIN [^-]+ PRIVATE KEY-----)[^-]+(-----END [^-]+ PRIVATE KEY-----)`
**Example**:
```
Input: -----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----
Output: -----BEGIN RSA PRIVATE KEY-----
[REDACTED]
-----END RSA PRIVATE KEY-----
```
**Severity**: High
**Tags**: `pem`, `key`
### Custom Rules
Define custom redaction rules for domain-specific secrets:
```go
customRule := shhh.RuleConfig{
Name: "database-password",
Pattern: `(?i)(db[_-]?pass(?:word)?\s*[:=]\s*["']?)([^"'\s]{8,})(["']?)`,
ReplacementTemplate: "$1[REDACTED]$3",
Severity: shhh.SeverityHigh,
Tags: []string{"database", "credentials"},
}
sentinel, err := shhh.NewSentinel(shhh.Config{
CustomRules: []shhh.RuleConfig{customRule},
})
```
### Pattern Syntax
Rules use Go's `regexp` package syntax:
- `(?i)` - Case-insensitive matching
- `(?s)` - Dot matches newlines (for multi-line patterns)
- `([^"'\s]{8,})` - Capture group: non-quote/space chars, min 8 length
- `$1`, `$2` - Backreferences in replacement template
**Best Practices**:
1. Use capture groups to preserve context (prefixes, quotes)
2. Be specific to reduce false positives
3. Test patterns against real data samples
4. Consider minimum length requirements
5. Use anchors when appropriate (`\b` for word boundaries)
## Redaction Mechanisms
### Text Redaction
Redact secrets from plain text:
```go
input := `
Config:
API_KEY=sk_live_1234567890abcdef
DB_PASSWORD=supersecret123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.secret
`
// Labels provide context for audit logs
labels := map[string]string{
"source": "config_file",
"path": "/etc/app/config.yaml",
}
redacted, findings := sentinel.RedactText(ctx, input, labels)
fmt.Println(redacted)
// Output:
// Config:
// API_KEY=[REDACTED]
// DB_PASSWORD=supersecret123
// Authorization: Bearer [REDACTED]
fmt.Printf("Found %d types of secrets\n", len(findings))
for _, f := range findings {
fmt.Printf(" %s: %d occurrences (severity: %s)\n",
f.Rule,
f.Count,
f.Severity,
)
}
```
### Map Redaction
Redact secrets from structured data (in-place):
```go
payload := map[string]any{
"user": "john@example.com",
"config": map[string]any{
"api_key": "sk_live_1234567890abcdef",
"timeout": 30,
},
"tokens": []any{
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.secret",
map[string]any{
"refresh": "refresh_token=abc123def456",
},
},
}
findings := sentinel.RedactMap(ctx, payload)
// payload is modified in-place
fmt.Printf("%+v\n", payload)
// Output:
// map[
// user:john@example.com
// config:map[api_key:[REDACTED] timeout:30]
// tokens:[
// [REDACTED]
// map[refresh:refresh_token=[REDACTED]]
// ]
// ]
// With base labels
baseLabels := map[string]string{
"source": "http_request",
"method": "POST",
}
findings := sentinel.RedactMapWithLabels(ctx, payload, baseLabels)
```
### Nested Structure Traversal
SHHH recursively traverses nested structures:
1. **Maps**: Scans all string values, recurses into nested maps/slices
2. **Slices**: Scans all elements, handles mixed types
3. **Strings**: Applies all rules in order
4. **Stringer Interface**: Converts to string and scans
**Path Generation**:
- Map keys: `parent.child.grandchild`
- Array indices: `parent[0]`, `parent[1].child`
- Root: Empty string or label-derived path
```go
// Complex nested structure
data := map[string]any{
"services": []any{
map[string]any{
"name": "api",
"auth": map[string]any{
"token": "Bearer secret123",
},
},
map[string]any{
"name": "worker",
"auth": map[string]any{
"token": "Bearer secret456",
},
},
},
}
findings := sentinel.RedactMap(ctx, data)
// Findings include location paths
for _, finding := range findings {
for _, loc := range finding.Locations {
fmt.Printf("%s: %d occurrences at %s\n",
finding.Rule,
loc.Count,
loc.Path,
)
}
}
// Output:
// bearer-token: 1 occurrences at services[0].auth.token
// bearer-token: 1 occurrences at services[1].auth.token
```
## Audit Logging
### Audit Events
Each redaction generates an audit event:
```go
type AuditEvent struct {
Rule string `json:"rule"`
Severity Severity `json:"severity"`
Tags []string `json:"tags,omitempty"`
Path string `json:"path,omitempty"`
Hash string `json:"hash"`
Metadata map[string]string `json:"metadata,omitempty"`
}
```
**Fields**:
- `Rule`: Name of the rule that matched
- `Severity`: Severity level (low, medium, high)
- `Tags`: Tags associated with the rule
- `Path`: Location in structure where secret was found
- `Hash`: SHA-256 hash of the secret value (for tracking)
- `Metadata`: Additional context (source, labels, etc.)
### Implementing Audit Sinks
Create custom audit sinks to persist events:
```go
// File audit sink
type FileAuditSink struct {
file *os.File
mu sync.Mutex
}
func (f *FileAuditSink) RecordRedaction(ctx context.Context, event shhh.AuditEvent) {
f.mu.Lock()
defer f.mu.Unlock()
eventJSON, _ := json.Marshal(event)
f.file.Write(eventJSON)
f.file.WriteString("\n")
}
// Database audit sink
type DBQuditSink struct {
db *sql.DB
}
func (d *DBAuditSink) RecordRedaction(ctx context.Context, event shhh.AuditEvent) {
_, err := d.db.ExecContext(ctx,
`INSERT INTO audit_events (rule, severity, path, hash, metadata, created_at)
VALUES ($1, $2, $3, $4, $5, NOW())`,
event.Rule,
event.Severity,
event.Path,
event.Hash,
event.Metadata,
)
if err != nil {
log.Printf("Failed to record audit event: %v", err)
}
}
// Syslog audit sink
type SyslogAuditSink struct {
writer *syslog.Writer
}
func (s *SyslogAuditSink) RecordRedaction(ctx context.Context, event shhh.AuditEvent) {
priority := syslog.LOG_WARNING
if event.Severity == shhh.SeverityHigh {
priority = syslog.LOG_ERR
}
msg := fmt.Sprintf("SHHH: %s detected at %s (hash: %s)",
event.Rule,
event.Path,
event.Hash,
)
s.writer.Write(priority, msg)
}
```
### Secret Hashing
SHHH hashes detected secrets using SHA-256 for tracking without storing plaintext:
```go
// Automatic hashing (internal)
secretValue := "sk_live_1234567890abcdef"
hash := sha256.Sum256([]byte(secretValue))
hashString := base64.RawStdEncoding.EncodeToString(hash[:])
// Hash in audit event
event := shhh.AuditEvent{
Rule: "api-key",
Hash: hashString, // "vJ8x3mHNqR8..."
}
```
**Use Cases**:
- Track repeated leaks of same secret
- Correlate incidents across systems
- Detect secret rotation failures
- Never store plaintext secrets in audit logs
## Finding Severity Levels
### Severity Enum
```go
const (
SeverityLow Severity = "low"
SeverityMedium Severity = "medium"
SeverityHigh Severity = "high"
)
```
### Severity Guidelines
#### Low Severity
**Impact**: Minimal security risk
**Examples**:
- Development/testing credentials
- Non-production API keys
- Internal documentation tokens
- Temporary access codes
**Response**: Log and notify, no immediate action required
#### Medium Severity
**Impact**: Moderate security risk, limited blast radius
**Examples**:
- Access tokens (short-lived)
- Bearer tokens (limited scope)
- OAuth refresh tokens
- Session identifiers
**Response**: Log, notify, consider rotation
#### High Severity
**Impact**: Critical security risk, potential full compromise
**Examples**:
- Private keys (RSA, ECDSA, Ed25519)
- Master API keys
- Database passwords
- Service account credentials
- Production secrets
**Response**: Immediate alert, mandatory rotation, incident investigation
### Finding Structure
```go
type Finding struct {
Rule string `json:"rule"`
Severity Severity `json:"severity"`
Tags []string `json:"tags,omitempty"`
Count int `json:"count"`
Locations []Location `json:"locations,omitempty"`
}
type Location struct {
Path string `json:"path"`
Count int `json:"count"`
}
```
### Finding Observers
React to findings in real-time:
```go
sentinel, err := shhh.NewSentinel(cfg,
shhh.WithFindingObserver(func(ctx context.Context, findings []shhh.Finding) {
for _, finding := range findings {
switch finding.Severity {
case shhh.SeverityHigh:
// Immediate alert
alerting.SendCritical(fmt.Sprintf(
"HIGH SEVERITY SECRET DETECTED: %s (%d occurrences)",
finding.Rule,
finding.Count,
))
// Log with full context
for _, loc := range finding.Locations {
log.Printf(" Location: %s (%d times)", loc.Path, loc.Count)
}
case shhh.SeverityMedium:
// Standard notification
log.Printf("MEDIUM SEVERITY: %s detected %d times",
finding.Rule,
finding.Count,
)
case shhh.SeverityLow:
// Debug logging
log.Printf("DEBUG: %s detected %d times",
finding.Rule,
finding.Count,
)
}
}
}),
)
```
## Configuration
### Config Structure
```go
type Config struct {
// Disabled toggles redaction off entirely
Disabled bool `json:"disabled"`
// RedactionPlaceholder overrides the default "[REDACTED]"
RedactionPlaceholder string `json:"redaction_placeholder"`
// DisableDefaultRules disables built-in curated rule set
DisableDefaultRules bool `json:"disable_default_rules"`
// CustomRules allows bespoke redaction patterns
CustomRules []RuleConfig `json:"custom_rules"`
}
type RuleConfig struct {
Name string `json:"name"`
Pattern string `json:"pattern"`
ReplacementTemplate string `json:"replacement_template"`
Severity Severity `json:"severity"`
Tags []string `json:"tags"`
}
```
### Configuration Examples
#### Minimal (Defaults)
```go
cfg := shhh.Config{}
sentinel, err := shhh.NewSentinel(cfg)
// Uses default rules, "[REDACTED]" placeholder
```
#### Custom Placeholder
```go
cfg := shhh.Config{
RedactionPlaceholder: "***SENSITIVE***",
}
sentinel, err := shhh.NewSentinel(cfg)
```
#### Custom Rules Only
```go
cfg := shhh.Config{
DisableDefaultRules: true,
CustomRules: []shhh.RuleConfig{
{
Name: "credit-card",
Pattern: `\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b`,
ReplacementTemplate: "[CARD-REDACTED]",
Severity: shhh.SeverityHigh,
Tags: []string{"pci", "payment"},
},
{
Name: "ssn",
Pattern: `\b\d{3}-\d{2}-\d{4}\b`,
ReplacementTemplate: "[SSN-REDACTED]",
Severity: shhh.SeverityHigh,
Tags: []string{"pii", "identity"},
},
},
}
sentinel, err := shhh.NewSentinel(cfg)
```
#### Augment Default Rules
```go
cfg := shhh.Config{
CustomRules: []shhh.RuleConfig{
{
Name: "internal-token",
Pattern: `(?i)(x-internal-token\s*:\s*)([A-Za-z0-9]{16,})`,
ReplacementTemplate: "$1[REDACTED]",
Severity: shhh.SeverityMedium,
Tags: []string{"internal", "http"},
},
},
}
sentinel, err := shhh.NewSentinel(cfg)
// Uses default rules + custom rule
```
#### From JSON
```json
{
"disabled": false,
"redaction_placeholder": "[***]",
"disable_default_rules": false,
"custom_rules": [
{
"name": "gitlab-token",
"pattern": "(?i)(gitlab[_-]token\\s*[:=]\\s*[\"']?)([A-Za-z0-9_-]{20,})([\"']?)",
"replacement_template": "$1[REDACTED]$3",
"severity": "high",
"tags": ["gitlab", "vcs"]
}
]
}
```
```go
var cfg shhh.Config
err := json.Unmarshal(configJSON, &cfg)
if err != nil {
log.Fatal(err)
}
sentinel, err := shhh.NewSentinel(cfg)
```
## Statistics
### Stats Tracking
SHHH maintains comprehensive operational metrics:
```go
type StatsSnapshot struct {
TotalScans uint64 `json:"total_scans"`
TotalFindings uint64 `json:"total_findings"`
PerRuleFindings map[string]uint64 `json:"per_rule_findings"`
}
```
### Retrieving Statistics
```go
// Get snapshot
snapshot := sentinel.StatsSnapshot()
fmt.Printf("Total scans: %d\n", snapshot.TotalScans)
fmt.Printf("Total findings: %d\n", snapshot.TotalFindings)
fmt.Printf("Average findings per scan: %.2f\n",
float64(snapshot.TotalFindings) / float64(snapshot.TotalScans),
)
fmt.Println("\nPer-rule statistics:")
for rule, count := range snapshot.PerRuleFindings {
fmt.Printf(" %s: %d\n", rule, count)
}
```
### Shared Stats Collector
Share stats across multiple sentinels:
```go
// Create shared stats
stats := shhh.NewStats()
// Create multiple sentinels sharing stats
sentinel1, _ := shhh.NewSentinel(cfg1, shhh.WithStats(stats))
sentinel2, _ := shhh.NewSentinel(cfg2, shhh.WithStats(stats))
// Both sentinels contribute to same stats
sentinel1.RedactText(ctx, text1, nil)
sentinel2.RedactText(ctx, text2, nil)
// Get combined statistics
snapshot := stats.Snapshot()
```
## API Reference
### Core Types
```go
// Sentinel - main redaction engine
type Sentinel struct { /* ... */ }
// Finding - detected secret information
type Finding struct {
Rule string
Severity Severity
Tags []string
Count int
Locations []Location
}
// Location - where secret was found
type Location struct {
Path string
Count int
}
// AuditEvent - audit log entry
type AuditEvent struct {
Rule string
Severity Severity
Tags []string
Path string
Hash string
Metadata map[string]string
}
```
### Sentinel Methods
```go
// NewSentinel creates a new secrets sentinel
func NewSentinel(cfg Config, opts ...Option) (*Sentinel, error)
// RedactText scans and redacts text
func (s *Sentinel) RedactText(ctx context.Context, text string, labels map[string]string) (string, []Finding)
// RedactMap scans and redacts map in-place
func (s *Sentinel) RedactMap(ctx context.Context, payload map[string]any) []Finding
// RedactMapWithLabels redacts map with base labels
func (s *Sentinel) RedactMapWithLabels(ctx context.Context, payload map[string]any, baseLabels map[string]string) []Finding
// Enabled reports if sentinel is active
func (s *Sentinel) Enabled() bool
// Toggle enables/disables sentinel
func (s *Sentinel) Toggle(enabled bool)
// SetAuditSink updates audit sink at runtime
func (s *Sentinel) SetAuditSink(sink AuditSink)
// AddFindingObserver registers finding observer
func (s *Sentinel) AddFindingObserver(observer FindingObserver)
// StatsSnapshot returns current statistics
func (s *Sentinel) StatsSnapshot() StatsSnapshot
```
### Options
```go
// WithAuditSink attaches audit sink
func WithAuditSink(sink AuditSink) Option
// WithStats supplies shared stats collector
func WithStats(stats *Stats) Option
// WithFindingObserver registers finding observer
func WithFindingObserver(observer FindingObserver) Option
```
### Interfaces
```go
// AuditSink receives redaction events
type AuditSink interface {
RecordRedaction(ctx context.Context, event AuditEvent)
}
// FindingObserver receives aggregated findings
type FindingObserver func(context.Context, []Finding)
```
## Usage Examples
### Example 1: Basic Text Redaction
```go
package main
import (
"context"
"fmt"
"log"
"chorus/pkg/shhh"
)
func main() {
// Create sentinel with defaults
sentinel, err := shhh.NewSentinel(shhh.Config{})
if err != nil {
log.Fatal(err)
}
// Sample text with secrets
input := `
API Configuration:
- API_KEY=sk_live_1234567890abcdef
- DB_PASSWORD=supersecret123
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload
`
// Redact
redacted, findings := sentinel.RedactText(
context.Background(),
input,
map[string]string{"source": "config"},
)
fmt.Println("Redacted output:")
fmt.Println(redacted)
fmt.Printf("\nFound %d types of secrets:\n", len(findings))
for _, finding := range findings {
fmt.Printf("- %s: %d occurrences [%s]\n",
finding.Rule,
finding.Count,
finding.Severity,
)
}
}
```
### Example 2: HTTP Request Redaction
```go
package main
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"chorus/pkg/shhh"
)
type Server struct {
sentinel *shhh.Sentinel
}
func NewServer() (*Server, error) {
sentinel, err := shhh.NewSentinel(shhh.Config{})
if err != nil {
return nil, err
}
return &Server{sentinel: sentinel}, nil
}
func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) {
// Read request body
body, _ := io.ReadAll(r.Body)
r.Body.Close()
// Parse JSON
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Redact secrets before logging
baseLabels := map[string]string{
"source": "http_request",
"method": r.Method,
"path": r.URL.Path,
"remote_ip": r.RemoteAddr,
}
findings := s.sentinel.RedactMapWithLabels(
context.Background(),
payload,
baseLabels,
)
if len(findings) > 0 {
log.Printf("⚠ Redacted %d types of secrets from request", len(findings))
for _, f := range findings {
if f.Severity == shhh.SeverityHigh {
log.Printf(" HIGH: %s (%d occurrences)", f.Rule, f.Count)
}
}
}
// Safe to log now
safeJSON, _ := json.Marshal(payload)
log.Printf("Request payload: %s", safeJSON)
// Process request...
w.WriteHeader(http.StatusOK)
}
func main() {
server, err := NewServer()
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/api/", server.HandleRequest)
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
### Example 3: Structured Logging Integration
```go
package main
import (
"context"
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"chorus/pkg/shhh"
)
// SecureLogger wraps zap.Logger with SHHH redaction
type SecureLogger struct {
logger *zap.Logger
sentinel *shhh.Sentinel
}
func NewSecureLogger() (*SecureLogger, error) {
logger, err := zap.NewProduction()
if err != nil {
return nil, err
}
sentinel, err := shhh.NewSentinel(shhh.Config{})
if err != nil {
return nil, err
}
return &SecureLogger{
logger: logger,
sentinel: sentinel,
}, nil
}
func (sl *SecureLogger) Info(msg string, fields ...zapcore.Field) {
// Redact message
redacted, _ := sl.sentinel.RedactText(
context.Background(),
msg,
nil,
)
// Redact field values
safeFields := make([]zapcore.Field, len(fields))
for i, field := range fields {
if field.Type == zapcore.StringType {
redactedValue, _ := sl.sentinel.RedactText(
context.Background(),
field.String,
nil,
)
safeFields[i] = zap.String(field.Key, redactedValue)
} else {
safeFields[i] = field
}
}
sl.logger.Info(redacted, safeFields...)
}
func (sl *SecureLogger) Warn(msg string, fields ...zapcore.Field) {
redacted, findings := sl.sentinel.RedactText(
context.Background(),
msg,
nil,
)
if len(findings) > 0 {
sl.logger.Warn("Secrets detected in log message",
zap.Int("finding_count", len(findings)),
)
}
sl.logger.Warn(redacted, fields...)
}
func main() {
logger, err := NewSecureLogger()
if err != nil {
panic(err)
}
// Safe logging - secrets automatically redacted
logger.Info("Connecting to API",
zap.String("api_key", "sk_live_1234567890abcdef"),
zap.String("endpoint", "https://api.example.com"),
)
logger.Warn("Authentication failed with token: Bearer eyJhbGci...")
}
```
### Example 4: Audit Trail with Database Sink
```go
package main
import (
"context"
"database/sql"
"encoding/json"
"log"
_ "github.com/lib/pq"
"chorus/pkg/shhh"
)
// DBAuditSink persists audit events to PostgreSQL
type DBAuditSink struct {
db *sql.DB
}
func NewDBAuditSink(connStr string) (*DBAuditSink, error) {
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}
// Create audit table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS shhh_audit (
id SERIAL PRIMARY KEY,
rule VARCHAR(100) NOT NULL,
severity VARCHAR(20) NOT NULL,
path TEXT,
hash VARCHAR(64) NOT NULL,
tags JSONB,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
)
`)
if err != nil {
return nil, err
}
return &DBAuditSink{db: db}, nil
}
func (d *DBAuditSink) RecordRedaction(ctx context.Context, event shhh.AuditEvent) {
tagsJSON, _ := json.Marshal(event.Tags)
metadataJSON, _ := json.Marshal(event.Metadata)
_, err := d.db.ExecContext(ctx,
`INSERT INTO shhh_audit (rule, severity, path, hash, tags, metadata)
VALUES ($1, $2, $3, $4, $5, $6)`,
event.Rule,
event.Severity,
event.Path,
event.Hash,
tagsJSON,
metadataJSON,
)
if err != nil {
log.Printf("Failed to record audit event: %v", err)
}
}
func (d *DBAuditSink) GetRecentFindings(hours int) ([]shhh.AuditEvent, error) {
rows, err := d.db.Query(`
SELECT rule, severity, path, hash, tags, metadata, created_at
FROM shhh_audit
WHERE created_at > NOW() - INTERVAL '$1 hours'
ORDER BY created_at DESC
`, hours)
if err != nil {
return nil, err
}
defer rows.Close()
var events []shhh.AuditEvent
for rows.Next() {
var event shhh.AuditEvent
var tagsJSON, metadataJSON []byte
var createdAt string
err := rows.Scan(
&event.Rule,
&event.Severity,
&event.Path,
&event.Hash,
&tagsJSON,
&metadataJSON,
&createdAt,
)
if err != nil {
continue
}
json.Unmarshal(tagsJSON, &event.Tags)
json.Unmarshal(metadataJSON, &event.Metadata)
events = append(events, event)
}
return events, nil
}
func main() {
// Create audit sink
auditSink, err := NewDBAuditSink("postgres://user:pass@localhost/chorus?sslmode=disable")
if err != nil {
log.Fatal(err)
}
// Create sentinel with audit sink
sentinel, err := shhh.NewSentinel(
shhh.Config{},
shhh.WithAuditSink(auditSink),
)
if err != nil {
log.Fatal(err)
}
// Redact text (events automatically recorded)
text := "API_KEY=sk_live_1234567890abcdef"
sentinel.RedactText(
context.Background(),
text,
map[string]string{
"source": "user_input",
"user_id": "user-123",
},
)
// Query recent findings
findings, err := auditSink.GetRecentFindings(24)
if err != nil {
log.Fatal(err)
}
log.Printf("Found %d audit events in last 24 hours", len(findings))
for _, finding := range findings {
log.Printf("- %s: %s (hash: %s)", finding.Rule, finding.Severity, finding.Hash)
}
}
```
### Example 5: Real-time Alerting
```go
package main
import (
"context"
"fmt"
"log"
"chorus/pkg/shhh"
)
// AlertingObserver sends alerts for high-severity findings
func AlertingObserver(ctx context.Context, findings []shhh.Finding) {
for _, finding := range findings {
if finding.Severity != shhh.SeverityHigh {
continue
}
// Send alert
alert := fmt.Sprintf(
"🚨 HIGH SEVERITY SECRET DETECTED\n"+
"Rule: %s\n"+
"Count: %d\n"+
"Tags: %v\n",
finding.Rule,
finding.Count,
finding.Tags,
)
if len(finding.Locations) > 0 {
alert += "Locations:\n"
for _, loc := range finding.Locations {
alert += fmt.Sprintf(" - %s (%d times)\n", loc.Path, loc.Count)
}
}
// Send via Slack, PagerDuty, email, etc.
sendAlert(alert)
}
}
// MetricsObserver tracks findings in Prometheus
func MetricsObserver(ctx context.Context, findings []shhh.Finding) {
for _, finding := range findings {
// Increment Prometheus counter
secretsDetectedCounter.WithLabelValues(
finding.Rule,
string(finding.Severity),
).Add(float64(finding.Count))
}
}
func sendAlert(message string) {
// Slack webhook
log.Printf("ALERT: %s", message)
// In production:
// slack.PostMessage(message)
// pagerduty.CreateIncident(message)
// email.Send(message)
}
func main() {
sentinel, err := shhh.NewSentinel(
shhh.Config{},
shhh.WithFindingObserver(AlertingObserver),
shhh.WithFindingObserver(MetricsObserver),
)
if err != nil {
log.Fatal(err)
}
// Simulate secret detection
text := `
Production credentials:
AWS_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----
`
sentinel.RedactText(
context.Background(),
text,
map[string]string{
"source": "production_config",
"environment": "prod",
},
)
// Alerts triggered automatically via observers
}
```
## Best Practices
### Rule Design
1. **Be Specific**: Minimize false positives with precise patterns
2. **Test Thoroughly**: Validate rules against real data samples
3. **Use Capture Groups**: Preserve context around secrets
4. **Consider Performance**: Avoid overly complex regex patterns
5. **Document Rules**: Add clear names and tags
### Integration
1. **Redact Early**: Apply redaction before logging or transmission
2. **Audit Everything**: Enable audit sinks in production
3. **Monitor Metrics**: Track detection rates and patterns
4. **Alert on High Severity**: Immediate alerts for critical secrets
5. **Regular Reviews**: Periodically review audit logs for patterns
### Performance
1. **Compile Rules Once**: Create sentinel at startup, reuse across requests
2. **Share Stats**: Use shared stats collector for multiple sentinels
3. **Batch Operations**: Redact entire structures rather than individual fields
4. **Minimize Rules**: Only include necessary custom rules
### Security
1. **Never Log Plaintext**: Always redact before logging
2. **Hash for Tracking**: Use audit event hashes to track without storing secrets
3. **Rotate on Detection**: Treat secret detection as potential compromise
4. **Principle of Least Privilege**: Restrict audit log access
5. **Encrypt Audit Logs**: Protect audit logs with encryption at rest
## Integration Points
### COOEE Logger Integration
```go
// In COOEE logger initialization
sentinel, _ := shhh.NewSentinel(shhh.Config{})
func (l *Logger) Log(level, message string, fields map[string]interface{}) {
// Redact message
redacted, _ := sentinel.RedactText(context.Background(), message, nil)
// Redact fields
findings := sentinel.RedactMap(context.Background(), fields)
if len(findings) > 0 {
fields["_shhh_redactions"] = len(findings)
}
// Safe to log
l.backend.Write(level, redacted, fields)
}
```
### WHOOSH Search Integration
```go
// Before indexing documents
func (idx *Indexer) IndexDocument(doc Document) error {
// Redact sensitive fields
findings := sentinel.RedactMap(context.Background(), doc.Fields)
if len(findings) > 0 {
log.Printf("Redacted %d secrets before indexing", len(findings))
}
// Safe to index
return idx.backend.Index(doc)
}
```
### CHORUS Agent Integration
```go
// In agent message handling
func (a *Agent) SendMessage(msg Message) error {
// Redact message content
redactedContent, _ := sentinel.RedactText(
context.Background(),
msg.Content,
map[string]string{
"agent": a.ID,
"channel": msg.Channel,
},
)
msg.Content = redactedContent
return a.transport.Send(msg)
}
```
## Troubleshooting
### High False Positive Rate
**Problem**: Rules matching non-secret content
**Solutions**:
- Make patterns more specific
- Add negative lookaheads to exclude known patterns
- Increase minimum length requirements
- Use word boundaries (`\b`)
### Performance Issues
**Problem**: Slow redaction on large payloads
**Solutions**:
- Profile regex patterns for complexity
- Reduce number of custom rules
- Process in chunks for very large inputs
- Consider async redaction for non-critical paths
### Missing Detections
**Problem**: Secrets not being caught
**Solutions**:
- Add custom rules for domain-specific secrets
- Review audit logs for patterns
- Test rules against known secret formats
- Consider case-insensitive matching (`(?i)`)
## See Also
- [CHORUS Security Architecture](../security/overview.md)
- [COOEE Logging Package](cooee.md)
- [UCXL Package](ucxl.md)
- [Audit Trail System](../audit/trail.md)