diff --git a/flake.lock b/flake.lock index 52e34315..728151f6 100644 --- a/flake.lock +++ b/flake.lock @@ -43,11 +43,11 @@ ] }, "locked": { - "lastModified": 1772665116, - "narHash": "sha256-XmjUDG/J8Z8lY5DVNVUf5aoZGc400FxcjsNCqHKiKtc=", + "lastModified": 1772893680, + "narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "39f53203a8458c330f61cc0759fe243f0ac0d198", + "rev": "8baab586afc9c9b57645a734c820e4ac0a604af9", "type": "github" }, "original": { @@ -79,11 +79,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772820052, - "narHash": "sha256-qXEI8n+4eXT+BqMzUMuKFdkltE82TjgIceiJCpdjzD4=", + "lastModified": 1773046814, + "narHash": "sha256-3CEw64UyzEk5QjfbcXNIl4TfmIpa2oY+duuo6aiawcU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d78e468770f4ab5e00c5015f4d77c1a499a76dc8", + "rev": "0c6c0dd2469abaa216599bb19bbf77a328af6564", "type": "github" }, "original": { diff --git a/internal/app/chat.go b/internal/app/chat.go index 1e5a9672..b382bcbd 100644 --- a/internal/app/chat.go +++ b/internal/app/chat.go @@ -345,8 +345,10 @@ func (m *Model) startSQLStream(query string) tea.Cmd { messages = append(messages, history...) messages = append(messages, llm.Message{Role: roleUser, Content: query}) - ctx, cancel := context.WithCancel(context.Background()) - + //nolint:gosec // cancel stored in CancelFn, called on ctrl+c + ctx, cancel := context.WithCancel( + context.Background(), + ) streamCh, err := client.ChatStream(ctx, messages) if err != nil { return sqlStreamStartedMsg{ diff --git a/internal/app/chat_test.go b/internal/app/chat_test.go index 2cd0cb73..0b54b5a6 100644 --- a/internal/app/chat_test.go +++ b/internal/app/chat_test.go @@ -120,7 +120,9 @@ func TestCancellationDuringSQLGeneration(t *testing.T) { m := newTestModel(t) m.openChat() - _, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + _ = ctx // only the cancel func is needed; ctx is cleaned up via t.Cleanup m.chat.CurrentQuery = testQuestion m.chat.Streaming = true m.chat.StreamingSQL = true @@ -150,7 +152,9 @@ func TestCancellationDuringAnswerStreaming(t *testing.T) { m := newTestModel(t) m.openChat() - _, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + _ = ctx m.chat.CurrentQuery = testQuestion m.chat.Streaming = true m.chat.StreamingSQL = false // stage 2 @@ -240,6 +244,7 @@ func TestNoSpinnerAfterCancellation(t *testing.T) { m.openChat() _, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) m.chat.Streaming = true m.chat.StreamingSQL = true m.chat.CancelFn = cancel @@ -274,6 +279,7 @@ func TestLateSQLChunkAfterCancellationIsDropped(t *testing.T) { m.openChat() _, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) m.chat.Streaming = true m.chat.StreamingSQL = true m.chat.CancelFn = cancel @@ -310,6 +316,7 @@ func TestLateChatChunkAfterCancellationIsDropped(t *testing.T) { m.openChat() _, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) m.chat.Streaming = true m.chat.CancelFn = cancel m.chat.Messages = []chatMessage{ diff --git a/internal/app/extraction.go b/internal/app/extraction.go index a4e2b301..8f759125 100644 --- a/internal/app/extraction.go +++ b/internal/app/extraction.go @@ -269,7 +269,10 @@ func (m *Model) startExtractionOverlay( sp := spinner.New(spinner.WithSpinner(spinner.Dot)) sp.Style = appStyles.AccentText() - ctx, cancel := context.WithCancel(context.Background()) + //nolint:gosec // cancel stored in ex.CancelFn, called on extraction close + ctx, cancel := context.WithCancel( + context.Background(), + ) // Text extraction only applies to PDFs and text files; skip for images. hasText := !extract.IsImageMIME(mime) @@ -935,7 +938,9 @@ func (m *Model) rerunLLMExtraction() tea.Cmd { // Replace a cancelled context so the rerun has a live one. if ex.ctx.Err() != nil { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel( //nolint:gosec // cancel stored in ex.CancelFn, called on extraction close + context.Background(), + ) ex.ctx = ctx ex.CancelFn = cancel } diff --git a/internal/app/extraction_test.go b/internal/app/extraction_test.go index 2acc417b..6409d5ec 100644 --- a/internal/app/extraction_test.go +++ b/internal/app/extraction_test.go @@ -26,6 +26,7 @@ func newExtractionModel(t *testing.T, steps map[extractionStep]stepStatus) *Mode m := newTestModel(t) ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) ex := &extractionLogState{ ID: nextExtractionID.Add(1), ctx: ctx, @@ -913,6 +914,7 @@ func TestForeground_SwapsCurrentToBackground(t *testing.T) { // Create a new foreground extraction. ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) m.ex.extraction = &extractionLogState{ ID: nextExtractionID.Add(1), Filename: "second.pdf", @@ -1051,6 +1053,7 @@ func TestMultipleBgExtractions(t *testing.T) { // Create and background second. ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) m.ex.extraction = &extractionLogState{ ID: nextExtractionID.Add(1), Filename: "b.pdf", @@ -1088,6 +1091,7 @@ func TestStartExtraction_AutoBackgroundsExisting(t *testing.T) { // This tests the backgrounding logic directly. m.backgroundExtraction() ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) m.ex.extraction = &extractionLogState{ ID: nextExtractionID.Add(1), Filename: "new.pdf", @@ -1134,6 +1138,7 @@ func TestCtrlQ_CancelsAllBgExtractions(t *testing.T) { // Create another foreground extraction. ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) m.ex.extraction = &extractionLogState{ ID: nextExtractionID.Add(1), Filename: "fg2.pdf", @@ -1817,6 +1822,7 @@ func TestDispatch_VendorCrossReference(t *testing.T) { require.NoError(t, sdb.Stage(ops)) ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) defer cancel() ex := &extractionLogState{ ID: nextExtractionID.Add(1), @@ -1885,6 +1891,7 @@ func TestDispatch_InvalidProjectIDShowsError(t *testing.T) { require.NoError(t, sdb.Stage(ops)) ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) defer cancel() ex := &extractionLogState{ ID: nextExtractionID.Add(1), @@ -1950,6 +1957,7 @@ func TestDispatch_OffsetCrossReference(t *testing.T) { require.NoError(t, sdb.Stage(ops)) ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) defer cancel() ex := &extractionLogState{ ID: nextExtractionID.Add(1), @@ -2032,6 +2040,7 @@ func TestDispatch_DuplicateVendorDedup(t *testing.T) { require.NoError(t, sdb.Stage(ops)) ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) defer cancel() ex := &extractionLogState{ ID: nextExtractionID.Add(1), @@ -2102,6 +2111,7 @@ func TestDispatch_DuplicateApplianceDedup(t *testing.T) { require.NoError(t, sdb.Stage(ops)) ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) defer cancel() ex := &extractionLogState{ ID: nextExtractionID.Add(1), @@ -2163,6 +2173,7 @@ func TestDispatch_TransactionRollbackOnFailure(t *testing.T) { require.NoError(t, sdb.Stage(ops)) ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) defer cancel() ex := &extractionLogState{ ID: nextExtractionID.Add(1), diff --git a/internal/app/table.go b/internal/app/table.go index 2ceef002..bde93ca9 100644 --- a/internal/app/table.go +++ b/internal/app/table.go @@ -879,8 +879,9 @@ func columnWidths( // Content exceeds terminal width — apply Max constraints. widths := make([]int, columnCount) for i, w := range natural { - if specs[i].Max > 0 && w > specs[i].Max { - w = specs[i].Max + if specs[i].Max > 0 && //nolint:gosec // specs and natural have equal length (columnCount) + w > specs[i].Max { //nolint:gosec // same bounds + w = specs[i].Max //nolint:gosec // same bounds } widths[i] = w } diff --git a/internal/extract/ocr.go b/internal/extract/ocr.go index cb48c33b..09f96fd1 100644 --- a/internal/extract/ocr.go +++ b/internal/extract/ocr.go @@ -38,7 +38,7 @@ func ocrPDF(ctx context.Context, data []byte, maxPages int) (string, []byte, err defer os.RemoveAll(tmpDir) //nolint:errcheck // best-effort cleanup pdfPath := filepath.Join(tmpDir, "input.pdf") - if err := os.WriteFile(pdfPath, data, 0o600); err != nil { + if err := os.WriteFile(pdfPath, data, 0o600); err != nil { //nolint:gosec // path is tmpDir + constant filename return "", nil, fmt.Errorf("write temp pdf: %w", err) } @@ -271,7 +271,7 @@ func ocrImage(ctx context.Context, data []byte) (string, []byte, error) { defer os.RemoveAll(tmpDir) //nolint:errcheck // best-effort cleanup imgPath := filepath.Join(tmpDir, "input") - if err := os.WriteFile(imgPath, data, 0o600); err != nil { + if err := os.WriteFile(imgPath, data, 0o600); err != nil { //nolint:gosec // path is tmpDir + constant filename return "", nil, fmt.Errorf("write temp image: %w", err) } diff --git a/internal/extract/ocr_bench_test.go b/internal/extract/ocr_bench_test.go index 63729591..00ab843f 100644 --- a/internal/extract/ocr_bench_test.go +++ b/internal/extract/ocr_bench_test.go @@ -44,7 +44,7 @@ func BenchmarkOcrPage(b *testing.B) { tmpDir := b.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - if err := os.WriteFile(pdfPath, data, 0o600); err != nil { + if err := os.WriteFile(pdfPath, data, 0o600); err != nil { //nolint:gosec // path is tmpDir + constant filename b.Fatal(err) } diff --git a/internal/extract/ocr_coverage_test.go b/internal/extract/ocr_coverage_test.go index 6866b129..9041f5f4 100644 --- a/internal/extract/ocr_coverage_test.go +++ b/internal/extract/ocr_coverage_test.go @@ -275,7 +275,11 @@ func TestPdfPageCount_ValidPDF(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - require.NoError(t, os.WriteFile(pdfPath, data, 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, data, 0o600), + ) count, err := pdfPageCount(context.Background(), pdfPath) require.NoError(t, err) @@ -290,7 +294,11 @@ func TestPdfPageCount_InvalidPDF(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "corrupt.pdf") - require.NoError(t, os.WriteFile(pdfPath, []byte("corrupt data"), 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, []byte("corrupt data"), 0o600), + ) _, err := pdfPageCount(context.Background(), pdfPath) assert.Error(t, err) @@ -309,7 +317,11 @@ func TestPdfPageCount_ContextCancelled(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - require.NoError(t, os.WriteFile(pdfPath, data, 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, data, 0o600), + ) ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -335,7 +347,11 @@ func TestOcrPage_ValidPDF(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - require.NoError(t, os.WriteFile(pdfPath, data, 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, data, 0o600), + ) result := ocrPage(context.Background(), pdfPath, 1, nil) require.NoError(t, result.err) @@ -351,7 +367,11 @@ func TestOcrPage_InvalidPDF(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "corrupt.pdf") - require.NoError(t, os.WriteFile(pdfPath, []byte("corrupt data"), 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, []byte("corrupt data"), 0o600), + ) result := ocrPage(context.Background(), pdfPath, 1, nil) assert.Error(t, result.err) @@ -370,7 +390,11 @@ func TestOcrPage_ContextCancelled(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - require.NoError(t, os.WriteFile(pdfPath, data, 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, data, 0o600), + ) ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -812,7 +836,11 @@ func TestOcrPDFPages_ValidPDF(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - require.NoError(t, os.WriteFile(pdfPath, data, 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, data, 0o600), + ) pageCount, err := pdfPageCount(context.Background(), pdfPath) require.NoError(t, err) @@ -844,7 +872,11 @@ func TestOcrPDFPages_ContextCancelled(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - require.NoError(t, os.WriteFile(pdfPath, data, 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, data, 0o600), + ) ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -867,7 +899,11 @@ func TestOcrPDFPages_ProgressReporting(t *testing.T) { tmpDir := t.TempDir() pdfPath := filepath.Join(tmpDir, "input.pdf") - require.NoError(t, os.WriteFile(pdfPath, data, 0o600)) + //nolint:gosec // path is tmpDir + constant filename + require.NoError( + t, + os.WriteFile(pdfPath, data, 0o600), + ) pageDone := make(chan struct{}, 2) results := ocrPDFPages(context.Background(), pdfPath, 1, nil, pageDone) diff --git a/internal/extract/ocr_progress.go b/internal/extract/ocr_progress.go index d42049cb..184f3bdf 100644 --- a/internal/extract/ocr_progress.go +++ b/internal/extract/ocr_progress.go @@ -73,7 +73,7 @@ func ocrImageWithProgress(ctx context.Context, data []byte, ch chan<- ExtractPro defer os.RemoveAll(tmpDir) //nolint:errcheck // best-effort cleanup imgPath := filepath.Join(tmpDir, "input.png") - if err := os.WriteFile(imgPath, data, 0o600); err != nil { + if err := os.WriteFile(imgPath, data, 0o600); err != nil { //nolint:gosec // path is tmpDir + constant filename ch <- ExtractProgress{Err: fmt.Errorf("write temp image: %w", err), Done: true} return } @@ -118,7 +118,7 @@ func ocrPDFWithProgress( defer os.RemoveAll(tmpDir) //nolint:errcheck // best-effort cleanup pdfPath := filepath.Join(tmpDir, "input.pdf") - if err := os.WriteFile(pdfPath, data, 0o600); err != nil { + if err := os.WriteFile(pdfPath, data, 0o600); err != nil { //nolint:gosec // path is tmpDir + constant filename ch <- ExtractProgress{Err: fmt.Errorf("write temp pdf: %w", err), Done: true} return } diff --git a/internal/extract/text.go b/internal/extract/text.go index 35cfb123..a3795453 100644 --- a/internal/extract/text.go +++ b/internal/extract/text.go @@ -59,7 +59,7 @@ func extractPDF(ctx context.Context, data []byte) (string, error) { defer os.RemoveAll(tmpDir) //nolint:errcheck // best-effort cleanup pdfPath := filepath.Join(tmpDir, "input.pdf") - if err := os.WriteFile(pdfPath, data, 0o600); err != nil { + if err := os.WriteFile(pdfPath, data, 0o600); err != nil { //nolint:gosec // path is tmpDir + constant filename return "", fmt.Errorf("write temp pdf: %w", err) } diff --git a/osv-scanner.toml b/osv-scanner.toml index dd683f76..059b65bb 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -17,6 +17,14 @@ reason = "minor TLS 1.3 info disclosure requiring network-local attacker; defaul id = "GO-2026-4342" reason = "archive/zip DoS via crafted ZIP filenames; micasa never opens or processes ZIP archives" +[[IgnoredVulns]] +id = "GO-2026-4601" +reason = "url.Parse IPv6 host parsing flaw; only caller is isLoopbackURL which parses the user's own llm.base_url config value, not attacker-supplied input" + +[[IgnoredVulns]] +id = "GO-2026-4602" +reason = "os.ReadDir directory traversal via Root-constrained File; micasa uses plain os.ReadDir on a local cache dir, never uses os.OpenRoot" + # ollama server-side vulnerabilities: micasa is a client that makes HTTP # requests to an Ollama server; it does not embed, host, or run the Ollama # server, GGUF parser, or model-serving endpoints. These code paths are