Skip to content
Open
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
6 changes: 6 additions & 0 deletions internal/daemon/ui_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ func (m *UIManager) NotifyTlsTerminationFailed(ev any) {
m.Client.NotifyTlsTerminationFailed(ev)
}

// NotifyMinPackageAge sends an informational minimum-package-age log entry to
// the UI. This path intentionally does not trigger native popup notifications.
func (m *UIManager) NotifyMinPackageAge(ev any) {
m.Client.NotifyMinPackageAge(ev)
}

// NotifyPermissionsUpdated sends the latest permissions to the UI.
func (m *UIManager) NotifyPermissionsUpdated(perms any) {
m.Client.NotifyPermissionsUpdated(perms)
Expand Down
13 changes: 13 additions & 0 deletions internal/ingress/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ type TlsTerminationFailedEvent struct {
Error string `json:"error"`
}

// MinPackageAgeEvent represents a passive log entry emitted when the proxy
// suppresses versions that do not meet the minimum package age policy.
// Unlike BlockEvent, this is intended for the Logs tab only and should not
// trigger any native popup notification.
type MinPackageAgeEvent struct {
ID string `json:"id,omitempty"`
TsMs int64 `json:"ts_ms"`
Ecosystem string `json:"ecosystem,omitempty"`
Artifact Artifact `json:"artifact,omitempty"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
}

type EcosystemExceptions struct {
AllowedPackages []string `json:"allowed_packages"`
RejectedPackages []string `json:"rejected_packages"`
Expand Down
54 changes: 54 additions & 0 deletions internal/ingress/min_package_age_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ingress

import (
"encoding/json"
"log"
"net/http"
)

func (s *Server) handleMinPackageAge(w http.ResponseWriter, r *http.Request) {
var event MinPackageAgeEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}

log.Println("Got min-package-age event:", event)

stored := s.minAgeStore.Add(event)

// These events are informational only: they should show up in the Logs tab,
// but must not create a native popup notification.
go s.ui.NotifyMinPackageAge(stored)

w.WriteHeader(http.StatusOK)
}

func (s *Server) handleMinPackageAgeEvents(w http.ResponseWriter, r *http.Request) {
if !s.validateUIToken(w, r) {
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(s.minAgeStore.List()); err != nil {
log.Printf("failed to encode min-package-age events: %v", err)
}
}

func (s *Server) handleGetMinPackageAgeEventByID(w http.ResponseWriter, r *http.Request) {
if !s.validateUIToken(w, r) {
return
}
id := r.PathValue("id")
if event, ok := s.minAgeStore.Get(id); ok {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(event); err != nil {
log.Printf("failed to encode min-package-age event %s: %v", id, err)
}
return
}
w.WriteHeader(http.StatusNotFound)
if _, err := w.Write([]byte("Event not found")); err != nil {
log.Printf("failed to write 404 response: %v", err)
}
}
67 changes: 67 additions & 0 deletions internal/ingress/min_package_age_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ingress

import (
"fmt"
"sync"
)

type minPackageAgeEventStore struct {
mu sync.RWMutex
events []MinPackageAgeEvent
}

const minPackageAgeEventID = "min-package-age-suppressed"

func (e *minPackageAgeEventStore) Add(ev MinPackageAgeEvent) MinPackageAgeEvent {
e.mu.Lock()
defer e.mu.Unlock()

ecosystem := ev.Ecosystem
if ecosystem == "" {
ecosystem = ev.Artifact.Product
}
if ecosystem == "" {
ecosystem = "unknown"
}

stableID := fmt.Sprintf("min-package-age-suppressed-%s", ecosystem)
title := fmt.Sprintf("%s package versions suppressed", ecosystem)

for i := range e.events {
if e.events[i].ID == stableID {
e.events[i].TsMs = ev.TsMs
return e.events[i]
}
}

stored := MinPackageAgeEvent{
ID: stableID,
TsMs: ev.TsMs,
Ecosystem: ecosystem,
Title: title,
Message: "One or more package versions were suppressed because they did not meet the minimum package age policy.",
}

e.events = append(e.events, stored)

return stored
}

func (e *minPackageAgeEventStore) Get(id string) (MinPackageAgeEvent, bool) {
e.mu.RLock()
defer e.mu.RUnlock()
for _, ev := range e.events {
if ev.ID == id {
return ev, true
}
}
return MinPackageAgeEvent{}, false
}

func (e *minPackageAgeEventStore) List() []MinPackageAgeEvent {
e.mu.RLock()
defer e.mu.RUnlock()
out := make([]MinPackageAgeEvent, len(e.events))
copy(out, e.events)
return out
}
62 changes: 62 additions & 0 deletions internal/ingress/min_package_age_store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package ingress

import "testing"

func TestMinPackageAgeEventStoreAddAssignsIDAndCopiesVersions(t *testing.T) {
store := &minPackageAgeEventStore{}

input := MinPackageAgeEvent{
TsMs: 123,
Ecosystem: "vscode",
}

stored := store.Add(input)
if stored.ID != "min-package-age-suppressed-vscode" {
t.Fatalf("expected stable id %q, got %q", "min-package-age-suppressed-vscode", stored.ID)
}
if stored.Title != "vscode package versions suppressed" || stored.Message == "" {
t.Fatalf("expected generic title and message to be populated")
}
if stored.Ecosystem != "vscode" {
t.Fatalf("expected ecosystem to be stored, got %q", stored.Ecosystem)
}

got, ok := store.Get(stored.ID)
if !ok {
t.Fatalf("expected stored event to exist")
}
if got.Title != stored.Title || got.Message != stored.Message {
t.Fatalf("expected stored event to preserve generic copy")
}
}

func TestMinPackageAgeEventStoreAddUpdatesExistingEntryInsteadOfDuplicating(t *testing.T) {
store := &minPackageAgeEventStore{}

first := store.Add(MinPackageAgeEvent{TsMs: 123, Ecosystem: "vscode"})
second := store.Add(MinPackageAgeEvent{TsMs: 456, Ecosystem: "vscode"})

if first.ID != second.ID {
t.Fatalf("expected stable aggregate id, got %q and %q", first.ID, second.ID)
}
if len(store.List()) != 1 {
t.Fatalf("expected a single aggregate event, got %d", len(store.List()))
}
if second.TsMs != 456 {
t.Fatalf("expected timestamp to be refreshed, got %d", second.TsMs)
}
}

func TestMinPackageAgeEventStoreAddCreatesOneEntryPerEcosystem(t *testing.T) {
store := &minPackageAgeEventStore{}

first := store.Add(MinPackageAgeEvent{TsMs: 123, Ecosystem: "vscode"})
second := store.Add(MinPackageAgeEvent{TsMs: 456, Ecosystem: "npm"})

if first.ID == second.ID {
t.Fatalf("expected different ids per ecosystem, got %q", first.ID)
}
if len(store.List()) != 2 {
t.Fatalf("expected one aggregate event per ecosystem, got %d", len(store.List()))
}
}
6 changes: 6 additions & 0 deletions internal/ingress/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type UIProvider interface {
NotifyBlocked(ev any)
NotifyBlockedUpdated(ev any)
NotifyTlsTerminationFailed(ev any)
NotifyMinPackageAge(ev any)
NotifyPermissionsUpdated(ev any)
StartSetupWizard(steps []string)
}
Expand All @@ -40,6 +41,7 @@ type Server struct {

eventStore *eventStore
tlsEventStore *tlsEventStore
minAgeStore *minPackageAgeEventStore
chromeNames *chromeExtensionNameResolver
mu sync.RWMutex
}
Expand All @@ -51,6 +53,7 @@ func New(cfg *config.ConfigInfo, ui UIProvider, proxy proxy.ProxyManager) *Serve
proxy: proxy,
eventStore: &eventStore{},
tlsEventStore: &tlsEventStore{},
minAgeStore: &minPackageAgeEventStore{},
chromeNames: newChromeExtensionNameResolver(),
}
}
Expand All @@ -65,6 +68,7 @@ func (s *Server) Start(ctx context.Context) error {
mux := http.NewServeMux()
mux.HandleFunc("POST /events/blocks", s.handleBlock)
mux.HandleFunc("POST /events/tls-termination-failed", s.handleTlsTerminationFailed)
mux.HandleFunc("POST /events/min-package-age", s.handleMinPackageAge)
mux.HandleFunc("POST /events/permissions", s.handlePermissionsUpdated)
mux.HandleFunc("GET /ping", s.handlePing)

Expand All @@ -74,6 +78,8 @@ func (s *Server) Start(ctx context.Context) error {

mux.HandleFunc("GET /v1/tls-events", s.handleTlsEvents)
mux.HandleFunc("GET /v1/tls-events/{id}", s.handleGetTlsEventByID)
mux.HandleFunc("GET /v1/min-package-age-events", s.handleMinPackageAgeEvents)
mux.HandleFunc("GET /v1/min-package-age-events/{id}", s.handleGetMinPackageAgeEventByID)

mux.HandleFunc("GET /v1/version", s.handleVersion)

Expand Down
9 changes: 9 additions & 0 deletions internal/uiclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ func (c *Client) NotifyTlsTerminationFailed(ev any) {
}
}

// NotifyMinPackageAge sends an informational minimum-package-age log entry to
// the UI. This is intentionally logs-only and should not be surfaced as a
// native popup notification.
func (c *Client) NotifyMinPackageAge(ev any) {
if err := c.post("/v1/min-package-age", ev); err != nil {
log.Printf("Failed to notify UI of min-package-age event: %v (UI may not be running)", err)
}
}

// NotifyPermissionsUpdated sends the latest permissions to the UI.
func (c *Client) NotifyPermissionsUpdated(perms any) {
if err := c.post("/v1/permissions", perms); err != nil {
Expand Down
26 changes: 26 additions & 0 deletions ui/appserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Server struct {
onBlocked func(ev daemon.BlockEvent)
onBlockedUpdated func(ev daemon.BlockEvent)
onTlsTerminationFailed func(ev daemon.TlsTerminationFailedEvent)
onMinPackageAge func(ev daemon.MinPackageAgeEvent)
onPermissionsUpdated func(ev daemon.PermissionsResponse)
onSetupWizard func(steps []string)
}
Expand All @@ -53,6 +54,7 @@ func (s *Server) SetHandlers(
onBlocked func(daemon.BlockEvent),
onBlockedUpdated func(daemon.BlockEvent),
onTlsTerminationFailed func(daemon.TlsTerminationFailedEvent),
onMinPackageAge func(daemon.MinPackageAgeEvent),
onPermissionsUpdated func(daemon.PermissionsResponse),
onSetupWizard func(steps []string),
) {
Expand All @@ -62,6 +64,7 @@ func (s *Server) SetHandlers(
s.onBlocked = onBlocked
s.onBlockedUpdated = onBlockedUpdated
s.onTlsTerminationFailed = onTlsTerminationFailed
s.onMinPackageAge = onMinPackageAge
s.onPermissionsUpdated = onPermissionsUpdated
s.onSetupWizard = onSetupWizard
}
Expand All @@ -73,6 +76,7 @@ func (s *Server) Start() {
mux.HandleFunc("POST /v1/blocked", s.handleBlocked)
mux.HandleFunc("POST /v1/blocked-update", s.handleBlockedUpdated)
mux.HandleFunc("POST /v1/tls-termination-failed", s.handleTlsTerminationFailed)
mux.HandleFunc("POST /v1/min-package-age", s.handleMinPackageAge)
mux.HandleFunc("POST /v1/permissions", s.handlePermissionsUpdated)
mux.HandleFunc("POST /v1/setup-wizard", s.handleSetupWizard)

Expand Down Expand Up @@ -165,6 +169,28 @@ func (s *Server) handlePermissionsUpdated(w http.ResponseWriter, r *http.Request
w.WriteHeader(http.StatusOK)
}

func (s *Server) handleMinPackageAge(w http.ResponseWriter, r *http.Request) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleMinPackageAge duplicates the token-validate/decode/validate/callback pattern in other handlers; extract common request-handling steps into a shared helper to avoid repeating the same logic.

Details

✨ AI Reasoning
​A new HTTP handler was added that performs the same sequence of steps as several existing handlers: validate token, decode JSON into a typed event, call Validate(), acquire lock to fetch a callback field, invoke the callback if present, and return HTTP 200. This repeats substantial, non-trivial logic already implemented in other handler functions in the same module, increasing maintenance burden because bug fixes or behavioral changes would need to be applied in multiple places. Consolidating the shared flow into a helper or common handler would reduce duplication and the risk of inconsistent behavior.

πŸ”§ How do I fix it?
Delete extra code. Extract repeated code sequences into reusable functions or methods. Use loops or data structures to eliminate repetitive patterns.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

if !validateToken(w, r) {
return
}
var ev daemon.MinPackageAgeEvent
if err := json.NewDecoder(r.Body).Decode(&ev); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if err := ev.Validate(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
cb := s.onMinPackageAge
s.mu.Unlock()
if cb != nil {
cb(ev)
}
w.WriteHeader(http.StatusOK)
}

type setupWizardBody struct {
Steps []string `json:"steps"`
}
Expand Down
43 changes: 43 additions & 0 deletions ui/daemon/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,49 @@ func GetTlsEvent(eventID string) (TlsTerminationFailedEvent, error) {
return out, nil
}

// ListMinPackageAgeEvents fetches GET /v1/min-package-age-events?limit=N.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ListMinPackageAgeEvents and GetMinPackageAgeEvent duplicate the request/response handling logic already used by ListTlsEvents/GetTlsEvent; consider extracting a shared helper to avoid repeating nearly identical code.

Details

✨ AI Reasoning
​Two new API helper functions were added that follow the exact same control flow and error handling as existing ListTlsEvents/GetTlsEvent: they validate input (for get), perform a doRequest, check StatusCode, decode JSON into a slice/struct, sort (for list), and return. This is a repeated business operation (fetching paginated events) implemented twice with only the endpoint path and Go type differing. Consolidation into a shared helper (parameterized by path and target type) would avoid duplicating the same request/response logic and reduce maintenance burden when behaviour changes (timeouts, decoding, status handling). The duplication was introduced by this change; it appears in the same file and is substantial (entire function bodies are near-identical).

πŸ”§ How do I fix it?
Delete extra code. Extract repeated code sequences into reusable functions or methods. Use loops or data structures to eliminate repetitive patterns.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

func ListMinPackageAgeEvents(limit int) ([]MinPackageAgeEvent, error) {
if limit <= 0 {
limit = 50
}
resp, err := doRequest(http.MethodGet, fmt.Sprintf("/v1/min-package-age-events?limit=%d", limit), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("list min package age events: %s", resp.Status)
}
var out []MinPackageAgeEvent
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
sort.Slice(out, func(i, j int) bool {
return out[i].TsMs > out[j].TsMs
})
return out, nil
}

// GetMinPackageAgeEvent fetches GET /v1/min-package-age-events/:id.
func GetMinPackageAgeEvent(eventID string) (MinPackageAgeEvent, error) {
var out MinPackageAgeEvent
if err := validateEventID(eventID); err != nil {
return out, err
}
resp, err := doRequest(http.MethodGet, "/v1/min-package-age-events/"+eventID, nil)
if err != nil {
return out, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return out, fmt.Errorf("get min package age event: %s", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return out, err
}
return out, nil
}

// CertificateStatus is returned by GET /v1/certificate/status.
type CertificateStatus struct {
NeedsInstall bool `json:"needs_install"`
Expand Down
Loading
Loading