/
module.go
273 lines (229 loc) · 8.08 KB
/
module.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// This file is part of CycloneDX GoMod
//
// Licensed under the Apache License, Version 2.0 (the “License”);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) OWASP Foundation. All Rights Reserved.
package module
import (
"encoding/base64"
"fmt"
"regexp"
"strings"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/rs/zerolog"
"github.com/CycloneDX/cyclonedx-gomod/internal/gomod"
pkgConv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/pkg"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect"
)
type Option func(zerolog.Logger, gomod.Module, *cdx.Component) error
// WithLicenses attempts to detect licenses for the module using a provided license detector
// and attach them to the component's license evidence.
func WithLicenses(detector licensedetect.Detector) Option {
return func(logger zerolog.Logger, module gomod.Module, component *cdx.Component) error {
if detector == nil {
logger.Debug().
Str("module", module.Coordinates()).
Str("reason", "no detector provided").
Msg("skipping license detection")
return nil
}
if module.Dir == "" {
logger.Warn().
Str("module", module.Coordinates()).
Str("reason", "module not in cache").
Msg("can't resolve module license")
return nil
}
detectedLicenses, err := detector.Detect(module.Path, module.Version, module.Dir)
if err != nil {
return fmt.Errorf("failed to detect licenses for %s: %v", module.Coordinates(), err)
}
if len(detectedLicenses) > 0 {
componentLicenses := make(cdx.Licenses, len(detectedLicenses))
for i := range detectedLicenses {
componentLicenses[i] = cdx.LicenseChoice{License: &detectedLicenses[i]}
}
component.Evidence = &cdx.Evidence{
Licenses: &componentLicenses,
}
} else {
logger.Warn().Str("module", module.Coordinates()).Msg("no licenses detected")
}
return nil
}
}
// WithComponentType overrides the type of the component.
func WithComponentType(ctype cdx.ComponentType) Option {
return func(_ zerolog.Logger, _ gomod.Module, component *cdx.Component) error {
component.Type = ctype
return nil
}
}
func WithModuleHashes() Option {
return func(logger zerolog.Logger, module gomod.Module, component *cdx.Component) error {
if module.Main {
// We currently don't have an accurate way of hashing the main module, as it may contain
// files that are .gitignore'd and thus not part of the hashes in Go's sumdb.
logger.Debug().Str("module", module.Coordinates()).Msg("not calculating hash for main module")
return nil
}
if module.Vendored {
// Go's vendoring mechanism doesn't copy all files that make up a module to the vendor dir.
// Hashing vendored modules thus won't result in the expected hash, probably causing more
// confusion than anything else.
logger.Debug().Str("module", module.Coordinates()).Msg("not calculating hash for vendored module")
return nil
}
if module.Path == gomod.StdlibModulePath {
// There are no module hashes published for the standard library.
logger.Debug().Str("module", module.Coordinates()).Msg("not calculating hash for stdlib module")
return nil
}
logger.Debug().Str("module", module.Coordinates()).Msg("calculating module hash")
h1, err := module.Hash()
if err != nil {
return fmt.Errorf("failed to calculate module hash: %w", err)
}
h1Bytes, err := base64.StdEncoding.DecodeString(h1[3:])
if err != nil {
return fmt.Errorf("failed to base64 decode module hash: %w", err)
}
component.Hashes = &[]cdx.Hash{
{Algorithm: cdx.HashAlgoSHA256, Value: fmt.Sprintf("%x", h1Bytes)},
}
return nil
}
}
func WithPackages(enabled bool, options ...pkgConv.Option) Option {
return func(logger zerolog.Logger, module gomod.Module, component *cdx.Component) error {
if !enabled {
return nil
}
var pkgComponents []cdx.Component
for i := range module.Packages {
pkgComponent, err := pkgConv.ToComponent(logger, module.Packages[i], module, options...)
if err != nil {
return fmt.Errorf("failed to convert package: %w", err)
}
pkgComponents = append(pkgComponents, *pkgComponent)
}
if len(pkgComponents) > 0 {
component.Components = &pkgComponents
}
return nil
}
}
// WithScope overrides the scope of the component.
func WithScope(scope cdx.Scope) Option {
return func(_ zerolog.Logger, _ gomod.Module, component *cdx.Component) error {
component.Scope = scope
return nil
}
}
// WithTestScope overrides the scope of the component,
// if the corresponding module has the TestOnly flag set.
func WithTestScope(scope cdx.Scope) Option {
return func(_ zerolog.Logger, module gomod.Module, component *cdx.Component) error {
if module.TestOnly {
component.Scope = scope
}
return nil
}
}
// ToComponent converts a gomod.Module to a CycloneDX component.
// The component can be further customized using options, before it's returned.
func ToComponent(logger zerolog.Logger, module gomod.Module, options ...Option) (*cdx.Component, error) {
if module.Replace != nil {
return ToComponent(logger, *module.Replace, options...)
}
logger.Debug().
Str("module", module.Coordinates()).
Msg("converting module to component")
component := cdx.Component{
BOMRef: module.BOMRef(),
Type: cdx.ComponentTypeLibrary,
Name: module.Path,
Version: module.Version,
PackageURL: module.PackageURL(),
}
if !module.Main { // Main component can't have a scope
if module.TestOnly {
component.Scope = cdx.ScopeOptional
} else {
component.Scope = cdx.ScopeRequired
}
}
if module.Sum != "" && strings.HasPrefix(module.Sum, "h1:") {
h1Bytes, err := base64.StdEncoding.DecodeString(module.Sum[3:])
if err != nil {
return nil, err
}
component.Hashes = &[]cdx.Hash{
{
Algorithm: cdx.HashAlgoSHA256,
Value: fmt.Sprintf("%x", h1Bytes),
},
}
}
vcsURL := resolveVCSURL(module.Path)
if vcsURL != "" {
component.ExternalReferences = &[]cdx.ExternalReference{
{
Type: cdx.ERTypeVCS,
URL: vcsURL,
},
}
}
for _, option := range options {
if err := option(logger, module, &component); err != nil {
return nil, err
}
}
return &component, nil
}
// ToComponents converts a slice of gomod.Module to a slice of CycloneDX components.
func ToComponents(logger zerolog.Logger, modules []gomod.Module, options ...Option) ([]cdx.Component, error) {
components := make([]cdx.Component, 0, len(modules))
for i := range modules {
component, err := ToComponent(logger, modules[i], options...)
if err != nil {
return nil, err
}
components = append(components, *component)
}
return components, nil
}
var (
// By convention, modules with a major version equal to or above v2
// have it as suffix in their module path.
vcsUrlMajorVersionSuffixRegex = regexp.MustCompile(`(/v[\d]+)$`)
// gopkg.in with user segment
// Example: gopkg.in/user/pkg.v3 -> github.com/user/pkg
vcsUrlGoPkgInRegexWithUser = regexp.MustCompile(`^gopkg\.in/([^/]+)/([^.]+)\..*$`)
// gopkg.in without user segment
// Example: gopkg.in/pkg.v3 -> github.com/go-pkg/pkg
vcsUrlGoPkgInRegexWithoutUser = regexp.MustCompile(`^gopkg\.in/([^.]+)\..*$`)
)
const vcsHttpsPrefix = "https://"
func resolveVCSURL(modulePath string) string {
switch {
case strings.HasPrefix(modulePath, "github.com/"):
return vcsHttpsPrefix + vcsUrlMajorVersionSuffixRegex.ReplaceAllString(modulePath, "")
case vcsUrlGoPkgInRegexWithUser.MatchString(modulePath):
return vcsHttpsPrefix + vcsUrlGoPkgInRegexWithUser.ReplaceAllString(modulePath, "github.com/$1/$2")
case vcsUrlGoPkgInRegexWithoutUser.MatchString(modulePath):
return vcsHttpsPrefix + vcsUrlGoPkgInRegexWithoutUser.ReplaceAllString(modulePath, "github.com/go-$1/$1")
}
return ""
}