diff --git a/.github/workflows/semantic-pr.yaml b/.github/workflows/semantic-pr.yaml index ac7a706971..6c31488458 100644 --- a/.github/workflows/semantic-pr.yaml +++ b/.github/workflows/semantic-pr.yaml @@ -34,6 +34,7 @@ jobs: vuln misconf secret + license image fs diff --git a/docs/community/contribute/pr.md b/docs/community/contribute/pr.md index 324e096f4e..041a3b4aad 100644 --- a/docs/community/contribute/pr.md +++ b/docs/community/contribute/pr.md @@ -42,6 +42,7 @@ checks: - vuln - misconf - secret +- license mode: diff --git a/pkg/fanal/analyzer/language/analyze.go b/pkg/fanal/analyzer/language/analyze.go index 8ad8e70720..637dafd498 100644 --- a/pkg/fanal/analyzer/language/analyze.go +++ b/pkg/fanal/analyzer/language/analyze.go @@ -1,6 +1,8 @@ package language import ( + "strings" + "golang.org/x/xerrors" dio "github.com/aquasecurity/go-dep-parser/pkg/io" @@ -35,7 +37,10 @@ func ToAnalysisResult(fileType, filePath, libFilePath string, libs []godeptypes. for _, lib := range libs { var licenses []string if lib.License != "" { - licenses = []string{licensing.Normalize(lib.License)} + licenses = strings.Split(lib.License, ",") + for i, license := range licenses { + licenses[i] = licensing.Normalize(strings.TrimSpace(license)) + } } pkgs = append(pkgs, types.Package{ ID: lib.ID, diff --git a/pkg/fanal/analyzer/language/ruby/gemspec/gemspec_test.go b/pkg/fanal/analyzer/language/ruby/gemspec/gemspec_test.go index f325517e1c..05c0dfae93 100644 --- a/pkg/fanal/analyzer/language/ruby/gemspec/gemspec_test.go +++ b/pkg/fanal/analyzer/language/ruby/gemspec/gemspec_test.go @@ -31,7 +31,7 @@ func Test_gemspecLibraryAnalyzer_Analyze(t *testing.T) { { Name: "test-unit", Version: "3.3.7", - Licenses: []string{"Ruby, BSDL, PSFL"}, + Licenses: []string{"Ruby", "BSDL", "PSFL"}, FilePath: "testdata/multiple_licenses.gemspec", }, }, diff --git a/pkg/fanal/analyzer/pkg/dpkg/copyright.go b/pkg/fanal/analyzer/pkg/dpkg/copyright.go index 1ee62cf278..0558c856b7 100644 --- a/pkg/fanal/analyzer/pkg/dpkg/copyright.go +++ b/pkg/fanal/analyzer/pkg/dpkg/copyright.go @@ -37,6 +37,7 @@ var ( licenseClassifier *classifier.Classifier commonLicenseReferenceRegexp = regexp.MustCompile(`/?usr/share/common-licenses/([0-9A-Za-z_.+-]+[0-9A-Za-z+])`) + licenseSplitRegexp = regexp.MustCompile("(,?[_ ]+or[_ ]+)|(,?[_ ]+and[_ ])|(,[ ]*)") ) // dpkgLicenseAnalyzer parses copyright files and detect licenses @@ -52,7 +53,7 @@ func (a dpkgLicenseAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisI } findings := lo.Map(licenses, func(license string, _ int) types.LicenseFinding { - return types.LicenseFinding{Name: licensing.Normalize(license)} + return types.LicenseFinding{Name: license} }) // e.g. "usr/share/doc/zlib1g/copyright" => "zlib1g" @@ -82,14 +83,29 @@ func (a dpkgLicenseAnalyzer) parseCopyright(r dio.ReadSeekerAt) ([]string, error // Machine-readable format // cf. https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#:~:text=The%20debian%2Fcopyright%20file%20must,in%20the%20Debian%20Policy%20Manual. l := strings.TrimSpace(line[8:]) - if len(l) > 0 && !slices.Contains(licenses, l) { - licenses = append(licenses, l) + if len(l) > 0 { + // Split licenses without considering "and"/"or" + // examples: + // 'GPL-1+,GPL-2' => {"GPL-1", "GPL-2"} + // 'GPL-1+ or Artistic or Artistic-dist' => {"GPL-1", "Artistic", "Artistic-dist"} + // 'LGPLv3+_or_GPLv2+' => {"LGPLv3", "GPLv2"} + // 'BSD-3-CLAUSE and GPL-2' => {"BSD-3-CLAUSE", "GPL-2"} + // 'GPL-1+ or Artistic, and BSD-4-clause-POWERDOG' => {"GPL-1+", "Artistic", "BSD-4-clause-POWERDOG"} + for _, lic := range licenseSplitRegexp.Split(l, -1) { + lic = licensing.Normalize(lic) + if !slices.Contains(licenses, lic) { + licenses = append(licenses, lic) + } + } } case strings.Contains(line, "/usr/share/common-licenses/"): // Common license pattern license := commonLicenseReferenceRegexp.FindStringSubmatch(line) - if len(license) == 2 && !slices.Contains(licenses, license[1]) { - licenses = append(licenses, license[1]) + if len(license) == 2 { + l := licensing.Normalize(license[1]) + if !slices.Contains(licenses, l) { + licenses = append(licenses, l) + } } } } diff --git a/pkg/fanal/analyzer/pkg/dpkg/copyright_test.go b/pkg/fanal/analyzer/pkg/dpkg/copyright_test.go index 65ef52b4b1..fe6c84d6ca 100644 --- a/pkg/fanal/analyzer/pkg/dpkg/copyright_test.go +++ b/pkg/fanal/analyzer/pkg/dpkg/copyright_test.go @@ -29,6 +29,9 @@ func Test_dpkgLicenseAnalyzer_Analyze(t *testing.T) { Type: types.LicenseTypeDpkg, FilePath: "usr/share/doc/zlib1g/copyright", Findings: []types.LicenseFinding{ + {Name: "GPL-1.0"}, + {Name: "Artistic"}, + {Name: "BSD-4-clause-POWERDOG"}, {Name: "Zlib"}, }, PkgName: "zlib1g", @@ -64,7 +67,6 @@ func Test_dpkgLicenseAnalyzer_Analyze(t *testing.T) { FilePath: "usr/share/doc/apt/copyright", Findings: []types.LicenseFinding{ {Name: "GPL-2.0"}, - {Name: "GPL-2.0"}, }, PkgName: "apt", }, diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/license-pattern-and-classifier-copyright b/pkg/fanal/analyzer/pkg/dpkg/testdata/license-pattern-and-classifier-copyright index 964a6ce390..80c06f2461 100644 --- a/pkg/fanal/analyzer/pkg/dpkg/testdata/license-pattern-and-classifier-copyright +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/license-pattern-and-classifier-copyright @@ -37,7 +37,7 @@ Files-Excluded: Files: * Copyright: 1995-2013 Jean-loup Gailly and Mark Adler -License: Zlib +License: GPL-1+ or Artistic, and BSD-4-clause-POWERDOG Files: amiga/Makefile.pup Copyright: 1998 by Andreas R. Kleinert diff --git a/pkg/fanal/artifact/image/image_test.go b/pkg/fanal/artifact/image/image_test.go index ea428e30a4..786704f6f0 100644 --- a/pkg/fanal/artifact/image/image_test.go +++ b/pkg/fanal/artifact/image/image_test.go @@ -316,7 +316,6 @@ func TestArtifact_Inspect(t *testing.T) { Type: types.LicenseTypeDpkg, FilePath: "usr/share/doc/ca-certificates/copyright", Findings: []types.LicenseFinding{ - {Name: "GPL-2.0"}, {Name: "GPL-2.0"}, {Name: "MPL-2.0"}, }, diff --git a/pkg/licensing/normalize.go b/pkg/licensing/normalize.go index 4a371b4cd7..7156f4c920 100644 --- a/pkg/licensing/normalize.go +++ b/pkg/licensing/normalize.go @@ -6,7 +6,11 @@ var mapping = map[string]string{ // GPL "GPL-1": GPL10, "GPL-1+": GPL10, + "GPL 1.0": GPL10, + "GPL 1": GPL10, "GPL2": GPL20, + "GPL 2.0": GPL20, + "GPL 2": GPL20, "GPL-2": GPL20, "GPL-2.0-ONLY": GPL20, "GPL2+": GPL20, @@ -16,6 +20,8 @@ var mapping = map[string]string{ "GPL-2.0-OR-LATER": GPL20, "GPL-2+ WITH AUTOCONF EXCEPTION": GPL20withautoconfexception, "GPL3": GPL30, + "GPL 3.0": GPL30, + "GPL 3": GPL30, "GPLV3+": GPL30, "GPL-3": GPL30, "GPL-3.0-ONLY": GPL30, @@ -27,22 +33,30 @@ var mapping = map[string]string{ // LGPL "LGPL2": LGPL20, + "LGPL 2": LGPL20, + "LGPL 2.0": LGPL20, "LGPL-2": LGPL20, "LGPL2+": LGPL20, "LGPL-2+": LGPL20, "LGPL-2.0+": LGPL20, "LGPL-2.1": LGPL21, + "LGPL 2.1": LGPL21, "LGPL-2.1+": LGPL21, "LGPLV2.1+": LGPL21, "LGPL-3": LGPL30, + "LGPL 3": LGPL30, "LGPL-3+": LGPL30, "LGPL": LGPL30, // 2? 3? // MPL - "MPL1.0": MPL10, - "MPL1": MPL10, - "MPL2.0": MPL20, - "MPL2": MPL20, + "MPL1.0": MPL10, + "MPL1": MPL10, + "MPL 1.0": MPL10, + "MPL 1": MPL10, + "MPL2.0": MPL20, + "MPL 2.0": MPL20, + "MPL2": MPL20, + "MPL 2": MPL20, // BSD "BSD": BSD3Clause, // 2? 3? @@ -50,9 +64,10 @@ var mapping = map[string]string{ "BSD-3-CLAUSE": BSD3Clause, "BSD-4-CLAUSE": BSD4Clause, - "APACHE": Apache20, // 1? 2? - "ZLIB": Zlib, - "RUBY": Ruby, + "APACHE": Apache20, // 1? 2? + "APACHE 2.0": Apache20, + "RUBY": Ruby, + "ZLIB": Zlib, } func Normalize(name string) string {