Skip to content

Commit

Permalink
refactor: decouple license detection logic (#118)
Browse files Browse the repository at this point in the history
* ci: update cyclonedx-cli: `0.17.2` -> `0.22.0`

Signed-off-by: nscuro <nscuro@protonmail.com>

* test: fix cyclonedx-cli invocation

Signed-off-by: nscuro <nscuro@protonmail.com>

* refactor: decouple license detection logic

Signed-off-by: nscuro <nscuro@protonmail.com>

* rename `standard` license detector to `local`

Signed-off-by: nscuro <nscuro@protonmail.com>

* add godoc

Signed-off-by: nscuro <nscuro@protonmail.com>

* add godoc

Signed-off-by: nscuro <nscuro@protonmail.com>

Closes #117
  • Loading branch information
nscuro committed Feb 4, 2022
1 parent bbe6d72 commit ac4a859
Show file tree
Hide file tree
Showing 22 changed files with 240 additions and 130 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ jobs:
run: |
mkdir -p "$HOME/.local/bin"
echo "$HOME/.local/bin" >> $GITHUB_PATH
wget -O "$HOME/.local/bin/cyclonedx" https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.17.2/cyclonedx-linux-x64
wget -O "$HOME/.local/bin/cyclonedx" https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.22.0/cyclonedx-linux-x64
echo "ae39404a9dc8b2e7be0a9559781ee9fe3492201d2629de139d702fd4535ffdd6 $HOME/.local/bin/cyclonedx" | sha256sum -c
chmod +x "$HOME/.local/bin/cyclonedx"
- name: Checkout Repository
uses: actions/checkout@v2
Expand Down
9 changes: 8 additions & 1 deletion internal/cli/cmd/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
cliUtil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util"
"github.com/CycloneDX/cyclonedx-gomod/internal/sbom"
"github.com/CycloneDX/cyclonedx-gomod/pkg/generate/app"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect/local"
)

func New() *ffcli.Command {
Expand Down Expand Up @@ -97,12 +99,17 @@ func Exec(options Options) error {

logger := options.Logger()

var licenseDetector licensedetect.Detector
if options.ResolveLicenses {
licenseDetector = local.NewDetector(logger)
}

generator, err := app.NewGenerator(options.ModuleDir,
app.WithLogger(logger),
app.WithIncludeFiles(options.IncludeFiles),
app.WithIncludePackages(options.IncludePackages),
app.WithIncludeStdlib(options.IncludeStd),
app.WithLicenseDetection(options.ResolveLicenses),
app.WithLicenseDetector(licenseDetector),
app.WithMainDir(options.Main))
if err != nil {
return err
Expand Down
9 changes: 8 additions & 1 deletion internal/cli/cmd/bin/bin.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
cliUtil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util"
"github.com/CycloneDX/cyclonedx-gomod/internal/sbom"
"github.com/CycloneDX/cyclonedx-gomod/pkg/generate/bin"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect/local"

"github.com/peterbourgon/ff/v3/ffcli"
)
Expand Down Expand Up @@ -77,10 +79,15 @@ func Exec(options Options) error {

logger := options.Logger()

var licenseDetector licensedetect.Detector
if options.ResolveLicenses {
licenseDetector = local.NewDetector(logger)
}

generator, err := bin.NewGenerator(options.BinaryPath,
bin.WithLogger(logger),
bin.WithIncludeStdlib(options.IncludeStd),
bin.WithLicenseDetection(options.ResolveLicenses),
bin.WithLicenseDetector(licenseDetector),
bin.WithVersionOverride(options.Version))
if err != nil {
return err
Expand Down
9 changes: 8 additions & 1 deletion internal/cli/cmd/mod/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
cliUtil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util"
"github.com/CycloneDX/cyclonedx-gomod/internal/sbom"
"github.com/CycloneDX/cyclonedx-gomod/pkg/generate/mod"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect/local"
)

func New() *ffcli.Command {
Expand Down Expand Up @@ -69,12 +71,17 @@ func Exec(options Options) error {

logger := options.Logger()

var licenseDetector licensedetect.Detector
if options.ResolveLicenses {
licenseDetector = local.NewDetector(logger)
}

generator, err := mod.NewGenerator(options.ModuleDir,
mod.WithLogger(logger),
mod.WithComponentType(cdx.ComponentType(options.ComponentType)),
mod.WithIncludeStdlib(options.IncludeStd),
mod.WithIncludeTestModules(options.IncludeTest),
mod.WithLicenseDetection(options.ResolveLicenses))
mod.WithLicenseDetector(licenseDetector))
if err != nil {
return err
}
Expand Down
35 changes: 18 additions & 17 deletions internal/sbom/convert/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package module

import (
"encoding/base64"
"errors"
"fmt"
"regexp"
"strings"
Expand All @@ -28,17 +27,21 @@ import (
"github.com/rs/zerolog"

"github.com/CycloneDX/cyclonedx-gomod/internal/gomod"
"github.com/CycloneDX/cyclonedx-gomod/internal/license"
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 resolve licenses for the module and attach them
// to the component's license evidence.
func WithLicenses(enabled bool) Option {
// 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 !enabled {
if detector == nil {
logger.Debug().
Str("module", module.Coordinates()).
Str("reason", "no detector provided").
Msg("skipping license detection")
return nil
}

Expand All @@ -50,24 +53,22 @@ func WithLicenses(enabled bool) Option {
return nil
}

resolvedLicenses, err := license.Resolve(logger, module)
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 err == nil {
componentLicenses := make(cdx.Licenses, len(resolvedLicenses))
for i := range resolvedLicenses {
componentLicenses[i] = cdx.LicenseChoice{License: &resolvedLicenses[i]}
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 {
if errors.Is(err, license.ErrNoLicenseDetected) {
logger.Warn().Str("module", module.Coordinates()).Msg("no license detected")
return nil
}

return fmt.Errorf("failed to resolve license for %s: %v", module.Coordinates(), err)
logger.Warn().Str("module", module.Coordinates()).Msg("no licenses detected")
}

return nil
Expand Down
58 changes: 41 additions & 17 deletions internal/sbom/convert/module/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package module

import (
"bytes"
"errors"
"io"
"os/exec"
"path/filepath"
Expand All @@ -32,49 +33,72 @@ import (
"github.com/CycloneDX/cyclonedx-gomod/internal/gomod"
)

type stubLicenseDetector struct {
Err error
Licenses []cdx.License
}

func (d stubLicenseDetector) Detect(_, _, _ string) ([]cdx.License, error) {
if d.Err != nil {
return nil, d.Err
}

return d.Licenses, nil
}

func TestWithLicenses(t *testing.T) {
t.Run("Success", func(t *testing.T) {
module := gomod.Module{
Dir: "../../../",
}
component := cdx.Component{}
detector := &stubLicenseDetector{
Licenses: []cdx.License{
{
ID: "Apache-2.0",
},
},
}

err := WithLicenses(true)(zerolog.New(io.Discard), module, &component)
err := WithLicenses(detector)(zerolog.Nop(), gomod.Module{Dir: t.TempDir()}, &component)
require.NoError(t, err)
require.NotNil(t, component.Evidence)
require.NotNil(t, component.Evidence.Licenses)
require.Len(t, *component.Evidence.Licenses, 1)
})

t.Run("Not Found", func(t *testing.T) {
module := gomod.Module{
Dir: ".",
t.Run("NoLicenseFound", func(t *testing.T) {
component := cdx.Component{}
detector := &stubLicenseDetector{
Licenses: []cdx.License{},
}

err := WithLicenses(detector)(zerolog.Nop(), gomod.Module{Dir: t.TempDir()}, &component)
require.NoError(t, err)
require.Nil(t, component.Evidence)
})

t.Run("ModuleNotInCache", func(t *testing.T) {
component := cdx.Component{}
detector := &stubLicenseDetector{}

err := WithLicenses(true)(zerolog.New(io.Discard), module, &component)
err := WithLicenses(detector)(zerolog.Nop(), gomod.Module{Dir: ""}, &component)
require.NoError(t, err)
require.Nil(t, component.Evidence)
})

t.Run("Other Error", func(t *testing.T) {
module := gomod.Module{
Dir: "./doesNotExist",
}
t.Run("OtherError", func(t *testing.T) {
component := cdx.Component{}
detector := &stubLicenseDetector{
Err: errors.New("test"),
}

err := WithLicenses(true)(zerolog.New(io.Discard), module, &component)
err := WithLicenses(detector)(zerolog.Nop(), gomod.Module{Dir: t.TempDir()}, &component)
require.Error(t, err)
require.Nil(t, component.Evidence)
})

t.Run("Disabled", func(t *testing.T) {
module := gomod.Module{
Dir: "../../../",
}
component := cdx.Component{}

err := WithLicenses(false)(zerolog.New(io.Discard), module, &component)
err := WithLicenses(nil)(zerolog.Nop(), gomod.Module{Dir: t.TempDir()}, &component)
require.NoError(t, err)
require.Nil(t, component.Evidence)
})
Expand Down
15 changes: 5 additions & 10 deletions internal/testutil/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,15 @@ func RequireMatchingSBOMSnapshot(t *testing.T, snapShooter *cupaloy.Config, bom

// RequireValidSBOM encodes the BOM and validates it using the CycloneDX CLI.
func RequireValidSBOM(t *testing.T, bom *cdx.BOM, fileFormat cdx.BOMFileFormat) {
var (
inputFormat string
fileExtension string
)
var inputFormat string
switch fileFormat {
case cdx.BOMFileFormatJSON:
fileExtension = "json"
inputFormat = "json_v1_3"
inputFormat = "json"
case cdx.BOMFileFormatXML:
fileExtension = "xml"
inputFormat = "xml_v1_3"
inputFormat = "xml"
}

bomFile, err := os.Create(filepath.Join(t.TempDir(), fmt.Sprintf("bom.%s", fileExtension)))
bomFile, err := os.Create(filepath.Join(t.TempDir(), fmt.Sprintf("bom.%s", inputFormat)))
require.NoError(t, err)
defer func() {
if err := bomFile.Close(); err != nil {
Expand All @@ -177,7 +172,7 @@ func RequireValidSBOM(t *testing.T, bom *cdx.BOM, fileFormat cdx.BOMFileFormat)
require.NoError(t, err)
require.NoError(t, bomFile.Close())

valCmd := exec.Command("cyclonedx", "validate", "--input-file", bomFile.Name(), "--input-format", inputFormat, "--fail-on-errors") // #nosec G204
valCmd := exec.Command("cyclonedx", "validate", "--input-file", bomFile.Name(), "--input-format", inputFormat, "--input-version", "v1_3", "--fail-on-errors") // #nosec G204
valOut, err := valCmd.CombinedOutput()
if !assert.NoError(t, err) {
// Provide some context when test is failing
Expand Down
7 changes: 4 additions & 3 deletions pkg/generate/app/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ import (
modConv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/module"
pkgConv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/pkg"
"github.com/CycloneDX/cyclonedx-gomod/pkg/generate"
"github.com/CycloneDX/cyclonedx-gomod/pkg/licensedetect"
)

type generator struct {
logger zerolog.Logger

detectLicenses bool
includeFiles bool
includePackages bool
includeStdlib bool
licenseDetector licensedetect.Detector
mainDir string
moduleDir string
}
Expand Down Expand Up @@ -95,7 +96,7 @@ func (g generator) Generate() (*cdx.BOM, error) {

mainComponent, err := modConv.ToComponent(g.logger, modules[0],
modConv.WithComponentType(cdx.ComponentTypeApplication),
modConv.WithLicenses(g.detectLicenses),
modConv.WithLicenses(g.licenseDetector),
modConv.WithPackages(g.includePackages,
pkgConv.WithFiles(g.includeFiles)),
)
Expand All @@ -114,7 +115,7 @@ func (g generator) Generate() (*cdx.BOM, error) {
}

components, err := modConv.ToComponents(g.logger, modules[1:],
modConv.WithLicenses(g.detectLicenses),
modConv.WithLicenses(g.licenseDetector),
modConv.WithModuleHashes(),
modConv.WithPackages(g.includePackages,
pkgConv.WithFiles(g.includeFiles)),
Expand Down

0 comments on commit ac4a859

Please sign in to comment.