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