pkg/vul/import.go (163 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vul import ( "context" "fmt" "sync" ca "cloud.google.com/go/containeranalysis/apiv1" "github.com/GoogleCloudPlatform/aactl/pkg/container" "github.com/GoogleCloudPlatform/aactl/pkg/types" "github.com/GoogleCloudPlatform/aactl/pkg/utils" "github.com/GoogleCloudPlatform/aactl/pkg/vul/convert" "github.com/pkg/errors" "github.com/rs/zerolog/log" "google.golang.org/api/iterator" g "google.golang.org/genproto/googleapis/grafeas/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func Import(ctx context.Context, options types.Options) error { opt, ok := options.(*types.VulnerabilityOptions) if !ok || opt == nil { return errors.New("valid options required") } if err := options.Validate(); err != nil { return errors.Wrap(err, "error validating options") } resourceURL, err := container.GetFullURL(opt.Source) if err != nil { return errors.Wrap(err, "error getting full url") } log.Info().Msgf("Resource URL: %s", resourceURL) opt.Source = resourceURL s, err := utils.NewFileSource(opt.Project, opt.File, opt.Source) if err != nil { return errors.Wrap(err, "error creating source") } converter, err := convert.GetConverter(s.Format) if err != nil { return errors.Wrap(err, "error getting converter") } noteOccurrencesMap, err := converter(s) if err != nil { return errors.Wrap(err, "error converting source") } // TODO: Debug code //_ = deleteNoteOccurrences(ctx, opt, noteOccurrencesMap) log.Info().Msgf("found %d vulnerabilities", len(noteOccurrencesMap)) if err := post(ctx, noteOccurrencesMap, opt); err != nil { return err } // Create/Update discovery occurrence. return updateDiscoveryNoteAndOcc(ctx, opt.Project, opt.Source) } func post(ctx context.Context, list types.NoteOccurrencesMap, opt *types.VulnerabilityOptions) error { if list == nil { return errors.New("expected non-nil result") } var wg sync.WaitGroup ctx, cancel := context.WithCancel(ctx) defer cancel() for noteID, nocc := range list { wg.Add(1) go func(noteID string, nocc types.NoteOccurrences) { defer wg.Done() select { case <-ctx.Done(): return default: } if err := postNoteOccurrences(ctx, opt.Project, noteID, nocc); err != nil { log.Error().Err(err).Msg("error posting notes & occurrences") cancel() } }(noteID, nocc) } wg.Wait() return nil } // postNoteOccurrences creates new Notes and its associated Occurrences. // Notes will be created only if it does not exist. func postNoteOccurrences(ctx context.Context, projectID string, noteID string, nocc types.NoteOccurrences) error { if projectID == "" { return types.ErrMissingProject } // don't submit end-to-end test if projectID == types.TestProjectID { return nil } c, err := ca.NewClient(ctx) if err != nil { return errors.Wrap(err, "error creating client") } defer c.Close() p := fmt.Sprintf("projects/%s", projectID) // Create Note req := &g.CreateNoteRequest{ Parent: p, NoteId: noteID, Note: nocc.Note, } noteName := fmt.Sprintf("%s/notes/%s", p, noteID) _, err = c.GetGrafeasClient().CreateNote(ctx, req) if err != nil { // If note already exists, skip if status.Code(err) == codes.AlreadyExists { log.Debug().Msgf("already exists: %s", noteName) } else { return errors.Wrap(err, "error posting note") } } mergedOcc := mergeOccurrences(nocc.Occurrences) if err := createOrUpdateOccurrence(ctx, p, noteID, mergedOcc, c); err != nil { return errors.Wrap(err, "unable to create or update occurrence") } return nil } func mergeOccurrences(occurrences []*g.Occurrence) *g.Occurrence { if len(occurrences) == 0 { return nil } if len(occurrences) == 1 { return occurrences[0] } // Take the first one as parent and only take the PackageIssue from the others // This assumes that all other information in the occ is the same. parent := occurrences[0] for i := 1; i < len(occurrences); i++ { packageIssues := occurrences[i].GetVulnerability().GetPackageIssue() parent.GetVulnerability().PackageIssue = append(parent.GetVulnerability().PackageIssue, packageIssues...) } return parent } func createOrUpdateOccurrence(ctx context.Context, p string, noteID string, o *g.Occurrence, c *ca.Client) error { // Create occurrence. If already exists, update. listReq := &g.ListOccurrencesRequest{ Parent: p, Filter: fmt.Sprintf("noteId=\"%s\" AND resource_url=\"%s\"", noteID, o.GetResourceUri()), PageSize: 10, } var listRes []*g.Occurrence it := c.GetGrafeasClient().ListOccurrences(ctx, listReq) for { resp, err := it.Next() if errors.Is(err, iterator.Done) { break } if err != nil { return err } listRes = append(listRes, resp) } switch len(listRes) { // If there were no occurrences, we create the occurrence. case 0: req := &g.CreateOccurrenceRequest{ Parent: p, Occurrence: o, } _, err := c.GetGrafeasClient().CreateOccurrence(ctx, req) if err != nil { return errors.Wrap(err, "error posting occurrence") } // If there was one occurrence, we update it. case 1: updateReq := &g.UpdateOccurrenceRequest{ Name: listRes[0].GetName(), Occurrence: o, } if _, err := c.GetGrafeasClient().UpdateOccurrence(ctx, updateReq); err != nil { return errors.Wrap(err, "error updating occurrence") } default: return errors.New("list occurrence expected to return one " + "occurrence but more than one was returned") } return nil }