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