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