github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/syftjson/to_syft_model.go (about) 1 package syftjson 2 3 import ( 4 "fmt" 5 "os" 6 "path" 7 "strconv" 8 "strings" 9 10 "github.com/google/go-cmp/cmp" 11 12 stereoscopeFile "github.com/anchore/stereoscope/pkg/file" 13 "github.com/anchore/syft/internal" 14 "github.com/anchore/syft/internal/log" 15 "github.com/anchore/syft/syft/artifact" 16 "github.com/anchore/syft/syft/cpe" 17 "github.com/anchore/syft/syft/file" 18 "github.com/anchore/syft/syft/formats/syftjson/model" 19 "github.com/anchore/syft/syft/linux" 20 "github.com/anchore/syft/syft/pkg" 21 "github.com/anchore/syft/syft/sbom" 22 "github.com/anchore/syft/syft/source" 23 ) 24 25 func toSyftModel(doc model.Document) (*sbom.SBOM, error) { 26 idAliases := make(map[string]string) 27 28 catalog := toSyftCatalog(doc.Artifacts, idAliases) 29 30 fileArtifacts := toSyftFiles(doc.Files) 31 32 return &sbom.SBOM{ 33 Artifacts: sbom.Artifacts{ 34 Packages: catalog, 35 FileMetadata: fileArtifacts.FileMetadata, 36 FileDigests: fileArtifacts.FileDigests, 37 FileContents: fileArtifacts.FileContents, 38 FileLicenses: fileArtifacts.FileLicenses, 39 LinuxDistribution: toSyftLinuxRelease(doc.Distro), 40 }, 41 Source: *toSyftSourceData(doc.Source), 42 Descriptor: toSyftDescriptor(doc.Descriptor), 43 Relationships: warnConversionErrors(toSyftRelationships(&doc, catalog, doc.ArtifactRelationships, idAliases)), 44 }, nil 45 } 46 47 func warnConversionErrors[T any](converted []T, errors []error) []T { 48 errorMessages := deduplicateErrors(errors) 49 for _, msg := range errorMessages { 50 log.Warn(msg) 51 } 52 return converted 53 } 54 55 func deduplicateErrors(errors []error) []string { 56 errorCounts := make(map[string]int) 57 var errorMessages []string 58 for _, e := range errors { 59 errorCounts[e.Error()] = errorCounts[e.Error()] + 1 60 } 61 for msg, count := range errorCounts { 62 errorMessages = append(errorMessages, fmt.Sprintf("%q occurred %d time(s)", msg, count)) 63 } 64 return errorMessages 65 } 66 67 func toSyftFiles(files []model.File) sbom.Artifacts { 68 ret := sbom.Artifacts{ 69 FileMetadata: make(map[file.Coordinates]file.Metadata), 70 FileDigests: make(map[file.Coordinates][]file.Digest), 71 FileContents: make(map[file.Coordinates]string), 72 FileLicenses: make(map[file.Coordinates][]file.License), 73 } 74 75 for _, f := range files { 76 coord := f.Location 77 if f.Metadata != nil { 78 mode, err := strconv.ParseInt(strconv.Itoa(f.Metadata.Mode), 8, 64) 79 if err != nil { 80 log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coord, f.Metadata.Mode, err) 81 mode = 0 82 } 83 84 fm := os.FileMode(mode) 85 86 ret.FileMetadata[coord] = file.Metadata{ 87 FileInfo: stereoscopeFile.ManualInfo{ 88 NameValue: path.Base(coord.RealPath), 89 SizeValue: f.Metadata.Size, 90 ModeValue: fm, 91 }, 92 Path: coord.RealPath, 93 LinkDestination: f.Metadata.LinkDestination, 94 UserID: f.Metadata.UserID, 95 GroupID: f.Metadata.GroupID, 96 Type: toSyftFileType(f.Metadata.Type), 97 MIMEType: f.Metadata.MIMEType, 98 } 99 } 100 101 for _, d := range f.Digests { 102 ret.FileDigests[coord] = append(ret.FileDigests[coord], file.Digest{ 103 Algorithm: d.Algorithm, 104 Value: d.Value, 105 }) 106 } 107 108 if f.Contents != "" { 109 ret.FileContents[coord] = f.Contents 110 } 111 112 for _, l := range f.Licenses { 113 var evidence *file.LicenseEvidence 114 if e := l.Evidence; e != nil { 115 evidence = &file.LicenseEvidence{ 116 Confidence: e.Confidence, 117 Offset: e.Offset, 118 Extent: e.Extent, 119 } 120 } 121 ret.FileLicenses[coord] = append(ret.FileLicenses[coord], file.License{ 122 Value: l.Value, 123 SPDXExpression: l.SPDXExpression, 124 Type: l.Type, 125 LicenseEvidence: evidence, 126 }) 127 } 128 } 129 130 return ret 131 } 132 133 func toSyftLicenses(m []model.License) (p []pkg.License) { 134 for _, l := range m { 135 p = append(p, pkg.License{ 136 Value: l.Value, 137 SPDXExpression: l.SPDXExpression, 138 Type: l.Type, 139 URLs: internal.NewStringSet(l.URLs...), 140 Locations: file.NewLocationSet(l.Locations...), 141 }) 142 } 143 return 144 } 145 146 func toSyftFileType(ty string) stereoscopeFile.Type { 147 switch ty { 148 case "SymbolicLink": 149 return stereoscopeFile.TypeSymLink 150 case "HardLink": 151 return stereoscopeFile.TypeHardLink 152 case "Directory": 153 return stereoscopeFile.TypeDirectory 154 case "Socket": 155 return stereoscopeFile.TypeSocket 156 case "BlockDevice": 157 return stereoscopeFile.TypeBlockDevice 158 case "CharacterDevice": 159 return stereoscopeFile.TypeCharacterDevice 160 case "FIFONode": 161 return stereoscopeFile.TypeFIFO 162 case "RegularFile": 163 return stereoscopeFile.TypeRegular 164 case "IrregularFile": 165 return stereoscopeFile.TypeIrregular 166 default: 167 return stereoscopeFile.TypeIrregular 168 } 169 } 170 171 func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release { 172 if cmp.Equal(d, model.LinuxRelease{}) { 173 return nil 174 } 175 return &linux.Release{ 176 PrettyName: d.PrettyName, 177 Name: d.Name, 178 ID: d.ID, 179 IDLike: d.IDLike, 180 Version: d.Version, 181 VersionID: d.VersionID, 182 VersionCodename: d.VersionCodename, 183 BuildID: d.BuildID, 184 ImageID: d.ImageID, 185 ImageVersion: d.ImageVersion, 186 Variant: d.Variant, 187 VariantID: d.VariantID, 188 HomeURL: d.HomeURL, 189 SupportURL: d.SupportURL, 190 BugReportURL: d.BugReportURL, 191 PrivacyPolicyURL: d.PrivacyPolicyURL, 192 CPEName: d.CPEName, 193 SupportEnd: d.SupportEnd, 194 } 195 } 196 197 func toSyftRelationships(doc *model.Document, catalog *pkg.Collection, relationships []model.Relationship, idAliases map[string]string) ([]artifact.Relationship, []error) { 198 idMap := make(map[string]interface{}) 199 200 for _, p := range catalog.Sorted() { 201 idMap[string(p.ID())] = p 202 locations := p.Locations.ToSlice() 203 for _, l := range locations { 204 idMap[string(l.Coordinates.ID())] = l.Coordinates 205 } 206 } 207 208 // set source metadata in identifier map 209 idMap[doc.Source.ID] = toSyftSource(doc.Source) 210 211 for _, f := range doc.Files { 212 idMap[f.ID] = f.Location 213 } 214 215 var out []artifact.Relationship 216 var conversionErrors []error 217 for _, r := range relationships { 218 syftRelationship, err := toSyftRelationship(idMap, r, idAliases) 219 if err != nil { 220 conversionErrors = append(conversionErrors, err) 221 } 222 if syftRelationship != nil { 223 out = append(out, *syftRelationship) 224 } 225 } 226 227 return out, conversionErrors 228 } 229 230 func toSyftSource(s model.Source) source.Source { 231 description := toSyftSourceData(s) 232 if description == nil { 233 return nil 234 } 235 return source.FromDescription(*description) 236 } 237 238 func toSyftRelationship(idMap map[string]interface{}, relationship model.Relationship, idAliases map[string]string) (*artifact.Relationship, error) { 239 id := func(id string) string { 240 aliased, ok := idAliases[id] 241 if ok { 242 return aliased 243 } 244 return id 245 } 246 247 from, ok := idMap[id(relationship.Parent)].(artifact.Identifiable) 248 if !ok { 249 return nil, fmt.Errorf("relationship mapping from key %s is not a valid artifact.Identifiable type: %+v", relationship.Parent, idMap[relationship.Parent]) 250 } 251 252 to, ok := idMap[id(relationship.Child)].(artifact.Identifiable) 253 if !ok { 254 return nil, fmt.Errorf("relationship mapping to key %s is not a valid artifact.Identifiable type: %+v", relationship.Child, idMap[relationship.Child]) 255 } 256 257 typ := artifact.RelationshipType(relationship.Type) 258 259 switch typ { 260 case artifact.OwnershipByFileOverlapRelationship, artifact.ContainsRelationship, artifact.DependencyOfRelationship, artifact.EvidentByRelationship: 261 default: 262 if !strings.Contains(string(typ), "dependency-of") { 263 return nil, fmt.Errorf("unknown relationship type: %s", string(typ)) 264 } 265 // lets try to stay as compatible as possible with similar relationship types without dropping the relationship 266 log.Warnf("assuming %q for relationship type %q", artifact.DependencyOfRelationship, typ) 267 typ = artifact.DependencyOfRelationship 268 } 269 return &artifact.Relationship{ 270 From: from, 271 To: to, 272 Type: typ, 273 Data: relationship.Metadata, 274 }, nil 275 } 276 277 func toSyftDescriptor(d model.Descriptor) sbom.Descriptor { 278 return sbom.Descriptor{ 279 Name: d.Name, 280 Version: d.Version, 281 Configuration: d.Configuration, 282 } 283 } 284 285 func toSyftSourceData(s model.Source) *source.Description { 286 return &source.Description{ 287 ID: s.ID, 288 Name: s.Name, 289 Version: s.Version, 290 Metadata: s.Metadata, 291 } 292 } 293 294 func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Collection { 295 catalog := pkg.NewCollection() 296 for _, p := range pkgs { 297 catalog.Add(toSyftPackage(p, idAliases)) 298 } 299 return catalog 300 } 301 302 func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package { 303 var cpes []cpe.CPE 304 for _, c := range p.CPEs { 305 value, err := cpe.New(c) 306 if err != nil { 307 log.Warnf("excluding invalid CPE %q: %v", c, err) 308 continue 309 } 310 311 cpes = append(cpes, value) 312 } 313 314 out := pkg.Package{ 315 Name: p.Name, 316 Version: p.Version, 317 FoundBy: p.FoundBy, 318 Locations: file.NewLocationSet(p.Locations...), 319 Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...), 320 Language: p.Language, 321 Type: p.Type, 322 CPEs: cpes, 323 PURL: p.PURL, 324 MetadataType: p.MetadataType, 325 Metadata: p.Metadata, 326 } 327 328 // we don't know if this package ID is truly unique, however, we need to trust the user input in case there are 329 // external references to it. That is, we can't derive our own ID (using pkg.SetID()) since consumers won't 330 // be able to historically interact with data that references the IDs from the original SBOM document being decoded now. 331 out.OverrideID(artifact.ID(p.ID)) 332 333 // this alias mapping is currently defunct, but could be useful in the future. 334 id := string(out.ID()) 335 if id != p.ID { 336 idAliases[p.ID] = id 337 } 338 339 return out 340 }