github.com/anchore/syft@v1.38.2/syft/format/internal/spdxutil/helpers/license.go (about) 1 package helpers 2 3 import ( 4 "sort" 5 "strings" 6 7 "github.com/spdx/tools-golang/spdx" 8 9 "github.com/anchore/syft/internal/spdxlicense" 10 "github.com/anchore/syft/syft/license" 11 "github.com/anchore/syft/syft/pkg" 12 ) 13 14 func License(p pkg.Package) (concluded, declared string, otherLicenses []spdx.OtherLicense) { 15 // source: https://spdx.github.io/spdx-spec/v2.3/package-information/#713-concluded-license-field 16 // The options to populate this field are limited to: 17 // A valid SPDX License Expression as defined in Annex D; 18 // NONE, if the SPDX file creator concludes there is no license available for this package; or 19 // NOASSERTION if: 20 // (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination; 21 // (ii) the SPDX file creator has made no attempt to determine this field; or 22 // (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so). 23 24 if p.Licenses.Empty() { 25 return NOASSERTION, NOASSERTION, nil 26 } 27 28 // take all licenses and assume an AND expression; 29 // for information about license expressions see: 30 // https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ 31 pc, pd, ol := ParseLicenses(p.Licenses.ToSlice()) 32 33 return joinLicenses(pc), joinLicenses(pd), ol 34 } 35 36 func joinLicenses(licenses []SPDXLicense) string { 37 if len(licenses) == 0 { 38 return NOASSERTION 39 } 40 41 var newLicenses []string 42 43 for _, l := range licenses { 44 v := l.ID 45 // check if license does not start or end with parens 46 if !strings.HasPrefix(v, "(") && !strings.HasSuffix(v, ")") { 47 // if license contains AND, OR, or WITH, then wrap in parens 48 if strings.Contains(v, " AND ") || 49 strings.Contains(v, " OR ") || 50 strings.Contains(v, " WITH ") { 51 newLicenses = append(newLicenses, "("+v+")") 52 continue 53 } 54 } 55 newLicenses = append(newLicenses, v) 56 } 57 58 return strings.Join(newLicenses, " AND ") 59 } 60 61 type SPDXLicense struct { 62 // Valid SPDX ID OR License Value (should have LicenseRef- prefix and be sanitized) 63 // OR combination of the above as a valid SPDX License Expression as defined in Annex D. 64 // https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ 65 ID string 66 // If the SPDX license is not on the SPDX License List 67 LicenseName string 68 FullText string // 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef). 69 URLs []string 70 } 71 72 func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense, otherLicenses []spdx.OtherLicense) { 73 for _, l := range raw { 74 candidate := createSPDXLicense(l) 75 76 // this determines if the candidate falls under https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/# 77 // of the SPDX spec, where: 78 // - we should not have a complex SPDX expression 79 // - if a single license, it should not be a known license (on the SPDX license list) 80 if l.SPDXExpression == "" && strings.Contains(candidate.ID, spdxlicense.LicenseRefPrefix) { 81 otherLicenses = append(otherLicenses, spdx.OtherLicense{ 82 LicenseIdentifier: candidate.ID, 83 ExtractedText: candidate.FullText, 84 LicenseName: candidate.LicenseName, 85 LicenseCrossReferences: candidate.URLs, 86 }) 87 } 88 switch l.Type { 89 case license.Concluded: 90 concluded = append(concluded, candidate) 91 case license.Declared: 92 declared = append(declared, candidate) 93 } 94 } 95 96 return concluded, declared, otherLicenses 97 } 98 99 func createSPDXLicense(l pkg.License) SPDXLicense { 100 // source: https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#102-extracted-text-field 101 // we need to populate this field in the spdx document if we have a license ref 102 // 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef). 103 ft := NOASSERTION 104 if l.Contents != "" { 105 ft = l.Contents 106 } 107 108 return SPDXLicense{ 109 ID: generateLicenseID(l), 110 LicenseName: l.Value, 111 FullText: ft, 112 URLs: l.URLs, 113 } 114 } 115 116 // generateLicenseID generates a license ID for the given license, which is either the license value or the SPDX expression. 117 func generateLicenseID(l pkg.License) string { 118 if l.SPDXExpression != "" { 119 return l.SPDXExpression 120 } 121 122 // syft format includes the algo for the sha in the values 123 // we can strip this and just make LicenseRef-<sum> for spdx consumption 124 id := strings.ReplaceAll(l.Value, "sha256:", "") 125 if !strings.HasPrefix(id, "LicenseRef-") { 126 id = "LicenseRef-" + id 127 } 128 return SanitizeElementID(id) 129 } 130 131 type SPDXOtherLicenseSet struct { 132 set map[string]spdx.OtherLicense 133 } 134 135 func NewSPDXOtherLicenseSet() *SPDXOtherLicenseSet { 136 return &SPDXOtherLicenseSet{ 137 set: make(map[string]spdx.OtherLicense), 138 } 139 } 140 141 func (s *SPDXOtherLicenseSet) Add(licenses ...spdx.OtherLicense) { 142 for _, l := range licenses { 143 s.set[l.LicenseIdentifier] = l 144 } 145 } 146 147 type ByLicenseIdentifier []spdx.OtherLicense 148 149 func (o ByLicenseIdentifier) Len() int { return len(o) } 150 func (o ByLicenseIdentifier) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 151 func (o ByLicenseIdentifier) Less(i, j int) bool { 152 return o[i].LicenseIdentifier < o[j].LicenseIdentifier 153 } 154 155 func (s *SPDXOtherLicenseSet) ToSlice() []spdx.OtherLicense { 156 values := make([]spdx.OtherLicense, 0, len(s.set)) 157 for _, v := range s.set { 158 values = append(values, v) 159 } 160 sort.Sort(ByLicenseIdentifier(values)) 161 return values 162 }