Skip to content

Codebase analyst — deferred findings 2026-06-13 #74

Description

@LiukScot

Deferred findings dal run del 2026-06-13. Ognuno è actionable in una run dedicata (es. implementa deferred <fingerprint>).

Issue creato automaticamente dal Codebase Analyst. Dedup: i finding già presenti in altri issue aperti con label codebase-analyst-deferred (es. #59) sono stati scartati.

Linkato al PR: #75


Deferred — bcrypt cost factor too low

Fingerprint: bcrypt-cost-factor@internal/auth/auth.go:31
File: internal/auth/auth.go:31
Severità: LOW
Soluzione proposta: Aumentare il cost factor di bcrypt da 10 (default) a 12+ come raccomandato da OWASP 2024. La modifica si applica alla creazione di nuovi hash in CreateUser; le password esistenti vengono migrated al login (re-hash on verify).
Patch indicativa:

// in auth.go
const bcryptWorkFactor = 12 // was bcrypt.DefaultCost (10)
// replace bcrypt.DefaultCost with bcryptWorkFactor in GenerateFromPassword calls

Decision points: Scegliere il work factor (12 = ~250ms/hash su hardware moderno). Decidere se fare lazy re-hash al login per gli utenti esistenti.
Trade-off: Login latency aumenta (10→12 = 4x); per un dashboard single-user è trascurabile. Nessuna migrazione DB richiesta.
Test richiesto: go test ./internal/auth/ -run TestLogin — verifica che il login funzioni ancora.


Deferred — CronWeek.Warnings espone path interni e syslog all'API

Fingerprint: cron-warnings-pii-leak@internal/collectors/cron.go:66
File: internal/collectors/cron.go:66, internal/server/server.go (handleCronWeek)
Severità: MEDIUM
Soluzione proposta: Filtrare o omettere il campo warnings dalla risposta JSON in produzione, oppure sanitizzare le stringhe per rimuovere path assoluti e linee raw di journalctl prima di includerle nel payload API.
Patch indicativa:

// opzione A: rimuovere warnings dalla risposta (se non usate dal frontend)
// in CronWeek struct: Warnings []string `json:"-"`

// opzione B: sanitize in handleCronWeek
sanitized := make([]string, 0, len(week.Warnings))
for _, w := range week.Warnings {
    sanitized = append(sanitized, sanitizeWarning(w))
}
week.Warnings = sanitized

Decision points: I warnings sono visibili nel frontend? Se sì, quale livello di dettaglio è accettabile? Rimuovere completamente vs. mostrare solo messaggi generici.
Trade-off: Rimuovere warnings peggiora debuggability per l'operatore; sanitize è più complesso. Endpoint richiede auth, rischio limitato.
Test richiesto: curl -H "Cookie: session=..." /api/v1/cron/week — verificare che path assoluti non appaiano in warnings.


Deferred — Colori hex hardcoded in TimeChart fuori dai design token

Fingerprint: timechart-hex-colors@frontend/src/components/TimeChart.svelte:43
File: frontend/src/components/TimeChart.svelte:43-48, frontend/src/app.css
Severità: LOW
Soluzione proposta: Sostituire i valori hex nel THEME object e le 4+ occorrenze del colore accent (#00d4aa) con CSS custom properties già definite in app.css. Leggere i valori a runtime con getComputedStyle(document.documentElement).getPropertyValue('--color-accent').
Patch indicativa:

const THEME = {
    bg: () => getComputedStyle(document.documentElement).getPropertyValue('--color-bg-base').trim(),
    accent: () => getComputedStyle(document.documentElement).getPropertyValue('--color-accent').trim(),
    // ...
};

Decision points: Leggere CSS vars in onMount (dopo mount) vs. derivarle staticamente. Impatta il SSR se Svelte renderizza lato server.
Trade-off: Approccio dinamico rompe se ECharts è inizializzato prima del mount; approccio statico richiede mantenimento manuale della sync.
Test richiesto: Verify dark mode toggle: TimeChart deve aggiornare i colori senza refresh pagina.


Deferred — Docker stats N+1 HTTP calls per ogni broadcast tick

Fingerprint: docker-stats-n1@internal/collectors/docker.go:240
File: internal/collectors/docker.go:240-289
Severità: HIGH
Soluzione proposta: Cachare il risultato di GetAllStats() con TTL pari all'intervallo di broadcast (3s). Le chiamate successive entro il TTL restituiscono la cache senza toccare il socket Docker.
Patch indicativa:

type DockerCollector struct {
    // ...
    statsMu    sync.RWMutex
    statsCache []ContainerStats
    statsAt    time.Time
    statsTTL   time.Duration // es. 3s
}

func (d *DockerCollector) GetAllStats() ([]ContainerStats, error) {
    d.statsMu.RLock()
    if time.Since(d.statsAt) < d.statsTTL {
        result := d.statsCache
        d.statsMu.RUnlock()
        return result, nil
    }
    d.statsMu.RUnlock()
    // fetch fresh...
}

Decision points: Scegliere TTL (3s = broadcast interval è ragionevole). Decidere se invalidare la cache al ricevimento di eventi Docker o solo per TTL.
Trade-off: Cache stale di 3s è accettabile per un dashboard. Aggiunge complessità di sync. Race condition se due goroutine entrano contemporaneamente nella sezione write.
Test richiesto: Profila con pprof prima/dopo: riduzione syscall verso il socket Docker.


Deferred — system_history SELECT senza LIMIT (fino a 43k righe per 30d)

Fingerprint: system-history-unbounded@internal/collectors/system_history.go:218
File: internal/collectors/system_history.go:218-263
Severità: MEDIUM
Soluzione proposta: Aggiungere downsampling server-side: per range lunghi (7d, 30d) restituire campioni aggregati (es. media per bucket temporale) invece di tutti i punti. In alternativa, aggiungere LIMIT 2000 e documentarlo come max resolution.
Patch indicativa:

-- opzione semplice: LIMIT
SELECT ... FROM metrics_history WHERE timestamp >= ? AND resolution = ?
ORDER BY timestamp ASC LIMIT 2000

-- opzione downsample: bucket da 1h per range 30d
SELECT strftime('%Y-%m-%dT%H:00:00Z', timestamp) as bucket,
       AVG(cpu_percent), AVG(mem_percent)...
FROM metrics_history WHERE timestamp >= ? AND resolution = '1m'
GROUP BY bucket ORDER BY bucket LIMIT 720

Decision points: LIMIT semplice vs. downsampling aggregato. Il frontend usa i dati per mostrare grafici — quanti punti sono significativi visivamente?
Trade-off: LIMIT può tagliare dati recenti se la query è ordinata ASC; usare DESC + LIMIT poi invertire. Downsampling richiede SQLite strftime e media ponderata.
Test richiesto: curl /api/v1/system/history?range=30d — misurare payload size prima/dopo.


Deferred — Test gap: handleFail2BanBans e handleLogs senza test HTTP

Fingerprint: test-gap-handle-fail2ban-bans@internal/server/server.go:371
File: internal/server/server.go:371-425, internal/server/server_test.go
Severità: HIGH
Soluzione proposta: Aggiungere test in server_test.go per i due handler: auth gate (401 senza cookie), limit clamping, e path di errore con collector nil/errore.
Patch indicativa:

func TestHandleFail2BanBansRejectsUnauthenticated(t *testing.T) {
    s := newTestServer(t)
    r := httptest.NewRequest("GET", "/api/v1/security/fail2ban/bans", nil)
    w := httptest.NewRecorder()
    s.handleFail2BanBans(w, r) // no cookie
    assert.Equal(t, 401, w.Code)
}
func TestHandleLogsRejectsInvalidPriority(t *testing.T) { ... }
func TestHandleLogsRejectsUnitTooLong(t *testing.T) { ... }

Decision points: Usare il pattern newTestServer esistente o un approccio diverso?
Trade-off: Nessuno rilevante. Test di integrazione a livello HTTP sono superiori ai test unitari per questi handler.
Test richiesto: go test ./internal/server/ -run TestHandle deve passare.


Deferred — Test gap: WebSocket client ws.ts senza Vitest

Fingerprint: test-gap-ws-client@frontend/src/lib/ws.ts:1
File: frontend/src/lib/ws.ts
Severità: MEDIUM
Soluzione proposta: Aggiungere ws.test.ts con mock di globalThis.WebSocket. Testare la state machine (connecting→connected→disconnected) e la logica di backoff esponenziale.
Patch indicativa:

vi.stubGlobal('WebSocket', MockWebSocket);
const { getState, subscribe } = await import('./ws');
subscribe(() => {});
expect(getState()).toBe('connecting');
MockWebSocket.instance.onopen?.({} as Event);
expect(getState()).toBe('connected');

Decision points: Mock WS nativo vs. jest-websocket-mock. Il file usa globalThis.WebSocketvi.stubGlobal è la via più semplice.
Trade-off: Mock non testa il protocollo WS reale; testa solo la state machine, che è il valore principale.
Test richiesto: bun test ws.test.ts deve passare con state transitions corrette.


Deferred — Test gap: formatBytes e toastError senza test

Fingerprint: test-gap-format-bytes@frontend/src/lib/format.ts:1
File: frontend/src/lib/format.ts, frontend/src/lib/stores/toast.svelte.ts:38
Severità: MEDIUM
Soluzione proposta: Aggiungere casi a format.test.ts (edge: 0, -1, boundary 1023/1024) e a Toast.test.ts per toastError (err instanceof Error vs. fallback string).
Patch indicativa:

// format.test.ts
it('handles zero', () => expect(formatBytes(0)).toBe('0 B'));
it('handles boundary', () => expect(formatBytes(1023)).toBe('1023 B'));
it('handles KB', () => expect(formatBytes(1024)).toBe('1.0 KB'));

// Toast.test.ts
it('toastError with Error', () => {
    toastError(new Error('boom'), 'fallback');
    expect(getToasts()[0].message).toBe('boom');
});

Decision points: Nessuno — test puri, nessuna scelta architetturale.
Trade-off: Nessuno.
Test richiesto: bun test deve passare con tutti i nuovi casi.


Deferred — Test gap: toast auto-dismiss timer non testato

Fingerprint: test-gap-toast-auto-dismiss@frontend/src/lib/stores/toast.svelte.ts:22
File: frontend/src/lib/stores/toast.svelte.ts:22-24
Severità: MEDIUM
Soluzione proposta: Usare vi.useFakeTimers() per testare il path di auto-dismiss con durationMs > 0.
Patch indicativa:

it('auto-dismisses after duration', async () => {
    vi.useFakeTimers();
    pushToast('info', 'auto', 1000);
    expect(getToasts()).toHaveLength(1);
    await vi.advanceTimersByTimeAsync(1001);
    expect(getToasts()).toHaveLength(0);
    vi.useRealTimers();
});

Decision points: Nessuno.
Trade-off: Fake timers possono interferire con altri test se non ripristinati correttamente.
Test richiesto: bun test Toast.test.ts deve passare.


Deferred — Dockerfile bun tag floating (oven/bun:1)

Fingerprint: dockerfile-bun-floating-tag@Dockerfile:2
File: Dockerfile:2
Severità: MEDIUM
Soluzione proposta: Pinnare a versione specifica es. oven/bun:1.2.19 (o la versione in uso) per riproducibilità build.
Patch indicativa:

FROM oven/bun:1.2.19 AS frontend-builder

Decision points: Scegliere versione da pinnare (verificare docker run oven/bun:1 bun --version nell'attuale build).
Trade-off: Tag fisso richiede aggiornamento manuale o Dependabot Docker. Floating tag rompe reproducibilità.
Test richiesto: docker build . deve completare senza errori dopo il pin.


Deferred — Dep major outdated: @sveltejs/vite-plugin-svelte e typescript

Fingerprint: vite-plugin-svelte-major-outdated@frontend/package.json:17
File: frontend/package.json:17,22
Severità: MEDIUM
Soluzione proposta: Aggiornare @sveltejs/vite-plugin-svelte (5.1.1 → 7.x) e typescript (^5.9.3 → ^6.x). Verificare breaking changes nel changelog prima di mergiare.
Patch indicativa:

cd frontend && bun update @sveltejs/vite-plugin-svelte typescript
bun run build && bun run test

Decision points: @sveltejs/vite-plugin-svelte v6/v7 può richiedere aggiornamento di @sveltejs/kit. Verificare peer deps. TypeScript 6.x può introdurre errori di tipo su codice esistente.
Trade-off: Major bumps = potenziali breaking changes. Aggiornare prima in un branch dedicato con CI verde prima di mergiare in main.
Test richiesto: bun run build && bun run test devono passare senza errori.


🤖 Generated by Codebase Analyst — 2026-06-13

Metadata

Metadata

Assignees

No one assigned

    Labels

    codebase-analyst-deferredDeferred findings tracked by weekly codebase analyst

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions