|
4 | 4 | "context" |
5 | 5 | "encoding/json" |
6 | 6 | "fmt" |
7 | | - "io" |
8 | | - "log/slog" |
9 | | - "sync" |
10 | 7 | "time" |
11 | 8 |
|
12 | 9 | "github.com/boringbin/sbomlicense/internal/cache" |
@@ -47,6 +44,40 @@ type ExternalRef struct { |
47 | 44 | ReferenceLocator string `json:"referenceLocator"` |
48 | 45 | } |
49 | 46 |
|
| 47 | +// GetPurl extracts the purl from the SPDX package's external references. |
| 48 | +func (p *Package) GetPurl() (string, error) { |
| 49 | + return GetSPDXPackagePurl(p) |
| 50 | +} |
| 51 | + |
| 52 | +// HasLicense returns true if the package already has license information. |
| 53 | +// Checks both LicenseConcluded and LicenseDeclared fields. |
| 54 | +func (p *Package) HasLicense() bool { |
| 55 | + return (p.LicenseConcluded != "" && |
| 56 | + p.LicenseConcluded != spdxLicenseNone && |
| 57 | + p.LicenseConcluded != spdxLicenseNoAssertion) || |
| 58 | + (p.LicenseDeclared != "" && |
| 59 | + p.LicenseDeclared != spdxLicenseNone && |
| 60 | + p.LicenseDeclared != spdxLicenseNoAssertion) |
| 61 | +} |
| 62 | + |
| 63 | +// SetLicense updates the package with the provided license string. |
| 64 | +// Sets LicenseConcluded as the primary field and also updates LicenseDeclared if it's empty. |
| 65 | +func (p *Package) SetLicense(license string) { |
| 66 | + // Set LicenseConcluded (primary field) |
| 67 | + p.LicenseConcluded = license |
| 68 | + // Also set LicenseDeclared if it's empty |
| 69 | + if p.LicenseDeclared == "" || |
| 70 | + p.LicenseDeclared == spdxLicenseNone || |
| 71 | + p.LicenseDeclared == spdxLicenseNoAssertion { |
| 72 | + p.LicenseDeclared = license |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +// GetLogID returns the SPDX ID for logging purposes. |
| 77 | +func (p *Package) GetLogID() string { |
| 78 | + return p.SPDXID |
| 79 | +} |
| 80 | + |
50 | 81 | // UnwrapGitHubSBOM checks if the data is wrapped in GitHub's {"sbom": {...}} format and returns the unwrapped SPDX |
51 | 82 | // data if so, or the original data otherwise. |
52 | 83 | func UnwrapGitHubSBOM(data []byte) ([]byte, error) { |
@@ -113,108 +144,30 @@ func NewSPDXEnricher(provider provider.Provider, cache cache.Cache, cacheTTL tim |
113 | 144 | } |
114 | 145 |
|
115 | 146 | // Enrich enriches the SPDX SBOM with license information. |
116 | | -// |
117 | | -//nolint:gocognit // Complexity is inherent to parallel enrichment with worker pool pattern |
118 | 147 | func (s *SPDXEnricher) Enrich(ctx context.Context, opts Options) ([]byte, error) { |
119 | 148 | // Parse the SBOM file into an SPDX document |
120 | 149 | doc, err := ParseSBOMFile(opts.SBOM) |
121 | 150 | if err != nil { |
122 | 151 | return nil, fmt.Errorf("failed to parse SBOM file: %w", err) |
123 | 152 | } |
124 | 153 |
|
125 | | - if len(doc.Packages) == 0 { |
126 | | - // No packages to enrich, return original SBOM |
127 | | - return opts.SBOM, nil |
128 | | - } |
129 | | - |
130 | | - // Determine parallelism |
131 | | - parallelism := opts.Parallelism |
132 | | - if parallelism <= 0 { |
133 | | - parallelism = 1 |
134 | | - } |
135 | | - |
136 | | - // Use provided logger or create a no-op logger |
137 | | - logger := opts.Logger |
138 | | - if logger == nil { |
139 | | - logger = slog.New(slog.NewTextHandler(io.Discard, nil)) |
140 | | - } |
141 | | - |
142 | | - // Create a channel for packages to enrich |
143 | | - type job struct { |
144 | | - pkg *Package |
145 | | - purl string |
146 | | - } |
147 | | - |
148 | | - jobs := make(chan job, len(doc.Packages)) |
149 | | - var wg sync.WaitGroup |
150 | | - |
151 | | - // Spawn workers |
152 | | - for range parallelism { |
153 | | - wg.Add(1) |
154 | | - go func() { |
155 | | - defer wg.Done() |
156 | | - for j := range jobs { |
157 | | - // Check if package already has a license |
158 | | - // Try LicenseConcluded first, fallback to LicenseDeclared |
159 | | - hasLicense := (j.pkg.LicenseConcluded != "" && |
160 | | - j.pkg.LicenseConcluded != spdxLicenseNone && |
161 | | - j.pkg.LicenseConcluded != spdxLicenseNoAssertion) || |
162 | | - (j.pkg.LicenseDeclared != "" && |
163 | | - j.pkg.LicenseDeclared != spdxLicenseNone && |
164 | | - j.pkg.LicenseDeclared != spdxLicenseNoAssertion) |
165 | | - |
166 | | - if hasLicense { |
167 | | - continue |
168 | | - } |
169 | | - |
170 | | - // Get the license from the service |
171 | | - lic, licErr := provider.Get(ctx, provider.GetOptions{ |
172 | | - Purl: j.purl, |
173 | | - Provider: s.provider, |
174 | | - Cache: s.cache, |
175 | | - CacheTTL: s.cacheTTL, |
176 | | - }) |
177 | | - if licErr != nil { |
178 | | - // Log error but continue processing other packages |
179 | | - logger.ErrorContext(ctx, "failed to get license for package", |
180 | | - "purl", j.purl, |
181 | | - "spdx_id", j.pkg.SPDXID, |
182 | | - "error", licErr) |
183 | | - continue |
184 | | - } |
185 | | - if lic != "" { |
186 | | - // Set LicenseConcluded (primary field) |
187 | | - j.pkg.LicenseConcluded = lic |
188 | | - // Also set LicenseDeclared if it's empty |
189 | | - if j.pkg.LicenseDeclared == "" || |
190 | | - j.pkg.LicenseDeclared == spdxLicenseNone || |
191 | | - j.pkg.LicenseDeclared == spdxLicenseNoAssertion { |
192 | | - j.pkg.LicenseDeclared = lic |
193 | | - } |
194 | | - } |
195 | | - } |
196 | | - }() |
197 | | - } |
198 | | - |
199 | | - // Queue all packages |
| 154 | + // Convert []Package to []*Package for interface satisfaction |
| 155 | + pkgs := make([]*Package, len(doc.Packages)) |
200 | 156 | for i := range doc.Packages { |
201 | | - pkg := &doc.Packages[i] |
202 | | - // Get the purl for the package if it exists |
203 | | - purl, purlErr := GetSPDXPackagePurl(pkg) |
204 | | - if purlErr != nil { |
205 | | - // Log error but continue processing other packages |
206 | | - logger.ErrorContext(ctx, "failed to get purl for package", |
207 | | - "spdx_id", pkg.SPDXID, |
208 | | - "error", purlErr) |
209 | | - continue |
210 | | - } |
211 | | - |
212 | | - jobs <- job{pkg: pkg, purl: purl} |
| 157 | + pkgs[i] = &doc.Packages[i] |
213 | 158 | } |
214 | 159 |
|
215 | | - // Close the channel and wait for workers to finish |
216 | | - close(jobs) |
217 | | - wg.Wait() |
218 | | - |
219 | | - return json.Marshal(doc) |
| 160 | + // Enrich and marshal using common helper |
| 161 | + return enrichDocument( |
| 162 | + ctx, |
| 163 | + opts, |
| 164 | + doc, |
| 165 | + pkgs, |
| 166 | + s.provider, |
| 167 | + s.cache, |
| 168 | + s.cacheTTL, |
| 169 | + func(d *Document) ([]byte, error) { |
| 170 | + return json.Marshal(d) |
| 171 | + }, |
| 172 | + ) |
220 | 173 | } |
0 commit comments