Skip to content

Commit 4360c74

Browse files
authored
fix slice aliasing safety
- improve Error handling - improve Package documentation - fix slice aliasing safety
1 parent 3bb09fb commit 4360c74

9 files changed

Lines changed: 76 additions & 22 deletions

File tree

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Egressor
22

3+
[![CI](https://github.com/ehsaniara/egressor/actions/workflows/ci.yml/badge.svg)](https://github.com/ehsaniara/egressor/actions/workflows/ci.yml)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/ehsaniara/egressor)](https://goreportcard.com/report/github.com/ehsaniara/egressor)
5+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6+
37
**Local-first egress monitoring and control for developer tools**
48

59
---
@@ -10,6 +14,7 @@ Egressor is a local HTTPS proxy that intercepts outbound traffic from developer
1014

1115
- **TLS interception** — decrypts and inspects HTTPS payloads via a local CA
1216
- **File detection** — identifies file paths and contents in API request bodies
17+
- **Directory scope enforcement** — blocks requests referencing files outside allowed project directories
1318
- **File blocking** — prevents sensitive files (`.env`, `.pem`, secrets) from being sent
1419
- **Desktop UI** — real-time session inspector with request/response viewer
1520
- **Audit logging** — structured JSON logs with automatic rotation
@@ -102,6 +107,11 @@ egressor --version
102107
listen_address: "127.0.0.1:8080"
103108

104109
policy:
110+
# Block requests that reference files outside these directories.
111+
# If empty, no directory scope is enforced.
112+
allowed_directories:
113+
# - "~/Projects/my-app"
114+
105115
deny_file_patterns:
106116
- "*.env"
107117
- "*.pem"
@@ -122,6 +132,15 @@ intercept:
122132
max_body_size: 1048576 # 1MB
123133
```
124134
135+
### Allowed directories
136+
137+
When `allowed_directories` is set, any request containing file references outside those directories is blocked. This prevents LLM tools from accessing files beyond the intended project scope (e.g. `~/.ssh`, `/etc/passwd`, `~/.aws/credentials`).
138+
139+
- Relative paths are resolved against the current working directory
140+
- Path traversals (`../`) are cleaned before evaluation
141+
- Multiple directories can be specified
142+
- Leave empty to allow all directories (default)
143+
125144
### Deny file patterns
126145

127146
Glob patterns that block requests containing matching file references:
@@ -144,7 +163,7 @@ The default mode opens a native desktop window (built with Wails + React):
144163

145164
- **Sessions tab** — live table of intercepted connections with method, host, status, file count
146165
- **Detail panel** — click a session to see full request/response headers, body (JSON-formatted), and detected files
147-
- **Policy tab** — edit deny file patterns, add/remove patterns, save to config
166+
- **Policy tab** — manage allowed directories and deny file patterns, save to config
148167
- **Bottom bar** — proxy start/stop, pause/resume policy, session stats
149168

150169
Blocked requests are highlighted in red with the matching deny pattern shown.
@@ -165,9 +184,10 @@ Client ──TLS(egressor cert)──► Egressor ──TLS(real cert)──►
165184
4. Egressor opens its own TLS connection to the real server
166185
5. Sitting between two decrypted streams, it reads the plaintext HTTP request
167186
6. Extracts file references from the JSON payload
168-
7. Checks file paths against `deny_file_patterns`
169-
8. If blocked: returns `403`, logs the attempt, never forwards to the server
170-
9. If allowed: forwards the request, captures the response, logs everything
187+
7. Checks file paths against `allowed_directories` — blocks if out of scope
188+
8. Checks file paths against `deny_file_patterns` — blocks if matched
189+
9. If blocked: returns `403`, logs the attempt, never forwards to the server
190+
10. If allowed: forwards the request, captures the response, logs everything
171191

172192
### File detection
173193

cmd/egressor/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ func fileExists(path string) bool {
112112
const defaultConfig = `listen_address: "127.0.0.1:8080"
113113
114114
policy:
115+
# Block requests that reference files outside these directories.
116+
# If empty, no directory scope is enforced.
117+
allowed_directories:
118+
# - "~/Projects/my-app"
119+
115120
deny_file_patterns:
116121
- "*.env"
117122
- "*.pem"

docs/design.md

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Egressor is a local HTTPS intercepting proxy that monitors and controls outbound
2626
│ └────┬─────────┘ │
2727
│ │ │
2828
│ ├──▶ Extract file references │
29+
│ ├──▶ Check allowed_directories │
30+
│ │ └─ OUT OF SCOPE → 403 │
2931
│ ├──▶ Check deny_file_patterns │
3032
│ │ ├─ BLOCKED → 403 to client │
3133
│ │ └─ ALLOWED → forward upstream │
@@ -68,7 +70,8 @@ For each connection:
6870
3. HTTP/1.1 relay loop:
6971
- Read full request body into buffer
7072
- Extract file references from the body
71-
- Evaluate file paths against `deny_file_patterns`
73+
- Evaluate file paths against `allowed_directories` — block if out of scope
74+
- Evaluate file paths against `deny_file_patterns` — block if matched
7275
- If blocked: send 403 back to client, log, stop
7376
- If allowed: forward request to upstream, relay response back
7477
4. Record exchange in session
@@ -87,11 +90,20 @@ Returns `[]FileRef{Path, Source}` where Source is `"json_field"` or `"text_patte
8790

8891
### Policy Engine (`internal/policy/policy.go`)
8992

90-
File-pattern-based policy enforcement:
93+
Two-layer policy enforcement:
9194

92-
- `EvaluateFiles(paths []string) Decision` — checks paths against `deny_file_patterns`
95+
**Directory scope**`EvaluateScope(paths []string) Decision`:
96+
- Checks if file paths fall within `allowed_directories`
97+
- Resolves relative paths against cwd, cleans `../` traversals
98+
- If no directories configured, all paths are allowed (default)
99+
- Runtime mutation: `GetAllowedDirectories()`, `SetAllowedDirectories()`
100+
101+
**File pattern deny**`EvaluateFiles(paths []string) Decision`:
102+
- Checks paths against `deny_file_patterns`
93103
- Pattern matching: `filepath.Match` for globs, `**/` prefix for recursive matching, basename fallback
94104
- Runtime mutation: `GetDenyPatterns()`, `SetDenyPatterns()`, `AddDenyPattern()`, `RemoveDenyPattern()`
105+
106+
Both layers:
95107
- Pause/bypass via atomic bool (for UI toggle)
96108
- Thread-safe with `sync.RWMutex`
97109

@@ -138,7 +150,7 @@ File-pattern-based policy enforcement:
138150
**React frontend** (`internal/ui/frontend/`):
139151
- Sessions tab: live table with real-time updates via `EventsOn("session:new")`
140152
- Detail panel: request/response inspector with JSON viewer, detected files, blocked indicator
141-
- Policy tab: editable deny patterns with save-to-config
153+
- Policy tab: allowed directories and deny patterns with save-to-config
142154
- Bottom bar: proxy controls, policy pause/resume, stats
143155

144156
### Configuration (`internal/config/config.go`)
@@ -160,22 +172,24 @@ File-pattern-based policy enforcement:
160172
5. Interceptor: TLS handshake with upstream (real cert)
161173
6. Interceptor: read HTTP request, buffer body
162174
7. Extract: scan body → detected_files: ["src/main.go"]
163-
8. Policy: EvaluateFiles(["src/main.go"]) → allowed
164-
9. Interceptor: forward request to upstream
165-
10. Interceptor: read response, forward to client
166-
11. Logger: write session JSON to audit.log
167-
12. Store: add session, emit "session:new" event → UI
175+
8. Policy: EvaluateScope(["src/main.go"]) → in scope
176+
9. Policy: EvaluateFiles(["src/main.go"]) → allowed
177+
10. Interceptor: forward request to upstream
178+
11. Interceptor: read response, forward to client
179+
12. Logger: write session JSON to audit.log
180+
13. Store: add session, emit "session:new" event → UI
168181
```
169182

170183
### Blocked request
171184

172185
```
173186
1-6. Same as above
174187
7. Extract: scan body → detected_files: [".env"]
175-
8. Policy: EvaluateFiles([".env"]) → denied (matches "*.env")
176-
9. Interceptor: send 403 back to client over TLS
177-
10. Logger: write session with blocked=true, block_reason
178-
11. Store: add session → UI shows red row
188+
8. Policy: EvaluateScope([".env"]) → in scope (or blocked if outside allowed dirs)
189+
9. Policy: EvaluateFiles([".env"]) → denied (matches "*.env")
190+
10. Interceptor: send 403 back to client over TLS
191+
11. Logger: write session with blocked=true, block_reason
192+
12. Store: add session → UI shows red row
179193
```
180194

181195
## Security Considerations
@@ -195,7 +209,7 @@ internal/
195209
proxy.go TCP listener, CONNECT handler, lifecycle
196210
intercept.go TLS MITM, HTTP relay, file extraction, blocking
197211
policy/
198-
policy.go deny_file_patterns matching engine
212+
policy.go Directory scope + deny pattern engine
199213
audit/
200214
session.go Session, InterceptedExchange, FileRef models
201215
logger.go JSON file logger with rotation
@@ -220,7 +234,7 @@ internal/
220234
SessionDetail.tsx Exchange inspector
221235
RequestPane.tsx Request headers + body + files
222236
ResponsePane.tsx Response headers + body
223-
PolicyEditor.tsx Deny pattern CRUD
237+
PolicyEditor.tsx Allowed dirs + deny pattern CRUD
224238
ProxyControls.tsx Start/stop/pause + stats
225239
JsonViewer.tsx Formatted JSON display
226240
hooks/

internal/audit/logger.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7+
"log/slog"
78
"os"
89
"path/filepath"
910
"sync"
@@ -54,6 +55,7 @@ func NewLogger(format, file string, maxSize int64) (*Logger, error) {
5455
func (l *Logger) Log(s *Session) {
5556
data, err := json.Marshal(s)
5657
if err != nil {
58+
slog.Error("failed to marshal session", "err", err)
5759
return
5860
}
5961
data = append(data, '\n')
@@ -74,7 +76,9 @@ func (l *Logger) rotate() {
7476

7577
// Rename current log to audit.log.<unix_epoch>
7678
rotated := fmt.Sprintf("%s.%d", l.file, time.Now().Unix())
77-
os.Rename(l.file, rotated)
79+
if err := os.Rename(l.file, rotated); err != nil {
80+
slog.Error("failed to rotate log file", "err", err)
81+
}
7882

7983
f, err := os.OpenFile(l.file, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
8084
if err != nil {

internal/audit/session.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Package audit provides session logging, storage, and observation
2+
// for intercepted proxy traffic.
13
package audit
24

35
import (

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package config handles YAML configuration loading, defaults, and persistence.
12
package config
23

34
import (

internal/extract/files.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Package extract detects file references in HTTP request bodies
2+
// sent to LLM APIs.
13
package extract
24

35
import (

internal/policy/policy.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Package policy implements directory scope and file pattern enforcement
2+
// for intercepted requests.
13
package policy
24

35
import (
@@ -106,7 +108,8 @@ func (e *Engine) EvaluateScope(paths []string) Decision {
106108
}
107109

108110
e.mu.RLock()
109-
allowedDirs := e.cfg.AllowedDirectories
111+
allowedDirs := make([]string, len(e.cfg.AllowedDirectories))
112+
copy(allowedDirs, e.cfg.AllowedDirectories)
110113
e.mu.RUnlock()
111114

112115
if len(allowedDirs) == 0 {
@@ -160,7 +163,8 @@ func (e *Engine) EvaluateFiles(paths []string) Decision {
160163
}
161164

162165
e.mu.RLock()
163-
patterns := e.cfg.DenyFilePatterns
166+
patterns := make([]string, len(e.cfg.DenyFilePatterns))
167+
copy(patterns, e.cfg.DenyFilePatterns)
164168
e.mu.RUnlock()
165169

166170
if len(patterns) == 0 {

internal/proxy/proxy.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Package proxy implements an HTTPS intercepting proxy with TLS MITM
2+
// for monitoring and controlling outbound traffic.
13
package proxy
24

35
import (

0 commit comments

Comments
 (0)