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