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