package github import ( "context" "encoding/json" "fmt" "io" "os" "strings" "time" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/clock" "github.com/aquasecurity/trivy/pkg/fanal/artifact" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/purl" "github.com/aquasecurity/trivy/pkg/types" ) const ( DirectRelationship string = "direct" IndirectRelationship string = "indirect" RuntimeScope string = "runtime" DevelopmentScope string = "development" ) type Package struct { PackageUrl string `json:"package_url,omitempty"` Relationship string `json:"relationship,omitempty"` Dependencies []string `json:"dependencies,omitempty"` Scope string `json:"scope,omitempty"` Metadata Metadata `json:"metadata,omitempty"` } type File struct { SrcLocation string `json:"source_location,omitempty"` } type Metadata map[string]any type Manifest struct { Name string `json:"name,omitempty"` File *File `json:"file,omitempty"` Metadata Metadata `json:"metadata,omitempty"` Resolved map[string]Package `json:"resolved,omitempty"` } type Job struct { Correlator string `json:"correlator,omitempty"` Id string `json:"id,omitempty"` } type Detector struct { Name string `json:"name"` Version string `json:"version"` Url string `json:"url"` } type DependencySnapshot struct { Version int `json:"version"` Detector Detector `json:"detector"` Metadata Metadata `json:"metadata,omitempty"` Ref string `json:"ref,omitempty"` Sha string `json:"sha,omitempty"` Job *Job `json:"job,omitempty"` Scanned string `json:"scanned,omitempty"` Manifests map[string]Manifest `json:"manifests,omitempty"` } // Writer generates JSON for GitHub Dependency Snapshots type Writer struct { Output io.Writer Version string } func (w Writer) Write(ctx context.Context, report types.Report) error { snapshot := &DependencySnapshot{} // use now() method that can be overwritten while integration tests run snapshot.Scanned = clock.Now(ctx).Format(time.RFC3339) snapshot.Detector = Detector{ Name: "trivy", Version: w.Version, Url: "https://github.com/aquasecurity/trivy", } snapshot.Version = 0 // The version of the repository snapshot submission. snapshot.Ref = os.Getenv("GITHUB_REF") snapshot.Sha = os.Getenv("GITHUB_SHA") snapshot.Job = &Job{ Correlator: fmt.Sprintf("%s_%s", os.Getenv("GITHUB_WORKFLOW"), os.Getenv("GITHUB_JOB")), Id: os.Getenv("GITHUB_RUN_ID"), } snapshot.Metadata = getMetadata(report) manifests := make(map[string]Manifest) for _, result := range report.Results { if result.Packages == nil { continue } manifest := Manifest{} manifest.Name = string(result.Type) // show path for language-specific packages only if result.Class == types.ClassLangPkg { if report.ArtifactType == artifact.TypeContainerImage { // `RepoDigests` ~= /@sha256: // `RepoTag` ~= /: // By concatenating the hash from `RepoDigests` at the end of `RepoTag` we get all the information imageReference := strings.Join(report.Metadata.RepoTags, ", ") imageWithHash := strings.Join(report.Metadata.RepoDigests, ", ") _, imageHash, found := strings.Cut(imageWithHash, "@") if found { imageReference += "@" + imageHash } // Replacing `source_location` in manifest by the image name, tag and hash manifest.File = &File{ SrcLocation: imageReference, } } else { manifest.File = &File{ SrcLocation: result.Target, } } } resolved := make(map[string]Package) for _, pkg := range result.Packages { var err error githubPkg := Package{} githubPkg.Scope = RuntimeScope githubPkg.Relationship = getPkgRelationshipType(pkg) githubPkg.Dependencies = pkg.DependsOn // TODO: replace with PURL githubPkg.PackageUrl, err = buildPurl(result.Type, report.Metadata, pkg) if err != nil { return xerrors.Errorf("unable to build purl for %s: %w", pkg.Name, err) } if pkg.FilePath != "" { githubPkg.Metadata = Metadata{"source_location": pkg.FilePath} } resolved[pkg.Name] = githubPkg } manifest.Resolved = resolved manifests[result.Target] = manifest } snapshot.Manifests = manifests output, err := json.MarshalIndent(snapshot, "", " ") if err != nil { return xerrors.Errorf("failed to marshal github dependency snapshots: %w", err) } if _, err = fmt.Fprint(w.Output, string(output)); err != nil { return xerrors.Errorf("failed to write github dependency snapshots: %w", err) } return nil } func getMetadata(report types.Report) Metadata { metadata := Metadata{} if report.Metadata.RepoTags != nil { metadata["aquasecurity:trivy:RepoTag"] = strings.Join(report.Metadata.RepoTags, ", ") } if report.Metadata.RepoDigests != nil { metadata["aquasecurity:trivy:RepoDigest"] = strings.Join(report.Metadata.RepoDigests, ", ") } return metadata } func getPkgRelationshipType(pkg ftypes.Package) string { if pkg.Relationship == ftypes.RelationshipIndirect { return IndirectRelationship } return DirectRelationship } func buildPurl(t ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) (string, error) { packageUrl, err := purl.New(t, metadata, pkg) if err != nil { return "", xerrors.Errorf("purl error: %w", err) } if packageUrl == nil { return "", nil } return packageUrl.String(), nil }