diff --git a/internal/formats/syftjson/model/document.go b/internal/formats/syftjson/model/document.go index 71e76f62d..0f07a55b6 100644 --- a/internal/formats/syftjson/model/document.go +++ b/internal/formats/syftjson/model/document.go @@ -4,10 +4,12 @@ package model type Document struct { Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog ArtifactRelationships []Relationship `json:"artifactRelationships"` - Source Source `json:"source"` // Source represents the original object that was cataloged - Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source - Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft - Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape + Files []File `json:"files,omitempty"` // note: must have omitempty + Secrets []Secrets `json:"secrets,omitempty"` // note: must have omitempty + Source Source `json:"source"` // Source represents the original object that was cataloged + Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source + Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft + Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape } // Descriptor describes what created the document as well as surrounding metadata diff --git a/internal/formats/syftjson/model/file.go b/internal/formats/syftjson/model/file.go new file mode 100644 index 000000000..be2c88df3 --- /dev/null +++ b/internal/formats/syftjson/model/file.go @@ -0,0 +1,25 @@ +package model + +import ( + "github.com/anchore/syft/syft/file" + + "github.com/anchore/syft/syft/source" +) + +type File struct { + ID string `json:"id"` + Location source.Coordinates `json:"location"` + Metadata *FileMetadataEntry `json:"metadata,omitempty"` + Contents string `json:"contents,omitempty"` + Digests []file.Digest `json:"digests,omitempty"` + Classifications []file.Classification `json:"classifications,omitempty"` +} + +type FileMetadataEntry struct { + Mode int `json:"mode"` + Type source.FileType `json:"type"` + LinkDestination string `json:"linkDestination,omitempty"` + UserID int `json:"userID"` + GroupID int `json:"groupID"` + MIMEType string `json:"mimeType"` +} diff --git a/internal/formats/syftjson/model/package.go b/internal/formats/syftjson/model/package.go index 74d660757..a1b967cef 100644 --- a/internal/formats/syftjson/model/package.go +++ b/internal/formats/syftjson/model/package.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/syft/syft/source" ) -// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling. +// Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling. type Package struct { PackageBasicData PackageCustomData diff --git a/internal/formats/syftjson/model/relationship.go b/internal/formats/syftjson/model/relationship.go index 8820156bd..46f6da22d 100644 --- a/internal/formats/syftjson/model/relationship.go +++ b/internal/formats/syftjson/model/relationship.go @@ -4,5 +4,5 @@ type Relationship struct { Parent string `json:"parent"` Child string `json:"child"` Type string `json:"type"` - Metadata interface{} `json:"metadata"` + Metadata interface{} `json:"metadata,omitempty"` } diff --git a/internal/formats/syftjson/model/secrets.go b/internal/formats/syftjson/model/secrets.go new file mode 100644 index 000000000..c5f468576 --- /dev/null +++ b/internal/formats/syftjson/model/secrets.go @@ -0,0 +1,11 @@ +package model + +import ( + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/source" +) + +type Secrets struct { + Location source.Coordinates `json:"location"` + Secrets []file.SearchResult `json:"secrets"` +} diff --git a/internal/formats/syftjson/to_format_model.go b/internal/formats/syftjson/to_format_model.go index ab8be0f7f..1871def81 100644 --- a/internal/formats/syftjson/to_format_model.go +++ b/internal/formats/syftjson/to_format_model.go @@ -2,6 +2,10 @@ package syftjson import ( "fmt" + "sort" + "strconv" + + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/artifact" @@ -16,7 +20,7 @@ import ( "github.com/anchore/syft/syft/source" ) -// TODO: this is export4ed for the use of the power-user command (temp) +// TODO: this is exported for the use of the power-user command (temp) func ToFormatModel(s sbom.SBOM, applicationConfig interface{}) model.Document { src, err := toSourceModel(s.Source) if err != nil { @@ -26,6 +30,8 @@ func ToFormatModel(s sbom.SBOM, applicationConfig interface{}) model.Document { return model.Document{ Artifacts: toPackageModels(s.Artifacts.PackageCatalog), ArtifactRelationships: toRelationshipModel(s.Relationships), + Files: toFile(s), + Secrets: toSecrets(s.Artifacts.Secrets), Source: src, Distro: toDistroModel(s.Artifacts.Distro), Descriptor: model.Descriptor{ @@ -40,6 +46,85 @@ func ToFormatModel(s sbom.SBOM, applicationConfig interface{}) model.Document { } } +func toSecrets(data map[source.Coordinates][]file.SearchResult) []model.Secrets { + results := make([]model.Secrets, 0) + for coordinates, secrets := range data { + results = append(results, model.Secrets{ + Location: coordinates, + Secrets: secrets, + }) + } + + // sort by real path then virtual path to ensure the result is stable across multiple runs + sort.SliceStable(results, func(i, j int) bool { + return results[i].Location.RealPath < results[j].Location.RealPath + }) + return results +} + +func toFile(s sbom.SBOM) []model.File { + results := make([]model.File, 0) + artifacts := s.Artifacts + + for _, coordinates := range sbom.AllCoordinates(s) { + var metadata *source.FileMetadata + if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists { + metadata = &metadataForLocation + } + + var digests []file.Digest + if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists { + digests = digestsForLocation + } + + var classifications []file.Classification + if classificationsForLocation, exists := artifacts.FileClassifications[coordinates]; exists { + classifications = classificationsForLocation + } + + var contents string + if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists { + contents = contentsForLocation + } + + results = append(results, model.File{ + ID: string(coordinates.ID()), + Location: coordinates, + Metadata: toFileMetadataEntry(coordinates, metadata), + Digests: digests, + Classifications: classifications, + Contents: contents, + }) + } + + // sort by real path then virtual path to ensure the result is stable across multiple runs + sort.SliceStable(results, func(i, j int) bool { + return results[i].Location.RealPath < results[j].Location.RealPath + }) + return results +} + +func toFileMetadataEntry(coordinates source.Coordinates, metadata *source.FileMetadata) *model.FileMetadataEntry { + if metadata == nil { + return nil + } + + mode, err := strconv.Atoi(fmt.Sprintf("%o", metadata.Mode)) + if err != nil { + log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err) + mode = 0 + } + + return &model.FileMetadataEntry{ + Mode: mode, + Type: metadata.Type, + LinkDestination: metadata.LinkDestination, + UserID: metadata.UserID, + GroupID: metadata.GroupID, + MIMEType: metadata.MIMEType, + } +} + func toPackageModels(catalog *pkg.Catalog) []model.Package { artifacts := make([]model.Package, 0) if catalog == nil { diff --git a/internal/mimetype_helper.go b/internal/mimetype_helper.go new file mode 100644 index 000000000..3dbc36098 --- /dev/null +++ b/internal/mimetype_helper.go @@ -0,0 +1,72 @@ +package internal + +import "github.com/scylladb/go-set/strset" + +var ( + ArchiveMIMETypeSet = strset.New( + // derived from https://en.wikipedia.org/wiki/List_of_archive_formats + []string{ + // archive only + "application/x-archive", + "application/x-cpio", + "application/x-shar", + "application/x-iso9660-image", + "application/x-sbx", + "application/x-tar", + // compression only + "application/x-bzip2", + "application/gzip", + "application/x-lzip", + "application/x-lzma", + "application/x-lzop", + "application/x-snappy-framed", + "application/x-xz", + "application/x-compress", + "application/zstd", + // archiving and compression + "application/x-7z-compressed", + "application/x-ace-compressed", + "application/x-astrotite-afa", + "application/x-alz-compressed", + "application/vnd.android.package-archive", + "application/x-freearc", + "application/x-arj", + "application/x-b1", + "application/vnd.ms-cab-compressed", + "application/x-cfs-compressed", + "application/x-dar", + "application/x-dgc-compressed", + "application/x-apple-diskimage", + "application/x-gca-compressed", + "application/java-archive", + "application/x-lzh", + "application/x-lzx", + "application/x-rar-compressed", + "application/x-stuffit", + "application/x-stuffitx", + "application/x-gtar", + "application/x-ms-wim", + "application/x-xar", + "application/zip", + "application/x-zoo", + }..., + ) + + ExecutableMIMETypeSet = strset.New( + []string{ + "application/x-executable", + "application/x-mach-binary", + "application/x-elf", + "application/x-sharedlib", + "application/vnd.microsoft.portable-executable", + }..., + ) +) + +func IsArchive(mimeType string) bool { + return ArchiveMIMETypeSet.Has(mimeType) +} + +func IsExecutable(mimeType string) bool { + return ExecutableMIMETypeSet.Has(mimeType) +}