github.com/anchore/syft@v1.38.2/syft/format/internal/cyclonedxutil/helpers/licenses.go (about) 1 package helpers 2 3 import ( 4 "context" 5 "encoding/base64" 6 "strings" 7 8 "github.com/CycloneDX/cyclonedx-go" 9 10 "github.com/anchore/syft/internal/licenses" 11 "github.com/anchore/syft/internal/spdxlicense" 12 "github.com/anchore/syft/syft/pkg" 13 ) 14 15 // This should be a function that just surfaces licenses already validated in the package struct 16 func encodeLicenses(p pkg.Package) *cyclonedx.Licenses { 17 spdx, other, ex := separateLicenses(p) 18 out := spdx 19 out = append(out, other...) 20 21 if len(other) > 0 || len(spdx) > 0 { 22 // found non spdx related licenses 23 // build individual license choices for each 24 // complex expressions are not combined and set as NAME fields 25 for _, e := range ex { 26 if e == "" { 27 continue 28 } 29 out = append(out, cyclonedx.LicenseChoice{ 30 License: &cyclonedx.License{ 31 Name: e, 32 }, 33 }) 34 } 35 } else if len(ex) > 0 { 36 // only expressions found 37 e := mergeSPDX(ex) 38 if e != "" { 39 out = append(out, cyclonedx.LicenseChoice{ 40 Expression: e, 41 }) 42 } 43 } 44 45 if len(out) > 0 { 46 return &out 47 } 48 49 return nil 50 } 51 52 func decodeLicenses(c *cyclonedx.Component) []pkg.License { 53 licenses := make([]pkg.License, 0) 54 if c == nil || c.Licenses == nil { 55 return licenses 56 } 57 58 for _, l := range *c.Licenses { 59 // these fields are mutually exclusive in the spec 60 switch { 61 case l.License != nil && l.License.ID != "": 62 licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.ID, l.License.URL)) 63 case l.License != nil && l.License.Name != "": 64 licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.Name, l.License.URL)) 65 case l.License != nil && l.License.URL != "": 66 // Try to enrich license from URL when ID and Name are empty 67 licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), "", l.License.URL)) 68 case l.Expression != "": 69 licenses = append(licenses, pkg.NewLicenseWithContext(context.TODO(), l.Expression)) 70 default: 71 } 72 } 73 74 return licenses 75 } 76 77 func separateLicenses(p pkg.Package) (spdx, other cyclonedx.Licenses, expressions []string) { 78 ex := make([]string, 0) 79 spdxc := cyclonedx.Licenses{} 80 otherc := cyclonedx.Licenses{} 81 /* 82 pkg.License can be a couple of things: see above declarations 83 - Complex SPDX expression 84 - Some other Valid license ID 85 - Some non-standard non spdx license 86 87 To determine if an expression is a singular ID we first run it against the SPDX license list. 88 89 The weird case we run into is if there is a package with a license that is not a valid SPDX expression 90 and a license that is a valid complex expression. In this case we will surface the valid complex expression 91 as a license choice and the invalid expression as a license string. 92 93 */ 94 seen := make(map[string]bool) 95 for _, l := range p.Licenses.ToSlice() { 96 // singular expression case 97 // only ID field here since we guarantee that the license is valid 98 if value, exists := spdxlicense.ID(l.SPDXExpression); exists { 99 if len(l.URLs) > 0 { 100 processLicenseURLs(l, value, &spdxc) 101 continue 102 } 103 104 if _, exists := seen[value]; exists { 105 continue 106 } 107 // try making set of license choices to avoid duplicates 108 // only update if the license has more information 109 spdxc = append(spdxc, cyclonedx.LicenseChoice{ 110 License: &cyclonedx.License{ 111 ID: value, 112 }, 113 }) 114 seen[value] = true 115 // we have added the license to the SPDX license list check next license 116 continue 117 } 118 119 if l.SPDXExpression != "" && !strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) { 120 // COMPLEX EXPRESSION CASE 121 ex = append(ex, l.SPDXExpression) 122 continue 123 } 124 125 // license string that are not valid spdx expressions or ids 126 // we only use license Name here since we cannot guarantee that the license is a valid SPDX expression 127 if len(l.URLs) > 0 && !strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) { 128 processLicenseURLs(l, "", &otherc) 129 continue 130 } 131 132 otherc = append(otherc, processCustomLicense(l)...) 133 } 134 return spdxc, otherc, ex 135 } 136 137 func processCustomLicense(l pkg.License) cyclonedx.Licenses { 138 result := cyclonedx.Licenses{} 139 if strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) { 140 cyclonedxLicense := &cyclonedx.License{ 141 Name: l.SPDXExpression, 142 } 143 if len(l.URLs) > 0 { 144 cyclonedxLicense.URL = l.URLs[0] 145 } 146 if len(l.Contents) > 0 { 147 cyclonedxLicense.Text = &cyclonedx.AttachedText{ 148 Content: base64.StdEncoding.EncodeToString([]byte(l.Contents)), 149 } 150 cyclonedxLicense.Text.ContentType = "text/plain" 151 cyclonedxLicense.Text.Encoding = "base64" 152 } 153 result = append(result, cyclonedx.LicenseChoice{ 154 License: cyclonedxLicense, 155 }) 156 } else { 157 result = append(result, cyclonedx.LicenseChoice{ 158 License: &cyclonedx.License{ 159 Name: l.Value, 160 }, 161 }) 162 } 163 return result 164 } 165 166 func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) { 167 for _, url := range l.URLs { 168 if spdxID == "" { 169 // CycloneDX requires either an id or name to be present for a license 170 // If l.Value is empty, use the URL as the name to ensure schema compliance 171 // at this point we've already tried to enrich the license we just don't want the format 172 // conversion to be lossy here 173 name := l.Value 174 if name == "" { 175 name = url 176 } 177 *populate = append(*populate, cyclonedx.LicenseChoice{ 178 License: &cyclonedx.License{ 179 URL: url, 180 Name: name, 181 }, 182 }) 183 } else { 184 *populate = append(*populate, cyclonedx.LicenseChoice{ 185 License: &cyclonedx.License{ 186 ID: spdxID, 187 URL: url, 188 }, 189 }) 190 } 191 } 192 } 193 194 func mergeSPDX(ex []string) string { 195 var candidate []string 196 for _, e := range ex { 197 // if the expression does not have balanced parens add them 198 if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") { 199 e = "(" + e + ")" 200 } 201 candidate = append(candidate, e) 202 } 203 204 if len(candidate) == 1 { 205 return reduceOuter(candidate[0]) 206 } 207 208 return reduceOuter(strings.Join(candidate, " AND ")) 209 } 210 211 func reduceOuter(expression string) string { 212 expression = strings.TrimSpace(expression) 213 214 // If the entire expression is wrapped in parentheses, check if they are redundant. 215 if strings.HasPrefix(expression, "(") && strings.HasSuffix(expression, ")") { 216 trimmed := expression[1 : len(expression)-1] 217 if isBalanced(trimmed) { 218 return reduceOuter(trimmed) // Recursively reduce the trimmed expression. 219 } 220 } 221 222 return expression 223 } 224 225 func isBalanced(expression string) bool { 226 count := 0 227 for _, c := range expression { 228 switch c { 229 case '(': 230 count++ 231 case ')': 232 count-- 233 if count < 0 { 234 return false 235 } 236 default: 237 } 238 } 239 return count == 0 240 }