Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 56 additions & 120 deletions pkg/analysis/passes/license/license.go
Original file line number Diff line number Diff line change
@@ -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,
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
}
23 changes: 0 additions & 23 deletions pkg/analysis/passes/license/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}