diff options
| author | Aleksandr Nogikh <nogikh@google.com> | 2025-02-13 11:57:14 +0100 |
|---|---|---|
| committer | Aleksandr Nogikh <nogikh@google.com> | 2025-02-14 13:40:12 +0000 |
| commit | f20e88b2468bdcdb631b14e384f1f9a67e984013 (patch) | |
| tree | d2758c08ab9deea9354a91237a3d9234de0efce3 | |
| parent | eaf86f3f4dc8a7190abf09fe840e20bcf83709d8 (diff) | |
syz-cluster: report session results
Provide an API to set up the reporting of finished sessions for which
syz-cluster collected reportable findings.
The actual sending of the results is to be done in a separate component
that would:
1) Call Next() to get the next report to send.
2) Call Confirm() to confirm that the report has been sent.
3) Call Upstream() if the report has been moderated and needs to be sent
to e.g. public mailing lists.
30 files changed, 921 insertions, 96 deletions
diff --git a/syz-cluster/Makefile b/syz-cluster/Makefile index b64e726d4..1f042b064 100644 --- a/syz-cluster/Makefile +++ b/syz-cluster/Makefile @@ -22,6 +22,10 @@ build-web-dashboard-dev: deploy-web-dashboard-dev: build-web-dashboard-dev @kubectl rollout restart deployment web-dashboard +build-reporter-dev: + eval $$(minikube docker-env);\ + docker build -t reporter-image-local -f ./reporter/Dockerfile ../ + install-dev-config: minikube kubectl -- apply -f ./overlays/dev/global-config.yaml @@ -51,7 +55,7 @@ build-go-tests-dev: build-workflow-dev: build-triage-step-dev build-build-step-dev build-boot-step-dev build-fuzz-step-dev -all-containers: build-controller-dev build-series-tracker-dev build-db-mgmt-dev build-web-dashboard-dev build-workflow-dev +all-containers: build-controller-dev build-series-tracker-dev build-db-mgmt-dev build-web-dashboard-dev build-reporter-dev build-workflow-dev run-go-tests-dev: build-go-tests-dev ./run-local.sh go-tests diff --git a/syz-cluster/dashboard/handler.go b/syz-cluster/dashboard/handler.go index 38ea527ee..2df0ed37f 100644 --- a/syz-cluster/dashboard/handler.go +++ b/syz-cluster/dashboard/handler.go @@ -128,7 +128,7 @@ func (h *dashboardHandler) seriesInfo(w http.ResponseWriter, r *http.Request) er if err != nil { return fmt.Errorf("failed to query session tests: %w", err) } - findings, err := h.findingRepo.ListForSession(ctx, session) + findings, err := h.findingRepo.ListForSession(ctx, session.ID) if err != nil { return fmt.Errorf("failed to query session findings: %w", err) } diff --git a/syz-cluster/overlays/dev/kustomization.yaml b/syz-cluster/overlays/dev/kustomization.yaml index 0aa368cb6..7283de8c1 100644 --- a/syz-cluster/overlays/dev/kustomization.yaml +++ b/syz-cluster/overlays/dev/kustomization.yaml @@ -6,6 +6,7 @@ resources: - ../../dashboard - ../../series-tracker - ../../kernel-disk + - ../../reporter - global-config.yaml - https://github.com/argoproj/argo-workflows/releases/download/v3.6.2/install.yaml - workflow-roles.yaml diff --git a/syz-cluster/pkg/api/api.go b/syz-cluster/pkg/api/api.go index 1b2f991dc..b9126520e 100644 --- a/syz-cluster/pkg/api/api.go +++ b/syz-cluster/pkg/api/api.go @@ -77,8 +77,8 @@ type BootResult struct { Success bool `json:"success"` } -// Finding is a kernel crash, boot error, etc. found during a test. -type Finding struct { +// NewFinding is a kernel crash, boot error, etc. found during a test. +type NewFinding struct { SessionID string `json:"session_id"` TestName string `json:"test_name"` Title string `json:"title"` @@ -118,6 +118,21 @@ type NewSession struct { Tags []string `json:"tags"` } +type SessionReport struct { + ID string `json:"id"` + Moderation bool `json:"moderation"` + // TODO: add some session info? + Series *Series `json:"series"` + Findings []*Finding `json:"findings"` + Link string `json:"link"` // URL to the web dashboard. +} + +type Finding struct { + Title string `json:"title"` + Report []byte `json:"report"` + LogURL string `json:"log_url"` +} + // For now, there's no reason to obtain these really via a real API call. var defaultTrees = []*Tree{ { diff --git a/syz-cluster/pkg/api/client.go b/syz-cluster/pkg/api/client.go index 8f3187042..4543f152d 100644 --- a/syz-cluster/pkg/api/client.go +++ b/syz-cluster/pkg/api/client.go @@ -4,7 +4,6 @@ package api import ( - "bytes" "context" "encoding/json" "fmt" @@ -68,8 +67,8 @@ func (client Client) UploadTestResult(ctx context.Context, req *TestResult) erro return err } -func (client Client) UploadFinding(ctx context.Context, req *Finding) error { - _, err := postJSON[Finding, any](ctx, client.baseURL+"/findings/upload", req) +func (client Client) UploadFinding(ctx context.Context, req *NewFinding) error { + _, err := postJSON[NewFinding, any](ctx, client.baseURL+"/findings/upload", req) return err } @@ -90,27 +89,6 @@ func (client Client) UploadSession(ctx context.Context, req *NewSession) (*Uploa return postJSON[NewSession, UploadSessionResp](ctx, client.baseURL+"/sessions/upload", req) } -func getJSON[Resp any](ctx context.Context, url string) (*Resp, error) { - httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - return finishRequest[Resp](httpReq) -} - -func postJSON[Req any, Resp any](ctx context.Context, url string, req *Req) (*Resp, error) { - jsonBody, err := json.Marshal(req) - if err != nil { - return nil, err - } - httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonBody)) - if err != nil { - return nil, err - } - httpReq.Header.Set("Content-Type", "application/json") - return finishRequest[Resp](httpReq) -} - const requestTimeout = time.Minute func finishRequest[Resp any](httpReq *http.Request) (*Resp, error) { diff --git a/syz-cluster/pkg/api/http.go b/syz-cluster/pkg/api/http.go new file mode 100644 index 000000000..420b787a7 --- /dev/null +++ b/syz-cluster/pkg/api/http.go @@ -0,0 +1,61 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package api + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" +) + +func getJSON[Resp any](ctx context.Context, url string) (*Resp, error) { + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + return finishRequest[Resp](httpReq) +} + +func postJSON[Req any, Resp any](ctx context.Context, url string, req *Req) (*Resp, error) { + jsonBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonBody)) + if err != nil { + return nil, err + } + httpReq.Header.Set("Content-Type", "application/json") + return finishRequest[Resp](httpReq) +} + +func ReplyJSON[T any](w http.ResponseWriter, resp T) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(resp) + if err != nil { + http.Error(w, "failed to serialize the response", http.StatusInternalServerError) + return + } +} + +func ParseJSON[T any](w http.ResponseWriter, r *http.Request) *T { + if r.Method != http.MethodPost { + http.Error(w, "must be called via POST", http.StatusMethodNotAllowed) + return nil + } + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "failed to read body", http.StatusBadRequest) + return nil + } + var data T + err = json.Unmarshal(body, &data) + if err != nil { + http.Error(w, "invalid body", http.StatusBadRequest) + return nil + } + return &data +} diff --git a/syz-cluster/pkg/api/reporter.go b/syz-cluster/pkg/api/reporter.go new file mode 100644 index 000000000..dc85db45b --- /dev/null +++ b/syz-cluster/pkg/api/reporter.go @@ -0,0 +1,51 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package api + +import ( + "context" + "strings" +) + +type ReporterClient struct { + baseURL string +} + +func NewReporterClient(url string) *ReporterClient { + return &ReporterClient{baseURL: strings.TrimRight(url, "/")} +} + +type NextReportResp struct { + Report *SessionReport `json:"report"` +} + +func (client ReporterClient) GetNextReport(ctx context.Context) (*NextReportResp, error) { + return postJSON[any, NextReportResp](ctx, client.baseURL+"/reports", nil) +} + +// TODO: What to do if sending the report failed? Retry or mark as failed? + +type UpdateReportReq struct { + Link string `json:"link"` +} + +func (client ReporterClient) UpdateReport(ctx context.Context, id string, req *UpdateReportReq) error { + _, err := postJSON[UpdateReportReq, any](ctx, client.baseURL+"/reports/"+id+"/update", req) + return err +} + +// ConfirmReport should be called to mark a report as sent. +func (client ReporterClient) ConfirmReport(ctx context.Context, id string) error { + _, err := postJSON[any, any](ctx, client.baseURL+"/reports/"+id+"/confirm", nil) + return err +} + +type UpstreamReportReq struct { + User string `json:"user"` +} + +func (client ReporterClient) UpstreamReport(ctx context.Context, id string, req *UpstreamReportReq) error { + _, err := postJSON[UpstreamReportReq, any](ctx, client.baseURL+"/reports/"+id+"/upstream", req) + return err +} diff --git a/syz-cluster/pkg/controller/api.go b/syz-cluster/pkg/controller/api.go index 270803c3b..85910bb06 100644 --- a/syz-cluster/pkg/controller/api.go +++ b/syz-cluster/pkg/controller/api.go @@ -1,14 +1,13 @@ // Copyright 2024 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. -// nolint: dupl // The methods look similar, but extracting the common parts will only make the code worse. +// Package controller provides the server part of the *api.Client interface. +// nolint: dupl package controller import ( - "encoding/json" "errors" "fmt" - "io" "net/http" "github.com/google/syzkaller/syz-cluster/pkg/api" @@ -57,7 +56,7 @@ func (c APIServer) getSessionSeries(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply(w, resp) + api.ReplyJSON(w, resp) } func (c APIServer) skipSession(w http.ResponseWriter, r *http.Request) { @@ -73,7 +72,7 @@ func (c APIServer) skipSession(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply[interface{}](w, nil) + api.ReplyJSON[interface{}](w, nil) } func (c APIServer) getSeries(w http.ResponseWriter, r *http.Request) { @@ -85,7 +84,7 @@ func (c APIServer) getSeries(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply(w, resp) + api.ReplyJSON(w, resp) } func (c APIServer) uploadBuild(w http.ResponseWriter, r *http.Request) { @@ -99,7 +98,7 @@ func (c APIServer) uploadBuild(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply(w, resp) + api.ReplyJSON(w, resp) } func (c APIServer) uploadTest(w http.ResponseWriter, r *http.Request) { @@ -113,11 +112,11 @@ func (c APIServer) uploadTest(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply[interface{}](w, nil) + api.ReplyJSON[interface{}](w, nil) } func (c APIServer) uploadFinding(w http.ResponseWriter, r *http.Request) { - req := api.ParseJSON[api.Finding](w, r) + req := api.ParseJSON[api.NewFinding](w, r) if req == nil { return } @@ -127,7 +126,7 @@ func (c APIServer) uploadFinding(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply[interface{}](w, nil) + api.ReplyJSON[interface{}](w, nil) } func (c APIServer) getLastBuild(w http.ResponseWriter, r *http.Request) { @@ -140,7 +139,7 @@ func (c APIServer) getLastBuild(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply[*api.Build](w, resp) + api.ReplyJSON[*api.Build](w, resp) } func (c APIServer) uploadSeries(w http.ResponseWriter, r *http.Request) { @@ -153,7 +152,7 @@ func (c APIServer) uploadSeries(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply[*api.UploadSeriesResp](w, resp) + api.ReplyJSON[*api.UploadSeriesResp](w, resp) } func (c APIServer) uploadSession(w http.ResponseWriter, r *http.Request) { @@ -166,33 +165,5 @@ func (c APIServer) uploadSession(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - reply[*api.UploadSessionResp](w, resp) -} - -func reply[T any](w http.ResponseWriter, resp T) { - w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(resp) - if err != nil { - http.Error(w, "failed to serialize the response", http.StatusInternalServerError) - return - } -} - -func parseBody[T any](w http.ResponseWriter, r *http.Request) *T { - if r.Method != http.MethodPost { - http.Error(w, "must be called via POST", http.StatusMethodNotAllowed) - return nil - } - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "failed to read body", http.StatusBadRequest) - return nil - } - var data T - err = json.Unmarshal(body, &data) - if err != nil { - http.Error(w, "invalid body", http.StatusBadRequest) - return nil - } - return &data + api.ReplyJSON[*api.UploadSessionResp](w, resp) } diff --git a/syz-cluster/pkg/controller/api_test.go b/syz-cluster/pkg/controller/api_test.go index 2e3483414..f3954d4f3 100644 --- a/syz-cluster/pkg/controller/api_test.go +++ b/syz-cluster/pkg/controller/api_test.go @@ -57,7 +57,7 @@ func TestAPISaveFinding(t *testing.T) { assert.NoError(t, err) t.Run("not existing test", func(t *testing.T) { - err = client.UploadFinding(ctx, &api.Finding{ + err = client.UploadFinding(ctx, &api.NewFinding{ SessionID: sessionID, TestName: "unknown test", }) @@ -65,7 +65,7 @@ func TestAPISaveFinding(t *testing.T) { }) t.Run("must succeed", func(t *testing.T) { - finding := &api.Finding{ + finding := &api.NewFinding{ SessionID: sessionID, TestName: "test", Report: []byte("report"), diff --git a/syz-cluster/pkg/db/entities.go b/syz-cluster/pkg/db/entities.go index db714e554..38d7c3a33 100644 --- a/syz-cluster/pkg/db/entities.go +++ b/syz-cluster/pkg/db/entities.go @@ -102,3 +102,15 @@ type Finding struct { ReportURI string `spanner:"ReportURI"` LogURI string `spanner:"LogURI"` } + +type SessionReport struct { + ID string `spanner:"ID"` + SessionID string `spanner:"SessionID"` + ReportedAt spanner.NullTime `spanner:"ReportedAt"` + Moderation bool `spanner:"Moderation"` + Link string `spanner:"Link"` +} + +func (s *SessionReport) SetReportedAt(t time.Time) { + s.ReportedAt = spanner.NullTime{Time: t, Valid: true} +} diff --git a/syz-cluster/pkg/db/finding_repo.go b/syz-cluster/pkg/db/finding_repo.go index dff2acdd8..c9682f439 100644 --- a/syz-cluster/pkg/db/finding_repo.go +++ b/syz-cluster/pkg/db/finding_repo.go @@ -65,10 +65,10 @@ func (repo *FindingRepository) Save(ctx context.Context, finding *Finding) error } // nolint: dupl -func (repo *FindingRepository) ListForSession(ctx context.Context, session *Session) ([]*Finding, error) { +func (repo *FindingRepository) ListForSession(ctx context.Context, sessionID string) ([]*Finding, error) { stmt := spanner.Statement{ SQL: "SELECT * FROM `Findings` WHERE `SessionID` = @session ORDER BY `TestName`, `Title`", - Params: map[string]interface{}{"session": session.ID}, + Params: map[string]interface{}{"session": sessionID}, } iter := repo.client.Single().Query(ctx, stmt) defer iter.Stop() diff --git a/syz-cluster/pkg/db/finding_repo_test.go b/syz-cluster/pkg/db/finding_repo_test.go index ac18c1f5d..f4668ad22 100644 --- a/syz-cluster/pkg/db/finding_repo_test.go +++ b/syz-cluster/pkg/db/finding_repo_test.go @@ -64,7 +64,7 @@ func TestFindingRepo(t *testing.T) { assert.ErrorIs(t, err, ErrFindingExists) } - list, err := findingRepo.ListForSession(ctx, session) + list, err := findingRepo.ListForSession(ctx, session.ID) assert.NoError(t, err) assert.Equal(t, toInsert, list) } diff --git a/syz-cluster/pkg/db/migrations/1_initialize.up.sql b/syz-cluster/pkg/db/migrations/1_initialize.up.sql index 0be0c7cef..e7a1b396d 100644 --- a/syz-cluster/pkg/db/migrations/1_initialize.up.sql +++ b/syz-cluster/pkg/db/migrations/1_initialize.up.sql @@ -61,6 +61,7 @@ CREATE TABLE Sessions ( ) PRIMARY KEY(ID); ALTER TABLE Series ADD CONSTRAINT FK_SeriesLatestSession FOREIGN KEY (LatestSessionID) REFERENCES Sessions (ID); +CREATE INDEX SessionsByFinishedAt ON Sessions (FinishedAt); -- Individual tests/steps completed within a session. CREATE TABLE SessionTests ( @@ -94,3 +95,16 @@ CREATE TABLE Findings ( ) PRIMARY KEY (ID); CREATE UNIQUE INDEX NoDupFindings ON Findings(SessionID, TestName, Title); + +-- Session's bug reports. +CREATE TABLE SessionReports ( + ID STRING(36) NOT NULL, -- UUID?? + SessionID STRING(36) NOT NULL, -- UUID + ReportedAt TIMESTAMP, + Moderation BOOL, + Link STRING(256), + CONSTRAINT FK_SessionReports FOREIGN KEY (SessionID) REFERENCES Sessions (ID), +) PRIMARY KEY(ID); + +CREATE UNIQUE INDEX NoDupSessionReports ON SessionReports(SessionID, Moderation); +CREATE INDEX SessionReportsByStatus ON SessionReports (Moderation, ReportedAt); diff --git a/syz-cluster/pkg/db/report_repo.go b/syz-cluster/pkg/db/report_repo.go new file mode 100644 index 000000000..0dec30e6f --- /dev/null +++ b/syz-cluster/pkg/db/report_repo.go @@ -0,0 +1,43 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package db + +import ( + "context" + + "cloud.google.com/go/spanner" + "github.com/google/uuid" +) + +type ReportRepository struct { + client *spanner.Client + *genericEntityOps[SessionReport, string] +} + +func NewReportRepository(client *spanner.Client) *ReportRepository { + return &ReportRepository{ + client: client, + genericEntityOps: &genericEntityOps[SessionReport, string]{ + client: client, + keyField: "ID", + table: "SessionReports", + }, + } +} + +func (repo *ReportRepository) Insert(ctx context.Context, rep *SessionReport) error { + if rep.ID == "" { + rep.ID = uuid.New().String() + } + return repo.genericEntityOps.Insert(ctx, rep) +} + +func (repo *ReportRepository) ListNotReported(ctx context.Context, limit int) ([]*SessionReport, error) { + stmt := spanner.Statement{ + SQL: "SELECT * FROM `SessionReports` WHERE `ReportedAt` IS NULL", + Params: map[string]interface{}{}, + } + addLimit(&stmt, limit) + return repo.readEntities(ctx, stmt) +} diff --git a/syz-cluster/pkg/db/report_repo_test.go b/syz-cluster/pkg/db/report_repo_test.go new file mode 100644 index 000000000..9648a7bc2 --- /dev/null +++ b/syz-cluster/pkg/db/report_repo_test.go @@ -0,0 +1,116 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package db + +import ( + "fmt" + "testing" + "time" + + "github.com/google/syzkaller/syz-cluster/pkg/api" + "github.com/stretchr/testify/assert" +) + +func TestReportRepository(t *testing.T) { + client, ctx := NewTransientDB(t) + sessionRepo := NewSessionRepository(client) + seriesRepo := NewSeriesRepository(client) + reportRepo := NewReportRepository(client) + + var keys []string + for i := 0; i < 3; i++ { + series := &Series{ExtID: fmt.Sprintf("series%d", i)} + err := seriesRepo.Insert(ctx, series, nil) + assert.NoError(t, err) + + session := &Session{SeriesID: series.ID} + err = sessionRepo.Insert(ctx, session) + assert.NoError(t, err) + + report := &SessionReport{SessionID: session.ID} + err = reportRepo.Insert(ctx, report) + assert.NoError(t, err) + keys = append(keys, report.ID) + } + + list, err := reportRepo.ListNotReported(ctx, 10) + assert.NoError(t, err) + assert.Len(t, list, 3) + + err = reportRepo.Update(ctx, keys[0], func(rep *SessionReport) error { + rep.SetReportedAt(time.Now()) + return nil + }) + assert.NoError(t, err) + + // Now one less. + list, err = reportRepo.ListNotReported(ctx, 10) + assert.NoError(t, err) + assert.Len(t, list, 2) +} + +func TestSessionsWithoutReports(t *testing.T) { + client, ctx := NewTransientDB(t) + sessionRepo := NewSessionRepository(client) + seriesRepo := NewSeriesRepository(client) + findingRepo := NewFindingRepository(client) + testsRepo := NewSessionTestRepository(client) + + series := &Series{ExtID: "some-series"} + err := seriesRepo.Insert(ctx, series, nil) + assert.NoError(t, err) + + // Set up 3 sessions, 2 of which would have a finding. + var sessions []*Session + for i := 0; i < 3; i++ { + session := &Session{SeriesID: series.ID} + sessions = append(sessions, session) + err = sessionRepo.Insert(ctx, session) + assert.NoError(t, err) + if i == 0 || i == 1 { + // Fake a test and a finding. + err = testsRepo.InsertOrUpdate(ctx, &SessionTest{ + SessionID: session.ID, + TestName: "test", + Result: api.TestPassed, + }) + assert.NoError(t, err) + err = findingRepo.Save(ctx, &Finding{ + SessionID: session.ID, + TestName: "test", + Title: "A", + }) + assert.NoError(t, err) + } + } + // For now it should be 0 -- none are finished. + list, err := sessionRepo.MissingReportList(ctx, time.Time{}, 10) + assert.NoError(t, err) + assert.Len(t, list, 0) + + // Finish all sessions. + for _, session := range sessions { + err := sessionRepo.Update(ctx, session.ID, func(session *Session) error { + session.SetFinishedAt(time.Now()) + return nil + }) + assert.NoError(t, err) + } + + // Now it should be 2. + list, err = sessionRepo.MissingReportList(ctx, time.Time{}, 10) + assert.NoError(t, err) + assert.Len(t, list, 2) + + // Create a report for the first session. + reportRepo := NewReportRepository(client) + err = reportRepo.Insert(ctx, &SessionReport{SessionID: sessions[0].ID}) + assert.NoError(t, err) + + // Now only the second session must be returned. + list, err = sessionRepo.MissingReportList(ctx, time.Time{}, 10) + assert.NoError(t, err) + assert.Len(t, list, 1) + assert.Equal(t, list[0].ID, sessions[1].ID) +} diff --git a/syz-cluster/pkg/db/session_repo.go b/syz-cluster/pkg/db/session_repo.go index 2ed5b8deb..6b72ed1aa 100644 --- a/syz-cluster/pkg/db/session_repo.go +++ b/syz-cluster/pkg/db/session_repo.go @@ -91,7 +91,6 @@ type NextSession struct { func (repo *SessionRepository) ListWaiting(ctx context.Context, from *NextSession, limit int) ([]*Session, *NextSession, error) { - // Here we assume that once the session is started, it never appears again. stmt := spanner.Statement{ SQL: "SELECT * FROM `Sessions` WHERE `StartedAt` IS NULL", Params: map[string]interface{}{}, @@ -102,13 +101,8 @@ func (repo *SessionRepository) ListWaiting(ctx context.Context, from *NextSessio stmt.Params["id"] = from.id } stmt.SQL += " ORDER BY `CreatedAt`, `ID`" - if limit > 0 { - stmt.SQL += " LIMIT @limit" - stmt.Params["limit"] = limit - } - iter := repo.client.Single().Query(ctx, stmt) - defer iter.Stop() - list, err := readEntities[Session](iter) + addLimit(&stmt, limit) + list, err := repo.readEntities(ctx, stmt) var next *NextSession if err == nil && len(list) > 0 { @@ -124,11 +118,30 @@ func (repo *SessionRepository) ListWaiting(ctx context.Context, from *NextSessio // golint sees too much similarity with SeriesRepository's ListPatches, but in reality there's not. // nolint:dupl func (repo *SessionRepository) ListForSeries(ctx context.Context, series *Series) ([]*Session, error) { - stmt := spanner.Statement{ + return repo.readEntities(ctx, spanner.Statement{ SQL: "SELECT * FROM `Sessions` WHERE `SeriesID` = @series ORDER BY CreatedAt DESC", Params: map[string]interface{}{"series": series.ID}, + }) +} + +// MissingReportList lists the session objects that are missing any SessionReport objects, +// but do have Findings. +// Once the conditions for creating a SessionRepor object become more complex, it will +// likely be not enough to have this simple method, but for now it should be fine. +func (repo *SessionRepository) MissingReportList(ctx context.Context, from time.Time, limit int) ([]*Session, error) { + stmt := spanner.Statement{ + SQL: "SELECT * FROM Sessions WHERE FinishedAt IS NOT NULL " + + " AND NOT EXISTS (" + + "SELECT 1 FROM SessionReports WHERE SessionReports.SessionID = Sessions.ID" + + ") AND EXISTS (" + + "SELECT 1 FROM Findings WHERE Findings.SessionID = Sessions.ID)", + Params: map[string]interface{}{}, } - iter := repo.client.Single().Query(ctx, stmt) - defer iter.Stop() - return readEntities[Session](iter) + if !from.IsZero() { + stmt.SQL += " AND `FinishedAt` > @from" + stmt.Params["from"] = from + } + stmt.SQL += " ORDER BY `FinishedAt`" + addLimit(&stmt, limit) + return repo.readEntities(ctx, stmt) } diff --git a/syz-cluster/pkg/db/spanner.go b/syz-cluster/pkg/db/spanner.go index 35020f3cd..77b126be7 100644 --- a/syz-cluster/pkg/db/spanner.go +++ b/syz-cluster/pkg/db/spanner.go @@ -257,6 +257,13 @@ func readEntities[T any](iter *spanner.RowIterator) ([]*T, error) { return ret, nil } +func addLimit(stmt *spanner.Statement, limit int) { + if limit > 0 { + stmt.SQL += " LIMIT @limit" + stmt.Params["limit"] = limit + } +} + type genericEntityOps[EntityType, KeyType any] struct { client *spanner.Client keyField string @@ -316,3 +323,10 @@ func (g *genericEntityOps[EntityType, KeyType]) Insert(ctx context.Context, obj }) return err } + +func (g *genericEntityOps[EntityType, KeyType]) readEntities(ctx context.Context, stmt spanner.Statement) ( + []*EntityType, error) { + iter := g.client.Single().Query(ctx, stmt) + defer iter.Stop() + return readEntities[EntityType](iter) +} diff --git a/syz-cluster/pkg/service/finding.go b/syz-cluster/pkg/service/finding.go index 21eb74c66..b0530a42c 100644 --- a/syz-cluster/pkg/service/finding.go +++ b/syz-cluster/pkg/service/finding.go @@ -26,7 +26,7 @@ func NewFindingService(env *app.AppEnvironment) *FindingService { } } -func (s *FindingService) Save(ctx context.Context, req *api.Finding) error { +func (s *FindingService) Save(ctx context.Context, req *api.NewFinding) error { var reportURI, logURI string var err error if len(req.Log) > 0 { @@ -55,3 +55,23 @@ func (s *FindingService) Save(ctx context.Context, req *api.Finding) error { } return err } + +func (s *FindingService) List(ctx context.Context, sessionID string) ([]*api.Finding, error) { + list, err := s.findingRepo.ListForSession(ctx, sessionID) + if err != nil { + return nil, fmt.Errorf("failed to query the list: %w", err) + } + var ret []*api.Finding + for _, item := range list { + finding := &api.Finding{ + Title: item.Title, + LogURL: "TODO", // TODO: where to take it from? + } + finding.Report, err = blob.ReadAllBytes(s.blobStorage, item.ReportURI) + if err != nil { + return nil, fmt.Errorf("failed to read the report: %w", err) + } + ret = append(ret, finding) + } + return ret, nil +} diff --git a/syz-cluster/pkg/service/report.go b/syz-cluster/pkg/service/report.go new file mode 100644 index 000000000..4e8bc4974 --- /dev/null +++ b/syz-cluster/pkg/service/report.go @@ -0,0 +1,116 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package service + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/syzkaller/syz-cluster/pkg/api" + "github.com/google/syzkaller/syz-cluster/pkg/app" + "github.com/google/syzkaller/syz-cluster/pkg/db" +) + +type ReportService struct { + reportRepo *db.ReportRepository + seriesService *SeriesService + findingService *FindingService +} + +func NewReportService(env *app.AppEnvironment) *ReportService { + return &ReportService{ + reportRepo: db.NewReportRepository(env.Spanner), + seriesService: NewSeriesService(env), + findingService: NewFindingService(env), + } +} + +var ErrReportNotFound = errors.New("report is not found") + +func (rs *ReportService) Update(ctx context.Context, id string, req *api.UpdateReportReq) error { + // TODO: validate the link? + err := rs.reportRepo.Update(ctx, id, func(rep *db.SessionReport) error { + if req.Link != "" { + rep.Link = req.Link + } + return nil + }) + if errors.Is(err, db.ErrEntityNotFound) { + return ErrReportNotFound + } + return err +} + +func (rs *ReportService) Confirm(ctx context.Context, id string) error { + err := rs.reportRepo.Update(ctx, id, func(rep *db.SessionReport) error { + if rep.ReportedAt.IsNull() { + rep.SetReportedAt(time.Now()) + } + // TODO: fail if already confirmed? + return nil + }) + if errors.Is(err, db.ErrEntityNotFound) { + return ErrReportNotFound + } + return err +} + +var ErrNotOnModeration = errors.New("the report is not on moderation") + +func (rs *ReportService) Upstream(ctx context.Context, id string, req *api.UpstreamReportReq) error { + rep, err := rs.query(ctx, id) + if err != nil { + return nil + } else if !rep.Moderation { + return ErrNotOnModeration + } + // In case of a concurrent Upstream() call or an Upstream() invocation on + // an already upstreamed report, the "NoDupSessionReports" index should + // prevent duplications. + err = rs.reportRepo.Insert(ctx, &db.SessionReport{ + SessionID: rep.SessionID, + }) + if err != nil { + return fmt.Errorf("failed to schedule a new report: %w", err) + } + return nil +} + +func (rs *ReportService) Next(ctx context.Context) (*api.NextReportResp, error) { + list, err := rs.reportRepo.ListNotReported(ctx, 1) + if err != nil { + return nil, err + } else if len(list) != 1 { + return &api.NextReportResp{}, nil + } + report := list[0] + series, err := rs.seriesService.GetSessionSeriesShort(ctx, report.SessionID) + if err != nil { + return nil, fmt.Errorf("failed to query series: %w", err) + } + findings, err := rs.findingService.List(ctx, report.SessionID) + if err != nil { + return nil, fmt.Errorf("failed to query findings: %w", err) + } + return &api.NextReportResp{ + Report: &api.SessionReport{ + ID: report.ID, + Moderation: report.Moderation, + Series: series, + Findings: findings, + }, + }, nil +} + +func (rs *ReportService) query(ctx context.Context, id string) (*db.SessionReport, error) { + rep, err := rs.reportRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to query the report: %w", err) + } else if rep == nil { + return nil, ErrReportNotFound + } + return rep, err +} diff --git a/syz-cluster/pkg/service/series.go b/syz-cluster/pkg/service/series.go index 13579cefe..8ab8b5cef 100644 --- a/syz-cluster/pkg/service/series.go +++ b/syz-cluster/pkg/service/series.go @@ -32,13 +32,23 @@ func NewSeriesService(env *app.AppEnvironment) *SeriesService { } func (s *SeriesService) GetSessionSeries(ctx context.Context, sessionID string) (*api.Series, error) { + return s.getSessionSeries(ctx, sessionID, true) +} + +func (s *SeriesService) GetSessionSeriesShort(ctx context.Context, + sessionID string) (*api.Series, error) { + return s.getSessionSeries(ctx, sessionID, false) +} + +func (s *SeriesService) getSessionSeries(ctx context.Context, sessionID string, + includePatches bool) (*api.Series, error) { session, err := s.sessionRepo.GetByID(ctx, sessionID) if err != nil { return nil, fmt.Errorf("failed to fetch the session: %w", err) } else if session == nil { return nil, fmt.Errorf("%w: %q", ErrSessionNotFound, sessionID) } - return s.GetSeries(ctx, session.SeriesID) + return s.getSeries(ctx, session.SeriesID, includePatches) } func (s *SeriesService) UploadSeries(ctx context.Context, series *api.Series) (*api.UploadSeriesResp, error) { @@ -84,6 +94,11 @@ func (s *SeriesService) UploadSeries(ctx context.Context, series *api.Series) (* var ErrSeriesNotFound = errors.New("series not found") func (s *SeriesService) GetSeries(ctx context.Context, seriesID string) (*api.Series, error) { + return s.getSeries(ctx, seriesID, true) +} + +func (s *SeriesService) getSeries(ctx context.Context, + seriesID string, includeBody bool) (*api.Series, error) { series, err := s.seriesRepo.GetByID(ctx, seriesID) if err != nil { return nil, fmt.Errorf("failed to fetch the series: %w", err) @@ -104,9 +119,12 @@ func (s *SeriesService) GetSeries(ctx context.Context, seriesID string) (*api.Se PublishedAt: series.PublishedAt, } for _, patch := range patches { - body, err := blob.ReadAllBytes(s.blobStorage, patch.BodyURI) - if err != nil { - return nil, fmt.Errorf("failed to read patch %q: %w", patch.ID, err) + var body []byte + if includeBody { + body, err = blob.ReadAllBytes(s.blobStorage, patch.BodyURI) + if err != nil { + return nil, fmt.Errorf("failed to read patch %q: %w", patch.ID, err) + } } ret.Patches = append(ret.Patches, api.SeriesPatch{ Seq: int(patch.Seq), diff --git a/syz-cluster/reporter/Dockerfile b/syz-cluster/reporter/Dockerfile new file mode 100644 index 000000000..efefeaec7 --- /dev/null +++ b/syz-cluster/reporter/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.23-alpine AS reporter-builder + +WORKDIR /build + +# Prepare the dependencies. +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download +COPY pkg/gcs/ pkg/gcs/ + +# Build the tool. +COPY syz-cluster/reporter/ syz-cluster/reporter/ +COPY syz-cluster/pkg/ syz-cluster/pkg/ +RUN go build -o /bin/reporter /build/syz-cluster/reporter + +# Build the container. +FROM alpine:latest +WORKDIR /app + +COPY --from=reporter-builder /bin/reporter /bin/reporter + +EXPOSE 8080 + +ENTRYPOINT ["/bin/reporter"] diff --git a/syz-cluster/reporter/api.go b/syz-cluster/reporter/api.go new file mode 100644 index 000000000..73c27f134 --- /dev/null +++ b/syz-cluster/reporter/api.go @@ -0,0 +1,75 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "errors" + "fmt" + "net/http" + + "github.com/google/syzkaller/syz-cluster/pkg/api" + "github.com/google/syzkaller/syz-cluster/pkg/service" +) + +type ReporterAPI struct { + service *service.ReportService +} + +func NewReporterAPI(service *service.ReportService) *ReporterAPI { + return &ReporterAPI{service: service} +} + +func (ra *ReporterAPI) Mux() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/reports/{report_id}/update", ra.updateReport) + mux.HandleFunc("/reports/{report_id}/upstream", ra.upstreamReport) + mux.HandleFunc("/reports/{report_id}/confirm", ra.confirmReport) + mux.HandleFunc("/reports", ra.nextReports) + return mux +} + +// nolint: dupl +func (ra *ReporterAPI) updateReport(w http.ResponseWriter, r *http.Request) { + req := api.ParseJSON[api.UpdateReportReq](w, r) + if req == nil { + return // TODO: return StatusBadRequest here and below. + } + err := ra.service.Update(r.Context(), r.PathValue("report_id"), req) + reply[interface{}](w, nil, err) +} + +// nolint: dupl +func (ra *ReporterAPI) upstreamReport(w http.ResponseWriter, r *http.Request) { + req := api.ParseJSON[api.UpstreamReportReq](w, r) + if req == nil { + return + } + // TODO: journal the action. + err := ra.service.Upstream(r.Context(), r.PathValue("report_id"), req) + reply[interface{}](w, nil, err) +} + +func (ra *ReporterAPI) nextReports(w http.ResponseWriter, r *http.Request) { + resp, err := ra.service.Next(r.Context()) + reply(w, resp, err) +} + +func (ra *ReporterAPI) confirmReport(w http.ResponseWriter, r *http.Request) { + err := ra.service.Confirm(r.Context(), r.PathValue("report_id")) + reply[interface{}](w, nil, err) +} + +func reply[T any](w http.ResponseWriter, obj T, err error) { + if errors.Is(err, service.ErrReportNotFound) { + http.Error(w, fmt.Sprint(err), http.StatusNotFound) + return + } else if errors.Is(err, service.ErrNotOnModeration) { + http.Error(w, fmt.Sprint(err), http.StatusBadRequest) + return + } else if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) + return + } + api.ReplyJSON[T](w, obj) +} diff --git a/syz-cluster/reporter/api_test.go b/syz-cluster/reporter/api_test.go new file mode 100644 index 000000000..b4933afd7 --- /dev/null +++ b/syz-cluster/reporter/api_test.go @@ -0,0 +1,143 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "context" + "fmt" + "net/http/httptest" + "testing" + "time" + + "github.com/google/syzkaller/syz-cluster/pkg/api" + "github.com/google/syzkaller/syz-cluster/pkg/app" + "github.com/google/syzkaller/syz-cluster/pkg/controller" + "github.com/google/syzkaller/syz-cluster/pkg/db" + "github.com/google/syzkaller/syz-cluster/pkg/service" + "github.com/stretchr/testify/assert" +) + +func TestAPIReportFlow(t *testing.T) { + env, ctx := app.TestEnvironment(t) + client := controller.TestServer(t, env) + + // Create series/session/test/findings. + _, sessionID := controller.UploadTestSeries(t, ctx, client, testSeries) + buildResp := controller.UploadTestBuild(t, ctx, client, &api.Build{ + Arch: "amd64", + TreeName: "mainline", + ConfigName: "config", + CommitHash: "abcd", + }) + err := client.UploadTestResult(ctx, &api.TestResult{ + SessionID: sessionID, + BaseBuildID: buildResp.ID, + TestName: "test", + Result: api.TestRunning, + }) + assert.NoError(t, err) + for i := 0; i < 2; i++ { + finding := &api.NewFinding{ + SessionID: sessionID, + Title: fmt.Sprintf("finding %d", i), + TestName: "test", + Report: []byte(fmt.Sprintf("report %d", i)), + } + err = client.UploadFinding(ctx, finding) + assert.NoError(t, err) + } + + markSessionFinished(t, env, sessionID) + + generator := newReportGenerator(env) + err = generator.process(ctx, 1) + assert.NoError(t, err) + + reportClient := ReporterServer(t, env) + // The same report will be returned multiple times. + nextResp, err := reportClient.GetNextReport(ctx) + assert.NoError(t, err) + nextResp2, err := reportClient.GetNextReport(ctx) + assert.NoError(t, err) + assert.Equal(t, nextResp2, nextResp) + // We don't know IDs in advance. + nextResp.Report.ID = "" + nextResp.Report.Series.ID = "" + assert.Equal(t, &api.SessionReport{ + Moderation: true, + Series: &api.Series{ + ExtID: testSeries.ExtID, + Title: testSeries.Title, + Patches: []api.SeriesPatch{ + { + Seq: 1, + Title: "first patch title", + // Body is empty. + }, + }, + }, + Findings: []*api.Finding{ + { + Title: "finding 0", + Report: []byte("report 0"), + LogURL: "TODO", // TODO + }, + { + Title: "finding 1", + Report: []byte("report 1"), + LogURL: "TODO", // TODO + }, + }, + }, nextResp.Report) + + // Now confirm it. + reportID := nextResp2.Report.ID + err = reportClient.ConfirmReport(ctx, reportID) + assert.NoError(t, err) + + // It should no longer appear in Next(). + emptyNext, err := reportClient.GetNextReport(ctx) + assert.NoError(t, err) + assert.Nil(t, emptyNext.Report) + + // "Upstream" it. + err = reportClient.UpstreamReport(ctx, reportID, &api.UpstreamReportReq{ + User: "name", + }) + assert.NoError(t, err) + + // It should appear again, now with Moderation=false. + nextResp3, err := reportClient.GetNextReport(ctx) + assert.NoError(t, err) + assert.False(t, nextResp3.Report.Moderation) + assert.Equal(t, nextResp2.Report.Series, nextResp3.Report.Series) +} + +func ReporterServer(t *testing.T, env *app.AppEnvironment) *api.ReporterClient { + apiServer := NewReporterAPI(service.NewReportService(env)) + server := httptest.NewServer(apiServer.Mux()) + t.Cleanup(server.Close) + return api.NewReporterClient(server.URL) +} + +func markSessionFinished(t *testing.T, env *app.AppEnvironment, sessionID string) { + repo := db.NewSessionRepository(env.Spanner) + err := repo.Update(context.Background(), sessionID, func(session *db.Session) error { + session.SetFinishedAt(time.Now()) + return nil + }) + assert.NoError(t, err) +} + +var testSeries = &api.Series{ + ExtID: "ext-id", + Title: "test series name", + Patches: []api.SeriesPatch{ + { + Seq: 1, + Title: "first patch title", + Body: []byte("first content"), + }, + }, +} diff --git a/syz-cluster/reporter/deployment.yaml b/syz-cluster/reporter/deployment.yaml new file mode 100644 index 000000000..e1a14690f --- /dev/null +++ b/syz-cluster/reporter/deployment.yaml @@ -0,0 +1,32 @@ +# Copyright 2025 syzkaller project authors. All rights reserved. +# Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: reporter-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: reporter + template: + metadata: + labels: + app: reporter + spec: + containers: + - name: reporter-image + image: reporter-image # The actual image name is set in overalys. + envFrom: + - configMapRef: + name: global-config + ports: + - containerPort: 8080 + resources: + requests: + cpu: 2 + memory: 8G + limits: + cpu: 4 + memory: 16G diff --git a/syz-cluster/reporter/kustomization.yaml b/syz-cluster/reporter/kustomization.yaml new file mode 100644 index 000000000..138983d7b --- /dev/null +++ b/syz-cluster/reporter/kustomization.yaml @@ -0,0 +1,6 @@ +# Copyright 2025 syzkaller project authors. All rights reserved. +# Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +resources: +- deployment.yaml +- service.yaml diff --git a/syz-cluster/reporter/main.go b/syz-cluster/reporter/main.go new file mode 100644 index 000000000..ee0c6af63 --- /dev/null +++ b/syz-cluster/reporter/main.go @@ -0,0 +1,84 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + "github.com/google/syzkaller/syz-cluster/pkg/app" + "github.com/google/syzkaller/syz-cluster/pkg/db" + "github.com/google/syzkaller/syz-cluster/pkg/service" +) + +func main() { + ctx := context.Background() + env, err := app.Environment(ctx) + if err != nil { + app.Fatalf("failed to set up environment: %v", err) + } + + generator := newReportGenerator(env) + go generator.Loop(ctx) + + api := NewReporterAPI(service.NewReportService(env)) + log.Printf("listening on port 8080") + app.Fatalf("listen failed: %v", http.ListenAndServe(":8080", api.Mux())) +} + +type reportGenerator struct { + sessionRepo *db.SessionRepository + reportRepo *db.ReportRepository +} + +func newReportGenerator(env *app.AppEnvironment) *reportGenerator { + return &reportGenerator{ + sessionRepo: db.NewSessionRepository(env.Spanner), + reportRepo: db.NewReportRepository(env.Spanner), + } +} + +func (rg *reportGenerator) Loop(ctx context.Context) { + const ( + // There are no deep ideas behind these numbers. + sleepTime = time.Minute + limit = 5 + ) + for { + err := rg.process(ctx, limit) + if err != nil { + app.Errorf("failed to process reports: %v", err) + } + select { + case <-ctx.Done(): + return + case <-time.After(sleepTime): + } + } +} + +func (rg *reportGenerator) process(ctx context.Context, limit int) error { + // Consider only recently finished sessions. + // It helps optimize DB queries + older sessions are not relevant anyway. + const relevantPeriod = time.Hour * 24 * 3 + list, err := rg.sessionRepo.MissingReportList(ctx, + time.Now().Add(-relevantPeriod), limit) + if err != nil { + return fmt.Errorf("failed to query sessions: %w", err) + } + for _, session := range list { + report := &db.SessionReport{ + SessionID: session.ID, + Moderation: true, + } + err := rg.reportRepo.Insert(ctx, report) + if err != nil { + return fmt.Errorf("failed to insert the report: %w", err) + } + } + return nil +} diff --git a/syz-cluster/reporter/service.yaml b/syz-cluster/reporter/service.yaml new file mode 100644 index 000000000..541c1221b --- /dev/null +++ b/syz-cluster/reporter/service.yaml @@ -0,0 +1,14 @@ +# Copyright 2025 syzkaller project authors. All rights reserved. +# Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +apiVersion: v1 +kind: Service +metadata: + name: reporter-service +spec: + selector: + app: controller + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 diff --git a/syz-cluster/workflow/boot-step/main.go b/syz-cluster/workflow/boot-step/main.go index 8adf15fac..d88202034 100644 --- a/syz-cluster/workflow/boot-step/main.go +++ b/syz-cluster/workflow/boot-step/main.go @@ -87,7 +87,7 @@ func runTest(ctx context.Context, client *api.Client) (bool, error) { log.Printf("found: %q", rep.Title) if *flagFindings { log.Printf("reporting the finding") - findingErr := client.UploadFinding(ctx, &api.Finding{ + findingErr := client.UploadFinding(ctx, &api.NewFinding{ SessionID: *flagSession, TestName: *flagTestName, Title: rep.Title, diff --git a/syz-cluster/workflow/build-step/main.go b/syz-cluster/workflow/build-step/main.go index 38698af1d..aac78de00 100644 --- a/syz-cluster/workflow/build-step/main.go +++ b/syz-cluster/workflow/build-step/main.go @@ -72,7 +72,7 @@ func main() { if commit != nil { uploadReq.CommitDate = commit.CommitDate } - var finding *api.Finding + var finding *api.NewFinding if err != nil { log.Printf("failed to checkout: %v", err) uploadReq.Log = []byte(err.Error()) @@ -84,7 +84,7 @@ func main() { log.Printf("%s", output.Bytes()) log.Printf("failed to build: %v", err) uploadReq.Log = []byte(err.Error()) - finding = &api.Finding{ + finding = &api.NewFinding{ SessionID: *flagSession, TestName: *flagTestName, Title: "failed to build the kernel", @@ -97,7 +97,7 @@ func main() { } func reportResults(ctx context.Context, client *api.Client, patched bool, - uploadReq *api.UploadBuildReq, finding *api.Finding, output []byte) { + uploadReq *api.UploadBuildReq, finding *api.NewFinding, output []byte) { buildInfo, err := client.UploadBuild(ctx, uploadReq) if err != nil { app.Fatalf("failed to upload build: %v", err) diff --git a/syz-cluster/workflow/fuzz-step/main.go b/syz-cluster/workflow/fuzz-step/main.go index 65e762b9b..6acddc55c 100644 --- a/syz-cluster/workflow/fuzz-step/main.go +++ b/syz-cluster/workflow/fuzz-step/main.go @@ -182,7 +182,7 @@ func reportStatus(ctx context.Context, client *api.Client, status string) error } func reportFinding(ctx context.Context, client *api.Client, bug *manager.UniqueBug) error { - finding := &api.Finding{ + finding := &api.NewFinding{ SessionID: *flagSession, TestName: testName, Title: bug.Report.Title, |
