From ac20e525f073fd9034ce5a7897ddf40e90608a5a Mon Sep 17 00:00:00 2001 From: RinZ27 <222222878+RinZ27@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:09:53 +0700 Subject: [PATCH 1/2] fix(security): address path traversal, buffer overflow and logic vulnerabilities --- cmd/eipc-cli/main.go | 12 +++++++----- cmd/eipc-client/main.go | 14 -------------- config/config.go | 2 ++ core/endpoint.go | 14 ++------------ core/lifecycle.go | 5 ++++- protocol/frame.go | 8 ++++++++ sdk/c/src/eipc_client.c | 13 +++++++++++++ security/auth/identity.go | 7 ++----- security/keyring/keyring.go | 4 +--- services/audit/audit.go | 2 +- transport/shm/shm.go | 16 +++++++++------- transport/tcp/tls_config.go | 5 +++++ 12 files changed, 54 insertions(+), 48 deletions(-) diff --git a/cmd/eipc-cli/main.go b/cmd/eipc-cli/main.go index 7c8176b..36952f2 100644 --- a/cmd/eipc-cli/main.go +++ b/cmd/eipc-cli/main.go @@ -11,6 +11,7 @@ package main import ( + "bytes" "encoding/json" "flag" "fmt" @@ -194,12 +195,13 @@ func cmdPing(args []string) { } func printMessage(label string, msg core.Message) { - var payload interface{} - if err := json.Unmarshal(msg.Payload, &payload); err != nil { - payload = string(msg.Payload) + var indented bytes.Buffer + if err := json.Indent(&indented, msg.Payload, " ", " "); err != nil { + fmt.Printf("[%s] type=%s source=%s req=%s priority=P%d cap=%s\n payload: %s\n", + label, msg.Type, msg.Source, msg.RequestID, msg.Priority, msg.Capability, string(msg.Payload)) + return } - payloadJSON, _ := json.MarshalIndent(payload, " ", " ") fmt.Printf("[%s] type=%s source=%s req=%s priority=P%d cap=%s\n payload: %s\n", - label, msg.Type, msg.Source, msg.RequestID, msg.Priority, msg.Capability, payloadJSON) + label, msg.Type, msg.Source, msg.RequestID, msg.Priority, msg.Capability, indented.String()) } diff --git a/cmd/eipc-client/main.go b/cmd/eipc-client/main.go index fd8bb7d..ef687c7 100644 --- a/cmd/eipc-client/main.go +++ b/cmd/eipc-client/main.go @@ -45,7 +45,6 @@ func main() { log.Fatalf("dial: %v", err) } defer conn.Close() - defer conn.Close() endpoint := core.NewClientEndpoint(conn, codec, sharedSecret, "") @@ -92,11 +91,6 @@ func main() { } else { log.Printf("[2] Received challenge nonce: %s", challenge.Nonce) } - log.Printf("[2] Received challenge nonce: %s...%s", - challenge.Nonce[:8], challenge.Nonce[len(challenge.Nonce)-8:]) - } else { - log.Printf("[2] Received challenge nonce: %s", challenge.Nonce) - } // Step 3: Compute HMAC-SHA256(secret, nonce) and send response nonceBytes, err := hex.DecodeString(challenge.Nonce) @@ -145,14 +139,6 @@ func main() { log.Fatalf("unmarshal auth response: %v", err) } - if len(sessionToken) >= 16 { - log.Printf("[3] Authenticated! token=%s...%s caps=%v", - sessionToken[:8], sessionToken[len(sessionToken)-8:], authRes.Capabilities) - } else { - log.Printf("[3] Authenticated! token=%s caps=%v", sessionToken, authRes.Capabilities) - } - } - sessionToken := authRes.SessionToken if len(sessionToken) >= 16 { log.Printf("[3] Authenticated! token=%s...%s caps=%v", diff --git a/config/config.go b/config/config.go index d52ae81..dc6f41e 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ package config import ( "fmt" "os" + "path/filepath" "strconv" "time" ) @@ -18,6 +19,7 @@ func LoadHMACKey() ([]byte, error) { } if path := os.Getenv("EIPC_KEY_FILE"); path != "" { + path = filepath.Clean(path) data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read key file %q: %w", path, err) diff --git a/core/endpoint.go b/core/endpoint.go index e984c62..22c5fcf 100644 --- a/core/endpoint.go +++ b/core/endpoint.go @@ -111,7 +111,7 @@ func (e *ClientEndpoint) Close() error { // ServerEndpoint handles a single server-side connection. type ServerEndpoint struct { conn transport.Connection - codec protocol.Codec + codec protocol.Codec hmacKey []byte replay *replay.Tracker sendMu sync.Mutex @@ -126,17 +126,10 @@ func (e *ServerEndpoint) SetPeerCapabilities(caps []string) { cpy := make([]string, len(caps)) copy(cpy, caps) e.peerCapabilities = cpy - defer e.sendMu.Unlock() - cpy := make([]string, len(caps)) - copy(cpy, caps) - e.peerCapabilities = cpy } // ValidateCapability checks if msgCap is in the peer's granted capabilities. - e.sendMu.Lock() - caps := e.peerCapabilities - e.sendMu.Unlock() - for _, c := range caps { +func (e *ServerEndpoint) ValidateCapability(msgCap string) error { if msgCap == "" { return nil } @@ -155,9 +148,6 @@ func (e *ServerEndpoint) SetPeerCapabilities(caps []string) { func NewServerEndpoint(conn transport.Connection, codec protocol.Codec, hmacKey []byte) *ServerEndpoint { return &ServerEndpoint{ conn: conn, - e.sendMu.Lock() - defer e.sendMu.Unlock() - codec: codec, hmacKey: hmacKey, replay: replay.NewTracker(0), diff --git a/core/lifecycle.go b/core/lifecycle.go index 6d259ae..68a7213 100644 --- a/core/lifecycle.go +++ b/core/lifecycle.go @@ -86,7 +86,9 @@ func (h *HeartbeatSender) Start() { Priority: PriorityP3, Payload: []byte(fmt.Sprintf(`{"service":"%s","status":"alive"}`, h.config.ServiceID)), } - h.endpoint.Send(msg) + if err := h.endpoint.Send(msg); err != nil { + fmt.Printf("heartbeat send failed: %v\n", err) + } } } }() @@ -114,5 +116,6 @@ func GracefulShutdown(endpoint Endpoint, timeout time.Duration) error { case err := <-done: return err case <-time.After(timeout): + return fmt.Errorf("shutdown timed out after %v", timeout) } } diff --git a/protocol/frame.go b/protocol/frame.go index 8f64cb8..70f79cb 100644 --- a/protocol/frame.go +++ b/protocol/frame.go @@ -50,6 +50,10 @@ const FrameFixedSize = 4 + 2 + 1 + 1 + 4 + 4 // 16 bytes // Encode writes the frame to w in the EIPC wire format. func (f *Frame) Encode(w io.Writer) error { + if uint64(len(f.Header))+uint64(len(f.Payload)) > uint64(MaxFrameSize) { + return ErrFrameTooLarge + } + buf := make([]byte, FrameFixedSize) binary.BigEndian.PutUint32(buf[0:4], MagicBytes) binary.BigEndian.PutUint16(buf[4:6], f.Version) @@ -102,6 +106,7 @@ func Decode(r io.Reader) (*Frame, error) { payloadLen := binary.BigEndian.Uint32(preamble[12:16]) if uint64(headerLen)+uint64(payloadLen) > uint64(MaxFrameSize) { + return nil, ErrFrameTooLarge } if headerLen > 0 { @@ -131,6 +136,9 @@ func Decode(r io.Reader) (*Frame, error) { // SignableBytes returns the portion of the frame that is covered by the MAC // (everything except the MAC itself). func (f *Frame) SignableBytes() []byte { + if uint64(len(f.Header))+uint64(len(f.Payload)) > uint64(MaxFrameSize) { + return nil + } size := FrameFixedSize + len(f.Header) + len(f.Payload) buf := make([]byte, 0, size) diff --git a/sdk/c/src/eipc_client.c b/sdk/c/src/eipc_client.c index 9cbc38a..2aef119 100644 --- a/sdk/c/src/eipc_client.c +++ b/sdk/c/src/eipc_client.c @@ -10,6 +10,7 @@ #include #include +#include #include /* --------------- utility implementations --------------- */ @@ -33,6 +34,7 @@ static void fill_header(eipc_header_t *h, uint64_t sequence) { memset(h, 0, sizeof(*h)); strncpy(h->service_id, service_id, sizeof(h->service_id) - 1); + h->service_id[sizeof(h->service_id) - 1] = '\0'; eipc_generate_request_id(h->request_id, sizeof(h->request_id)); h->sequence = sequence; eipc_timestamp_now(h->timestamp, sizeof(h->timestamp)); @@ -91,6 +93,7 @@ eipc_status_t eipc_client_init(eipc_client_t *c, const char *service_id) { memset(c, 0, sizeof(*c)); c->sock = EIPC_INVALID_SOCKET; strncpy(c->service_id, service_id, sizeof(c->service_id) - 1); + c->service_id[sizeof(c->service_id) - 1] = '\0'; return EIPC_OK; } @@ -133,6 +136,7 @@ eipc_status_t eipc_client_send_intent(eipc_client_t *c, memset(&ev, 0, sizeof(ev)); strncpy(ev.intent, intent, sizeof(ev.intent) - 1); + ev.intent[sizeof(ev.intent) - 1] = '\0'; ev.confidence = confidence; size_t payload_written = 0; @@ -160,6 +164,7 @@ eipc_status_t eipc_client_send_tool_request(eipc_client_t *c, memset(&req, 0, sizeof(req)); strncpy(req.tool, tool, sizeof(req.tool) - 1); + req.tool[sizeof(req.tool) - 1] = '\0'; if (args && arg_count > 0) { int n = arg_count; if (n > EIPC_MAX_ARGS) n = EIPC_MAX_ARGS; @@ -189,7 +194,9 @@ eipc_status_t eipc_client_send_heartbeat(eipc_client_t *c) { memset(&hb, 0, sizeof(hb)); strncpy(hb.service, c->service_id, sizeof(hb.service) - 1); + hb.service[sizeof(hb.service) - 1] = '\0'; strncpy(hb.status, "alive", sizeof(hb.status) - 1); + hb.status[sizeof(hb.status) - 1] = '\0'; size_t payload_written = 0; rc = eipc_heartbeat_to_json(&hb, payload_json, sizeof(payload_json), &payload_written); @@ -210,6 +217,7 @@ eipc_status_t eipc_client_send_chat(eipc_client_t *c, c->sequence++; fill_header(&hdr, c->service_id, c->sequence); strncpy(hdr.capability, "ai:chat", sizeof(hdr.capability) - 1); + hdr.capability[sizeof(hdr.capability) - 1] = '\0'; size_t chat_written = 0; rc = eipc_chat_request_to_json(req, payload_json, sizeof(payload_json), &chat_written); @@ -231,6 +239,7 @@ eipc_status_t eipc_client_send_complete(eipc_client_t *c, c->sequence++; fill_header(&hdr, c->service_id, c->sequence); strncpy(hdr.capability, "ai:chat", sizeof(hdr.capability) - 1); + hdr.capability[sizeof(hdr.capability) - 1] = '\0'; n = snprintf(payload_json, sizeof(payload_json), "{\"session_id\":\"%s\",\"prompt\":\"%s\",\"model\":\"\",\"max_tokens\":0}", @@ -272,10 +281,14 @@ eipc_status_t eipc_client_receive(eipc_client_t *c, eipc_message_t *msg) { if (rc != EIPC_OK) return rc; strncpy(msg->source, hdr.service_id, sizeof(msg->source) - 1); + msg->source[sizeof(msg->source) - 1] = '\0'; strncpy(msg->session_id, hdr.session_id, sizeof(msg->session_id) - 1); + msg->session_id[sizeof(msg->session_id) - 1] = '\0'; strncpy(msg->request_id, hdr.request_id, sizeof(msg->request_id) - 1); + msg->request_id[sizeof(msg->request_id) - 1] = '\0'; msg->priority = hdr.priority; strncpy(msg->capability, hdr.capability, sizeof(msg->capability) - 1); + msg->capability[sizeof(msg->capability) - 1] = '\0'; if (frame.payload_len > 0) { if (frame.payload_len > sizeof(msg->payload)) diff --git a/security/auth/identity.go b/security/auth/identity.go index 7a9ef97..a9da571 100644 --- a/security/auth/identity.go +++ b/security/auth/identity.go @@ -122,7 +122,7 @@ func (a *Authenticator) VerifyResponse(serviceID string, response []byte) (*Peer capsSrc := a.knownServices[serviceID] caps := make([]string, len(capsSrc)) copy(caps, capsSrc) - copy(caps, a.knownServices[serviceID]) + token, err := generateToken() if err != nil { return nil, fmt.Errorf("generate token: %w", err) @@ -144,14 +144,11 @@ func (a *Authenticator) VerifyResponse(serviceID string, response []byte) (*Peer func (a *Authenticator) Authenticate(serviceID string) (*PeerIdentity, error) { a.mu.Lock() defer a.mu.Unlock() - capsSrc, ok := a.knownServices[serviceID] + capsSrc, ok := a.knownServices[serviceID] if !ok { return nil, fmt.Errorf("%w: unknown service %q", core.ErrAuth, serviceID) } - caps := make([]string, len(capsSrc)) - copy(caps, capsSrc) - caps := make([]string, len(capsSrc)) copy(caps, capsSrc) diff --git a/security/keyring/keyring.go b/security/keyring/keyring.go index 8b75f5c..871ce41 100644 --- a/security/keyring/keyring.go +++ b/security/keyring/keyring.go @@ -144,9 +144,7 @@ func (kr *Keyring) ListActive() []KeyEntry { } func (kr *Keyring) Cleanup() int { - if _, err := rand.Read(b); err != nil { - panic("crypto/rand: " + err.Error()) - } + kr.mu.Lock() defer kr.mu.Unlock() now := time.Now() removed := 0 diff --git a/services/audit/audit.go b/services/audit/audit.go index 8b6d815..468e538 100644 --- a/services/audit/audit.go +++ b/services/audit/audit.go @@ -42,7 +42,7 @@ func NewFileLogger(path string) (*FileLogger, error) { if path == "" { w = os.Stdout } else { - f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if err != nil { return nil, fmt.Errorf("open audit log: %w", err) } diff --git a/transport/shm/shm.go b/transport/shm/shm.go index 04b70d1..e3022e0 100644 --- a/transport/shm/shm.go +++ b/transport/shm/shm.go @@ -73,8 +73,7 @@ func NewRingBuffer(cfg Config) *RingBuffer { // Write places a frame into the next available slot. // Returns ErrBackpressure if the buffer is full. func (rb *RingBuffer) Write(frame *protocol.Frame) error { - data := make([]byte, 0, protocol.FrameFixedSize+len(frame.Header)+len(frame.Payload)) - data = append(data, frame.SignableBytes()...) + data := frame.SignableBytes() if len(data) > rb.slotSize-2 { return fmt.Errorf("frame too large for slot (%d > %d)", len(data), rb.slotSize-2) } @@ -82,9 +81,6 @@ func (rb *RingBuffer) Write(frame *protocol.Frame) error { rb.mu.Lock() defer rb.mu.Unlock() - rb.mu.Lock() - defer rb.mu.Unlock() - head := rb.head.Load() tail := rb.tail.Load() @@ -92,9 +88,11 @@ func (rb *RingBuffer) Write(frame *protocol.Frame) error { return fmt.Errorf("ring buffer full (backpressure)") } + idx := int(head % uint64(rb.slots)) offset := idx * rb.slotSize rb.buf[offset] = byte(len(data) >> 8) + rb.buf[offset+1] = byte(len(data) & 0xFF) copy(rb.buf[offset+2:], data) rb.head.Add(1) @@ -104,6 +102,9 @@ func (rb *RingBuffer) Write(frame *protocol.Frame) error { // Read retrieves the next frame from the buffer. // Returns nil if the buffer is empty. func (rb *RingBuffer) Read() (*protocol.Frame, error) { + rb.mu.Lock() + defer rb.mu.Unlock() + head := rb.head.Load() tail := rb.tail.Load() @@ -114,11 +115,12 @@ func (rb *RingBuffer) Read() (*protocol.Frame, error) { idx := int(tail % uint64(rb.slots)) offset := idx * rb.slotSize - rb.mu.Lock() length := int(rb.buf[offset])<<8 | int(rb.buf[offset+1]) + if length > rb.slotSize-2 { + return nil, fmt.Errorf("invalid slot length (%d)", length) + } data := make([]byte, length) copy(data, rb.buf[offset+2:offset+2+length]) - rb.mu.Unlock() rb.tail.Add(1) diff --git a/transport/tcp/tls_config.go b/transport/tcp/tls_config.go index cf74bdf..dce3e21 100644 --- a/transport/tcp/tls_config.go +++ b/transport/tcp/tls_config.go @@ -14,6 +14,7 @@ import ( "fmt" "math/big" "os" + "path/filepath" "time" ) @@ -63,6 +64,9 @@ func GenerateSelfSignedCert() (tls.Certificate, error) { // LoadTLSConfig builds a *tls.Config from cert/key/CA file paths. // If caFile is provided, mTLS (RequireAndVerifyClientCert) is enabled. func LoadTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error) { + certFile = filepath.Clean(certFile) + keyFile = filepath.Clean(keyFile) + cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("load key pair: %w", err) @@ -74,6 +78,7 @@ func LoadTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error) { } if caFile != "" { + caFile = filepath.Clean(caFile) caPEM, err := os.ReadFile(caFile) if err != nil { return nil, fmt.Errorf("read CA file: %w", err) From 5b56a9300ce320d5331f75a1727f224f5fb26ff4 Mon Sep 17 00:00:00 2001 From: RinZ27 <222222878+RinZ27@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:36:09 +0700 Subject: [PATCH 2/2] chore(ci): fix linting and cross-platform path issues in tests --- cmd/eipc-client/main.go | 36 +++-- cmd/eipc-server/main.go | 239 +++++++++++++++++++++---------- config/config_test.go | 3 +- core/endpoint.go | 24 +++- core/types.go | 3 + security/encryption/aes_test.go | 61 ++++++-- security/keyring/keyring_test.go | 61 ++++++-- services/audit/audit.go | 2 + services/audit/audit_test.go | 25 +++- 9 files changed, 325 insertions(+), 129 deletions(-) diff --git a/cmd/eipc-client/main.go b/cmd/eipc-client/main.go index ef687c7..84a6066 100644 --- a/cmd/eipc-client/main.go +++ b/cmd/eipc-client/main.go @@ -53,17 +53,19 @@ func main() { type authRequest struct { ServiceID string `json:"service_id"` } - authPayload, _ := codec.Marshal(authRequest{ServiceID: serviceID}) + authPayload, err := codec.Marshal(authRequest{ServiceID: serviceID}) + if err != nil { + log.Fatalf("marshal auth request: %v", err) + } if err := endpoint.Send(core.Message{ Version: core.ProtocolVersion, - Type: core.TypeAck, + Type: core.TypeAuth, Source: serviceID, Timestamp: time.Now().UTC(), - RequestID: "auth-1", Payload: authPayload, }); err != nil { - log.Fatalf("send auth: %v", err) + log.Fatalf("send auth request: %v", err) } // Step 2: Receive challenge (nonce) @@ -71,6 +73,9 @@ func main() { if err != nil { log.Fatalf("receive challenge: %v", err) } + if challengeMsg.Type != core.TypeChallenge { + log.Fatalf("[AUTH] expected TypeChallenge, got %s", challengeMsg.Type) + } type challengeResponse struct { Status string `json:"status"` @@ -106,20 +111,22 @@ func main() { ServiceID string `json:"service_id"` Response string `json:"response"` } - chalRespPayload, _ := codec.Marshal(authChallengeResponse{ + chalRespPayload, err := codec.Marshal(authChallengeResponse{ ServiceID: serviceID, Response: hex.EncodeToString(response), }) + if err != nil { + log.Fatalf("marshal challenge response: %v", err) + } if err := endpoint.Send(core.Message{ Version: core.ProtocolVersion, - Type: core.TypeAck, + Type: core.TypeAuthResponse, Source: serviceID, Timestamp: time.Now().UTC(), - RequestID: "auth-2", Payload: chalRespPayload, }); err != nil { - log.Fatalf("send challenge response: %v", err) + log.Fatalf("send auth response: %v", err) } // Step 4: Receive session token @@ -127,6 +134,9 @@ func main() { if err != nil { log.Fatalf("receive auth response: %v", err) } + if authResp.Type != core.TypeAuthResponse { + log.Fatalf("[AUTH] expected TypeAuthResponse, got %s", authResp.Type) + } type authResult struct { Status string `json:"status"` @@ -149,11 +159,14 @@ func main() { // Step 5: Send HMAC-protected intent log.Println("[4] Sending intent: move_left (confidence=0.91)") - intentPayload, _ := codec.Marshal(core.IntentEvent{ + intentPayload, err := codec.Marshal(core.IntentEvent{ Intent: "move_left", Confidence: 0.91, SessionID: sessionToken, }) + if err != nil { + log.Fatalf("marshal intent: %v", err) + } if err := endpoint.Send(core.Message{ Version: core.ProtocolVersion, @@ -184,10 +197,13 @@ func main() { // Step 7: Send heartbeat log.Println("[6] Sending heartbeat...") - hbPayload, _ := codec.Marshal(core.HeartbeatEvent{ + hbPayload, err := codec.Marshal(core.HeartbeatEvent{ Service: serviceID, Status: "ready", }) + if err != nil { + log.Fatalf("marshal heartbeat: %v", err) + } if err := endpoint.Send(core.Message{ Version: core.ProtocolVersion, diff --git a/cmd/eipc-server/main.go b/cmd/eipc-server/main.go index 7afda34..2d2d447 100644 --- a/cmd/eipc-server/main.go +++ b/cmd/eipc-server/main.go @@ -66,20 +66,27 @@ func main() { healthSvc := health.NewService(5*time.Second, 15*time.Second) reg := registry.NewRegistry() - _ = reg.Register(registry.ServiceInfo{ + if err := reg.Register(registry.ServiceInfo{ ServiceID: "eipc-server", Capabilities: []string{"ui:control", "device:read", "device:write", "ai:chat"}, Versions: []uint16{1}, - MessageTypes: []core.MessageType{core.TypeIntent, core.TypeAck, core.TypeHeartbeat, core.TypeAudit, core.TypeChat, core.TypeComplete}, - Priority: core.PriorityP0, - }) - _ = reg.Register(registry.ServiceInfo{ + MessageTypes: []core.MessageType{ + core.TypeIntent, core.TypeAck, core.TypeHeartbeat, core.TypeAudit, + core.TypeChat, core.TypeComplete, core.TypeAuth, core.TypeChallenge, core.TypeAuthResponse, + }, + Priority: core.PriorityP0, + }); err != nil { + log.Printf("[REGISTRY] failed to register eipc-server: %v", err) + } + if err := reg.Register(registry.ServiceInfo{ ServiceID: "ebot.client", Capabilities: []string{"ai:chat"}, Versions: []uint16{1}, - MessageTypes: []core.MessageType{core.TypeChat, core.TypeComplete, core.TypeAck}, + MessageTypes: []core.MessageType{core.TypeChat, core.TypeComplete, core.TypeAck, core.TypeAuth, core.TypeAuthResponse}, Priority: core.PriorityP1, - }) + }); err != nil { + log.Printf("[REGISTRY] failed to register ebot.client: %v", err) + } router := core.NewRouter() @@ -95,32 +102,39 @@ func main() { if err := capChecker.Check([]string{msg.Capability}, "ui.cursor.move"); err != nil { log.Printf("[POLICY] DENIED: %v", err) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: msg.RequestID, Source: msg.Source, Target: "eipc-server", Action: intent.Intent, Decision: "denied", Result: err.Error(), - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } return nil, err } log.Printf("[POLICY] ALLOWED: capability=%s action=%s", msg.Capability, intent.Intent) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: msg.RequestID, Source: msg.Source, Target: "eipc-server", Action: intent.Intent, Decision: "allowed", Result: "success", - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } - ackPayload, _ := codec.Marshal(core.AckEvent{ + ackPayload, err := codec.Marshal(core.AckEvent{ RequestID: msg.RequestID, Status: "ok", }) + if err != nil { + return nil, fmt.Errorf("marshal ack: %w", err) + } ack := core.Message{ Version: core.ProtocolVersion, @@ -155,28 +169,32 @@ func main() { if err := capChecker.Check([]string{msg.Capability}, "ai.chat.send"); err != nil { log.Printf("[POLICY] DENIED chat: %v", err) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: msg.RequestID, Source: msg.Source, Target: "eipc-server", Action: "ai.chat.send", Decision: "denied", Result: err.Error(), - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } return nil, err } log.Printf("[CHAT] from=%s session=%s prompt=%q", msg.Source, chatReq.SessionID, chatReq.UserPrompt) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: msg.RequestID, Source: msg.Source, Target: "eai", Action: "ai.chat.send", Decision: "allowed", Result: "forwarded", - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } // TODO: Forward to EAI agent loop. For now, echo acknowledgment. chatResp := core.ChatResponseEvent{ @@ -185,7 +203,10 @@ func main() { Model: chatReq.Model, TokensUsed: 0, } - respPayload, _ := codec.Marshal(chatResp) + respPayload, err := codec.Marshal(chatResp) + if err != nil { + return nil, fmt.Errorf("marshal chat response: %w", err) + } return &core.Message{ Version: core.ProtocolVersion, @@ -208,28 +229,32 @@ func main() { if err := capChecker.Check([]string{msg.Capability}, "ai.complete.send"); err != nil { log.Printf("[POLICY] DENIED complete: %v", err) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: msg.RequestID, Source: msg.Source, Target: "eipc-server", Action: "ai.complete.send", Decision: "denied", Result: err.Error(), - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } return nil, err } log.Printf("[COMPLETE] from=%s session=%s prompt=%q", msg.Source, completeReq.SessionID, completeReq.Prompt) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: msg.RequestID, Source: msg.Source, Target: "eai", Action: "ai.complete.send", Decision: "allowed", Result: "forwarded", - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } completeResp := core.CompleteResponseEvent{ SessionID: completeReq.SessionID, @@ -237,7 +262,10 @@ func main() { Model: completeReq.Model, TokensUsed: 0, } - respPayload, _ := codec.Marshal(completeResp) + respPayload, err := codec.Marshal(completeResp) + if err != nil { + return nil, fmt.Errorf("marshal complete response: %w", err) + } return &core.Message{ Version: core.ProtocolVersion, @@ -308,13 +336,15 @@ func main() { }() default: log.Printf("[CONN] rejected connection from %s: max connections (%d) reached", conn.RemoteAddr(), maxConns) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ Source: conn.RemoteAddr(), Target: "eipc-server", Action: "connect", Decision: "denied", Result: "connection limit exceeded", - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } conn.Close() } } @@ -352,6 +382,11 @@ func handleConnection( close(authDone) return } + if authMsg.Type != core.TypeAuth { + log.Printf("[AUTH] expected TypeAuth, got %s", authMsg.Type) + close(authDone) + return + } type authRequest struct { ServiceID string `json:"service_id"` @@ -367,27 +402,35 @@ func handleConnection( challenge, err := authenticator.CreateChallenge(authReq.ServiceID) if err != nil { log.Printf("[AUTH] REJECTED: %v", err) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: authMsg.RequestID, Source: authReq.ServiceID, Target: "eipc-server", Action: "authenticate", Decision: "denied", Result: err.Error(), - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } type authResponse struct { Status string `json:"status"` Error string `json:"error,omitempty"` } - respPayload, _ := codec.Marshal(authResponse{Status: "denied", Error: err.Error()}) - _ = endpoint.Send(core.Message{ - Version: core.ProtocolVersion, - Type: core.TypeAck, - Source: "eipc-server", - Timestamp: time.Now().UTC(), - RequestID: authMsg.RequestID, - Payload: respPayload, - }) + respPayload, err := codec.Marshal(authResponse{Status: "denied", Error: err.Error()}) + if err != nil { + log.Printf("[AUTH] failed to marshal auth response: %v", err) + } else { + if err := endpoint.Send(core.Message{ + Version: core.ProtocolVersion, + Type: core.TypeAuthResponse, + Source: "eipc-server", + Timestamp: time.Now().UTC(), + RequestID: authMsg.RequestID, + Payload: respPayload, + }); err != nil { + log.Printf("[AUTH] failed to send auth response: %v", err) + } + } close(authDone) return } @@ -396,13 +439,18 @@ func handleConnection( Status string `json:"status"` Nonce string `json:"nonce"` } - challengePayload, _ := codec.Marshal(challengeMessage{ + challengePayload, err := codec.Marshal(challengeMessage{ Status: "challenge", Nonce: hex.EncodeToString(challenge.Nonce), }) + if err != nil { + log.Printf("[AUTH] failed to marshal challenge: %v", err) + close(authDone) + return + } if err := endpoint.Send(core.Message{ Version: core.ProtocolVersion, - Type: core.TypeAck, + Type: core.TypeChallenge, Source: "eipc-server", Timestamp: time.Now().UTC(), RequestID: authMsg.RequestID, @@ -420,6 +468,11 @@ func handleConnection( close(authDone) return } + if responseMsg.Type != core.TypeAuthResponse { + log.Printf("[AUTH] expected TypeAuthResponse, got %s", responseMsg.Type) + close(authDone) + return + } type challengeResponse struct { ServiceID string `json:"service_id"` @@ -443,27 +496,35 @@ func handleConnection( peer, err := authenticator.VerifyResponse(authReq.ServiceID, responseBytes) if err != nil { log.Printf("[AUTH] REJECTED (challenge-response): %v", err) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: authMsg.RequestID, Source: authReq.ServiceID, Target: "eipc-server", Action: "authenticate", Decision: "denied", Result: "challenge-response failed", - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } type authResponse struct { Status string `json:"status"` Error string `json:"error,omitempty"` } - respPayload, _ := codec.Marshal(authResponse{Status: "denied", Error: err.Error()}) - _ = endpoint.Send(core.Message{ - Version: core.ProtocolVersion, - Type: core.TypeAck, - Source: "eipc-server", - Timestamp: time.Now().UTC(), - RequestID: authMsg.RequestID, - Payload: respPayload, - }) + respPayload, err := codec.Marshal(authResponse{Status: "denied", Error: err.Error()}) + if err != nil { + log.Printf("[AUTH] failed to marshal auth response: %v", err) + } else { + if err := endpoint.Send(core.Message{ + Version: core.ProtocolVersion, + Type: core.TypeAuthResponse, + Source: "eipc-server", + Timestamp: time.Now().UTC(), + RequestID: authMsg.RequestID, + Payload: respPayload, + }); err != nil { + log.Printf("[AUTH] failed to send auth response: %v", err) + } + } close(authDone) return } @@ -473,14 +534,16 @@ func handleConnection( log.Printf("[AUTH] ACCEPTED: service=%s token=%s...%s caps=%v", peer.ServiceID, peer.SessionToken[:8], peer.SessionToken[len(peer.SessionToken)-8:], peer.Capabilities) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: authMsg.RequestID, Source: peer.ServiceID, Target: "eipc-server", Action: "authenticate", Decision: "allowed", Result: "session created", - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } // Set peer capabilities on the endpoint for validation endpoint.SetPeerCapabilities(peer.Capabilities) @@ -490,14 +553,18 @@ func handleConnection( SessionToken string `json:"session_token"` Capabilities []string `json:"capabilities"` } - respPayload, _ := codec.Marshal(authResult{ + respPayload, err := codec.Marshal(authResult{ Status: "ok", SessionToken: peer.SessionToken, Capabilities: peer.Capabilities, }) + if err != nil { + log.Printf("[AUTH] failed to marshal auth result: %v", err) + return + } if err := endpoint.Send(core.Message{ Version: core.ProtocolVersion, - Type: core.TypeAck, + Type: core.TypeAuthResponse, Source: "eipc-server", Timestamp: time.Now().UTC(), RequestID: authMsg.RequestID, @@ -518,63 +585,79 @@ func handleConnection( // Check session TTL if peer.IsExpired() { log.Printf("[SESSION] expired for %s", peer.ServiceID) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ Source: peer.ServiceID, Target: "eipc-server", Action: "session_check", Decision: "denied", Result: "session expired", - }) + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } return } // Enforce capability binding if err := endpoint.ValidateCapability(msg.Capability); err != nil { log.Printf("[CAPABILITY] DENIED: %s tried %s", peer.ServiceID, msg.Capability) - _ = auditLogger.Log(audit.Entry{ + if err := auditLogger.Log(audit.Entry{ RequestID: msg.RequestID, Source: peer.ServiceID, Target: "eipc-server", Action: msg.Capability, Decision: "denied", Result: "capability violation", - }) - errPayload, _ := codec.Marshal(core.AckEvent{ + }); err != nil { + log.Printf("[AUDIT] failed: %v", err) + } + errPayload, err := codec.Marshal(core.AckEvent{ RequestID: msg.RequestID, Status: "error", Error: err.Error(), }) - _ = endpoint.Send(core.Message{ - Version: core.ProtocolVersion, - Type: core.TypeAck, - Source: "eipc-server", - Timestamp: time.Now().UTC(), - SessionID: msg.SessionID, - RequestID: msg.RequestID, - Priority: core.PriorityP0, - Payload: errPayload, - }) + if err != nil { + log.Printf("[CAPABILITY] failed to marshal error: %v", err) + } else { + if err := endpoint.Send(core.Message{ + Version: core.ProtocolVersion, + Type: core.TypeAck, + Source: "eipc-server", + Timestamp: time.Now().UTC(), + SessionID: msg.SessionID, + RequestID: msg.RequestID, + Priority: core.PriorityP0, + Payload: errPayload, + }); err != nil { + log.Printf("[CAPABILITY] failed to send error: %v", err) + } + } continue } resp, err := router.Dispatch(msg) if err != nil { log.Printf("[DISPATCH] error: %v", err) - errPayload, _ := codec.Marshal(core.AckEvent{ + errPayload, err := codec.Marshal(core.AckEvent{ RequestID: msg.RequestID, Status: "error", Error: err.Error(), }) - _ = endpoint.Send(core.Message{ - Version: core.ProtocolVersion, - Type: core.TypeAck, - Source: "eipc-server", - Timestamp: time.Now().UTC(), - SessionID: msg.SessionID, - RequestID: msg.RequestID, - Priority: core.PriorityP0, - Payload: errPayload, - }) + if err != nil { + log.Printf("[DISPATCH] failed to marshal error: %v", err) + } else { + if err := endpoint.Send(core.Message{ + Version: core.ProtocolVersion, + Type: core.TypeAck, + Source: "eipc-server", + Timestamp: time.Now().UTC(), + SessionID: msg.SessionID, + RequestID: msg.RequestID, + Priority: core.PriorityP0, + Payload: errPayload, + }); err != nil { + log.Printf("[DISPATCH] failed to send error: %v", err) + } + } continue } diff --git a/config/config_test.go b/config/config_test.go index 431af86..bbce3fd 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -5,6 +5,7 @@ package config import ( "os" + "path/filepath" "testing" "time" ) @@ -26,7 +27,7 @@ func TestLoadHMACKey_EnvVar(t *testing.T) { func TestLoadHMACKey_File(t *testing.T) { os.Unsetenv("EIPC_HMAC_KEY") - tmpFile := t.TempDir() + "/hmac.key" + tmpFile := filepath.Join(t.TempDir(), "hmac.key") os.WriteFile(tmpFile, []byte("file-based-key"), 0600) os.Setenv("EIPC_KEY_FILE", tmpFile) diff --git a/core/endpoint.go b/core/endpoint.go index 22c5fcf..e3edaa1 100644 --- a/core/endpoint.go +++ b/core/endpoint.go @@ -89,7 +89,10 @@ func (e *ClientEndpoint) Receive() (Message, error) { return Message{}, fmt.Errorf("unmarshal header: %w", err) } - ts, _ := time.Parse(time.RFC3339Nano, hdr.Timestamp) + var ts time.Time + if t, err := time.Parse(time.RFC3339Nano, hdr.Timestamp); err == nil { + ts = t + } return Message{ Version: frame.Version, @@ -111,7 +114,7 @@ func (e *ClientEndpoint) Close() error { // ServerEndpoint handles a single server-side connection. type ServerEndpoint struct { conn transport.Connection - codec protocol.Codec + codec protocol.Codec hmacKey []byte replay *replay.Tracker sendMu sync.Mutex @@ -209,7 +212,10 @@ func (e *ServerEndpoint) Receive() (Message, error) { return Message{}, err } - ts, _ := time.Parse(time.RFC3339Nano, hdr.Timestamp) + var ts time.Time + if t, err := time.Parse(time.RFC3339Nano, hdr.Timestamp); err == nil { + ts = t + } return Message{ Version: frame.Version, @@ -243,6 +249,12 @@ func msgTypeFromByte(b uint8) MessageType { return TypeToolRequest case 'a': return TypeAck + case 'A': + return TypeAuth + case 'H': + return TypeChallenge + case 'R': + return TypeAuthResponse case 'p': return TypePolicyResult case 'h': @@ -269,6 +281,12 @@ func MsgTypeToByte(mt MessageType) uint8 { return 't' case TypeAck: return 'a' + case TypeAuth: + return 'A' + case TypeChallenge: + return 'H' + case TypeAuthResponse: + return 'R' case TypePolicyResult: return 'p' case TypeHeartbeat: diff --git a/core/types.go b/core/types.go index 176f966..5371b17 100644 --- a/core/types.go +++ b/core/types.go @@ -10,6 +10,9 @@ const ( TypeIntent MessageType = "intent" TypeFeatures MessageType = "features" TypeToolRequest MessageType = "tool_request" + TypeAuth MessageType = "auth" + TypeChallenge MessageType = "challenge" + TypeAuthResponse MessageType = "auth_response" TypeAck MessageType = "ack" TypePolicyResult MessageType = "policy_result" TypeHeartbeat MessageType = "heartbeat" diff --git a/security/encryption/aes_test.go b/security/encryption/aes_test.go index 2ca2d17..7a613f1 100644 --- a/security/encryption/aes_test.go +++ b/security/encryption/aes_test.go @@ -6,12 +6,15 @@ package encryption import ( "bytes" "crypto/rand" + "io" "testing" ) func TestEncryptDecrypt(t *testing.T) { key := make([]byte, KeySize) - rand.Read(key) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + t.Fatalf("rand.Read: %v", err) + } plaintext := []byte("hello EIPC encryption") @@ -43,14 +46,21 @@ func TestEncrypt_WrongKeySize(t *testing.T) { func TestDecrypt_WrongKey(t *testing.T) { key := make([]byte, KeySize) - rand.Read(key) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + t.Fatalf("rand.Read: %v", err) + } - ciphertext, _ := Encrypt(key, []byte("secret data")) + ciphertext, err := Encrypt(key, []byte("secret data")) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } wrongKey := make([]byte, KeySize) - rand.Read(wrongKey) + if _, err := io.ReadFull(rand.Reader, wrongKey); err != nil { + t.Fatalf("rand.Read: %v", err) + } - _, err := Decrypt(wrongKey, ciphertext) + _, err = Decrypt(wrongKey, ciphertext) if err == nil { t.Fatal("expected error for wrong key") } @@ -58,16 +68,21 @@ func TestDecrypt_WrongKey(t *testing.T) { func TestDecrypt_TamperedCiphertext(t *testing.T) { key := make([]byte, KeySize) - rand.Read(key) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + t.Fatalf("rand.Read: %v", err) + } - ciphertext, _ := Encrypt(key, []byte("secret data")) + ciphertext, err := Encrypt(key, []byte("secret data")) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } // Tamper with the ciphertext (not the nonce) if len(ciphertext) > NonceSize+1 { ciphertext[NonceSize+1] ^= 0xff } - _, err := Decrypt(key, ciphertext) + _, err = Decrypt(key, ciphertext) if err == nil { t.Fatal("expected error for tampered ciphertext") } @@ -75,7 +90,9 @@ func TestDecrypt_TamperedCiphertext(t *testing.T) { func TestDecrypt_TooShort(t *testing.T) { key := make([]byte, KeySize) - rand.Read(key) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + t.Fatalf("rand.Read: %v", err) + } _, err := Decrypt(key, []byte{1, 2, 3}) if err == nil { @@ -85,7 +102,9 @@ func TestDecrypt_TooShort(t *testing.T) { func TestEncryptDecrypt_EmptyPlaintext(t *testing.T) { key := make([]byte, KeySize) - rand.Read(key) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + t.Fatalf("rand.Read: %v", err) + } ciphertext, err := Encrypt(key, []byte{}) if err != nil { @@ -104,10 +123,14 @@ func TestEncryptDecrypt_EmptyPlaintext(t *testing.T) { func TestEncryptDecrypt_LargePayload(t *testing.T) { key := make([]byte, KeySize) - rand.Read(key) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + t.Fatalf("rand.Read: %v", err) + } plaintext := make([]byte, 1<<16) // 64KB - rand.Read(plaintext) + if _, err := io.ReadFull(rand.Reader, plaintext); err != nil { + t.Fatalf("rand.Read: %v", err) + } ciphertext, err := Encrypt(key, plaintext) if err != nil { @@ -126,10 +149,18 @@ func TestEncryptDecrypt_LargePayload(t *testing.T) { func TestEncrypt_UniqueNonces(t *testing.T) { key := make([]byte, KeySize) - rand.Read(key) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + t.Fatalf("rand.Read: %v", err) + } - ct1, _ := Encrypt(key, []byte("same data")) - ct2, _ := Encrypt(key, []byte("same data")) + ct1, err := Encrypt(key, []byte("same data")) + if err != nil { + t.Fatalf("Encrypt 1: %v", err) + } + ct2, err := Encrypt(key, []byte("same data")) + if err != nil { + t.Fatalf("Encrypt 2: %v", err) + } if bytes.Equal(ct1, ct2) { t.Error("two encryptions of same data should produce different ciphertexts (different nonces)") diff --git a/security/keyring/keyring_test.go b/security/keyring/keyring_test.go index b5d859d..c941360 100644 --- a/security/keyring/keyring_test.go +++ b/security/keyring/keyring_test.go @@ -36,8 +36,12 @@ func TestLookupNotFound(t *testing.T) { func TestRevoke(t *testing.T) { kr := New() - kr.Generate("key1", 16, 0) - kr.Revoke("key1") + if _, err := kr.Generate("key1", 16, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } + if err := kr.Revoke("key1"); err != nil { + t.Fatalf("Revoke failed: %v", err) + } _, err := kr.Lookup("key1") if err == nil { t.Error("expected error for revoked key") @@ -46,7 +50,9 @@ func TestRevoke(t *testing.T) { func TestExpiry(t *testing.T) { kr := New() - kr.Generate("short", 16, 1*time.Millisecond) + if _, err := kr.Generate("short", 16, 1*time.Millisecond); err != nil { + t.Fatalf("Generate failed: %v", err) + } time.Sleep(5 * time.Millisecond) _, err := kr.Lookup("short") if err == nil { @@ -57,7 +63,9 @@ func TestExpiry(t *testing.T) { func TestStore(t *testing.T) { kr := New() key := []byte("my-secret-key-32-bytes-00000000") - kr.Store("ext", key, 0) + if err := kr.Store("ext", key, 0); err != nil { + t.Fatalf("Store failed: %v", err) + } found, err := kr.Lookup("ext") if err != nil { t.Fatalf("Lookup failed: %v", err) @@ -69,12 +77,17 @@ func TestStore(t *testing.T) { func TestRotate(t *testing.T) { kr := New() - kr.Generate("rot", 32, 0) + if _, err := kr.Generate("rot", 32, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } newEntry, err := kr.Rotate("rot", 32, 0) if err != nil { t.Fatalf("Rotate failed: %v", err) } - found, _ := kr.Lookup("rot") + found, err := kr.Lookup("rot") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } if string(found.Key) != string(newEntry.Key) { t.Error("rotated key mismatch") } @@ -82,10 +95,18 @@ func TestRotate(t *testing.T) { func TestListActive(t *testing.T) { kr := New() - kr.Generate("a1", 16, 0) - kr.Generate("a2", 16, 0) - kr.Generate("r1", 16, 0) - kr.Revoke("r1") + if _, err := kr.Generate("a1", 16, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } + if _, err := kr.Generate("a2", 16, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } + if _, err := kr.Generate("r1", 16, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } + if err := kr.Revoke("r1"); err != nil { + t.Fatalf("Revoke failed: %v", err) + } if len(kr.ListActive()) != 2 { t.Errorf("expected 2 active, got %d", len(kr.ListActive())) } @@ -93,10 +114,18 @@ func TestListActive(t *testing.T) { func TestCleanup(t *testing.T) { kr := New() - kr.Generate("keep", 16, 0) - kr.Generate("exp", 16, 1*time.Millisecond) - kr.Generate("rev", 16, 0) - kr.Revoke("rev") + if _, err := kr.Generate("keep", 16, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } + if _, err := kr.Generate("exp", 16, 1*time.Millisecond); err != nil { + t.Fatalf("Generate failed: %v", err) + } + if _, err := kr.Generate("rev", 16, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } + if err := kr.Revoke("rev"); err != nil { + t.Fatalf("Revoke failed: %v", err) + } time.Sleep(5 * time.Millisecond) removed := kr.Cleanup() if removed != 2 { @@ -106,7 +135,9 @@ func TestCleanup(t *testing.T) { func TestDelete(t *testing.T) { kr := New() - kr.Generate("del", 16, 0) + if _, err := kr.Generate("del", 16, 0); err != nil { + t.Fatalf("Generate failed: %v", err) + } kr.Delete("del") _, err := kr.Lookup("del") if err == nil { diff --git a/services/audit/audit.go b/services/audit/audit.go index 468e538..e469884 100644 --- a/services/audit/audit.go +++ b/services/audit/audit.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "path/filepath" "sync" "time" ) @@ -42,6 +43,7 @@ func NewFileLogger(path string) (*FileLogger, error) { if path == "" { w = os.Stdout } else { + path = filepath.Clean(path) f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if err != nil { return nil, fmt.Errorf("open audit log: %w", err) diff --git a/services/audit/audit_test.go b/services/audit/audit_test.go index 3119bad..a3b0798 100644 --- a/services/audit/audit_test.go +++ b/services/audit/audit_test.go @@ -6,6 +6,7 @@ package audit import ( "encoding/json" "os" + "path/filepath" "strings" "testing" ) @@ -31,7 +32,7 @@ func TestFileLogger_Stdout(t *testing.T) { } func TestFileLogger_File(t *testing.T) { - tmpFile := t.TempDir() + "/audit.jsonl" + tmpFile := filepath.Join(t.TempDir(), "audit.jsonl") logger, err := NewFileLogger(tmpFile) if err != nil { t.Fatalf("NewFileLogger file: %v", err) @@ -86,7 +87,7 @@ func TestFileLogger_File(t *testing.T) { } func TestFileLogger_TimestampAutoFill(t *testing.T) { - tmpFile := t.TempDir() + "/audit-ts.jsonl" + tmpFile := filepath.Join(t.TempDir(), "audit-ts.jsonl") logger, err := NewFileLogger(tmpFile) if err != nil { t.Fatalf("NewFileLogger: %v", err) @@ -95,16 +96,21 @@ func TestFileLogger_TimestampAutoFill(t *testing.T) { logger.Log(Entry{Source: "test", Action: "check"}) logger.Close() - data, _ := os.ReadFile(tmpFile) + data, err := os.ReadFile(tmpFile) + if err != nil { + t.Fatalf("read audit file: %v", err) + } var entry Entry - json.Unmarshal([]byte(strings.TrimSpace(string(data))), &entry) + if err := json.Unmarshal([]byte(strings.TrimSpace(string(data))), &entry); err != nil { + t.Fatalf("unmarshal: %v", err) + } if entry.Timestamp == "" { t.Error("timestamp should be auto-filled when empty") } } func TestFileLogger_PresetTimestamp(t *testing.T) { - tmpFile := t.TempDir() + "/audit-preset.jsonl" + tmpFile := filepath.Join(t.TempDir(), "audit-preset.jsonl") logger, err := NewFileLogger(tmpFile) if err != nil { t.Fatalf("NewFileLogger: %v", err) @@ -114,9 +120,14 @@ func TestFileLogger_PresetTimestamp(t *testing.T) { logger.Log(Entry{Timestamp: customTS, Source: "test", Action: "check"}) logger.Close() - data, _ := os.ReadFile(tmpFile) + data, err := os.ReadFile(tmpFile) + if err != nil { + t.Fatalf("read audit file: %v", err) + } var entry Entry - json.Unmarshal([]byte(strings.TrimSpace(string(data))), &entry) + if err := json.Unmarshal([]byte(strings.TrimSpace(string(data))), &entry); err != nil { + t.Fatalf("unmarshal: %v", err) + } if entry.Timestamp != customTS { t.Errorf("expected preset timestamp %q, got %q", customTS, entry.Timestamp) }