github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/common/cyclonedxhelpers/licenses.go (about)

     1  package cyclonedxhelpers
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/CycloneDX/cyclonedx-go"
     8  
     9  	"github.com/anchore/syft/syft/pkg"
    10  	"github.com/lineaje-labs/syft/internal/spdxlicense"
    11  )
    12  
    13  // This should be a function that just surfaces licenses already validated in the package struct
    14  func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
    15  	spdx, other, ex := separateLicenses(p)
    16  	out := spdx
    17  	out = append(out, other...)
    18  
    19  	if len(other) > 0 || len(spdx) > 0 {
    20  		// found non spdx related licenses
    21  		// build individual license choices for each
    22  		// complex expressions are not combined and set as NAME fields
    23  		for _, e := range ex {
    24  			if e == "" {
    25  				continue
    26  			}
    27  			out = append(out, cyclonedx.LicenseChoice{
    28  				License: &cyclonedx.License{
    29  					Name: e,
    30  				},
    31  			})
    32  		}
    33  	} else if len(ex) > 0 {
    34  		// only expressions found
    35  		e := mergeSPDX(ex)
    36  		if e != "" {
    37  			out = append(out, cyclonedx.LicenseChoice{
    38  				Expression: e,
    39  			})
    40  		}
    41  	}
    42  
    43  	if len(out) > 0 {
    44  		return &out
    45  	}
    46  
    47  	return nil
    48  }
    49  
    50  func decodeLicenses(c *cyclonedx.Component) []pkg.License {
    51  	licenses := make([]pkg.License, 0)
    52  	if c == nil || c.Licenses == nil {
    53  		return licenses
    54  	}
    55  
    56  	for _, l := range *c.Licenses {
    57  		if l.License == nil {
    58  			continue
    59  		}
    60  		// these fields are mutually exclusive in the spec
    61  		switch {
    62  		case l.License.ID != "":
    63  			licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.ID, l.License.URL))
    64  		case l.License.Name != "":
    65  			licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL))
    66  		case l.Expression != "":
    67  			licenses = append(licenses, pkg.NewLicenseFromURLs(l.Expression, l.License.URL))
    68  		default:
    69  		}
    70  	}
    71  
    72  	return licenses
    73  }
    74  
    75  // nolint:funlen
    76  func separateLicenses(p pkg.Package) (spdx, other cyclonedx.Licenses, expressions []string) {
    77  	ex := make([]string, 0)
    78  	spdxc := cyclonedx.Licenses{}
    79  	otherc := cyclonedx.Licenses{}
    80  	/*
    81  			pkg.License can be a couple of things: see above declarations
    82  			- Complex SPDX expression
    83  			- Some other Valid license ID
    84  			- Some non-standard non spdx license
    85  
    86  			To determine if an expression is a singular ID we first run it against the SPDX license list.
    87  
    88  		The weird case we run into is if there is a package with a license that is not a valid SPDX expression
    89  			and a license that is a valid complex expression. In this case we will surface the valid complex expression
    90  			as a license choice and the invalid expression as a license string.
    91  
    92  	*/
    93  	seen := make(map[string]bool)
    94  	for _, l := range p.Licenses.ToSlice() {
    95  		// singular expression case
    96  		// only ID field here since we guarantee that the license is valid
    97  		if value, exists := spdxlicense.ID(l.SPDXExpression); exists {
    98  			if len(l.URLs) > 0 {
    99  				processLicenseURLs(l, value, &spdxc)
   100  				continue
   101  			}
   102  
   103  			if _, exists := seen[value]; exists {
   104  				continue
   105  			}
   106  			// try making set of license choices to avoid duplicates
   107  			// only update if the license has more information
   108  			spdxc = append(spdxc, cyclonedx.LicenseChoice{
   109  				License: &cyclonedx.License{
   110  					ID: value,
   111  				},
   112  			})
   113  			seen[value] = true
   114  			// we have added the license to the SPDX license list check next license
   115  			continue
   116  		}
   117  
   118  		if l.SPDXExpression != "" {
   119  			// COMPLEX EXPRESSION CASE
   120  			ex = append(ex, l.SPDXExpression)
   121  			continue
   122  		}
   123  
   124  		// license string that are not valid spdx expressions or ids
   125  		// we only use license Name here since we cannot guarantee that the license is a valid SPDX expression
   126  		if len(l.URLs) > 0 {
   127  			processLicenseURLs(l, "", &otherc)
   128  			continue
   129  		}
   130  		otherc = append(otherc, cyclonedx.LicenseChoice{
   131  			License: &cyclonedx.License{
   132  				Name: l.Value,
   133  			},
   134  		})
   135  	}
   136  	return spdxc, otherc, ex
   137  }
   138  
   139  func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) {
   140  	for _, url := range l.URLs {
   141  		if spdxID == "" {
   142  			*populate = append(*populate, cyclonedx.LicenseChoice{
   143  				License: &cyclonedx.License{
   144  					URL:  url,
   145  					Name: l.Value,
   146  				},
   147  			})
   148  		} else {
   149  			*populate = append(*populate, cyclonedx.LicenseChoice{
   150  				License: &cyclonedx.License{
   151  					ID:  spdxID,
   152  					URL: url,
   153  				},
   154  			})
   155  		}
   156  	}
   157  }
   158  
   159  func mergeSPDX(ex []string) string {
   160  	var candidate []string
   161  	for _, e := range ex {
   162  		// if the expression does not have balanced parens add them
   163  		if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") {
   164  			e = "(" + e + ")"
   165  			candidate = append(candidate, e)
   166  		}
   167  	}
   168  
   169  	if len(candidate) == 1 {
   170  		return reduceOuter(strings.Join(candidate, " AND "))
   171  	}
   172  
   173  	return strings.Join(candidate, " AND ")
   174  }
   175  
   176  func reduceOuter(expression string) string {
   177  	var (
   178  		sb        strings.Builder
   179  		openCount int
   180  	)
   181  
   182  	for _, c := range expression {
   183  		if string(c) == "(" && openCount > 0 {
   184  			_, _ = fmt.Fprintf(&sb, "%c", c)
   185  		}
   186  		if string(c) == "(" {
   187  			openCount++
   188  			continue
   189  		}
   190  		if string(c) == ")" && openCount > 1 {
   191  			_, _ = fmt.Fprintf(&sb, "%c", c)
   192  		}
   193  		if string(c) == ")" {
   194  			openCount--
   195  			continue
   196  		}
   197  		_, _ = fmt.Fprintf(&sb, "%c", c)
   198  	}
   199  
   200  	return sb.String()
   201  }