From 217157102cfbc168422475b3892d4dd442a20612 Mon Sep 17 00:00:00 2001 From: Lapo Docs Date: Tue, 15 Apr 2025 16:20:05 +0200 Subject: [PATCH] Make license validator work with a single license file to prevent library oddities --- pkg/analysis/passes/license/license.go | 176 +++++++------------- pkg/analysis/passes/license/license_test.go | 23 --- 2 files changed, 56 insertions(+), 143 deletions(-) diff --git a/pkg/analysis/passes/license/license.go b/pkg/analysis/passes/license/license.go index e707e578..a73d77cb 100644 --- a/pkg/analysis/passes/license/license.go +++ b/pkg/analysis/passes/license/license.go @@ -1,27 +1,21 @@ package license import ( - "net/http" "os" "path/filepath" "regexp" "strings" - "time" "github.com/go-enry/go-license-detector/v4/licensedb" - "github.com/go-enry/go-license-detector/v4/licensedb/api" - "github.com/go-enry/go-license-detector/v4/licensedb/filer" "github.com/grafana/plugin-validator/pkg/analysis" "github.com/grafana/plugin-validator/pkg/analysis/passes/archive" - "github.com/grafana/plugin-validator/pkg/logme" ) var ( - licenseNotProvided = &analysis.Rule{Name: "license-not-provided", Severity: analysis.Error} - licenseNotValid = &analysis.Rule{Name: "license-not-valid", Severity: analysis.Error} - licenseDetectionTimeout = &analysis.Rule{Name: "license-detection-timeout", Severity: analysis.Error} - licenseWithGenericText = &analysis.Rule{ + licenseNotProvided = &analysis.Rule{Name: "license-not-provided", Severity: analysis.Error} + licenseNotValid = &analysis.Rule{Name: "license-not-valid", Severity: analysis.Error} + licenseWithGenericText = &analysis.Rule{ Name: "license-with-generic-text", Severity: analysis.Warning, } @@ -60,8 +54,8 @@ func run(pass *analysis.Pass) (interface{}, error) { // validate that a LICENSE file is provided (go standard lib method) licenseFilePath := filepath.Join(archiveDir, "LICENSE") - licenseFile, err := os.Stat(licenseFilePath) - if err != nil || licenseFile.IsDir() { + licenseContents, err := os.ReadFile(licenseFilePath) + if err != nil { pass.ReportResult( pass.AnalyzerName, licenseNotProvided, @@ -71,9 +65,21 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } - // validate that the LICENSE file exists (filer lib method) - f, err := filer.FromDirectory(archiveDir) + // create a new temporal directory + tmpDir, err := os.MkdirTemp(os.TempDir(), "plugin-validator") + if err != nil { + return nil, nil + } + defer os.RemoveAll(tmpDir) + + // copy the license file to the temporal directory + err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), licenseContents, 0644) if err != nil { + return nil, nil + } + + result := licensedb.Analyse(tmpDir) + if len(result) == 0 { pass.ReportResult( pass.AnalyzerName, licenseNotProvided, @@ -83,80 +89,51 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } - // Filter out all non-text files, or the license detector may time out if, for some reason, - // it decides to scan backend executables. - f = newMimeTypeFiler(f, "text/") - - resultCh := make(chan map[string]api.Match, 1) - errCh := make(chan error, 1) - go func() { - // validate that the LICENSE file is parseable (go-license-detector lib method) - licenses, err := licensedb.Detect(f) - if err != nil { - errCh <- err - close(resultCh) - return - } - resultCh <- licenses - close(errCh) - }() - - select { - case err = <-errCh: - if err != nil { - pass.ReportResult( - pass.AnalyzerName, - licenseNotProvided, - "LICENSE file could not be parsed.", - "Could not parse the license file inside the plugin archive. Please make sure to include a valid license in your LICENSE file in your archive.", - ) - return nil, nil - } - case licenses := <-resultCh: - var foundLicense = false - for licenseName, licenseData := range licenses { - if licenseData.Confidence >= minRequiredConfidenceLevel && isValidLicense(licenseName) { - foundLicense = true - break - } - } + liceneseErr := result[0].ErrStr + if liceneseErr != "" { + pass.ReportResult( + pass.AnalyzerName, + licenseNotProvided, + "LICENSE file could not be parsed.", + "Could not parse the license file inside the plugin archive. Please make sure to include a valid license in your LICENSE file in your archive.", + ) + return nil, nil + } - if !foundLicense { - pass.ReportResult( - pass.AnalyzerName, - licenseNotValid, - "Valid license not found", - "The provided license is not compatible with Grafana plugins. Please refer to https://grafana.com/licensing/ for more information.", - ) - } else if licenseNotProvided.ReportAll { - licenseNotProvided.Severity = analysis.OK - pass.ReportResult(pass.AnalyzerName, licenseNotProvided, "License found", "Found a valid license file inside the plugin archive.") - } + licenses := result[0].Matches - licenseContent, err := os.ReadFile(licenseFilePath) - if err != nil { - logme.Debugln("Could not read LICENSE file", err) - return nil, nil + var foundLicense = false + for _, licenseData := range licenses { + if licenseData.Confidence >= minRequiredConfidenceLevel && + isValidLicense(licenseData.License) { + foundLicense = true + break } + } - licenseContentStr := string(licenseContent) - if strings.Contains(licenseContentStr, "{name of copyright owner}") || - strings.Contains(licenseContentStr, "{yyyy}") { - pass.ReportResult( - pass.AnalyzerName, - licenseWithGenericText, - "License file contains generic text", - "Your current license file contains generic text from the license template. Please make sure to replace {name of copyright owner} and {yyyy} with the correct values in your LICENSE file.", - ) - } - case <-time.After(time.Second * 30): + if !foundLicense { pass.ReportResult( pass.AnalyzerName, - licenseDetectionTimeout, - "License file detection timeout.", - "Could not detect the license file inside the plugin archive within 30s. Please make sure to include a valid license in your LICENSE file in your archive.", + licenseNotValid, + "Valid license not found", + "The provided license is not compatible with Grafana plugins. Please refer to https://grafana.com/licensing/ for more information.", ) return nil, nil + + } else if licenseNotProvided.ReportAll { + licenseNotProvided.Severity = analysis.OK + pass.ReportResult(pass.AnalyzerName, licenseNotProvided, "License found", "Found a valid license file inside the plugin archive.") + } + + licenseContentStr := string(licenseContents) + if strings.Contains(licenseContentStr, "{name of copyright owner}") || + strings.Contains(licenseContentStr, "{yyyy}") { + pass.ReportResult( + pass.AnalyzerName, + licenseWithGenericText, + "License file contains generic text", + "Your current license file contains generic text from the license template. Please make sure to replace {name of copyright owner} and {yyyy} with the correct values in your LICENSE file.", + ) } return nil, nil @@ -170,44 +147,3 @@ func isValidLicense(licenseName string) bool { } return false } - -// mimeTypeFiler is a filer that filters files by their MIME type. -// Only the files with the MIME type starting with wantedMimeTypePrefix are returned by ReadDir. -type mimeTypeFiler struct { - filer.Filer - - // wantedMimeTypePrefix is the prefix of the MIME type that the files must have to be returned by ReadDir. - wantedMimeTypePrefix string -} - -// newMimeTypeFiler creates a new mimeTypeFiler. -func newMimeTypeFiler(f filer.Filer, wantedMimeTypePrefix string) *mimeTypeFiler { - return &mimeTypeFiler{ - Filer: f, - wantedMimeTypePrefix: wantedMimeTypePrefix, - } -} - -// ReadDir reads the directory and returns only the files with the MIME type starting with wantedMimeTypePrefix. -func (f *mimeTypeFiler) ReadDir(path string) ([]filer.File, error) { - originalFiles, err := f.Filer.ReadDir(path) - if err != nil { - return nil, err - } - filteredFiles := make([]filer.File, 0, len(originalFiles)) - for _, ff := range originalFiles { - if ff.IsDir { - continue - } - content, err := f.ReadFile(ff.Name) - if err != nil { - return nil, err - } - mimeType := http.DetectContentType(content) - if !strings.HasPrefix(mimeType, f.wantedMimeTypePrefix) { - continue - } - filteredFiles = append(filteredFiles, ff) - } - return filteredFiles, nil -} diff --git a/pkg/analysis/passes/license/license_test.go b/pkg/analysis/passes/license/license_test.go index b463a673..3fe014d0 100644 --- a/pkg/analysis/passes/license/license_test.go +++ b/pkg/analysis/passes/license/license_test.go @@ -4,7 +4,6 @@ import ( "path/filepath" "testing" - "github.com/go-enry/go-license-detector/v4/licensedb/filer" "github.com/stretchr/testify/require" "github.com/grafana/plugin-validator/pkg/analysis" @@ -157,25 +156,3 @@ func TestValidBackendExecutable(t *testing.T) { require.NoError(t, err) require.Len(t, interceptor.Diagnostics, 0) } - -func TestMimeTypeFiler(t *testing.T) { - t.Run("text", func(t *testing.T) { - f, err := filer.FromDirectory(filepath.Join("testdata", "mime")) - require.NoError(t, err) - f = newMimeTypeFiler(f, "text/") - files, err := f.ReadDir(".") - require.NoError(t, err) - require.Len(t, files, 1) - require.Equal(t, "LICENSE", files[0].Name) - }) - - t.Run("binary", func(t *testing.T) { - f, err := filer.FromDirectory(filepath.Join("testdata", "mime")) - require.NoError(t, err) - f = newMimeTypeFiler(f, "application/octet-stream") - files, err := f.ReadDir(".") - require.NoError(t, err) - require.Len(t, files, 1) - require.Equal(t, "executable", files[0].Name) - }) -}