github.com/anchore/syft@v1.38.2/syft/format/syftjson/to_format_model.go (about) 1 package syftjson 2 3 import ( 4 "fmt" 5 "sort" 6 "strconv" 7 8 stereoscopeFile "github.com/anchore/stereoscope/pkg/file" 9 "github.com/anchore/syft/internal" 10 "github.com/anchore/syft/internal/log" 11 "github.com/anchore/syft/internal/packagemetadata" 12 "github.com/anchore/syft/internal/sourcemetadata" 13 "github.com/anchore/syft/syft/artifact" 14 "github.com/anchore/syft/syft/file" 15 formatInternal "github.com/anchore/syft/syft/format/internal" 16 "github.com/anchore/syft/syft/format/syftjson/model" 17 "github.com/anchore/syft/syft/linux" 18 "github.com/anchore/syft/syft/pkg" 19 "github.com/anchore/syft/syft/sbom" 20 "github.com/anchore/syft/syft/source" 21 ) 22 23 // MetadataType infers the metadata type value based on the pkg.Metadata payload. 24 func MetadataType(metadata interface{}) string { 25 return metadataType(metadata, false) 26 } 27 28 func metadataType(metadata interface{}, legacy bool) string { 29 if legacy { 30 return packagemetadata.JSONLegacyName(metadata) 31 } 32 return packagemetadata.JSONName(metadata) 33 } 34 35 // ToFormatModel transforms the sbom import a format-specific model. 36 func ToFormatModel(s sbom.SBOM, cfg EncoderConfig) model.Document { 37 locationSorter, coordinateSorter := formatInternal.GetLocationSorters(s) 38 39 return model.Document{ 40 Artifacts: toPackageModels(s.Artifacts.Packages, locationSorter, cfg), 41 ArtifactRelationships: toRelationshipModel(s.Relationships), 42 Files: toFile(s, coordinateSorter), 43 Source: toSourceModel(s.Source), 44 Distro: toLinuxRelease(s.Artifacts.LinuxDistribution), 45 Descriptor: toDescriptor(s.Descriptor), 46 Schema: model.Schema{ 47 Version: internal.JSONSchemaVersion, 48 URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion), 49 }, 50 } 51 } 52 53 func toLinuxRelease(d *linux.Release) model.LinuxRelease { 54 if d == nil { 55 return model.LinuxRelease{} 56 } 57 return model.LinuxRelease{ 58 PrettyName: d.PrettyName, 59 Name: d.Name, 60 ID: d.ID, 61 IDLike: d.IDLike, 62 Version: d.Version, 63 VersionID: d.VersionID, 64 VersionCodename: d.VersionCodename, 65 BuildID: d.BuildID, 66 ImageID: d.ImageID, 67 ImageVersion: d.ImageVersion, 68 Variant: d.Variant, 69 VariantID: d.VariantID, 70 HomeURL: d.HomeURL, 71 SupportURL: d.SupportURL, 72 BugReportURL: d.BugReportURL, 73 PrivacyPolicyURL: d.PrivacyPolicyURL, 74 CPEName: d.CPEName, 75 SupportEnd: d.SupportEnd, 76 ExtendedSupport: d.ExtendedSupport, 77 } 78 } 79 80 func toDescriptor(d sbom.Descriptor) model.Descriptor { 81 return model.Descriptor{ 82 Name: d.Name, 83 Version: d.Version, 84 Configuration: d.Configuration, 85 } 86 } 87 88 func toFile(s sbom.SBOM, coordinateSorter func(a, b file.Coordinates) int) []model.File { 89 results := make([]model.File, 0) 90 artifacts := s.Artifacts 91 92 for _, coordinates := range s.AllCoordinates() { 93 var metadata *file.Metadata 94 if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists { 95 metadata = &metadataForLocation 96 } 97 98 var digests []file.Digest 99 if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists { 100 digests = digestsForLocation 101 } 102 103 var contents string 104 if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists { 105 contents = contentsForLocation 106 } 107 108 var unknowns []string 109 if unknownsForLocation, exists := artifacts.Unknowns[coordinates]; exists { 110 unknowns = unknownsForLocation 111 } 112 113 var licenses []model.FileLicense 114 for _, l := range artifacts.FileLicenses[coordinates] { 115 var evidence *model.FileLicenseEvidence 116 if e := l.LicenseEvidence; e != nil { 117 evidence = &model.FileLicenseEvidence{ 118 Confidence: e.Confidence, 119 Offset: e.Offset, 120 Extent: e.Extent, 121 } 122 } 123 licenses = append(licenses, model.FileLicense{ 124 Value: l.Value, 125 SPDXExpression: l.SPDXExpression, 126 Type: l.Type, 127 Evidence: evidence, 128 }) 129 } 130 131 var executable *file.Executable 132 if exec, exists := artifacts.Executables[coordinates]; exists { 133 executable = &exec 134 } 135 136 results = append(results, model.File{ 137 ID: string(coordinates.ID()), 138 Location: coordinates, 139 Metadata: toFileMetadataEntry(coordinates, metadata), 140 Digests: digests, 141 Contents: contents, 142 Licenses: licenses, 143 Executable: executable, 144 Unknowns: unknowns, 145 }) 146 } 147 148 // sort to ensure we're stable across multiple runs 149 // should order by the layer order from the container image then by real path 150 sortFiles(results, coordinateSorter) 151 return results 152 } 153 154 func sortFiles(files []model.File, coordinateSorter func(a, b file.Coordinates) int) { 155 fileSorter := func(a, b model.File) int { 156 return coordinateSorter(a.Location, b.Location) 157 } 158 sort.SliceStable(files, func(i, j int) bool { 159 return fileSorter(files[i], files[j]) < 0 160 }) 161 } 162 163 func toFileMetadataEntry(coordinates file.Coordinates, metadata *file.Metadata) *model.FileMetadataEntry { 164 if metadata == nil { 165 return nil 166 } 167 168 var mode int 169 var size int64 170 if metadata.FileInfo != nil { 171 var err error 172 173 mode, err = strconv.Atoi(fmt.Sprintf("%o", metadata.Mode())) 174 if err != nil { 175 log.Debugf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err) 176 mode = 0 177 } 178 179 size = metadata.Size() 180 } 181 182 return &model.FileMetadataEntry{ 183 Mode: mode, 184 Type: toFileType(metadata.Type), 185 LinkDestination: metadata.LinkDestination, 186 UserID: metadata.UserID, 187 GroupID: metadata.GroupID, 188 MIMEType: metadata.MIMEType, 189 Size: size, 190 } 191 } 192 193 func toFileType(ty stereoscopeFile.Type) string { 194 switch ty { 195 case stereoscopeFile.TypeSymLink: 196 return "SymbolicLink" 197 case stereoscopeFile.TypeHardLink: 198 return "HardLink" 199 case stereoscopeFile.TypeDirectory: 200 return "Directory" 201 case stereoscopeFile.TypeSocket: 202 return "Socket" 203 case stereoscopeFile.TypeBlockDevice: 204 return "BlockDevice" 205 case stereoscopeFile.TypeCharacterDevice: 206 return "CharacterDevice" 207 case stereoscopeFile.TypeFIFO: 208 return "FIFONode" 209 case stereoscopeFile.TypeRegular: 210 return "RegularFile" 211 case stereoscopeFile.TypeIrregular: 212 return "IrregularFile" 213 default: 214 return "Unknown" 215 } 216 } 217 218 func toPackageModels(catalog *pkg.Collection, locationSorter func(a, b file.Location) int, cfg EncoderConfig) []model.Package { 219 artifacts := make([]model.Package, 0) 220 if catalog == nil { 221 return artifacts 222 } 223 for _, p := range catalog.Sorted() { 224 artifacts = append(artifacts, toPackageModel(p, locationSorter, cfg)) 225 } 226 return artifacts 227 } 228 229 func toLicenseModel(pkgLicenses []pkg.License, locationSorter func(a, b file.Location) int) (modelLicenses []model.License) { 230 for _, l := range pkgLicenses { 231 // format model must have allocated collections 232 urls := l.URLs 233 if urls == nil { 234 urls = []string{} 235 } 236 237 modelLicenses = append(modelLicenses, model.License{ 238 Value: l.Value, 239 SPDXExpression: l.SPDXExpression, 240 Contents: l.Contents, 241 Type: l.Type, 242 URLs: urls, 243 Locations: toLocationsModel(l.Locations, locationSorter), 244 }) 245 } 246 return 247 } 248 249 // toPackageModel crates a new Package from the given pkg.Package. 250 func toPackageModel(p pkg.Package, locationSorter func(a, b file.Location) int, cfg EncoderConfig) model.Package { 251 var cpes = make([]model.CPE, len(p.CPEs)) 252 for i, c := range p.CPEs { 253 convertedCPE := model.CPE{ 254 Value: c.Attributes.String(), 255 Source: c.Source.String(), 256 } 257 cpes[i] = convertedCPE 258 } 259 260 // we want to make sure all catalogers are 261 // initializing the array; this is a good choke point for this check 262 var licenses = make([]model.License, 0) 263 if !p.Licenses.Empty() { 264 licenses = toLicenseModel(p.Licenses.ToSlice(), locationSorter) 265 } 266 267 return model.Package{ 268 PackageBasicData: model.PackageBasicData{ 269 ID: string(p.ID()), 270 Name: p.Name, 271 Version: p.Version, 272 Type: p.Type, 273 FoundBy: p.FoundBy, 274 Locations: toLocationsModel(p.Locations, locationSorter), 275 Licenses: licenses, 276 Language: p.Language, 277 CPEs: cpes, 278 PURL: p.PURL, 279 }, 280 PackageCustomData: model.PackageCustomData{ 281 MetadataType: metadataType(p.Metadata, cfg.Legacy), 282 Metadata: p.Metadata, 283 }, 284 } 285 } 286 287 func toLocationsModel(locations file.LocationSet, locationSorter func(a, b file.Location) int) []file.Location { 288 if locations.Empty() { 289 return []file.Location{} 290 } 291 292 return locations.ToSlice(locationSorter) 293 } 294 295 func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship { 296 result := make([]model.Relationship, len(relationships)) 297 for i, r := range relationships { 298 result[i] = model.Relationship{ 299 Parent: string(r.From.ID()), 300 Child: string(r.To.ID()), 301 Type: string(r.Type), 302 Metadata: r.Data, 303 } 304 } 305 sort.Slice(result, func(i, j int) bool { 306 if iParent, jParent := result[i].Parent, result[j].Parent; iParent != jParent { 307 return iParent < jParent 308 } 309 if iChild, jChild := result[i].Child, result[j].Child; iChild != jChild { 310 return iChild < jChild 311 } 312 return result[i].Type < result[j].Type 313 }) 314 return result 315 } 316 317 // toSourceModel creates a new source object to be represented into JSON. 318 func toSourceModel(src source.Description) model.Source { 319 m := model.Source{ 320 ID: src.ID, 321 Name: src.Name, 322 Version: src.Version, 323 Supplier: src.Supplier, 324 Type: sourcemetadata.JSONName(src.Metadata), 325 Metadata: src.Metadata, 326 } 327 328 if metadata, ok := src.Metadata.(source.ImageMetadata); ok { 329 // ensure that empty collections are not shown as null 330 if metadata.RepoDigests == nil { 331 metadata.RepoDigests = []string{} 332 } 333 if metadata.Tags == nil { 334 metadata.Tags = []string{} 335 } 336 m.Metadata = metadata 337 } 338 339 return m 340 }