Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,6 @@ linters:
- path: cmd/
linters:
- noctx
- path: internal/intelligence/memory/
linters:
- errcheck
- path: internal/intelligence/repomap/
linters:
- errcheck
- path: internal/codegraph/
linters:
- errcheck
- path: internal/tool/codegraph.go
linters:
- errcheck
- path: internal/tool/lsp.go
linters:
- errcheck
- path: internal/magicdocs/
linters:
- errcheck

issues:
max-issues-per-linter: 0
Expand Down
9 changes: 8 additions & 1 deletion internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@ func NewTokenStore() *TokenStore {
}

// Load loads tokens from secure storage.
// Deprecated: Use SecureStorage directly to load tokens. This stub always
// returns an empty token map. Migrate callers to SecureStorage.Get/Set.
func (t *TokenStore) Load() error {
// Try to load from keychain/keyring
// Stub: no-op. Existing callers that relied on this get an empty token
// map. New code should use SecureStorage directly.
t.tokens = make(map[string]string)
return nil
}

// Save saves tokens to secure storage.
// Deprecated: Use SecureStorage directly to persist tokens. This stub is a
// no-op. Migrate callers to SecureStorage.Set.
func (t *TokenStore) Save() error {
// Stub: no-op. Tokens held in-memory only; they are lost on process exit.
// Use SecureStorage for persistent, OS-keychain-backed storage.
return nil
}

Expand Down
16 changes: 11 additions & 5 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,12 @@ func New(cfg Config, factory SessionFactory) *Server {
// for poll-based gateways at Start time.
s.gateways = newGatewayManager(cfg.Gateways, "http://"+s.addr, cfg.APIKey, s)
s.server = &http.Server{
Addr: s.addr,
Handler: s.mux,
ReadTimeout: 30 * time.Second,
WriteTimeout: 300 * time.Second,
IdleTimeout: 60 * time.Second,
Addr: s.addr,
Handler: s.mux,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 300 * time.Second,
IdleTimeout: 60 * time.Second,
}
return s
}
Expand All @@ -196,6 +197,11 @@ func (s *Server) Start() (string, error) {
s.addr = actualAddr

go func() {
defer func() {
if r := recover(); r != nil {
slog.Error("daemon server goroutine panicked", "recover", r)
}
}()
if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed {
slog.Error("daemon server error", "error", err)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/daemon/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ func (g *DiscordGateway) handleMessage(ctx context.Context, senderID, channelID,
if err != nil {
resp = fmt.Sprintf("Error: %v", err)
}
if len(resp) > 1900 { // Discord 2000-char limit, leave headroom
resp = resp[:1900] + "\n\n... (truncated)"
if len([]rune(resp)) > 1900 { // Discord 2000-char limit, leave headroom
resp = string([]rune(resp)[:1900]) + "\n\n... (truncated)"
}
reply(resp)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/daemon/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func (tg *TelegramGateway) sendMessage(ctx context.Context, chatID int64, text s
data := url.Values{
"chat_id": {fmt.Sprintf("%d", chatID)},
"text": {text},
"parse_mode": {"Markdown"},
"parse_mode": {""},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(data.Encode()))
if err != nil {
Expand Down
74 changes: 49 additions & 25 deletions internal/permissions/egress.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import (

// Pre-compiled patterns for exfiltration detection.
var (
curlPostWithData = regexp.MustCompile(`(?i)curl\s.*-[A-Za-z]*X\s*POST.*-d\s+@`)
curlDataWithPost = regexp.MustCompile(`(?i)curl\s.*-d\s+@.*-[A-Za-z]*X\s*POST`)
curlDataBinary = regexp.MustCompile(`(?i)curl\s.*--data-binary\s+@`)
pipeToNetwork = regexp.MustCompile(`\|\s*(curl|wget|nc|netcat|ncat)\b`)
base64PipeNetwork = regexp.MustCompile(`base64.*\|\s*(curl|wget|nc|netcat)`)
base64Network = regexp.MustCompile(`\bbase64\b.*\b(curl|wget|nc|netcat)\b`)
envVarInURL = regexp.MustCompile(`(curl|wget)\s.*\$[A-Za-z_]`)
curlFileUpload = regexp.MustCompile(`(?i)curl\s.*-F\s+["']?[^=]+=@`)
cachedRegex = &sync.Map{} // string(pattern) -> *regexp.Regexp

// Pre-compiled patterns for exfiltration detection.
curlPostWithData = regexp.MustCompile(`(?i)curl\s.*-[A-Za-z]*X\s*POST.*-d\s+@`)
curlDataWithPost = regexp.MustCompile(`(?i)curl\s.*-d\s+@.*-[A-Za-z]*X\s*POST`)
curlDataBinary = regexp.MustCompile(`(?i)curl\s.*--data-binary\s+@`)
pipeToNetwork = regexp.MustCompile(`\|\s*(curl|wget|nc|netcat|ncat)\b`)
base64PipeNet = regexp.MustCompile(`base64.*\|\s*(curl|wget|nc|netcat)`)
base64Net = regexp.MustCompile(`\bbase64\b.*\b(curl|wget|nc|netcat)\b`)
envVarInURL = regexp.MustCompile(`(curl|wget)\s.*\$[A-Za-z_]`)
curlFileUpload = regexp.MustCompile(`(?i)curl\s.*-F\s+["']?[^=]+=@`)
)

// EgressInspector detects and blocks data exfiltration attempts in shell commands
Expand All @@ -30,6 +33,7 @@ type EgressInspector struct {
BlockedDomains []string
AllowedProtocols []string
mu sync.RWMutex
compiledPatterns sync.Map // string pattern -> *regexp.Regexp
}

// EgressAttempt represents the result of inspecting a command for egress activity.
Expand Down Expand Up @@ -75,6 +79,26 @@ func NewEgressInspector() *EgressInspector {
}
}

// compilePattern pre-compiles a wildcard glob pattern to a compiled regexp and
// caches it in the inspector's compiledPatterns map for reuse.
func (e *EgressInspector) compilePattern(pattern string) *regexp.Regexp {
if cached, ok := e.compiledPatterns.Load(pattern); ok {
return cached.(*regexp.Regexp)
}

regexStr := "^" + regexp.QuoteMeta(pattern) + "$"
regexStr = strings.ReplaceAll(regexStr, `\*`, `[A-Za-z0-9._-]*`)
re, err := regexp.Compile(regexStr)
if err != nil {
return nil
}

if val, loaded := e.compiledPatterns.LoadOrStore(pattern, re); loaded {
return val.(*regexp.Regexp)
}
return re
}

// Inspect analyzes a command for network egress destinations and returns
// an EgressAttempt indicating whether the command is allowed.
func (e *EgressInspector) Inspect(command string) *EgressAttempt {
Expand Down Expand Up @@ -236,8 +260,8 @@ func (e *EgressInspector) IsSuspicious(command string) bool {
}

// Base64 encoding combined with network send
if base64PipeNetwork.MatchString(command) ||
base64Network.MatchString(command) {
if base64PipeNet.MatchString(command) ||
base64Net.MatchString(command) {
return true
}

Expand Down Expand Up @@ -381,16 +405,16 @@ func matchDomain(pattern, host string) bool {
return true
}

// Wildcard matching
if strings.Contains(pattern, "*") {
// Convert glob pattern to regex
// Wildcard matching
if strings.Contains(pattern, "*") {
// Convert glob pattern to regex with caching
regexStr := "^" + regexp.QuoteMeta(pattern) + "$"
regexStr = strings.ReplaceAll(regexStr, `\*`, `[A-Za-z0-9._-]*`)
re, err := regexp.Compile(regexStr)
if err != nil {
return false
re, loaded := cachedRegex.LoadOrStore(regexStr, regexp.MustCompile(regexStr))
if loaded {
return re.(*regexp.Regexp).MatchString(host)
}
return re.MatchString(host)
return re.(*regexp.Regexp).MatchString(host)
}

// Subdomain match: pattern "example.com" matches "sub.example.com"
Expand All @@ -405,8 +429,8 @@ func matchDomain(pattern, host string) bool {
func (e *EgressInspector) describeSuspicious(command string) []string {
var patterns []string

if regexp.MustCompile(`(?i)curl\s.*-[A-Za-z]*X\s*POST.*-d\s+@`).MatchString(command) ||
regexp.MustCompile(`(?i)curl\s.*-d\s+@.*-[A-Za-z]*X\s*POST`).MatchString(command) {
if curlPostWithData.MatchString(command) ||
curlDataWithPost.MatchString(command) {
// Try to extract the file name
fileRe := regexp.MustCompile(`-d\s+@([^\s"']+)`)
if m := fileRe.FindStringSubmatch(command); len(m) >= 2 {
Expand All @@ -416,24 +440,24 @@ func (e *EgressInspector) describeSuspicious(command string) []string {
}
}

if regexp.MustCompile(`(?i)curl\s.*--data-binary\s+@`).MatchString(command) {
if curlDataBinary.MatchString(command) {
patterns = append(patterns, "Binary file upload")
}

if regexp.MustCompile(`\|\s*(curl|wget|nc|netcat|ncat)\b`).MatchString(command) {
if pipeToNetwork.MatchString(command) {
patterns = append(patterns, "Pipe to network command")
}

if regexp.MustCompile(`base64.*\|\s*(curl|wget|nc|netcat)`).MatchString(command) ||
regexp.MustCompile(`\bbase64\b.*\b(curl|wget|nc|netcat)\b`).MatchString(command) {
if base64PipeNet.MatchString(command) ||
base64Net.MatchString(command) {
patterns = append(patterns, "Base64 encoding with network send")
}

if regexp.MustCompile(`(curl|wget)\s.*\$[A-Za-z_]`).MatchString(command) {
if envVarInURL.MatchString(command) {
patterns = append(patterns, "Environment variable in URL")
}

if regexp.MustCompile(`(?i)curl\s.*-F\s+["']?[^=]+=@`).MatchString(command) {
if curlFileUpload.MatchString(command) {
patterns = append(patterns, "File upload via form data")
}

Expand Down
Loading