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>
1461 lines
36 KiB
Markdown
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) |