security

package
v0.11.0-beta.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: Apache-2.0, MIT Imports: 19 Imported by: 0

Documentation

Overview

Package security — encoding-aware scan layer.

Plain regex on input bytes misses encoded bypasses. An agent (hallucinating or adversarial) can write:

echo cm0gLXJmIC8K | base64 -d | sh

where `cm0gLXJmIC8K` decodes to `rm -rf /`. The original Scan pattern set only matches plain-text — without this file, the dangerous command slips through every deny pattern.

Approach: decode every plausible-looking base64 / hex run in the input and feed each decoded fragment back through the same Scan pipeline. If a decoded fragment matches a deny pattern, the finding gets reported as if it had been written in plain text. Original Scan still runs first so legitimate plain-text commands behave exactly as before.

Defenses against the obvious counter:

  • Bare base64-looking strings (JWTs, image data URIs, hashes) decode to garbage that won't match a command pattern. False positives only fire when the decoded text actually contains a dangerous command.
  • We don't decode unboundedly — runs shorter than 12 chars skip (too noisy), longer than 4KB skip (almost certainly not a command).
  • "decode-and-exec" SHAPES (base64 -d | sh, eval $(...|base64), xxd -r -p | bash) get a dedicated high-severity pattern regardless of decode success — those constructs have no legitimate purpose in agent traffic.

Package security's pairing.go implements DM pairing — sender identity verification for messaging gateways. Stolen from OpenClaw's src/pairing/.

OpenClaw's fix is dead simple: unknown senders get an 8-char code (no ambiguous 0/O/1/I), owner approves via CLI or web dashboard, sender gets added to per-channel allowFrom whitelist. Storage is JSON files with atomic temp+rename writes under ~/.overkill/pairing/.

Package security — privilege separation (master plan §4.3 / paper #21).

PrivilegeMode toggles whether write-like tool calls are permitted. The reader/planner phase runs in ReaderMode; once the user confirms the plan the agent flips to WriterMode. This keeps an over-eager planner from silently mutating the repo.

Enforcement lives at the tool layer: the agent's pre-tool hook asks PrivilegeGate.Allow(toolName, input) before dispatching.

Package security provides shared security primitives used across internal packages — path containment, credential masking, and prompt-injection detection.

Index

Constants

This section is empty.

Variables

View Source
var ErrWriteDenied = errors.New("privilege: write denied — agent is in reader mode")

ErrWriteDenied is returned when a write-like call is blocked by ReaderMode.

Functions

func IsWriteLikeTool

func IsWriteLikeTool(name string, raw json.RawMessage) bool

Reader mode admits explicitly classified observations. Arbitrary command execution and unknown adapters require writer authority: command text cannot establish absence of redirections, interpreters, hooks or remote side effects. This is tool admission, not an OS sandbox for trusted hooks/plugins.

func SafePath

func SafePath(dir, name string) (string, error)

SafePath joins a trusted directory with a caller-supplied name, enforcing that the resulting path stays within the directory.

It rejects:

  • Absolute paths (e.g. "/etc/passwd").
  • Path traversal via ".." components.
  • Symlink-based escapes (by resolving absolute paths and checking the prefix).

Returns the sanitised full path on success.

Types

type CommandScanner

type CommandScanner struct {
	// contains filtered or unexported fields
}

func NewCommandScanner

func NewCommandScanner(opts ...CommandScannerOption) *CommandScanner

func (*CommandScanner) Name

func (s *CommandScanner) Name() string

func (*CommandScanner) Scan

func (s *CommandScanner) Scan(input string) (*ScanResult, error)

type CommandScannerOption

type CommandScannerOption func(*CommandScanner)

func WithExtraDenyPatterns

func WithExtraDenyPatterns(patterns []string) CommandScannerOption

WithExtraDenyPatterns appends user-supplied regexes to the scanner's deny list. Each entry is compiled at construction; invalid regexes are silently dropped (logging is left to the caller). All matches produce ThreatHigh blocks under the id "user_deny_<n>".

func WithForbiddenPaths

func WithForbiddenPaths(paths []string) CommandScannerOption

WithForbiddenPaths adds a regex that blocks any command containing any of the supplied path substrings. Empty list is a no-op.

func WithMaxCommandLen

func WithMaxCommandLen(n int) CommandScannerOption

WithMaxCommandLen caps the length of an input passed to Scan. Inputs over the cap are blocked with a ThreatHigh finding. 0 disables.

func WithPermissionManager

func WithPermissionManager(pm *PermissionManager) CommandScannerOption

func WithProjectPath

func WithProjectPath(path string) CommandScannerOption

func WithSandbox

func WithSandbox(enabled bool) CommandScannerOption

WithSandbox injects additional deny patterns that restrict what the agent can do on the host. When enabled:

  • No privilege escalation (sudo, su, pkexec)
  • No network exfiltration tools (nc, socat beyond localhost)
  • No writes outside the project directory
  • No systemd/service manipulation

type Finding

type Finding struct {
	Type        string
	Level       ThreatLevel
	Description string
	Match       string
	Confidence  float64
}

func ShellSyntaxFindings

func ShellSyntaxFindings(input string) ([]Finding, error)

ShellSyntaxFindings statically inspects shell syntax without expanding words or executing anything. It supplements the legacy text patterns (including user patterns); it is not a shell sandbox. Callers accepting commands must reject parse errors. Generic text scanners may ignore them because they also scan prose.

type InjectionScanner

type InjectionScanner struct {
	// contains filtered or unexported fields
}

func NewInjectionScanner

func NewInjectionScanner() *InjectionScanner

func (*InjectionScanner) Name

func (s *InjectionScanner) Name() string

func (*InjectionScanner) Scan

func (s *InjectionScanner) Scan(input string) (*ScanResult, error)

type KnownSender

type KnownSender struct {
	ID      string    `json:"id"`
	Channel string    `json:"channel"`
	AddedAt time.Time `json:"addedAt"`
	AddedBy string    `json:"addedBy"` // "cli" | "web"
}

KnownSender is an approved sender on a channel.

type Ledger

type Ledger struct {
	// contains filtered or unexported fields
}

Ledger appends permission decisions to a per-session JSONL file and serves them back to the TUI for the /permissions overlay.

func NewLedger

func NewLedger(path string) (*Ledger, error)

NewLedger constructs a session ledger at ~/.overkill/sessions/<id>/permissions.jsonl (or the path given). The directory is created if missing.

func (*Ledger) Append

func (l *Ledger) Append(e LedgerEntry) error

Append writes a new entry to disk and memory.

func (*Ledger) Entries

func (l *Ledger) Entries() []LedgerEntry

Entries returns a snapshot of all entries (oldest first).

func (*Ledger) Filter

func (l *Ledger) Filter(pred func(LedgerEntry) bool) []LedgerEntry

Filter returns entries matching the predicate.

type LedgerEntry

type LedgerEntry struct {
	Time     time.Time `json:"ts"`
	Tool     string    `json:"tool"`
	Args     string    `json:"args"`
	Decision string    `json:"decision"` // allow_once | allow_session | allow_global | deny
	Risk     string    `json:"risk,omitempty"`
}

LedgerEntry records a single permission decision for audit/recall.

type PairingConfig

type PairingConfig struct {
	Enabled    bool                   `json:"enabled"`
	Channels   map[string]PairingMode `json:"channels"`   // per-channel mode
	TTLMinutes int                    `json:"ttlMinutes"` // override default TTL
	MaxPending int                    `json:"maxPending"` // override max pending
}

PairingConfig is embedded in the main SecurityConfig.

type PairingMode

type PairingMode string

PairingMode controls sender verification behavior per channel.

const (
	// PairingOff — all senders are processed (legacy behavior).
	PairingOff PairingMode = "off"
	// PairingOn — unknown senders get a pairing challenge.
	PairingOn PairingMode = "pairing"
	// PairingOpen — all senders are processed (explicit opt-in to legacy).
	PairingOpen PairingMode = "open"
)

type PairingRequest

type PairingRequest struct {
	ID         string    `json:"id"`        // sender ID (phone number, user ID)
	Code       string    `json:"code"`      // 8-char one-time code
	Channel    string    `json:"channel"`   // "telegram", "discord", etc.
	AccountID  string    `json:"accountId"` // multi-account support
	CreatedAt  time.Time `json:"createdAt"`
	LastSeenAt time.Time `json:"lastSeenAt"`
}

PairingRequest is a pending approval record for one sender.

type PairingStore

type PairingStore struct {
	// contains filtered or unexported fields
}

PairingStore manages pending pairing requests and approved senders. Storage: JSON files under ~/.overkill/pairing/ with atomic temp+rename.

func NewPairingStore

func NewPairingStore(dir string) *PairingStore

NewPairingStore wires the store to a directory. Creates the directory lazily on first write.

func (*PairingStore) ApproveCode

func (ps *PairingStore) ApproveCode(channel, code string) (*KnownSender, error)

ApproveCode looks up a pending request by code (case-insensitive match), removes it from pending, and adds the sender to the allowFrom whitelist.

func (*PairingStore) IsKnown

func (ps *PairingStore) IsKnown(channel, senderID, accountID string) bool

IsKnown reports whether a sender has been approved for a channel.

func (*PairingStore) IssueChallenge

func (ps *PairingStore) IssueChallenge(channel, senderID, accountID string) (*PairingRequest, error)

IssueChallenge creates a new pairing request for an unknown sender. If the sender already has a pending request, bumps LastSeenAt and reuses the code. If max pending is exceeded, the oldest (by LastSeenAt) is pruned.

func (*PairingStore) ListKnown

func (ps *PairingStore) ListKnown(channel string) ([]KnownSender, error)

ListKnown returns all approved senders for a channel.

func (*PairingStore) ListPending

func (ps *PairingStore) ListPending() ([]PairingRequest, error)

ListPending returns all pending pairing requests across all channels.

func (*PairingStore) RemoveSender

func (ps *PairingStore) RemoveSender(channel, senderID, accountID string) error

RemoveSender removes a sender from the allowFrom whitelist.

type PermissionAction

type PermissionAction int
const (
	ActionDeny PermissionAction = iota
	ActionAllowOnce
	ActionAllowProject
	ActionAllowGlobal
)

func (PermissionAction) String

func (a PermissionAction) String() string

type PermissionDecision

type PermissionDecision struct {
	Action      PermissionAction
	Pattern     string
	Command     string
	ProjectPath string
	ExpiresAt   time.Time
}

type PermissionManager

type PermissionManager struct {
	// contains filtered or unexported fields
}

func NewPermissionManager

func NewPermissionManager() *PermissionManager

func (*PermissionManager) Allow

func (pm *PermissionManager) Allow(decision PermissionDecision)

func (*PermissionManager) Check

func (pm *PermissionManager) Check(pattern, command, projectPath string) PermissionDecision

func (*PermissionManager) ClearProject

func (pm *PermissionManager) ClearProject(projectPath string)

func (*PermissionManager) ClearSession

func (pm *PermissionManager) ClearSession()

func (*PermissionManager) IsAllowed

func (pm *PermissionManager) IsAllowed(pattern, command, projectPath string) bool

IsAllowed is a non-consuming predicate. It must NOT consume an AllowOnce grant — that's what Check does. Otherwise a caller asking "are we allowed?" would silently burn the user's one-shot permission.

type PrivilegeGate

type PrivilegeGate struct {
	// contains filtered or unexported fields
}

PrivilegeGate gates tool calls based on the current mode. Thread-safe.

func NewPrivilegeGate

func NewPrivilegeGate(start PrivilegeMode) *PrivilegeGate

NewPrivilegeGate starts in writer mode (preserves legacy behavior). Callers flip to reader for plan-only sessions.

func (*PrivilegeGate) Allow

func (g *PrivilegeGate) Allow(toolName string, rawInput json.RawMessage) (bool, string)

Allow reports whether a tool call is permitted under the current mode. Always true in writer mode. Reader mode permits only classified observations; opaque commands/adapters and malformed or mutating actions require writer mode.

func (*PrivilegeGate) Mode

func (g *PrivilegeGate) Mode() PrivilegeMode

Mode returns the current mode.

func (*PrivilegeGate) SetMode

SetMode switches the mode. Returns the previous mode.

type PrivilegeMode

type PrivilegeMode string

PrivilegeMode is "reader" (read-only) or "writer" (full).

const (
	ModeReader PrivilegeMode = "reader"
	ModeWriter PrivilegeMode = "writer"
)

type ScanResult

type ScanResult struct {
	Findings  []Finding
	MaxLevel  ThreatLevel
	Blocked   bool
	Sanitized string
}

type Scanner

type Scanner interface {
	Scan(input string) (*ScanResult, error)
	Name() string
}

type SecretScanner

type SecretScanner struct {
	// contains filtered or unexported fields
}

func NewSecretScanner

func NewSecretScanner() *SecretScanner

func (*SecretScanner) Name

func (s *SecretScanner) Name() string

func (*SecretScanner) Scan

func (s *SecretScanner) Scan(input string) (*ScanResult, error)

type ThreatLevel

type ThreatLevel int
const (
	ThreatNone ThreatLevel = iota
	ThreatLow
	ThreatMedium
	ThreatHigh
	ThreatCritical
)

func (ThreatLevel) String

func (l ThreatLevel) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL