From bc3b36c3daee57eb17ebe454997d7f5ff250debd Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 07:44:10 +0000 Subject: [PATCH] refactor(ax): tighten remaining AX naming and examples Co-Authored-By: Virgil --- ai/metrics.go | 66 +++++++++--------- ai/metrics_bench_test.go | 85 ++++++++++------------- ai/metrics_test.go | 64 ++++++----------- ai/rag.go | 20 +++--- cmd/embed-bench/main.go | 131 ++++++++++++++++------------------- cmd/lab/cmd_lab.go | 118 +++++++++++++++---------------- cmd/metrics/cmd.go | 64 ++++++++--------- cmd/metrics/cmd_test.go | 6 +- cmd/security/cmd_alerts.go | 60 ++++++++-------- cmd/security/cmd_deps.go | 48 +++++++------ cmd/security/cmd_jobs.go | 80 ++++++++++----------- cmd/security/cmd_scan.go | 46 ++++++------ cmd/security/cmd_secrets.go | 35 +++++----- cmd/security/cmd_security.go | 108 +++++++++++++++-------------- 14 files changed, 452 insertions(+), 479 deletions(-) diff --git a/ai/metrics.go b/ai/metrics.go index 00cab91..7c067e2 100644 --- a/ai/metrics.go +++ b/ai/metrics.go @@ -16,7 +16,7 @@ import ( var metricsWriteMutex sync.Mutex -// Event is a recorded AI or security metric entry. +// Event records an AI or security metric entry. // // ai.Record(ai.Event{Type: "security.scan", Repo: "core-php", AgentID: "codex-1"}) type Event struct { @@ -39,14 +39,14 @@ func metricsDir() (string, error) { return filepath.Join(home, ".core", "ai", "metrics"), nil } -// metricsFilePath returns the JSONL path for a given date. +// metricsFilePath joins the daily JSONL filename onto dir. // // ai.metricsFilePath("/home/user/.core/ai/metrics", time.Now()) // → "/home/user/.core/ai/metrics/2026-03-31.jsonl" func metricsFilePath(dir string, t time.Time) string { return filepath.Join(dir, t.Format("2006-01-02")+".jsonl") } -// Record appends an event to the daily JSONL file at ~/.core/ai/metrics/YYYY-MM-DD.jsonl. +// Record appends an event to ~/.core/ai/metrics/YYYY-MM-DD.jsonl. // // ai.Record(ai.Event{Type: "security.scan", Repo: "go-ai", AgentID: "codex-1"}) func Record(event Event) (err error) { @@ -68,13 +68,13 @@ func Record(event Event) (err error) { path := metricsFilePath(dir, event.Timestamp) - w, err := coreio.Local.Append(path) + writer, err := coreio.Local.Append(path) if err != nil { return coreerr.E("ai.Record", "open metrics file for append", err) } defer func() { - if cerr := w.Close(); cerr != nil && err == nil { - err = coreerr.E("ai.Record", "close metrics file", cerr) + if closeErr := writer.Close(); closeErr != nil && err == nil { + err = coreerr.E("ai.Record", "close metrics file", closeErr) } }() @@ -83,14 +83,14 @@ func Record(event Event) (err error) { return coreerr.E("ai.Record", "marshal event", err) } - if _, err := w.Write(append(data, '\n')); err != nil { + if _, err := writer.Write(append(data, '\n')); err != nil { return coreerr.E("ai.Record", "write event", err) } return nil } -// ReadEvents reads all events recorded on or after since. +// ReadEvents returns events recorded on or after since. // // events, _ := ai.ReadEvents(time.Now().Add(-7 * 24 * time.Hour)) func ReadEvents(since time.Time) ([]Event, error) { @@ -102,8 +102,8 @@ func ReadEvents(since time.Time) ([]Event, error) { var events []Event now := time.Now() - for d := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, time.Local); !d.After(now); d = d.AddDate(0, 0, 1) { - path := metricsFilePath(dir, d) + for currentDay := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, time.Local); !currentDay.After(now); currentDay = currentDay.AddDate(0, 0, 1) { + path := metricsFilePath(dir, currentDay) dayEvents, err := readMetricsFile(path, since) if err != nil { @@ -115,7 +115,7 @@ func ReadEvents(since time.Time) ([]Event, error) { return events, nil } -// readMetricsFile reads events from a single JSONL file, skipping lines before since. +// readMetricsFile returns events from one JSONL file on or after since. // // events, _ := readMetricsFile("/home/user/.core/ai/metrics/2026-03-31.jsonl", time.Now().Add(-24*time.Hour)) func readMetricsFile(path string, since time.Time) ([]Event, error) { @@ -123,21 +123,21 @@ func readMetricsFile(path string, since time.Time) ([]Event, error) { return nil, nil } - r, err := coreio.Local.ReadStream(path) + reader, err := coreio.Local.ReadStream(path) if err != nil { return nil, coreerr.E("ai.readMetricsFile", "open metrics file", err) } - defer func() { _ = r.Close() }() + defer func() { _ = reader.Close() }() var events []Event - scanner := bufio.NewScanner(r) + scanner := bufio.NewScanner(reader) for scanner.Scan() { - var ev Event - if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil { + var event Event + if err := json.Unmarshal(scanner.Bytes(), &event); err != nil { continue } - if !ev.Timestamp.Before(since) { - events = append(events, ev) + if !event.Timestamp.Before(since) { + events = append(events, event) } } if err := scanner.Err(); err != nil { @@ -146,11 +146,11 @@ func readMetricsFile(path string, since time.Time) ([]Event, error) { return events, nil } -// Summary aggregates events into counts by type, repo, and agent. +// Summary groups events by type, repo, and agent. // // summary := ai.Summary(events) -// summary["total"] // int — total event count -// summary["by_type"] // []map[string]any — sorted by count descending +// summary["total"] // int — total event count +// summary["by_type"] // []map[string]any — sorted by count descending func Summary(events []Event) map[string]any { byType := make(map[string]int) byRepo := make(map[string]int) @@ -174,26 +174,26 @@ func Summary(events []Event) map[string]any { } } -// sortedMap converts a string→count map to a slice sorted by count descending. +// sortedMap converts counts into rows sorted by count descending. // // sortedMap(map[string]int{"build": 5, "test": 2}) // → [{key: "build", count: 5}, ...] -func sortedMap(m map[string]int) []map[string]any { - type entry struct { +func sortedMap(countByName map[string]int) []map[string]any { + type countEntry struct { key string count int } - entries := make([]entry, 0, len(m)) - for k, v := range m { - entries = append(entries, entry{k, v}) + countEntries := make([]countEntry, 0, len(countByName)) + for name, count := range countByName { + countEntries = append(countEntries, countEntry{name, count}) } - slices.SortFunc(entries, func(a, b entry) int { - return cmp.Compare(b.count, a.count) + slices.SortFunc(countEntries, func(left, right countEntry) int { + return cmp.Compare(right.count, left.count) }) - result := make([]map[string]any, len(entries)) - for i, entry := range entries { - result[i] = map[string]any{"key": entry.key, "count": entry.count} + rows := make([]map[string]any, len(countEntries)) + for rowIndex, countEntry := range countEntries { + rows[rowIndex] = map[string]any{"key": countEntry.key, "count": countEntry.count} } - return result + return rows } diff --git a/ai/metrics_bench_test.go b/ai/metrics_bench_test.go index fe16bb7..7bffef6 100644 --- a/ai/metrics_bench_test.go +++ b/ai/metrics_bench_test.go @@ -10,8 +10,6 @@ import ( coreio "dappco.re/go/core/io" ) -// --- Helpers --- - // setupBenchMetricsDir overrides HOME to a temp dir and returns the metrics path. // // metricsPath := setupBenchMetricsDir(b) // → "/tmp/.../home/.core/ai/metrics" @@ -36,22 +34,20 @@ func setupBenchMetricsDir(b *testing.B) string { func seedEvents(b *testing.B, count int) { b.Helper() now := time.Now() - for i := range count { - ev := Event{ - Type: fmt.Sprintf("type-%d", i%10), - Timestamp: now.Add(-time.Duration(i) * time.Millisecond), - AgentID: fmt.Sprintf("agent-%d", i%5), - Repo: fmt.Sprintf("repo-%d", i%3), - Data: map[string]any{"i": i, "tool": "bench_tool"}, + for eventIndex := range count { + event := Event{ + Type: fmt.Sprintf("type-%d", eventIndex%10), + Timestamp: now.Add(-time.Duration(eventIndex) * time.Millisecond), + AgentID: fmt.Sprintf("agent-%d", eventIndex%5), + Repo: fmt.Sprintf("repo-%d", eventIndex%3), + Data: map[string]any{"index": eventIndex, "tool": "bench_tool"}, } - if err := Record(ev); err != nil { - b.Fatalf("Failed to record event %d: %v", i, err) + if err := Record(event); err != nil { + b.Fatalf("Failed to record event %d: %v", eventIndex, err) } } } -// --- Benchmarks --- - // BenchmarkMetricsRecord benchmarks writing individual metric events. func BenchmarkMetricsRecord(b *testing.B) { setupBenchMetricsDir(b) @@ -59,16 +55,16 @@ func BenchmarkMetricsRecord(b *testing.B) { now := time.Now() b.ResetTimer() - for i := range b.N { - ev := Event{ + for sampleIndex := range b.N { + event := Event{ Type: "bench_record", Timestamp: now, AgentID: "bench-agent", Repo: "bench-repo", - Data: map[string]any{"i": i}, + Data: map[string]any{"index": sampleIndex}, } - if err := Record(ev); err != nil { - b.Fatalf("Record failed at iteration %d: %v", i, err) + if err := Record(event); err != nil { + b.Fatalf("Record failed at iteration %d: %v", sampleIndex, err) } } } @@ -81,19 +77,19 @@ func BenchmarkMetricsRecord_Parallel(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { - i := 0 + sampleIndex := 0 for pb.Next() { - ev := Event{ + event := Event{ Type: "bench_parallel", Timestamp: now, AgentID: "bench-agent", Repo: "bench-repo", - Data: map[string]any{"i": i}, + Data: map[string]any{"index": sampleIndex}, } - if err := Record(ev); err != nil { + if err := Record(event); err != nil { b.Fatalf("Parallel Record failed: %v", err) } - i++ + sampleIndex++ } }) } @@ -163,14 +159,14 @@ func BenchmarkMetricsRecordAndQuery(b *testing.B) { now := time.Now() // Write 10K events - for i := range 10_000 { - ev := Event{ - Type: fmt.Sprintf("type-%d", i%10), + for eventIndex := range 10_000 { + event := Event{ + Type: fmt.Sprintf("type-%d", eventIndex%10), Timestamp: now, AgentID: "bench", Repo: "bench-repo", } - if err := Record(ev); err != nil { + if err := Record(event); err != nil { b.Fatalf("Record failed: %v", err) } } @@ -187,52 +183,45 @@ func BenchmarkMetricsRecordAndQuery(b *testing.B) { } } -// --- Unit tests for metrics at scale --- - // TestMetricsBench_RecordAndRead_10K_Good writes 10K events and reads them back. func TestMetricsBench_RecordAndRead_10K_Good(t *testing.T) { withTempHome(t) now := time.Now() - const n = 10_000 + const eventCount = 10_000 - // Write events - for i := range n { - ev := Event{ - Type: fmt.Sprintf("type-%d", i%10), - Timestamp: now.Add(-time.Duration(i) * time.Millisecond), - AgentID: fmt.Sprintf("agent-%d", i%5), - Repo: fmt.Sprintf("repo-%d", i%3), - Data: map[string]any{"index": i}, + for eventIndex := range eventCount { + event := Event{ + Type: fmt.Sprintf("type-%d", eventIndex%10), + Timestamp: now.Add(-time.Duration(eventIndex) * time.Millisecond), + AgentID: fmt.Sprintf("agent-%d", eventIndex%5), + Repo: fmt.Sprintf("repo-%d", eventIndex%3), + Data: map[string]any{"index": eventIndex}, } - if err := Record(ev); err != nil { - t.Fatalf("Record failed at %d: %v", i, err) + if err := Record(event); err != nil { + t.Fatalf("Record failed at %d: %v", eventIndex, err) } } - // Read back since := now.Add(-24 * time.Hour) events, err := ReadEvents(since) if err != nil { t.Fatalf("ReadEvents failed: %v", err) } - if len(events) != n { - t.Errorf("Expected %d events, got %d", n, len(events)) + if len(events) != eventCount { + t.Errorf("Expected %d events, got %d", eventCount, len(events)) } - // Summarise summary := Summary(events) total, ok := summary["total"].(int) - if !ok || total != n { - t.Errorf("Expected total %d, got %v", n, summary["total"]) + if !ok || total != eventCount { + t.Errorf("Expected total %d, got %v", eventCount, summary["total"]) } - // Verify aggregation counts byType, ok := summary["by_type"].([]map[string]any) if !ok || len(byType) == 0 { t.Fatal("Expected non-empty by_type") } - // Each of 10 types should have n/10 = 1000 events for _, entry := range byType { count, _ := entry["count"].(int) if count != 1000 { diff --git a/ai/metrics_test.go b/ai/metrics_test.go index 45431ed..1e2de54 100644 --- a/ai/metrics_test.go +++ b/ai/metrics_test.go @@ -22,18 +22,16 @@ func withTempHome(t *testing.T) { t.Cleanup(func() { os.Setenv("HOME", origHome) }) } -// --- Record --- - func TestMetrics_Record_Good(t *testing.T) { withTempHome(t) - ev := Event{ + event := Event{ Type: "test_event", AgentID: "agent-1", Repo: "repo-1", Data: map[string]any{"key": "value"}, } - if err := Record(ev); err != nil { + if err := Record(event); err != nil { t.Fatalf("Record: %v", err) } @@ -56,8 +54,8 @@ func TestMetrics_Record_Good_AutoTimestamp(t *testing.T) { withTempHome(t) before := time.Now() - ev := Event{Type: "auto_ts"} - if err := Record(ev); err != nil { + event := Event{Type: "auto_ts"} + if err := Record(event); err != nil { t.Fatalf("Record: %v", err) } after := time.Now() @@ -79,8 +77,8 @@ func TestMetrics_Record_Good_PresetTimestamp(t *testing.T) { withTempHome(t) fixed := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC) - ev := Event{Type: "preset_ts", Timestamp: fixed} - if err := Record(ev); err != nil { + event := Event{Type: "preset_ts", Timestamp: fixed} + if err := Record(event); err != nil { t.Fatalf("Record: %v", err) } @@ -96,8 +94,6 @@ func TestMetrics_Record_Good_PresetTimestamp(t *testing.T) { } } -// --- ReadEvents --- - func TestMetrics_ReadEvents_Good_Empty(t *testing.T) { withTempHome(t) @@ -116,19 +112,16 @@ func TestMetrics_ReadEvents_Good_MultiDay(t *testing.T) { now := time.Now() yesterday := now.AddDate(0, 0, -1) - // Write event for yesterday - ev1 := Event{Type: "yesterday", Timestamp: yesterday} - if err := Record(ev1); err != nil { + yesterdayEvent := Event{Type: "yesterday", Timestamp: yesterday} + if err := Record(yesterdayEvent); err != nil { t.Fatalf("Record yesterday: %v", err) } - // Write event for today - ev2 := Event{Type: "today", Timestamp: now} - if err := Record(ev2); err != nil { + todayEvent := Event{Type: "today", Timestamp: now} + if err := Record(todayEvent); err != nil { t.Fatalf("Record today: %v", err) } - // Read from 2 days ago — should get both events, err := ReadEvents(now.AddDate(0, 0, -2)) if err != nil { t.Fatalf("ReadEvents: %v", err) @@ -142,17 +135,15 @@ func TestMetrics_ReadEvents_Good_FiltersBySince(t *testing.T) { withTempHome(t) now := time.Now() - // Write an old event and a recent one - old := Event{Type: "old", Timestamp: now.Add(-2 * time.Hour)} - if err := Record(old); err != nil { + oldEvent := Event{Type: "old", Timestamp: now.Add(-2 * time.Hour)} + if err := Record(oldEvent); err != nil { t.Fatalf("Record old: %v", err) } - recent := Event{Type: "recent", Timestamp: now} - if err := Record(recent); err != nil { + recentEvent := Event{Type: "recent", Timestamp: now} + if err := Record(recentEvent); err != nil { t.Fatalf("Record recent: %v", err) } - // Read only events from the last hour events, err := ReadEvents(now.Add(-time.Hour)) if err != nil { t.Fatalf("ReadEvents: %v", err) @@ -165,8 +156,6 @@ func TestMetrics_ReadEvents_Good_FiltersBySince(t *testing.T) { } } -// --- readMetricsFile --- - func TestMetrics_ReadMetricsFile_Good_MalformedLines(t *testing.T) { withTempHome(t) @@ -175,7 +164,6 @@ func TestMetrics_ReadMetricsFile_Good_MalformedLines(t *testing.T) { t.Fatalf("metricsDir: %v", err) } - // Write a file with a mix of valid and invalid lines path := metricsFilePath(dir, time.Now()) content := `{"type":"valid","timestamp":"2026-03-15T10:00:00Z"} not-json @@ -205,8 +193,6 @@ func TestMetrics_ReadMetricsFile_Good_NonExistent(t *testing.T) { } } -// --- metricsFilePath --- - func TestMetrics_MetricsFilePath_Good(t *testing.T) { ts := time.Date(2026, 3, 17, 14, 30, 0, 0, time.UTC) got := metricsFilePath("/base", ts) @@ -216,13 +202,11 @@ func TestMetrics_MetricsFilePath_Good(t *testing.T) { } } -// --- Summary --- - func TestMetrics_Summary_Good_Empty(t *testing.T) { - s := Summary(nil) - total, ok := s["total"].(int) + summary := Summary(nil) + total, ok := summary["total"].(int) if !ok || total != 0 { - t.Errorf("expected total 0, got %v", s["total"]) + t.Errorf("expected total 0, got %v", summary["total"]) } } @@ -233,25 +217,22 @@ func TestMetrics_Summary_Good(t *testing.T) { {Type: "test", Repo: "core-api", AgentID: "agent-1"}, } - s := Summary(events) + summary := Summary(events) - total, _ := s["total"].(int) + total, _ := summary["total"].(int) if total != 3 { t.Errorf("expected total 3, got %d", total) } - byType, _ := s["by_type"].([]map[string]any) + byType, _ := summary["by_type"].([]map[string]any) if len(byType) != 2 { t.Fatalf("expected 2 types, got %d", len(byType)) } - // Sorted by count descending — "build" (2) first if byType[0]["key"] != "build" || byType[0]["count"] != 2 { t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"]) } } -// --- sortedMap --- - func TestMetrics_SortedMap_Good_Empty(t *testing.T) { result := sortedMap(map[string]int{}) if len(result) != 0 { @@ -260,12 +241,11 @@ func TestMetrics_SortedMap_Good_Empty(t *testing.T) { } func TestMetrics_SortedMap_Good_Ordering(t *testing.T) { - m := map[string]int{"a": 1, "b": 3, "c": 2} - result := sortedMap(m) + counts := map[string]int{"a": 1, "b": 3, "c": 2} + result := sortedMap(counts) if len(result) != 3 { t.Fatalf("expected 3 entries, got %d", len(result)) } - // Descending by count if result[0]["key"] != "b" { t.Errorf("expected first key 'b', got %v", result[0]["key"]) } diff --git a/ai/rag.go b/ai/rag.go index 60fa7ff..d7a2f6e 100644 --- a/ai/rag.go +++ b/ai/rag.go @@ -8,7 +8,7 @@ import ( "forge.lthn.ai/core/go-rag" ) -// TaskInfo carries the task data used to build a RAG query. +// TaskInfo is the input for QueryRAGForTask. // // ai.QueryRAGForTask(ai.TaskInfo{Title: "Fix auth", Description: "JWT expiry not checked"}) type TaskInfo struct { @@ -20,22 +20,22 @@ type TaskInfo struct { // // ctx, _ := ai.QueryRAGForTask(ai.TaskInfo{Title: "Fix auth bug", Description: "JWT expiry not checked"}) func QueryRAGForTask(task TaskInfo) (string, error) { - query := task.Title + " " + task.Description + queryText := task.Title + " " + task.Description - runes := []rune(query) + runes := []rune(queryText) if len(runes) > 500 { - query = string(runes[:500]) + queryText = string(runes[:500]) } - qdrantConfig := rag.DefaultQdrantConfig() - qdrantClient, err := rag.NewQdrantClient(qdrantConfig) + qdrantConfiguration := rag.DefaultQdrantConfig() + qdrantClient, err := rag.NewQdrantClient(qdrantConfiguration) if err != nil { return "", coreerr.E("ai.QueryRAGForTask", "rag qdrant client", err) } defer func() { _ = qdrantClient.Close() }() - ollamaConfig := rag.DefaultOllamaConfig() - ollamaClient, err := rag.NewOllamaClient(ollamaConfig) + ollamaConfiguration := rag.DefaultOllamaConfig() + ollamaClient, err := rag.NewOllamaClient(ollamaConfiguration) if err != nil { return "", coreerr.E("ai.QueryRAGForTask", "rag ollama client", err) } @@ -43,13 +43,13 @@ func QueryRAGForTask(task TaskInfo) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - queryConfig := rag.QueryConfig{ + searchConfiguration := rag.QueryConfig{ Collection: "hostuk-docs", Limit: 3, Threshold: 0.5, } - results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, queryConfig) + results, err := rag.Query(ctx, qdrantClient, ollamaClient, queryText, searchConfiguration) if err != nil { return "", coreerr.E("ai.QueryRAGForTask", "rag query", err) } diff --git a/cmd/embed-bench/main.go b/cmd/embed-bench/main.go index b579e7a..9b4f72b 100644 --- a/cmd/embed-bench/main.go +++ b/cmd/embed-bench/main.go @@ -27,14 +27,11 @@ import ( var ollamaURL = flag.String("ollama", "http://localhost:11434", "Ollama base URL") -// models to benchmark var models = []string{ "nomic-embed-text", "embeddinggemma", } -// Test corpus: real-ish agent memories grouped by topic. -// Memories within a group should be similar; across groups should be distant. var memoryGroups = []struct { topic string memories []string @@ -73,7 +70,6 @@ var memoryGroups = []struct { }, } -// Queries to test recall quality — each has a target topic it should match best. var queries = []struct { query string targetTopic string @@ -98,46 +94,43 @@ func main() { fmt.Printf("\n## Model: %s\n", model) fmt.Println(strings.Repeat("-", 40)) - // Check model is available if !modelAvailable(model) { fmt.Printf(" SKIPPED — model not pulled (run: ollama pull %s)\n", model) continue } - // 1. Embed all memories - allMemories := []string{} - allTopics := []string{} + memoryTexts := []string{} + memoryTopics := []string{} for _, group := range memoryGroups { - for _, mem := range group.memories { - allMemories = append(allMemories, mem) - allTopics = append(allTopics, group.topic) + for _, memoryText := range group.memories { + memoryTexts = append(memoryTexts, memoryText) + memoryTopics = append(memoryTopics, group.topic) } } - fmt.Printf(" Embedding %d memories...\n", len(allMemories)) + fmt.Printf(" Embedding %d memories...\n", len(memoryTexts)) start := time.Now() - memVectors := make([][]float64, len(allMemories)) - for i, mem := range allMemories { - vec, err := embed(model, mem) + memoryVectors := make([][]float64, len(memoryTexts)) + for memoryIndex, memoryText := range memoryTexts { + vector, err := embed(model, memoryText) if err != nil { - fmt.Printf(" ERROR embedding memory %d: %v\n", i, err) + fmt.Printf(" ERROR embedding memory %d: %v\n", memoryIndex, err) break } - memVectors[i] = vec + memoryVectors[memoryIndex] = vector } embedTime := time.Since(start) - fmt.Printf(" Embedded in %v (%.0fms/memory)\n", embedTime, float64(embedTime.Milliseconds())/float64(len(allMemories))) - fmt.Printf(" Vector dimension: %d\n", len(memVectors[0])) + fmt.Printf(" Embedded in %v (%.0fms/memory)\n", embedTime, float64(embedTime.Milliseconds())/float64(len(memoryTexts))) + fmt.Printf(" Vector dimension: %d\n", len(memoryVectors[0])) - // 2. Intra-group vs inter-group similarity var intraSims, interSims []float64 - for i := 0; i < len(allMemories); i++ { - for j := i + 1; j < len(allMemories); j++ { - sim := cosine(memVectors[i], memVectors[j]) - if allTopics[i] == allTopics[j] { - intraSims = append(intraSims, sim) + for leftIndex := 0; leftIndex < len(memoryTexts); leftIndex++ { + for rightIndex := leftIndex + 1; rightIndex < len(memoryTexts); rightIndex++ { + similarity := cosine(memoryVectors[leftIndex], memoryVectors[rightIndex]) + if memoryTopics[leftIndex] == memoryTopics[rightIndex] { + intraSims = append(intraSims, similarity) } else { - interSims = append(interSims, sim) + interSims = append(interSims, similarity) } } } @@ -151,28 +144,26 @@ func main() { fmt.Printf(" Inter-group similarity (diff topic): %.4f\n", interAvg) fmt.Printf(" Separation gap: %.4f %s\n", separation, qualityLabel(separation)) - // 3. Query recall accuracy fmt.Printf("\n Query recall (top-1 accuracy):\n") correct := 0 for _, queryCase := range queries { - queryVec, err := embed(model, queryCase.query) + queryVector, err := embed(model, queryCase.query) if err != nil { fmt.Printf(" ERROR: %v\n", err) continue } - // Find best match - bestIdx := 0 - bestSim := -1.0 - for i, mv := range memVectors { - sim := cosine(queryVec, mv) - if sim > bestSim { - bestSim = sim - bestIdx = i + bestIndex := 0 + bestSimilarity := -1.0 + for memoryIndex, memoryVector := range memoryVectors { + similarity := cosine(queryVector, memoryVector) + if similarity > bestSimilarity { + bestSimilarity = similarity + bestIndex = memoryIndex } } - matchTopic := allTopics[bestIdx] + matchTopic := memoryTopics[bestIndex] hit := matchTopic == queryCase.targetTopic if hit { correct++ @@ -181,29 +172,30 @@ func main() { if !hit { marker = "✗" } - fmt.Printf(" %s %.4f %q → %s (want: %s)\n", marker, bestSim, truncate(queryCase.query, 40), matchTopic, queryCase.targetTopic) + fmt.Printf(" %s %.4f %q → %s (want: %s)\n", marker, bestSimilarity, truncate(queryCase.query, 40), matchTopic, queryCase.targetTopic) } accuracy := float64(correct) / float64(len(queries)) * 100 fmt.Printf("\n Top-1 accuracy: %.0f%% (%d/%d)\n", accuracy, correct, len(queries)) - // 4. Top-3 recall correct3 := 0 for _, queryCase := range queries { - queryVec, _ := embed(model, queryCase.query) + queryVector, _ := embed(model, queryCase.query) - type scored struct { - idx int - sim float64 + type scoredResult struct { + index int + sim float64 } - var ranked []scored - for i, mv := range memVectors { - ranked = append(ranked, scored{i, cosine(queryVec, mv)}) + var rankedResults []scoredResult + for memoryIndex, memoryVector := range memoryVectors { + rankedResults = append(rankedResults, scoredResult{memoryIndex, cosine(queryVector, memoryVector)}) } - sort.Slice(ranked, func(a, b int) bool { return ranked[a].sim > ranked[b].sim }) + sort.Slice(rankedResults, func(leftIndex, rightIndex int) bool { + return rankedResults[leftIndex].sim > rankedResults[rightIndex].sim + }) - for _, rankedResult := range ranked[:3] { - if allTopics[rankedResult.idx] == queryCase.targetTopic { + for _, rankedResult := range rankedResults[:3] { + if memoryTopics[rankedResult.index] == queryCase.targetTopic { correct3++ break } @@ -217,7 +209,6 @@ func main() { fmt.Println("Done.") } -// httpClient trusts self-signed certs for .lan domains behind Traefik. var httpClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // .lan only @@ -272,7 +263,6 @@ func modelAvailable(model string) bool { } json.NewDecoder(resp.Body).Decode(&result) for _, registeredModel := range result.Models { - // Match "nomic-embed-text:latest" against "nomic-embed-text" if registeredModel.Name == model || strings.HasPrefix(registeredModel.Name, model+":") { return true } @@ -284,32 +274,32 @@ func modelAvailable(model string) bool { // // cosine([]float64{1, 0}, []float64{1, 0}) // → 1.0 // cosine([]float64{1, 0}, []float64{0, 1}) // → 0.0 -func cosine(a, b []float64) float64 { - var dot, normA, normB float64 - for i := range a { - dot += a[i] * b[i] - normA += a[i] * a[i] - normB += b[i] * b[i] +func cosine(firstVector, secondVector []float64) float64 { + var dotProduct, firstMagnitudeSquared, secondMagnitudeSquared float64 + for index := range firstVector { + dotProduct += firstVector[index] * secondVector[index] + firstMagnitudeSquared += firstVector[index] * firstVector[index] + secondMagnitudeSquared += secondVector[index] * secondVector[index] } - denom := math.Sqrt(normA) * math.Sqrt(normB) - if denom == 0 { + denominator := math.Sqrt(firstMagnitudeSquared) * math.Sqrt(secondMagnitudeSquared) + if denominator == 0 { return 0 } - return dot / denom + return dotProduct / denominator } // avg returns the arithmetic mean of a float64 slice, or 0 for an empty slice. // // avg([]float64{0.8, 0.6, 0.9}) // → 0.7666... -func avg(vals []float64) float64 { - if len(vals) == 0 { +func avg(values []float64) float64 { + if len(values) == 0 { return 0 } - sum := 0.0 - for _, value := range vals { - sum += value + total := 0.0 + for _, value := range values { + total += value } - return sum / float64(len(vals)) + return total / float64(len(values)) } // qualityLabel maps a cosine separation gap to a human-readable quality band. @@ -332,14 +322,13 @@ func qualityLabel(gap float64) string { // truncate shortens s to at most n characters, appending "..." if truncated. // // truncate("How does the emotional scoring work?", 20) // → "How does the emot..." -func truncate(s string, n int) string { - if len(s) <= n { - return s +func truncate(text string, limit int) string { + if len(text) <= limit { + return text } - return s[:n-3] + "..." + return text[:limit-3] + "..." } func init() { - // Ensure stderr doesn't buffer os.Stderr.Sync() } diff --git a/cmd/lab/cmd_lab.go b/cmd/lab/cmd_lab.go index 9436902..e246be2 100644 --- a/cmd/lab/cmd_lab.go +++ b/cmd/lab/cmd_lab.go @@ -21,17 +21,17 @@ func init() { cli.RegisterCommands(AddLabCommands) } -var labCmd = &cli.Command{ +var labCommand = &cli.Command{ Use: "lab", Short: "Homelab monitoring dashboard", Long: "Lab dashboard with real-time monitoring of machines, training runs, models, and services.", } var ( - labBind string + labBindAddress string ) -var serveCmd = &cli.Command{ +var serveCommand = &cli.Command{ Use: "serve", Short: "Start the lab dashboard web server", Long: "Starts the lab dashboard HTTP server with live-updating collectors for system stats, Docker, Forgejo, HuggingFace, InfluxDB, and more.", @@ -39,99 +39,99 @@ var serveCmd = &cli.Command{ } func init() { - serveCmd.Flags().StringVar(&labBind, "bind", ":8080", "HTTP listen address") + serveCommand.Flags().StringVar(&labBindAddress, "bind", ":8080", "HTTP listen address") } // AddLabCommands registers the lab command tree with the root command. // // lab.AddLabCommands(rootCmd) // → core lab serve --bind :8080 func AddLabCommands(root *cli.Command) { - labCmd.AddCommand(serveCmd) - root.AddCommand(labCmd) + labCommand.AddCommand(serveCommand) + root.AddCommand(labCommand) } -func runServe(cmd *cli.Command, args []string) error { - cfg := lab.LoadConfig() - cfg.Addr = labBind +func runServe(_ *cli.Command, _ []string) error { + configuration := lab.LoadConfig() + configuration.Addr = labBindAddress store := lab.NewStore() logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - reg := collector.NewRegistry(logger) - reg.Register(collector.NewSystem(cfg, store), 60*time.Second) - reg.Register(collector.NewPrometheus(cfg.PrometheusURL, store), - time.Duration(cfg.PrometheusInterval)*time.Second) - reg.Register(collector.NewHuggingFace(cfg.HFAuthor, store), - time.Duration(cfg.HFInterval)*time.Second) - reg.Register(collector.NewDocker(store), - time.Duration(cfg.DockerInterval)*time.Second) + registry := collector.NewRegistry(logger) + registry.Register(collector.NewSystem(configuration, store), 60*time.Second) + registry.Register(collector.NewPrometheus(configuration.PrometheusURL, store), + time.Duration(configuration.PrometheusInterval)*time.Second) + registry.Register(collector.NewHuggingFace(configuration.HFAuthor, store), + time.Duration(configuration.HFInterval)*time.Second) + registry.Register(collector.NewDocker(store), + time.Duration(configuration.DockerInterval)*time.Second) - if cfg.ForgeToken != "" { - reg.Register(collector.NewForgejo(cfg.ForgeURL, cfg.ForgeToken, store), - time.Duration(cfg.ForgeInterval)*time.Second) + if configuration.ForgeToken != "" { + registry.Register(collector.NewForgejo(configuration.ForgeURL, configuration.ForgeToken, store), + time.Duration(configuration.ForgeInterval)*time.Second) } - reg.Register(collector.NewTraining(cfg, store), - time.Duration(cfg.TrainingInterval)*time.Second) - reg.Register(collector.NewServices(store), 60*time.Second) + registry.Register(collector.NewTraining(configuration, store), + time.Duration(configuration.TrainingInterval)*time.Second) + registry.Register(collector.NewServices(store), 60*time.Second) - if cfg.InfluxToken != "" { - reg.Register(collector.NewInfluxDB(cfg, store), - time.Duration(cfg.InfluxInterval)*time.Second) + if configuration.InfluxToken != "" { + registry.Register(collector.NewInfluxDB(configuration, store), + time.Duration(configuration.InfluxInterval)*time.Second) } - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + shutdownContext, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() - reg.Start(ctx) - defer reg.Stop() + registry.Start(shutdownContext) + defer registry.Stop() - web := handler.NewWebHandler(store) - api := handler.NewAPIHandler(store) + webHandler := handler.NewWebHandler(store) + apiHandler := handler.NewAPIHandler(store) - mux := http.NewServeMux() + serveMux := http.NewServeMux() - mux.HandleFunc("GET /", web.Dashboard) - mux.HandleFunc("GET /models", web.Models) - mux.HandleFunc("GET /training", web.Training) - mux.HandleFunc("GET /dataset", web.Dataset) - mux.HandleFunc("GET /golden-set", func(w http.ResponseWriter, r *http.Request) { + serveMux.HandleFunc("GET /", webHandler.Dashboard) + serveMux.HandleFunc("GET /models", webHandler.Models) + serveMux.HandleFunc("GET /training", webHandler.Training) + serveMux.HandleFunc("GET /dataset", webHandler.Dataset) + serveMux.HandleFunc("GET /golden-set", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/dataset", http.StatusMovedPermanently) }) - mux.HandleFunc("GET /runs", func(w http.ResponseWriter, r *http.Request) { + serveMux.HandleFunc("GET /runs", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/training", http.StatusMovedPermanently) }) - mux.HandleFunc("GET /agents", web.Agents) - mux.HandleFunc("GET /services", web.Services) + serveMux.HandleFunc("GET /agents", webHandler.Agents) + serveMux.HandleFunc("GET /services", webHandler.Services) - mux.HandleFunc("GET /events", web.Events) + serveMux.HandleFunc("GET /events", webHandler.Events) - mux.HandleFunc("GET /api/status", api.Status) - mux.HandleFunc("GET /api/models", api.Models) - mux.HandleFunc("GET /api/training", api.Training) - mux.HandleFunc("GET /api/dataset", api.GoldenSet) - mux.HandleFunc("GET /api/golden-set", api.GoldenSet) - mux.HandleFunc("GET /api/runs", api.Runs) - mux.HandleFunc("GET /api/agents", api.Agents) - mux.HandleFunc("GET /api/services", api.Services) - mux.HandleFunc("GET /health", api.Health) + serveMux.HandleFunc("GET /api/status", apiHandler.Status) + serveMux.HandleFunc("GET /api/models", apiHandler.Models) + serveMux.HandleFunc("GET /api/training", apiHandler.Training) + serveMux.HandleFunc("GET /api/dataset", apiHandler.GoldenSet) + serveMux.HandleFunc("GET /api/golden-set", apiHandler.GoldenSet) + serveMux.HandleFunc("GET /api/runs", apiHandler.Runs) + serveMux.HandleFunc("GET /api/agents", apiHandler.Agents) + serveMux.HandleFunc("GET /api/services", apiHandler.Services) + serveMux.HandleFunc("GET /health", apiHandler.Health) - srv := &http.Server{ - Addr: cfg.Addr, - Handler: mux, + server := &http.Server{ + Addr: configuration.Addr, + Handler: serveMux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } go func() { - <-ctx.Done() + <-shutdownContext.Done() logger.Info("shutting down") - shutCtx, shutCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer shutCancel() - srv.Shutdown(shutCtx) + shutdownDeadline, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + server.Shutdown(shutdownDeadline) }() - logger.Info("lab dashboard starting", "addr", cfg.Addr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Info("lab dashboard starting", "addr", configuration.Addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { return err } return nil diff --git a/cmd/metrics/cmd.go b/cmd/metrics/cmd.go index 811ba87..039709c 100644 --- a/cmd/metrics/cmd.go +++ b/cmd/metrics/cmd.go @@ -4,12 +4,14 @@ package metrics import ( "encoding/json" "fmt" + "strconv" + "strings" "time" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/ai/ai" "dappco.re/go/core/i18n" coreerr "dappco.re/go/core/log" + "forge.lthn.ai/core/cli/pkg/cli" ) var ( @@ -21,7 +23,7 @@ var metricsCmd = &cli.Command{ Use: "metrics", Short: i18n.T("cmd.ai.metrics.short"), Long: i18n.T("cmd.ai.metrics.long"), - RunE: func(cmd *cli.Command, args []string) error { + RunE: func(_ *cli.Command, _ []string) error { return runMetrics() }, } @@ -40,54 +42,54 @@ func AddMetricsCommand(parent *cli.Command) { } func runMetrics() error { - since, err := parseDuration(metricsSince) + sinceDuration, err := parseDuration(metricsSince) if err != nil { return cli.Err("invalid --since value %q: %v", metricsSince, err) } - sinceTime := time.Now().Add(-since) + sinceTime := time.Now().Add(-sinceDuration) events, err := ai.ReadEvents(sinceTime) if err != nil { return cli.WrapVerb(err, "read", "metrics") } if metricsJSON { - summary := ai.Summary(events) - output, err := json.MarshalIndent(summary, "", " ") + eventSummary := ai.Summary(events) + jsonOutput, err := json.MarshalIndent(eventSummary, "", " ") if err != nil { return cli.Wrap(err, "marshal JSON output") } - cli.Text(string(output)) + cli.Text(string(jsonOutput)) return nil } - summary := ai.Summary(events) + eventSummary := ai.Summary(events) cli.Blank() cli.Print("%s %s\n", cli.DimStyle.Render("Period:"), metricsSince) - total, _ := summary["total"].(int) - cli.Print("%s %d\n", cli.DimStyle.Render("Total events:"), total) + eventCount, _ := eventSummary["total"].(int) + cli.Print("%s %d\n", cli.DimStyle.Render("Total events:"), eventCount) cli.Blank() - if byType, ok := summary["by_type"].([]map[string]any); ok && len(byType) > 0 { + if typeCounts, ok := eventSummary["by_type"].([]map[string]any); ok && len(typeCounts) > 0 { cli.Print("%s\n", cli.DimStyle.Render("By type:")) - for _, entry := range byType { + for _, entry := range typeCounts { cli.Print(" %-30s %v\n", entry["key"], entry["count"]) } cli.Blank() } - if byRepo, ok := summary["by_repo"].([]map[string]any); ok && len(byRepo) > 0 { + if repoCounts, ok := eventSummary["by_repo"].([]map[string]any); ok && len(repoCounts) > 0 { cli.Print("%s\n", cli.DimStyle.Render("By repo:")) - for _, entry := range byRepo { + for _, entry := range repoCounts { cli.Print(" %-30s %v\n", entry["key"], entry["count"]) } cli.Blank() } - if byAgent, ok := summary["by_agent"].([]map[string]any); ok && len(byAgent) > 0 { + if agentCounts, ok := eventSummary["by_agent"].([]map[string]any); ok && len(agentCounts) > 0 { cli.Print("%s\n", cli.DimStyle.Render("By contributor:")) - for _, entry := range byAgent { + for _, entry := range agentCounts { cli.Print(" %-30s %v\n", entry["key"], entry["count"]) } cli.Blank() @@ -105,31 +107,31 @@ func runMetrics() error { // parseDuration("7d") // → 7 * 24 * time.Hour // parseDuration("24h") // → 24 * time.Hour // parseDuration("30m") // → 30 * time.Minute -func parseDuration(s string) (time.Duration, error) { - if len(s) < 2 { - return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil) +func parseDuration(durationText string) (time.Duration, error) { + if len(durationText) < 2 { + return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", durationText), nil) } - unitChar := s[len(s)-1] - numericPart := s[:len(s)-1] + unitSuffix := durationText[len(durationText)-1] + countText := strings.TrimSpace(durationText[:len(durationText)-1]) - var count int - if _, err := fmt.Sscanf(numericPart, "%d", &count); err != nil { - return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil) + quantity, err := strconv.Atoi(countText) + if err != nil { + return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", durationText), nil) } - if count <= 0 { - return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("duration must be positive: %s", s), nil) + if quantity <= 0 { + return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("duration must be positive: %s", durationText), nil) } - switch unitChar { + switch unitSuffix { case 'd': - return time.Duration(count) * 24 * time.Hour, nil + return time.Duration(quantity) * 24 * time.Hour, nil case 'h': - return time.Duration(count) * time.Hour, nil + return time.Duration(quantity) * time.Hour, nil case 'm': - return time.Duration(count) * time.Minute, nil + return time.Duration(quantity) * time.Minute, nil default: - return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("unknown unit %c in duration: %s", unitChar, s), nil) + return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("unknown unit %c in duration: %s", unitSuffix, durationText), nil) } } diff --git a/cmd/metrics/cmd_test.go b/cmd/metrics/cmd_test.go index 54848bf..376a5dc 100644 --- a/cmd/metrics/cmd_test.go +++ b/cmd/metrics/cmd_test.go @@ -53,15 +53,15 @@ func TestCmd_ParseDuration_Ugly(t *testing.T) { input string wantErr bool }{ - // Leading whitespace — Sscanf skips it, so " 7d" parses as 7d (valid). + // Leading whitespace is trimmed from the count, so " 7d" still parses. {" 7d", false}, - // Trailing whitespace after unit — unit is last char, not whitespace, so this is invalid unit ' '. + // Trailing whitespace changes the unit suffix to space, so this is invalid. {"7d ", true}, // Very large count that still parses — 9999d is valid Go duration arithmetic. {"9999d", false}, // Mixed case unit — 'D' is not 'd', so unknown unit. {"7D", true}, - // Float value — Sscanf %d won't accept "7.5", so invalid numeric. + // Float value — strconv.Atoi rejects "7.5" as non-integer text. {"7.5d", true}, // Just the unit char, no number — too short (len < 2 for "d"; "hd" has no valid number). {"hd", true}, diff --git a/cmd/security/cmd_alerts.go b/cmd/security/cmd_alerts.go index b3aaff7..14f999e 100644 --- a/cmd/security/cmd_alerts.go +++ b/cmd/security/cmd_alerts.go @@ -4,30 +4,32 @@ import ( "encoding/json" "fmt" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/i18n" + "forge.lthn.ai/core/cli/pkg/cli" ) func addAlertsCommand(parent *cli.Command) { - cmd := &cli.Command{ + command := &cli.Command{ Use: "alerts", Short: i18n.T("cmd.security.alerts.short"), Long: i18n.T("cmd.security.alerts.long"), - RunE: func(c *cli.Command, args []string) error { + RunE: func(_ *cli.Command, _ []string) error { return runAlerts() }, } - cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) - cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) - cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) - cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) - cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) + command.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) + command.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) + command.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) + command.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + command.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) - parent.AddCommand(cmd) + parent.AddCommand(command) } -// AlertOutput is the unified alert shape written to JSON or the terminal. +// AlertOutput is the unified alert row used by the security commands. +// +// AlertOutput{Repo: "core-php", Severity: "high", Type: "dependabot"} type AlertOutput struct { Repo string `json:"repo"` Severity string `json:"severity"` @@ -48,12 +50,12 @@ func runAlerts() error { return runAlertsForTarget(securityTarget) } - reg, err := loadRegistry(securityRegistryPath) + registry, err := loadRegistry(securityRegistryPath) if err != nil { return err } - repoList := getReposToCheck(reg, securityRepo) + repoList := getReposToCheck(registry, securityRepo) if len(repoList) == 0 { return cli.Err("repo not found: %s", securityRepo) } @@ -62,11 +64,11 @@ func runAlerts() error { summary := &AlertSummary{} for _, repo := range repoList { - repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) + repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name) - depAlerts, err := fetchDependabotAlerts(repoFullName) + dependabotAlerts, err := fetchDependabotAlerts(repoFullName) if err == nil { - for _, alert := range depAlerts { + for _, alert := range dependabotAlerts { if alert.State != "open" { continue } @@ -87,9 +89,9 @@ func runAlerts() error { } } - codeAlerts, err := fetchCodeScanningAlerts(repoFullName) + codeScanningAlerts, err := fetchCodeScanningAlerts(repoFullName) if err == nil { - for _, alert := range codeAlerts { + for _, alert := range codeScanningAlerts { if alert.State != "open" { continue } @@ -110,9 +112,9 @@ func runAlerts() error { } } - secretAlerts, err := fetchSecretScanningAlerts(repoFullName) + secretScanningAlerts, err := fetchSecretScanningAlerts(repoFullName) if err == nil { - for _, alert := range secretAlerts { + for _, alert := range secretScanningAlerts { if alert.State != "open" { continue } @@ -149,7 +151,7 @@ func runAlerts() error { } for _, alert := range allAlerts { - sevStyle := severityStyle(alert.Severity) + severityRenderer := severityStyle(alert.Severity) location := alert.Package if location == "" { @@ -161,7 +163,7 @@ func runAlerts() error { cli.Print("%-20s %s %-16s %-40s %s\n", cli.ValueStyle.Render(alert.Repo), - sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)), alert.ID, location, cli.DimStyle.Render(alert.Type), @@ -184,9 +186,9 @@ func runAlertsForTarget(target string) error { var allAlerts []AlertOutput summary := &AlertSummary{} - depAlerts, err := fetchDependabotAlerts(fullName) + dependabotAlerts, err := fetchDependabotAlerts(fullName) if err == nil { - for _, alert := range depAlerts { + for _, alert := range dependabotAlerts { if alert.State != "open" { continue } @@ -207,9 +209,9 @@ func runAlertsForTarget(target string) error { } } - codeAlerts, err := fetchCodeScanningAlerts(fullName) + codeScanningAlerts, err := fetchCodeScanningAlerts(fullName) if err == nil { - for _, alert := range codeAlerts { + for _, alert := range codeScanningAlerts { if alert.State != "open" { continue } @@ -230,9 +232,9 @@ func runAlertsForTarget(target string) error { } } - secretAlerts, err := fetchSecretScanningAlerts(fullName) + secretScanningAlerts, err := fetchSecretScanningAlerts(fullName) if err == nil { - for _, alert := range secretAlerts { + for _, alert := range secretScanningAlerts { if alert.State != "open" { continue } @@ -268,7 +270,7 @@ func runAlertsForTarget(target string) error { } for _, alert := range allAlerts { - sevStyle := severityStyle(alert.Severity) + severityRenderer := severityStyle(alert.Severity) location := alert.Package if location == "" { location = alert.Location @@ -278,7 +280,7 @@ func runAlertsForTarget(target string) error { } cli.Print("%-20s %s %-16s %-40s %s\n", cli.ValueStyle.Render(alert.Repo), - sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)), alert.ID, location, cli.DimStyle.Render(alert.Type), diff --git a/cmd/security/cmd_deps.go b/cmd/security/cmd_deps.go index 755b2b0..ee18d29 100644 --- a/cmd/security/cmd_deps.go +++ b/cmd/security/cmd_deps.go @@ -4,30 +4,32 @@ import ( "encoding/json" "fmt" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/i18n" + "forge.lthn.ai/core/cli/pkg/cli" ) func addDepsCommand(parent *cli.Command) { - cmd := &cli.Command{ + command := &cli.Command{ Use: "deps", Short: i18n.T("cmd.security.deps.short"), Long: i18n.T("cmd.security.deps.long"), - RunE: func(c *cli.Command, args []string) error { + RunE: func(_ *cli.Command, _ []string) error { return runDeps() }, } - cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) - cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) - cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) - cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) - cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) + command.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) + command.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) + command.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) + command.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + command.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) - parent.AddCommand(cmd) + parent.AddCommand(command) } -// DepAlert is a Dependabot dependency vulnerability entry written to JSON or the terminal. +// DepAlert is the Dependabot row used by `core security deps`. +// +// DepAlert{Repo: "core-php", CVE: "CVE-2026-1234", Vulnerable: ">=1.2 <1.3"} type DepAlert struct { Repo string `json:"repo"` Severity string `json:"severity"` @@ -49,12 +51,12 @@ func runDeps() error { return runDepsForTarget(securityTarget) } - reg, err := loadRegistry(securityRegistryPath) + registry, err := loadRegistry(securityRegistryPath) if err != nil { return err } - repoList := getReposToCheck(reg, securityRepo) + repoList := getReposToCheck(registry, securityRepo) if len(repoList) == 0 { return cli.Err("repo not found: %s", securityRepo) } @@ -63,15 +65,15 @@ func runDeps() error { summary := &AlertSummary{} for _, repo := range repoList { - repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) + repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name) - alerts, err := fetchDependabotAlerts(repoFullName) + dependabotAlerts, err := fetchDependabotAlerts(repoFullName) if err != nil { cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err) continue } - for _, alert := range alerts { + for _, alert := range dependabotAlerts { if alert.State != "open" { continue } @@ -83,7 +85,7 @@ func runDeps() error { summary.Add(severity) - depAlert := DepAlert{ + dependabotAlert := DepAlert{ Repo: repo.Name, Severity: severity, CVE: alert.Advisory.CVEID, @@ -94,7 +96,7 @@ func runDeps() error { Manifest: alert.Dependency.ManifestPath, Summary: alert.Advisory.Summary, } - allAlerts = append(allAlerts, depAlert) + allAlerts = append(allAlerts, dependabotAlert) } } @@ -116,7 +118,7 @@ func runDeps() error { } for _, alert := range allAlerts { - sevStyle := severityStyle(alert.Severity) + severityRenderer := severityStyle(alert.Severity) upgrade := alert.Vulnerable if alert.PatchedVersion != "" { @@ -125,7 +127,7 @@ func runDeps() error { cli.Print("%-16s %s %-16s %-30s %s\n", cli.ValueStyle.Render(alert.Repo), - sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)), alert.CVE, alert.Package, upgrade, @@ -148,12 +150,12 @@ func runDepsForTarget(target string) error { var allAlerts []DepAlert summary := &AlertSummary{} - alerts, err := fetchDependabotAlerts(fullName) + dependabotAlerts, err := fetchDependabotAlerts(fullName) if err != nil { return cli.Wrap(err, "fetch dependabot alerts for "+fullName) } - for _, alert := range alerts { + for _, alert := range dependabotAlerts { if alert.State != "open" { continue } @@ -189,14 +191,14 @@ func runDepsForTarget(target string) error { cli.Blank() for _, alert := range allAlerts { - sevStyle := severityStyle(alert.Severity) + severityRenderer := severityStyle(alert.Severity) upgrade := alert.Vulnerable if alert.PatchedVersion != "" { upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion)) } cli.Print("%-16s %s %-16s %-30s %s\n", cli.ValueStyle.Render(alert.Repo), - sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)), alert.CVE, alert.Package, upgrade, diff --git a/cmd/security/cmd_jobs.go b/cmd/security/cmd_jobs.go index 6d17690..22fad9e 100644 --- a/cmd/security/cmd_jobs.go +++ b/cmd/security/cmd_jobs.go @@ -6,10 +6,10 @@ import ( "strings" "time" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/ai/ai" "dappco.re/go/core/i18n" coreerr "dappco.re/go/core/log" + "forge.lthn.ai/core/cli/pkg/cli" ) var ( @@ -20,21 +20,21 @@ var ( ) func addJobsCommand(parent *cli.Command) { - cmd := &cli.Command{ + command := &cli.Command{ Use: "jobs", Short: i18n.T("cmd.security.jobs.short"), Long: i18n.T("cmd.security.jobs.long"), - RunE: func(c *cli.Command, args []string) error { + RunE: func(_ *cli.Command, _ []string) error { return runJobs() }, } - cmd.Flags().StringSliceVar(&jobsTargets, "targets", nil, i18n.T("cmd.security.jobs.flag.targets")) - cmd.Flags().StringVar(&jobsIssueRepo, "issue-repo", "host-uk/core", i18n.T("cmd.security.jobs.flag.issue_repo")) - cmd.Flags().BoolVar(&jobsDryRun, "dry-run", false, i18n.T("cmd.security.jobs.flag.dry_run")) - cmd.Flags().IntVar(&jobsCopies, "copies", 1, i18n.T("cmd.security.jobs.flag.copies")) + command.Flags().StringSliceVar(&jobsTargets, "targets", nil, i18n.T("cmd.security.jobs.flag.targets")) + command.Flags().StringVar(&jobsIssueRepo, "issue-repo", "host-uk/core", i18n.T("cmd.security.jobs.flag.issue_repo")) + command.Flags().BoolVar(&jobsDryRun, "dry-run", false, i18n.T("cmd.security.jobs.flag.dry_run")) + command.Flags().IntVar(&jobsCopies, "copies", 1, i18n.T("cmd.security.jobs.flag.copies")) - parent.AddCommand(cmd) + parent.AddCommand(command) } func runJobs() error { @@ -70,8 +70,8 @@ func runJobs() error { // // createJobForTarget("wailsapp/wails") func createJobForTarget(target string) error { - parts := strings.SplitN(target, "/", 2) - if len(parts) != 2 { + targetParts := strings.SplitN(target, "/", 2) + if len(targetParts) != 2 || targetParts[0] == "" || targetParts[1] == "" { return coreerr.E("security.createJobForTarget", "invalid target format: use owner/repo", nil) } @@ -79,13 +79,13 @@ func createJobForTarget(target string) error { var findings []string var fetchErrors int - codeAlerts, err := fetchCodeScanningAlerts(target) + codeScanningAlerts, err := fetchCodeScanningAlerts(target) if err != nil { cli.Print("%s %s: failed to fetch code scanning alerts: %v\n", cli.WarningStyle.Render(">>"), target, err) fetchErrors++ } if err == nil { - for _, alert := range codeAlerts { + for _, alert := range codeScanningAlerts { if alert.State != "open" { continue } @@ -100,13 +100,13 @@ func createJobForTarget(target string) error { } } - depAlerts, err := fetchDependabotAlerts(target) + dependabotAlerts, err := fetchDependabotAlerts(target) if err != nil { cli.Print("%s %s: failed to fetch dependabot alerts: %v\n", cli.WarningStyle.Render(">>"), target, err) fetchErrors++ } if err == nil { - for _, alert := range depAlerts { + for _, alert := range dependabotAlerts { if alert.State != "open" { continue } @@ -117,13 +117,13 @@ func createJobForTarget(target string) error { } } - secretAlerts, err := fetchSecretScanningAlerts(target) + secretScanningAlerts, err := fetchSecretScanningAlerts(target) if err != nil { cli.Print("%s %s: failed to fetch secret scanning alerts: %v\n", cli.WarningStyle.Render(">>"), target, err) fetchErrors++ } if err == nil { - for _, alert := range secretAlerts { + for _, alert := range secretScanningAlerts { if alert.State != "open" { continue } @@ -142,12 +142,12 @@ func createJobForTarget(target string) error { } title := fmt.Sprintf("Security scan: %s", target) - body := buildJobIssueBody(target, summary, findings) + issueBody := buildJobIssueBody(target, summary, findings) - for i := range jobsCopies { + for copyIndex := range jobsCopies { issueTitle := title if jobsCopies > 1 { - issueTitle = fmt.Sprintf("%s (#%d)", title, i+1) + issueTitle = fmt.Sprintf("%s (#%d)", title, copyIndex+1) } if jobsDryRun { @@ -159,14 +159,14 @@ func createJobForTarget(target string) error { continue } - cmd := exec.Command("gh", "issue", "create", + issueCommand := exec.Command("gh", "issue", "create", "--repo", jobsIssueRepo, "--title", issueTitle, - "--body", body, + "--body", issueBody, "--label", "type:security-scan,repo:"+target, ) - output, err := cmd.CombinedOutput() + output, err := issueCommand.CombinedOutput() if err != nil { return cli.Wrap(err, fmt.Sprintf("create issue for %s: %s", target, string(output))) } @@ -195,34 +195,34 @@ func createJobForTarget(target string) error { // // body := buildJobIssueBody("wailsapp/wails", summary, findings) func buildJobIssueBody(target string, summary *AlertSummary, findings []string) string { - var builder strings.Builder + var issueBodyBuilder strings.Builder - fmt.Fprintf(&builder, "## Security Scan: %s\n\n", target) - fmt.Fprintf(&builder, "**Summary:** %s\n\n", summary.String()) + fmt.Fprintf(&issueBodyBuilder, "## Security Scan: %s\n\n", target) + fmt.Fprintf(&issueBodyBuilder, "**Summary:** %s\n\n", summary.String()) - builder.WriteString("### Findings\n\n") + issueBodyBuilder.WriteString("### Findings\n\n") if len(findings) > 50 { for _, finding := range findings[:50] { - builder.WriteString(finding + "\n") + issueBodyBuilder.WriteString(finding + "\n") } - fmt.Fprintf(&builder, "\n... and %d more\n", len(findings)-50) + fmt.Fprintf(&issueBodyBuilder, "\n... and %d more\n", len(findings)-50) } else { for _, finding := range findings { - builder.WriteString(finding + "\n") + issueBodyBuilder.WriteString(finding + "\n") } } - builder.WriteString("\n### Checklist\n\n") - builder.WriteString("- [ ] Review findings above\n") - builder.WriteString("- [ ] Triage by severity (critical/high first)\n") - builder.WriteString("- [ ] Create PRs for fixes\n") - builder.WriteString("- [ ] Verify fixes resolve alerts\n") + issueBodyBuilder.WriteString("\n### Checklist\n\n") + issueBodyBuilder.WriteString("- [ ] Review findings above\n") + issueBodyBuilder.WriteString("- [ ] Triage by severity (critical/high first)\n") + issueBodyBuilder.WriteString("- [ ] Create PRs for fixes\n") + issueBodyBuilder.WriteString("- [ ] Verify fixes resolve alerts\n") - builder.WriteString("\n### Instructions\n\n") - builder.WriteString("1. Claim this issue by assigning yourself\n") - fmt.Fprintf(&builder, "2. Run `core security alerts --target %s` for the latest findings\n", target) - builder.WriteString("3. Work through the checklist above\n") - builder.WriteString("4. Close this issue when all findings are addressed\n") + issueBodyBuilder.WriteString("\n### Instructions\n\n") + issueBodyBuilder.WriteString("1. Claim this issue by assigning yourself\n") + fmt.Fprintf(&issueBodyBuilder, "2. Run `core security alerts --target %s` for the latest findings\n", target) + issueBodyBuilder.WriteString("3. Work through the checklist above\n") + issueBodyBuilder.WriteString("4. Close this issue when all findings are addressed\n") - return builder.String() + return issueBodyBuilder.String() } diff --git a/cmd/security/cmd_scan.go b/cmd/security/cmd_scan.go index 2ca99c4..d636b75 100644 --- a/cmd/security/cmd_scan.go +++ b/cmd/security/cmd_scan.go @@ -6,8 +6,8 @@ import ( "time" "dappco.re/go/core/ai/ai" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/i18n" + "forge.lthn.ai/core/cli/pkg/cli" ) var ( @@ -15,26 +15,28 @@ var ( ) func addScanCommand(parent *cli.Command) { - cmd := &cli.Command{ + command := &cli.Command{ Use: "scan", Short: i18n.T("cmd.security.scan.short"), Long: i18n.T("cmd.security.scan.long"), - RunE: func(c *cli.Command, args []string) error { + RunE: func(_ *cli.Command, _ []string) error { return runScan() }, } - cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) - cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) - cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) - cmd.Flags().StringVar(&scanTool, "tool", "", i18n.T("cmd.security.scan.flag.tool")) - cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) - cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) + command.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) + command.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) + command.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) + command.Flags().StringVar(&scanTool, "tool", "", i18n.T("cmd.security.scan.flag.tool")) + command.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + command.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) - parent.AddCommand(cmd) + parent.AddCommand(command) } -// ScanAlert is a code scanning alert entry written to JSON or the terminal. +// ScanAlert is the code-scanning row used by `core security scan`. +// +// ScanAlert{Repo: "core-php", RuleID: "go/unused", Path: "main.go", Line: 42} type ScanAlert struct { Repo string `json:"repo"` Severity string `json:"severity"` @@ -55,12 +57,12 @@ func runScan() error { return runScanForTarget(securityTarget) } - reg, err := loadRegistry(securityRegistryPath) + registry, err := loadRegistry(securityRegistryPath) if err != nil { return err } - repoList := getReposToCheck(reg, securityRepo) + repoList := getReposToCheck(registry, securityRepo) if len(repoList) == 0 { return cli.Err("repo not found: %s", securityRepo) } @@ -69,15 +71,15 @@ func runScan() error { summary := &AlertSummary{} for _, repo := range repoList { - repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) + repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name) - alerts, err := fetchCodeScanningAlerts(repoFullName) + codeScanningAlerts, err := fetchCodeScanningAlerts(repoFullName) if err != nil { cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err) continue } - for _, alert := range alerts { + for _, alert := range codeScanningAlerts { if alert.State != "open" { continue } @@ -141,13 +143,13 @@ func runScan() error { } for _, alert := range allAlerts { - sevStyle := severityStyle(alert.Severity) + severityRenderer := severityStyle(alert.Severity) location := fmt.Sprintf("%s:%d", alert.Path, alert.Line) cli.Print("%-16s %s %-20s %-40s %s\n", cli.ValueStyle.Render(alert.Repo), - sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)), alert.RuleID, location, cli.DimStyle.Render(alert.Tool), @@ -170,12 +172,12 @@ func runScanForTarget(target string) error { var allAlerts []ScanAlert summary := &AlertSummary{} - alerts, err := fetchCodeScanningAlerts(fullName) + codeScanningAlerts, err := fetchCodeScanningAlerts(fullName) if err != nil { return cli.Wrap(err, "fetch code-scanning alerts for "+fullName) } - for _, alert := range alerts { + for _, alert := range codeScanningAlerts { if alert.State != "open" { continue } @@ -234,11 +236,11 @@ func runScanForTarget(target string) error { } for _, alert := range allAlerts { - sevStyle := severityStyle(alert.Severity) + severityRenderer := severityStyle(alert.Severity) location := fmt.Sprintf("%s:%d", alert.Path, alert.Line) cli.Print("%-16s %s %-20s %-40s %s\n", cli.ValueStyle.Render(alert.Repo), - sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)), alert.RuleID, location, cli.DimStyle.Render(alert.Tool), diff --git a/cmd/security/cmd_secrets.go b/cmd/security/cmd_secrets.go index d8956c8..bd56977 100644 --- a/cmd/security/cmd_secrets.go +++ b/cmd/security/cmd_secrets.go @@ -4,29 +4,31 @@ import ( "encoding/json" "fmt" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/i18n" + "forge.lthn.ai/core/cli/pkg/cli" ) func addSecretsCommand(parent *cli.Command) { - cmd := &cli.Command{ + command := &cli.Command{ Use: "secrets", Short: i18n.T("cmd.security.secrets.short"), Long: i18n.T("cmd.security.secrets.long"), - RunE: func(c *cli.Command, args []string) error { + RunE: func(_ *cli.Command, _ []string) error { return runSecrets() }, } - cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) - cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) - cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) - cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) + command.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) + command.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) + command.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + command.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) - parent.AddCommand(cmd) + parent.AddCommand(command) } -// SecretAlert is a secret scanning alert entry written to JSON or the terminal. +// SecretAlert is the secret-scanning row used by `core security secrets`. +// +// SecretAlert{Repo: "core-php", Number: 42, SecretType: "github_token"} type SecretAlert struct { Repo string `json:"repo"` Number int `json:"number"` @@ -45,12 +47,12 @@ func runSecrets() error { return runSecretsForTarget(securityTarget) } - reg, err := loadRegistry(securityRegistryPath) + registry, err := loadRegistry(securityRegistryPath) if err != nil { return err } - repoList := getReposToCheck(reg, securityRepo) + repoList := getReposToCheck(registry, securityRepo) if len(repoList) == 0 { return cli.Err("repo not found: %s", securityRepo) } @@ -59,19 +61,18 @@ func runSecrets() error { openCount := 0 for _, repo := range repoList { - repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) + repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name) - alerts, err := fetchSecretScanningAlerts(repoFullName) + secretScanningAlerts, err := fetchSecretScanningAlerts(repoFullName) if err != nil { continue } - for _, alert := range alerts { + for _, alert := range secretScanningAlerts { if alert.State != "open" { continue } openCount++ - secretAlert := SecretAlert{ Repo: repo.Name, Number: alert.Number, @@ -135,12 +136,12 @@ func runSecretsForTarget(target string) error { var allAlerts []SecretAlert openCount := 0 - alerts, err := fetchSecretScanningAlerts(fullName) + secretScanningAlerts, err := fetchSecretScanningAlerts(fullName) if err != nil { return cli.Wrap(err, "fetch secret-scanning alerts for "+fullName) } - for _, alert := range alerts { + for _, alert := range secretScanningAlerts { if alert.State != "open" { continue } diff --git a/cmd/security/cmd_security.go b/cmd/security/cmd_security.go index 5428b6f..0fa5f21 100644 --- a/cmd/security/cmd_security.go +++ b/cmd/security/cmd_security.go @@ -6,11 +6,11 @@ import ( "slices" "strings" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/i18n" "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/repos" + "forge.lthn.ai/core/cli/pkg/cli" ) var ( @@ -25,22 +25,24 @@ var ( // // security.AddSecurityCommands(rootCmd) // → core security alerts|deps|scan|secrets|jobs func AddSecurityCommands(root *cli.Command) { - secCmd := &cli.Command{ + command := &cli.Command{ Use: "security", Short: i18n.T("cmd.security.short"), Long: i18n.T("cmd.security.long"), } - addAlertsCommand(secCmd) - addDepsCommand(secCmd) - addScanCommand(secCmd) - addSecretsCommand(secCmd) - addJobsCommand(secCmd) + addAlertsCommand(command) + addDepsCommand(command) + addScanCommand(command) + addSecretsCommand(command) + addJobsCommand(command) - root.AddCommand(secCmd) + root.AddCommand(command) } -// DependabotAlert is a GitHub Dependabot vulnerability alert (from repos/{org}/{repo}/dependabot/alerts). +// DependabotAlert maps one response item from GitHub's dependabot alerts API. +// +// alerts, _ := fetchDependabotAlerts("host-uk/core-php") type DependabotAlert struct { Number int `json:"number"` State string `json:"state"` @@ -69,7 +71,9 @@ type DependabotAlert struct { } `json:"security_vulnerability"` } -// CodeScanningAlert is a GitHub code scanning alert (from repos/{org}/{repo}/code-scanning/alerts). +// CodeScanningAlert maps one response item from GitHub's code-scanning alerts API. +// +// alerts, _ := fetchCodeScanningAlerts("host-uk/core-php") type CodeScanningAlert struct { Number int `json:"number"` State string `json:"state"` @@ -96,7 +100,9 @@ type CodeScanningAlert struct { } `json:"most_recent_instance"` } -// SecretScanningAlert is a GitHub secret scanning alert (from repos/{org}/{repo}/secret-scanning/alerts). +// SecretScanningAlert maps one response item from GitHub's secret-scanning alerts API. +// +// alerts, _ := fetchSecretScanningAlerts("host-uk/core-php") type SecretScanningAlert struct { Number int `json:"number"` State string `json:"state"` @@ -112,22 +118,22 @@ type SecretScanningAlert struct { // loadRegistry("/path/to/repos.yaml") func loadRegistry(registryPath string) (*repos.Registry, error) { if registryPath != "" { - reg, err := repos.LoadRegistry(io.Local, registryPath) + registry, err := repos.LoadRegistry(io.Local, registryPath) if err != nil { return nil, cli.Wrap(err, "load registry") } - return reg, nil + return registry, nil } path, err := repos.FindRegistry(io.Local) if err != nil { return nil, cli.Wrap(err, "find registry") } - reg, err := repos.LoadRegistry(io.Local, path) + registry, err := repos.LoadRegistry(io.Local, path) if err != nil { return nil, cli.Wrap(err, "load registry") } - return reg, nil + return registry, nil } // checkGH returns an error if the gh CLI is not on PATH. @@ -144,8 +150,8 @@ func checkGH() error { // // runGHAPI("repos/host-uk/core-php/dependabot/alerts?state=open") func runGHAPI(endpoint string) ([]byte, error) { - cmd := exec.Command("gh", "api", endpoint, "--paginate") - output, err := cmd.Output() + ghCommand := exec.Command("gh", "api", endpoint, "--paginate") + output, err := ghCommand.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { stderr := string(exitErr.Stderr) @@ -196,34 +202,34 @@ func filterBySeverity(severity, filter string) bool { // getReposToCheck returns a single repo when repoFilter is set, or all repos in the registry. // -// getReposToCheck(reg, "core-php") // → [core-php] -// getReposToCheck(reg, "") // → all repos -func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo { +// getReposToCheck(registry, "core-php") // → [core-php] +// getReposToCheck(registry, "") // → all repos +func getReposToCheck(registry *repos.Registry, repoFilter string) []*repos.Repo { if repoFilter != "" { - if repo, ok := reg.Get(repoFilter); ok { - return []*repos.Repo{repo} + if matchedRepo, ok := registry.Get(repoFilter); ok { + return []*repos.Repo{matchedRepo} } return nil } - return reg.List() + return registry.List() } // buildTargetRepo parses an owner/repo target string into a Repo and its full name. // // buildTargetRepo("wailsapp/wails") // → &Repo{Name:"wails"}, "wailsapp/wails" func buildTargetRepo(target string) (*repos.Repo, string) { - parts := strings.SplitN(target, "/", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + targetParts := strings.SplitN(target, "/", 2) + if len(targetParts) != 2 || targetParts[0] == "" || targetParts[1] == "" { return nil, "" } - return &repos.Repo{Name: parts[1]}, target + return &repos.Repo{Name: targetParts[1]}, target } // AlertSummary tracks alert counts by severity across a scan run. // -// s := &AlertSummary{} -// s.Add("critical") -// s.String() // → "1 critical" +// summary := &AlertSummary{} +// summary.Add("critical") +// summary.String() // → "1 critical" type AlertSummary struct { Critical int High int @@ -235,45 +241,45 @@ type AlertSummary struct { // Add increments the counter for the given severity level. // -// s.Add("critical") // s.Critical == 1, s.Total == 1 -func (s *AlertSummary) Add(severity string) { - s.Total++ +// summary.Add("critical") // summary.Critical == 1, summary.Total == 1 +func (summary *AlertSummary) Add(severity string) { + summary.Total++ switch strings.ToLower(severity) { case "critical": - s.Critical++ + summary.Critical++ case "high": - s.High++ + summary.High++ case "medium": - s.Medium++ + summary.Medium++ case "low": - s.Low++ + summary.Low++ default: - s.Unknown++ + summary.Unknown++ } } // String renders a styled, human-readable summary of alert counts. // // (&AlertSummary{Critical: 1, High: 2}).String() // → "1 critical | 2 high" -func (s *AlertSummary) String() string { - parts := []string{} - if s.Critical > 0 { - parts = append(parts, cli.ErrorStyle.Render(fmt.Sprintf("%d critical", s.Critical))) +func (summary *AlertSummary) String() string { + segments := []string{} + if summary.Critical > 0 { + segments = append(segments, cli.ErrorStyle.Render(fmt.Sprintf("%d critical", summary.Critical))) } - if s.High > 0 { - parts = append(parts, cli.WarningStyle.Render(fmt.Sprintf("%d high", s.High))) + if summary.High > 0 { + segments = append(segments, cli.WarningStyle.Render(fmt.Sprintf("%d high", summary.High))) } - if s.Medium > 0 { - parts = append(parts, cli.ValueStyle.Render(fmt.Sprintf("%d medium", s.Medium))) + if summary.Medium > 0 { + segments = append(segments, cli.ValueStyle.Render(fmt.Sprintf("%d medium", summary.Medium))) } - if s.Low > 0 { - parts = append(parts, cli.DimStyle.Render(fmt.Sprintf("%d low", s.Low))) + if summary.Low > 0 { + segments = append(segments, cli.DimStyle.Render(fmt.Sprintf("%d low", summary.Low))) } - if s.Unknown > 0 { - parts = append(parts, cli.DimStyle.Render(fmt.Sprintf("%d unknown", s.Unknown))) + if summary.Unknown > 0 { + segments = append(segments, cli.DimStyle.Render(fmt.Sprintf("%d unknown", summary.Unknown))) } - if len(parts) == 0 { + if len(segments) == 0 { return cli.SuccessStyle.Render("No alerts") } - return strings.Join(parts, " | ") + return strings.Join(segments, " | ") }