diff --git a/.github/audit-exceptions.yml b/.github/audit-exceptions.yml index 4e05aae66b..2d245b9cf4 100644 --- a/.github/audit-exceptions.yml +++ b/.github/audit-exceptions.yml @@ -5,14 +5,14 @@ exceptions: severity: high reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2023-30533)" mitigation: "Load only on export; restrict export permissions and data scope" - expires_on: "2026-07-06" + expires_on: "2026-08-06" owner: "security@your-domain" - package: xlsx advisory: "GHSA-5pgg-2g8v-p4x9" severity: high reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2024-22363)" mitigation: "Load only on export; restrict export permissions and data scope" - expires_on: "2026-07-06" + expires_on: "2026-08-06" owner: "security@your-domain" - package: lodash advisory: "GHSA-r5fr-rjxr-66jc" diff --git a/backend/internal/handler/batch_image_handler.go b/backend/internal/handler/batch_image_handler.go index 22c719bcb3..3a36634618 100644 --- a/backend/internal/handler/batch_image_handler.go +++ b/backend/internal/handler/batch_image_handler.go @@ -147,7 +147,7 @@ func (h *BatchImageHandler) ItemContent(c *gin.Context) { batchImageError(c, err) return } - defer stream.Reader.Close() + defer func() { _ = stream.Reader.Close() }() c.Header("Content-Type", stream.ContentType) c.Header("Content-Disposition", service.BatchImageContentDispositionAttachment(stream.Filename)) @@ -181,7 +181,7 @@ func (h *BatchImageHandler) Download(c *gin.Context) { IncludeManifest: true, }, c.Writer) if err != nil { - if result == nil || c.Writer.Written() == false { + if result == nil || !c.Writer.Written() { batchImageError(c, err) } return diff --git a/backend/internal/repository/batch_image_download_limiter.go b/backend/internal/repository/batch_image_download_limiter.go index 8a7b9a4ab7..ffcd0dfe81 100644 --- a/backend/internal/repository/batch_image_download_limiter.go +++ b/backend/internal/repository/batch_image_download_limiter.go @@ -62,16 +62,6 @@ func NewBatchImageDownloadLimiter(rdb *redis.Client, cfg *config.Config) service } } -func newBatchImageDownloadLimiterForTest(rdb *redis.Client, maxActive int, ttl time.Duration) *batchImageDownloadLimiter { - if maxActive <= 0 { - maxActive = defaultBatchImageDownloadConcurrency - } - if ttl <= 0 { - ttl = defaultBatchImageDownloadActiveTTL - } - return &batchImageDownloadLimiter{rdb: rdb, activePrefix: defaultBatchImageDownloadActivePrefix, maxActive: maxActive, ttl: ttl} -} - func (l *batchImageDownloadLimiter) Acquire(ctx context.Context, userID string, kind string) (service.BatchImageDownloadPermit, error) { if l == nil || l.rdb == nil { return nil, service.ErrBatchImageDownloadLimited diff --git a/backend/internal/repository/batch_image_download_limiter_test.go b/backend/internal/repository/batch_image_download_limiter_test.go index 3a4a8be937..f261003aa6 100644 --- a/backend/internal/repository/batch_image_download_limiter_test.go +++ b/backend/internal/repository/batch_image_download_limiter_test.go @@ -18,7 +18,12 @@ func TestBatchImageDownloadLimiter_AcquireDenyReleaseAndTTL(t *testing.T) { mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) t.Cleanup(func() { _ = rdb.Close() }) - limiter := newBatchImageDownloadLimiterForTest(rdb, 1, time.Minute) + limiter := &batchImageDownloadLimiter{ + rdb: rdb, + activePrefix: defaultBatchImageDownloadActivePrefix, + maxActive: 1, + ttl: time.Minute, + } permit, err := limiter.Acquire(ctx, "11", "zip") require.NoError(t, err) diff --git a/backend/internal/repository/batch_image_repo.go b/backend/internal/repository/batch_image_repo.go index 1d2cf0c13f..cd4bf70339 100644 --- a/backend/internal/repository/batch_image_repo.go +++ b/backend/internal/repository/batch_image_repo.go @@ -25,10 +25,6 @@ func NewBatchImageRepository(db *sql.DB) service.BatchImageRepository { return &batchImageRepository{db: db, sql: db} } -func newBatchImageRepositoryWithSQL(sqlq batchImageSQLExecutor) *batchImageRepository { - return &batchImageRepository{sql: sqlq} -} - func (r *batchImageRepository) CreateBatchImageJob(ctx context.Context, params service.CreateBatchImageJobParams) (*service.BatchImageJob, error) { if !service.IsSupportedBatchImageProvider(params.Provider) { return nil, service.ErrBatchImageInvalidProvider @@ -125,7 +121,7 @@ func (r *batchImageRepository) ListBatchImageJobsForOwner(ctx context.Context, u if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() return scanBatchImageJobs(rows) } @@ -449,7 +445,7 @@ func (r *batchImageRepository) batchImageItemPromptPreviews(ctx context.Context, if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make(map[string]string) for rows.Next() { var customID string @@ -486,7 +482,7 @@ func (r *batchImageRepository) ListBatchImageItems(ctx context.Context, batchID if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() var items []*service.BatchImageItem for rows.Next() { @@ -540,7 +536,7 @@ func (r *batchImageRepository) ListBatchImageJobsDueForInputCleanup(ctx context. if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() return scanBatchImageJobs(rows) } @@ -559,7 +555,7 @@ func (r *batchImageRepository) ListBatchImageJobsDueForOutputCleanup(ctx context if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() return scanBatchImageJobs(rows) } @@ -577,7 +573,7 @@ func (r *batchImageRepository) ListStaleUnsubmittedBatchImageJobs(ctx context.Co if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() return scanBatchImageJobs(rows) } diff --git a/backend/internal/repository/batch_image_repo_integration_test.go b/backend/internal/repository/batch_image_repo_integration_test.go index 973e3d8c2e..51a436fb71 100644 --- a/backend/internal/repository/batch_image_repo_integration_test.go +++ b/backend/internal/repository/batch_image_repo_integration_test.go @@ -4,7 +4,11 @@ package repository import ( "context" + "crypto/sha1" + "encoding/hex" "errors" + "regexp" + "strings" "testing" "time" @@ -12,6 +16,10 @@ import ( "github.com/stretchr/testify/require" ) +func newBatchImageRepositoryWithSQL(sqlq batchImageSQLExecutor) *batchImageRepository { + return &batchImageRepository{sql: sqlq} +} + func TestBatchImageRepository_CreateJobAndDuplicates(t *testing.T) { ctx := context.Background() tx := testTx(t) @@ -325,7 +333,26 @@ func TestBatchImageRepository_AppendEvent(t *testing.T) { func batchImageTestID(t *testing.T, prefix string) string { t.Helper() - return "imgbatch_" + uniqueTestValue(t, prefix) + safePrefix := batchImageSafeTestIDSegment(prefix, 20) + sum := sha1.Sum([]byte(t.Name())) + return "imgbatch_" + safePrefix + "_" + hex.EncodeToString(sum[:])[:16] +} + +func batchImageSafeTestIDSegment(v string, maxLen int) string { + v = strings.ToLower(strings.TrimSpace(v)) + v = regexp.MustCompile(`[^a-z0-9_-]+`).ReplaceAllString(v, "-") + v = strings.Trim(v, "-_") + if v == "" { + v = "job" + } + if len(v) > maxLen { + v = v[:maxLen] + v = strings.Trim(v, "-_") + } + if v == "" { + return "job" + } + return v } func batchImageTestStringPtr(v string) *string { diff --git a/backend/internal/service/batch_image_download.go b/backend/internal/service/batch_image_download.go index f5a0cdb5c7..b08b99c75f 100644 --- a/backend/internal/service/batch_image_download.go +++ b/backend/internal/service/batch_image_download.go @@ -144,7 +144,7 @@ func (s *BatchImageDownloadService) OpenItemContent(ctx context.Context, owner B if err != nil { return nil, ErrBatchImageResultMissing.WithCause(err) } - defer r.Close() + defer func() { _ = r.Close() }() line, err := findBatchImageLineImages(r, item.CustomID) if err != nil { @@ -207,7 +207,7 @@ func (s *BatchImageDownloadService) StreamZip(ctx context.Context, owner BatchIm return nil, err } if permit != nil { - defer permit.Release(ctx) + defer func() { _ = permit.Release(ctx) }() } provider, account, err := s.providerAndAccount(ctx, job) @@ -218,7 +218,7 @@ func (s *BatchImageDownloadService) StreamZip(ctx context.Context, owner BatchIm if err != nil { return nil, ErrBatchImageResultMissing.WithCause(err) } - defer r.Close() + defer func() { _ = r.Close() }() streamCtx := ctx cancel := func() {} @@ -546,13 +546,13 @@ func sanitizeBatchImageFilenameBase(value string) string { for _, r := range value { switch { case r == '/' || r == '\\' || r == ':' || r == 0: - b.WriteByte('_') + _ = b.WriteByte('_') case unicode.IsControl(r): - b.WriteByte('_') + _ = b.WriteByte('_') case unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.': - b.WriteRune(r) + _, _ = b.WriteRune(r) default: - b.WriteByte('_') + _ = b.WriteByte('_') } } out := strings.Trim(b.String(), ". ") @@ -577,7 +577,7 @@ func sanitizeBatchImageFilenameExtension(extension string) string { var b strings.Builder for _, r := range extension { if unicode.IsLetter(r) || unicode.IsDigit(r) { - b.WriteRune(r) + _, _ = b.WriteRune(r) } } out := b.String() diff --git a/backend/internal/service/batch_image_processor.go b/backend/internal/service/batch_image_processor.go index b82bff85ef..24496f6bfa 100644 --- a/backend/internal/service/batch_image_processor.go +++ b/backend/internal/service/batch_image_processor.go @@ -285,7 +285,7 @@ func (i *BatchImageResultIndexer) Index(ctx context.Context, job *BatchImageJob, if err != nil { return nil, ErrBatchImageIndexOutputMissing.WithCause(err) } - defer r.Close() + defer func() { _ = r.Close() }() scanner := bufio.NewScanner(r) scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024) diff --git a/backend/internal/service/batch_image_provider_gemini.go b/backend/internal/service/batch_image_provider_gemini.go index bc0e574642..ccf91de714 100644 --- a/backend/internal/service/batch_image_provider_gemini.go +++ b/backend/internal/service/batch_image_provider_gemini.go @@ -577,7 +577,7 @@ func (c *GeminiBatchHTTPClient) DownloadFile(ctx context.Context, apiKey string, return nil, "", err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() return nil, "", readGeminiAPIError(resp) } contentType := resp.Header.Get("Content-Type") @@ -612,7 +612,7 @@ func (c *GeminiBatchHTTPClient) doNoBody(req *http.Request) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return readGeminiAPIError(resp) } @@ -624,7 +624,7 @@ func (c *GeminiBatchHTTPClient) doJSON(req *http.Request, out any) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return readGeminiAPIError(resp) } diff --git a/backend/internal/service/batch_image_provider_vertex.go b/backend/internal/service/batch_image_provider_vertex.go index fe28481c59..9289c1484c 100644 --- a/backend/internal/service/batch_image_provider_vertex.go +++ b/backend/internal/service/batch_image_provider_vertex.go @@ -450,11 +450,11 @@ func sanitizeVertexGCSPathSegment(v string) string { for _, r := range v { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9': - b.WriteRune(r) + _, _ = b.WriteRune(r) case r == '-', r == '_', r == '.': - b.WriteRune(r) + _, _ = b.WriteRune(r) default: - b.WriteByte('-') + _ = b.WriteByte('-') } } return b.String() @@ -876,7 +876,7 @@ func (s *VertexGCSObjectStore) OpenObject(ctx context.Context, accessToken strin return nil, "", err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() return nil, "", readVertexAPIError(resp) } contentType := resp.Header.Get("Content-Type") @@ -955,7 +955,7 @@ func doVertexDecodeJSON(client *http.Client, req *http.Request, out any) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return readVertexAPIError(resp) } @@ -967,7 +967,7 @@ func doVertexNoBody(client *http.Client, req *http.Request) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return readVertexAPIError(resp) } diff --git a/backend/internal/service/batch_image_public.go b/backend/internal/service/batch_image_public.go index cffbab609d..0b792aacb2 100644 --- a/backend/internal/service/batch_image_public.go +++ b/backend/internal/service/batch_image_public.go @@ -310,13 +310,7 @@ func (s *BatchImagePublicService) Submit(ctx context.Context, owner BatchImageOw for _, item := range normalized.Items { refs := make([]BatchImageReference, 0, len(item.ReferenceImages)) for _, ref := range item.ReferenceImages { - refs = append(refs, BatchImageReference{ - ID: ref.ID, - Type: ref.Type, - MimeType: ref.MimeType, - Data: ref.Data, - FileURI: ref.FileURI, - }) + refs = append(refs, BatchImageReference(ref)) } input.Items = append(input.Items, BatchImageInputItem{ CustomID: item.CustomID,