github.com/anchore/syft@v1.38.2/syft/format/syftjson/model/package.go (about) 1 package model 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "reflect" 8 "strings" 9 10 "github.com/anchore/syft/internal/log" 11 "github.com/anchore/syft/internal/packagemetadata" 12 "github.com/anchore/syft/syft/file" 13 "github.com/anchore/syft/syft/license" 14 "github.com/anchore/syft/syft/pkg" 15 ) 16 17 var errUnknownMetadataType = errors.New("unknown metadata type") 18 19 // Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling. 20 type Package struct { 21 PackageBasicData 22 PackageCustomData 23 } 24 25 // PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package. 26 type PackageBasicData struct { 27 ID string `json:"id"` 28 Name string `json:"name"` 29 Version string `json:"version"` 30 Type pkg.Type `json:"type"` 31 FoundBy string `json:"foundBy"` 32 Locations []file.Location `json:"locations"` 33 Licenses licenses `json:"licenses"` 34 Language pkg.Language `json:"language"` 35 CPEs cpes `json:"cpes"` 36 PURL string `json:"purl"` 37 } 38 39 // cpes is a collection of Common Platform Enumeration identifiers for a package. 40 type cpes []CPE 41 42 // CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases. 43 type CPE struct { 44 // Value is the CPE string identifier. 45 Value string `json:"cpe"` 46 47 // Source is the source where this CPE was obtained or generated from. 48 Source string `json:"source,omitempty"` 49 } 50 51 // licenses is a collection of license findings associated with a package. 52 type licenses []License 53 54 // License represents software license information discovered for a package, including SPDX expressions and supporting evidence locations. 55 type License struct { 56 // Value is the raw license identifier or expression as found. 57 Value string `json:"value"` 58 59 // SPDXExpression is the parsed SPDX license expression. 60 SPDXExpression string `json:"spdxExpression"` 61 62 // Type is the license type classification (e.g., declared, concluded, discovered). 63 Type license.Type `json:"type"` 64 65 // URLs are URLs where license text or information can be found. 66 URLs []string `json:"urls"` 67 68 // Locations are file locations where this license was discovered. 69 Locations []file.Location `json:"locations"` 70 71 // Contents is the full license text content. 72 Contents string `json:"contents,omitempty"` 73 } 74 75 func newModelLicensesFromValues(licenses []string) (ml []License) { 76 for _, v := range licenses { 77 expression, err := license.ParseExpression(v) 78 if err != nil { 79 log.Tracef("could not find valid spdx expression for %s: %w", v, err) 80 } 81 ml = append(ml, License{ 82 Value: v, 83 SPDXExpression: expression, 84 Type: license.Declared, 85 }) 86 } 87 return ml 88 } 89 90 func (f *licenses) UnmarshalJSON(b []byte) error { 91 var lics []License 92 if err := json.Unmarshal(b, &lics); err != nil { 93 var simpleLicense []string 94 if err := json.Unmarshal(b, &simpleLicense); err != nil { 95 return fmt.Errorf("unable to unmarshal license: %w", err) 96 } 97 lics = newModelLicensesFromValues(simpleLicense) 98 } 99 *f = lics 100 return nil 101 } 102 103 func sourcedCPESfromSimpleCPEs(simpleCPEs []string) []CPE { 104 var result []CPE 105 for _, s := range simpleCPEs { 106 result = append(result, CPE{ 107 Value: s, 108 }) 109 } 110 return result 111 } 112 113 func (c *cpes) UnmarshalJSON(b []byte) error { 114 var cs []CPE 115 if err := json.Unmarshal(b, &cs); err != nil { 116 var simpleCPEs []string 117 if err := json.Unmarshal(b, &simpleCPEs); err != nil { 118 return fmt.Errorf("unable to unmarshal cpes: %w", err) 119 } 120 cs = sourcedCPESfromSimpleCPEs(simpleCPEs) 121 } 122 *c = cs 123 return nil 124 } 125 126 // PackageCustomData contains ambiguous values (type-wise) from pkg.Package. 127 type PackageCustomData struct { 128 MetadataType string `json:"metadataType,omitempty"` 129 Metadata any `json:"metadata,omitempty"` 130 } 131 132 // packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling. 133 type packageMetadataUnpacker struct { 134 MetadataType string `json:"metadataType"` 135 Metadata json.RawMessage `json:"metadata"` 136 } 137 138 func (p *packageMetadataUnpacker) String() string { 139 return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata)) 140 } 141 142 // UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types. 143 func (p *Package) UnmarshalJSON(b []byte) error { 144 var basic PackageBasicData 145 if err := json.Unmarshal(b, &basic); err != nil { 146 return err 147 } 148 p.PackageBasicData = basic 149 150 var unpacker packageMetadataUnpacker 151 if err := json.Unmarshal(b, &unpacker); err != nil { 152 log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err) 153 return err 154 } 155 156 err := unpackPkgMetadata(p, unpacker) 157 if errors.Is(err, errUnknownMetadataType) { 158 log.Warnf("unknown package metadata type=%q for packageID=%q", p.MetadataType, p.ID) 159 return nil 160 } 161 162 return err 163 } 164 165 func unpackPkgMetadata(p *Package, unpacker packageMetadataUnpacker) error { 166 if unpacker.MetadataType == "" { 167 return nil 168 } 169 170 // check for legacy correction cases from schema v11 -> v12 171 ty := unpacker.MetadataType 172 switch unpacker.MetadataType { 173 case "HackageMetadataType": 174 for _, l := range p.Locations { 175 if strings.HasSuffix(l.RealPath, ".yaml.lock") { 176 ty = "haskell-hackage-stack-lock-entry" 177 break 178 } else if strings.HasSuffix(l.RealPath, ".yaml") { 179 ty = "haskell-hackage-stack-entry" 180 break 181 } 182 } 183 case "RpmMetadata": 184 for _, l := range p.Locations { 185 if strings.HasSuffix(l.RealPath, ".rpm") { 186 ty = "rpm-archive" 187 break 188 } 189 } 190 case "RustCargoPackageMetadata": 191 var found bool 192 for _, l := range p.Locations { 193 if strings.HasSuffix(strings.ToLower(l.RealPath), "cargo.lock") { 194 ty = "rust-cargo-lock-entry" 195 found = true 196 break 197 } 198 } 199 if !found { 200 ty = "rust-cargo-audit-entry" 201 } 202 } 203 204 typ := packagemetadata.ReflectTypeFromJSONName(ty) 205 if typ == nil { 206 // capture unknown metadata as a generic struct 207 if len(unpacker.Metadata) > 0 { 208 var val interface{} 209 if err := json.Unmarshal(unpacker.Metadata, &val); err != nil { 210 return err 211 } 212 p.Metadata = val 213 } 214 215 return errUnknownMetadataType 216 } 217 218 val := reflect.New(typ).Interface() 219 if len(unpacker.Metadata) > 0 { 220 if err := json.Unmarshal(unpacker.Metadata, val); err != nil { 221 return err 222 } 223 } 224 p.Metadata = reflect.ValueOf(val).Elem().Interface() 225 p.MetadataType = ty 226 return nil 227 }