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