github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/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/syft/file"
    12  	"github.com/anchore/syft/syft/internal/packagemetadata"
    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  type cpes []CPE
    40  
    41  type CPE struct {
    42  	Value  string `json:"cpe"`
    43  	Source string `json:"source,omitempty"`
    44  }
    45  
    46  type licenses []License
    47  
    48  type License struct {
    49  	Value          string          `json:"value"`
    50  	SPDXExpression string          `json:"spdxExpression"`
    51  	Type           license.Type    `json:"type"`
    52  	URLs           []string        `json:"urls"`
    53  	Locations      []file.Location `json:"locations"`
    54  }
    55  
    56  func newModelLicensesFromValues(licenses []string) (ml []License) {
    57  	for _, v := range licenses {
    58  		expression, err := license.ParseExpression(v)
    59  		if err != nil {
    60  			log.Trace("could not find valid spdx expression for %s: %w", v, err)
    61  		}
    62  		ml = append(ml, License{
    63  			Value:          v,
    64  			SPDXExpression: expression,
    65  			Type:           license.Declared,
    66  		})
    67  	}
    68  	return ml
    69  }
    70  
    71  func (f *licenses) UnmarshalJSON(b []byte) error {
    72  	var lics []License
    73  	if err := json.Unmarshal(b, &lics); err != nil {
    74  		var simpleLicense []string
    75  		if err := json.Unmarshal(b, &simpleLicense); err != nil {
    76  			return fmt.Errorf("unable to unmarshal license: %w", err)
    77  		}
    78  		lics = newModelLicensesFromValues(simpleLicense)
    79  	}
    80  	*f = lics
    81  	return nil
    82  }
    83  
    84  func sourcedCPESfromSimpleCPEs(simpleCPEs []string) []CPE {
    85  	var result []CPE
    86  	for _, s := range simpleCPEs {
    87  		result = append(result, CPE{
    88  			Value: s,
    89  		})
    90  	}
    91  	return result
    92  }
    93  
    94  func (c *cpes) UnmarshalJSON(b []byte) error {
    95  	var cs []CPE
    96  	if err := json.Unmarshal(b, &cs); err != nil {
    97  		var simpleCPEs []string
    98  		if err := json.Unmarshal(b, &simpleCPEs); err != nil {
    99  			return fmt.Errorf("unable to unmarshal cpes: %w", err)
   100  		}
   101  		cs = sourcedCPESfromSimpleCPEs(simpleCPEs)
   102  	}
   103  	*c = cs
   104  	return nil
   105  }
   106  
   107  // PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
   108  type PackageCustomData struct {
   109  	MetadataType string `json:"metadataType,omitempty"`
   110  	Metadata     any    `json:"metadata,omitempty"`
   111  }
   112  
   113  // packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
   114  type packageMetadataUnpacker struct {
   115  	MetadataType string          `json:"metadataType"`
   116  	Metadata     json.RawMessage `json:"metadata"`
   117  }
   118  
   119  func (p *packageMetadataUnpacker) String() string {
   120  	return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata))
   121  }
   122  
   123  // UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
   124  func (p *Package) UnmarshalJSON(b []byte) error {
   125  	var basic PackageBasicData
   126  	if err := json.Unmarshal(b, &basic); err != nil {
   127  		return err
   128  	}
   129  	p.PackageBasicData = basic
   130  
   131  	var unpacker packageMetadataUnpacker
   132  	if err := json.Unmarshal(b, &unpacker); err != nil {
   133  		log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err)
   134  		return err
   135  	}
   136  
   137  	err := unpackPkgMetadata(p, unpacker)
   138  	if errors.Is(err, errUnknownMetadataType) {
   139  		log.Warnf("unknown package metadata type=%q for packageID=%q", p.MetadataType, p.ID)
   140  		return nil
   141  	}
   142  
   143  	return err
   144  }
   145  
   146  func unpackPkgMetadata(p *Package, unpacker packageMetadataUnpacker) error {
   147  	if unpacker.MetadataType == "" {
   148  		return nil
   149  	}
   150  
   151  	// check for legacy correction cases from schema v11 -> v12
   152  	ty := unpacker.MetadataType
   153  	switch unpacker.MetadataType {
   154  	case "HackageMetadataType":
   155  		for _, l := range p.Locations {
   156  			if strings.HasSuffix(l.RealPath, ".yaml.lock") {
   157  				ty = "haskell-hackage-stack-lock-entry"
   158  				break
   159  			} else if strings.HasSuffix(l.RealPath, ".yaml") {
   160  				ty = "haskell-hackage-stack-entry"
   161  				break
   162  			}
   163  		}
   164  	case "RpmMetadata":
   165  		for _, l := range p.Locations {
   166  			if strings.HasSuffix(l.RealPath, ".rpm") {
   167  				ty = "rpm-archive"
   168  				break
   169  			}
   170  		}
   171  	case "RustCargoPackageMetadata":
   172  		var found bool
   173  		for _, l := range p.Locations {
   174  			if strings.HasSuffix(strings.ToLower(l.RealPath), "cargo.lock") {
   175  				ty = "rust-cargo-lock-entry"
   176  				found = true
   177  				break
   178  			}
   179  		}
   180  		if !found {
   181  			ty = "rust-cargo-audit-entry"
   182  		}
   183  	}
   184  
   185  	typ := packagemetadata.ReflectTypeFromJSONName(ty)
   186  	if typ == nil {
   187  		// capture unknown metadata as a generic struct
   188  		if len(unpacker.Metadata) > 0 {
   189  			var val interface{}
   190  			if err := json.Unmarshal(unpacker.Metadata, &val); err != nil {
   191  				return err
   192  			}
   193  			p.Metadata = val
   194  		}
   195  
   196  		return errUnknownMetadataType
   197  	}
   198  
   199  	val := reflect.New(typ).Interface()
   200  	if len(unpacker.Metadata) > 0 {
   201  		if err := json.Unmarshal(unpacker.Metadata, val); err != nil {
   202  			return err
   203  		}
   204  	}
   205  	p.Metadata = reflect.ValueOf(val).Elem().Interface()
   206  	return nil
   207  }