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
1 change: 1 addition & 0 deletions internal/audit/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)

// Logger writes and reads audit events from PostgreSQL.
type Logger struct {
Pool *pgxpool.Pool
}
Expand Down
2 changes: 2 additions & 0 deletions internal/audit/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"
)

// Limits for JSON payload sanitization.
const (
MaxJSONBytes = 4096
MaxJSONDepth = 6
Expand Down Expand Up @@ -35,6 +36,7 @@ var sensitiveKeys = map[string]struct{}{
"x-api-key_hash": {},
}

// Event represents a single audit log entry.
type Event struct {
TS time.Time
APIKeyID *string
Expand Down
2 changes: 2 additions & 0 deletions internal/auth/api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (

const DefaultAPIKeyRole = "readonly"

// ResolvedKey is the result of a successful API key lookup.
type ResolvedKey struct {
APIKeyID string
Role string
}

// ListedKey is a summary of an API key for listing endpoints.
type ListedKey struct {
APIKeyID string
Role string
Expand Down
4 changes: 3 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
UpstreamTransportHTTP UpstreamTransport = "http"
)

// Settings holds all configuration values for Bansho.
type Settings struct {
BanshoListenHost string
BanshoListenPort int
Expand All @@ -31,6 +32,7 @@ type Settings struct {
RedisURL string
}

// Load reads environment variables and returns a Settings struct.
func Load() (Settings, error) {
// Best-effort `.env` support for local dev parity. If missing, continue.
_ = godotenv.Overload()
Expand Down Expand Up @@ -69,7 +71,7 @@ func Load() (Settings, error) {
case UpstreamTransportStdio, UpstreamTransportHTTP:
s.UpstreamTransport = transport
default:
return Settings{}, fmt.Errorf("UPSTREAM_TRANSPORT must be one of: stdio, http")
return Settings{}, fmt.Errorf("UPSTREAM_TRANSPORT %q must be one of: stdio, http", t)
}
}
s.UpstreamCmd = strings.TrimSpace(os.Getenv("UPSTREAM_CMD"))
Expand Down
1 change: 1 addition & 0 deletions internal/policy/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

const DefaultPolicyPath = "config/policies.yaml"

// LoadError wraps a policy file load failure with its path.
type LoadError struct {
Path string
Err error
Expand Down
1 change: 1 addition & 0 deletions internal/policy/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

const ToolWildcard = "*"

// RoleToolPolicy defines which tools a role is allowed to use.
type RoleToolPolicy struct {
Allow []string `yaml:"allow"`
}
Expand Down
7 changes: 5 additions & 2 deletions internal/ratelimit/limiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ const (
unknownToolSegment = "__unknown_tool__"
)

// RateLimitResult holds the outcome of a rate limit check.
type RateLimitResult struct {
Allowed bool
Remaining int
ResetS int
}

// CheckAPIKeyLimit checks the per-API-key rate limit.
func CheckAPIKeyLimit(ctx context.Context, client *redis.Client, apiKeyID string, requests int, windowSeconds int, nowS *int64) (RateLimitResult, error) {
currentEpoch := currentEpoch(nowS)
windowBucket, err := windowBucket(currentEpoch, windowSeconds)
Expand All @@ -34,6 +36,7 @@ func CheckAPIKeyLimit(ctx context.Context, client *redis.Client, apiKeyID string
return checkFixedWindowLimit(ctx, client, key, requests, windowSeconds, currentEpoch)
}

// CheckToolLimit checks the per-tool rate limit for an API key.
func CheckToolLimit(ctx context.Context, client *redis.Client, apiKeyID string, toolName string, requests int, windowSeconds int, nowS *int64) (RateLimitResult, error) {
currentEpoch := currentEpoch(nowS)
windowBucket, err := windowBucket(currentEpoch, windowSeconds)
Expand Down Expand Up @@ -126,10 +129,10 @@ func coerceInt(value any) (int, error) {
// go-redis can return a string for integer replies in some cases.
parsed, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, fmt.Errorf("unexpected counter value")
return 0, fmt.Errorf("unexpected counter value: %w", err)
}
return int(parsed), nil
default:
return 0, fmt.Errorf("unexpected counter value")
return 0, fmt.Errorf("unexpected counter type %T", value)
}
}
1 change: 1 addition & 0 deletions internal/storage/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var (
postgresDSN string
)

// GetPostgresPool returns a singleton pgxpool.Pool for the given DSN.
func GetPostgresPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
postgresMu.Lock()
defer postgresMu.Unlock()
Expand Down
1 change: 1 addition & 0 deletions internal/storage/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var (
redisURL string
)

// GetRedisClient returns a singleton redis.Client for the given URL.
func GetRedisClient(url string) (*redis.Client, error) {
redisMu.Lock()
defer redisMu.Unlock()
Expand Down
7 changes: 2 additions & 5 deletions internal/ui/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ const (
maxEventLimit = 200
)

// DashboardAuthContext holds the authenticated identity for a dashboard request.
type DashboardAuthContext struct {
APIKeyID string
Role string
}

// RunDashboard starts the audit dashboard HTTP server.
func RunDashboard(settings config.Settings) error {
ctx := context.Background()
pool, err := storage.GetPostgresPool(ctx, settings.PostgresDSN)
Expand Down Expand Up @@ -302,8 +304,6 @@ func renderDashboardHTML(authCtx DashboardAuthContext, filters dashboardFilters,
rows := ""
for i, e := range events {
decisionJSON, _ := json.Marshal(e.Decision)
requestJSON, _ := json.Marshal(e.Request)
responseJSON, _ := json.Marshal(e.Response)

// Build detail object for row expansion
detail := map[string]any{
Expand All @@ -329,9 +329,6 @@ func renderDashboardHTML(authCtx DashboardAuthContext, filters dashboardFilters,
rowClass = "row-err"
}

_ = requestJSON
_ = responseJSON

rows += fmt.Sprintf(
`<tr class="%s" data-idx="%d" data-ts="%s" data-status="%d" data-latency="%d" data-key="%s" data-role="%s" data-method="%s" data-tool="%s">`+
`<td class="ts">%s</td>`+
Expand Down
Loading