github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/service/report.go (about) 1 // Copyright 2025 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package service 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "time" 11 12 "github.com/google/syzkaller/syz-cluster/pkg/api" 13 "github.com/google/syzkaller/syz-cluster/pkg/app" 14 "github.com/google/syzkaller/syz-cluster/pkg/db" 15 ) 16 17 type ReportService struct { 18 reportRepo *db.ReportRepository 19 seriesService *SeriesService 20 findingService *FindingService 21 urls *api.URLGenerator 22 } 23 24 func NewReportService(env *app.AppEnvironment) *ReportService { 25 return &ReportService{ 26 urls: env.URLs, 27 reportRepo: db.NewReportRepository(env.Spanner), 28 seriesService: NewSeriesService(env), 29 findingService: NewFindingService(env), 30 } 31 } 32 33 var ErrReportNotFound = errors.New("report is not found") 34 35 func (rs *ReportService) Confirm(ctx context.Context, id string) error { 36 err := rs.reportRepo.Update(ctx, id, func(rep *db.SessionReport) error { 37 if rep.ReportedAt.IsNull() { 38 rep.SetReportedAt(time.Now()) 39 } 40 // TODO: fail if already confirmed? 41 return nil 42 }) 43 if errors.Is(err, db.ErrEntityNotFound) { 44 return ErrReportNotFound 45 } 46 return err 47 } 48 49 var ErrNotOnModeration = errors.New("the report is not on moderation") 50 51 func (rs *ReportService) Upstream(ctx context.Context, id string, req *api.UpstreamReportReq) error { 52 rep, err := rs.query(ctx, id) 53 if err != nil { 54 return err 55 } else if !rep.Moderation { 56 return ErrNotOnModeration 57 } 58 // In case of a concurrent Upstream() call or an Upstream() invocation on 59 // an already upstreamed report, the "NoDupSessionReports" index should 60 // prevent duplications. 61 err = rs.reportRepo.Insert(ctx, &db.SessionReport{ 62 SessionID: rep.SessionID, 63 Reporter: rep.Reporter, 64 }) 65 if err != nil { 66 return fmt.Errorf("failed to schedule a new report: %w", err) 67 } 68 return nil 69 } 70 71 func (rs *ReportService) Invalidate(ctx context.Context, id string) error { 72 rep, err := rs.query(ctx, id) 73 if err != nil { 74 return err 75 } 76 // For now, invalidate all the findings at once - later we can do it more selectively. 77 return rs.findingService.InvalidateSession(ctx, rep.SessionID) 78 } 79 80 const maxFindingsPerReport = 5 81 82 func (rs *ReportService) Next(ctx context.Context, reporter string) (*api.NextReportResp, error) { 83 list, err := rs.reportRepo.ListNotReported(ctx, reporter, 1) 84 if err != nil { 85 return nil, err 86 } else if len(list) != 1 { 87 return &api.NextReportResp{}, nil 88 } 89 report := list[0] 90 series, err := rs.seriesService.GetSessionSeriesShort(ctx, report.SessionID) 91 if err != nil { 92 return nil, fmt.Errorf("failed to query series: %w", err) 93 } 94 findings, err := rs.findingService.List(ctx, report.SessionID, maxFindingsPerReport) 95 if err != nil { 96 return nil, fmt.Errorf("failed to query findings: %w", err) 97 } 98 return &api.NextReportResp{ 99 Report: &api.SessionReport{ 100 ID: report.ID, 101 Moderation: report.Moderation, 102 Series: series, 103 Link: rs.urls.Series(series.ID), 104 Findings: findings, 105 }, 106 }, nil 107 } 108 109 func (rs *ReportService) query(ctx context.Context, id string) (*db.SessionReport, error) { 110 rep, err := rs.reportRepo.GetByID(ctx, id) 111 if err != nil { 112 return nil, fmt.Errorf("failed to query the report: %w", err) 113 } else if rep == nil { 114 return nil, ErrReportNotFound 115 } 116 return rep, err 117 }