Advisor: Check plugin signature (#106044)

This commit is contained in:
Andres Martinez Gotor 2025-05-29 11:33:19 +02:00 committed by GitHub
parent 27ab895eef
commit e2e8de29ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 397 additions and 143 deletions

View File

@ -28,6 +28,7 @@ type Service struct {
pluginContextProvider *plugincontext.Provider
pluginClient plugins.Client
pluginRepo repo.Service
pluginErrorResolver plugins.ErrorResolver
updateChecker pluginchecker.PluginUpdateChecker
pluginPreinstall pluginchecker.Preinstall
managedPlugins managedplugins.Manager
@ -42,6 +43,7 @@ func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore plu
updateChecker pluginchecker.PluginUpdateChecker,
pluginRepo repo.Service, pluginPreinstall pluginchecker.Preinstall, managedPlugins managedplugins.Manager,
provisionedPlugins provisionedplugins.Manager, ssoSettingsSvc ssosettings.Service, cfg *setting.Cfg,
pluginErrorResolver plugins.ErrorResolver,
) *Service {
return &Service{
datasourceSvc: datasourceSvc,
@ -49,6 +51,7 @@ func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore plu
pluginContextProvider: pluginContextProvider,
pluginClient: pluginClient,
pluginRepo: pluginRepo,
pluginErrorResolver: pluginErrorResolver,
updateChecker: updateChecker,
pluginPreinstall: pluginPreinstall,
managedPlugins: managedPlugins,
@ -73,6 +76,7 @@ func (s *Service) Checks() []checks.Check {
s.pluginStore,
s.pluginRepo,
s.updateChecker,
s.pluginErrorResolver,
s.GrafanaVersion,
),
authchecks.New(s.ssoSettingsSvc),

View File

@ -2,43 +2,42 @@ package plugincheck
import (
"context"
"fmt"
sysruntime "runtime"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
const (
CheckID = "plugin"
DeprecationStepID = "deprecation"
UpdateStepID = "update"
CheckID = "plugin"
)
func New(
pluginStore pluginstore.Store,
pluginRepo repo.Service,
updateChecker pluginchecker.PluginUpdateChecker,
pluginErrorResolver plugins.ErrorResolver,
grafanaVersion string,
) checks.Check {
return &check{
PluginStore: pluginStore,
PluginRepo: pluginRepo,
GrafanaVersion: grafanaVersion,
updateChecker: updateChecker,
PluginStore: pluginStore,
PluginRepo: pluginRepo,
GrafanaVersion: grafanaVersion,
updateChecker: updateChecker,
pluginErrorResolver: pluginErrorResolver,
}
}
type check struct {
PluginStore pluginstore.Store
PluginRepo repo.Service
updateChecker pluginchecker.PluginUpdateChecker
GrafanaVersion string
pluginIndex map[string]repo.PluginInfo
PluginStore pluginstore.Store
PluginRepo repo.Service
updateChecker pluginchecker.PluginUpdateChecker
pluginErrorResolver plugins.ErrorResolver
GrafanaVersion string
pluginIndex map[string]repo.PluginInfo
}
func (c *check) ID() string {
@ -49,12 +48,40 @@ func (c *check) Name() string {
return "plugin"
}
type pluginItem struct {
Plugin *pluginstore.Plugin
Err *plugins.Error
}
func (c *check) Items(ctx context.Context) ([]any, error) {
ps := c.PluginStore.Plugins(ctx)
res := make([]any, len(ps))
for i, p := range ps {
res[i] = p
resMap := map[string]*pluginItem{}
for _, p := range ps {
resMap[p.ID] = &pluginItem{
Plugin: &p,
Err: c.pluginErrorResolver.PluginError(ctx, p.ID),
}
}
// Plugins with errors are not added to the plugin store but
// we still want to show them in the check results so we add them to the map
pluginErrors := c.pluginErrorResolver.PluginErrors(ctx)
for _, e := range pluginErrors {
if _, exists := resMap[e.PluginID]; exists {
resMap[e.PluginID].Err = e
} else {
resMap[e.PluginID] = &pluginItem{
Plugin: nil,
Err: e,
}
}
}
res := make([]any, 0, len(resMap))
for _, p := range resMap {
res = append(res, p)
}
return res, nil
}
@ -63,7 +90,10 @@ func (c *check) Item(ctx context.Context, id string) (any, error) {
if !exists {
return nil, nil
}
return p, nil
return &pluginItem{
Plugin: &p,
Err: c.pluginErrorResolver.PluginError(ctx, p.ID),
}, nil
}
func (c *check) Init(ctx context.Context) error {
@ -99,117 +129,8 @@ func (c *check) Steps() []checks.Step {
updateChecker: c.updateChecker,
pluginIndex: c.pluginIndex,
},
&unsignedStep{
pluginIndex: c.pluginIndex,
},
}
}
type deprecationStep struct {
GrafanaVersion string
updateChecker pluginchecker.PluginUpdateChecker
pluginIndex map[string]repo.PluginInfo
}
func (s *deprecationStep) Title() string {
return "Deprecation check"
}
func (s *deprecationStep) Description() string {
return "Check if any installed plugins are deprecated."
}
func (s *deprecationStep) Resolution() string {
return "Check the <a href='https://grafana.com/legal/plugin-deprecation/#a-plugin-i-use-is-deprecated-what-should-i-do'" +
"target=_blank>documentation</a> for recommended steps or delete the plugin."
}
func (s *deprecationStep) ID() string {
return DeprecationStepID
}
func (s *deprecationStep) Run(ctx context.Context, log logging.Logger, _ *advisor.CheckSpec, it any) ([]advisor.CheckReportFailure, error) {
p, ok := it.(pluginstore.Plugin)
if !ok {
return nil, fmt.Errorf("invalid item type %T", it)
}
if !s.updateChecker.IsUpdatable(ctx, p) {
return nil, nil
}
// Check if plugin is deprecated
i, ok := s.pluginIndex[p.ID]
if !ok {
// Unable to check deprecation status
return nil, nil
}
if i.Status == "deprecated" {
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityHigh,
s.ID(),
p.Name,
p.ID,
[]advisor.CheckErrorLink{
{
Message: "View plugin",
Url: fmt.Sprintf("/plugins/%s", p.ID),
},
},
)}, nil
}
return nil, nil
}
type updateStep struct {
GrafanaVersion string
updateChecker pluginchecker.PluginUpdateChecker
pluginIndex map[string]repo.PluginInfo
}
func (s *updateStep) Title() string {
return "Update check"
}
func (s *updateStep) Description() string {
return "Checks if an installed plugins has a newer version available."
}
func (s *updateStep) Resolution() string {
return "Go to the plugin admin page and upgrade to the latest version."
}
func (s *updateStep) ID() string {
return UpdateStepID
}
func (s *updateStep) Run(ctx context.Context, log logging.Logger, _ *advisor.CheckSpec, i any) ([]advisor.CheckReportFailure, error) {
p, ok := i.(pluginstore.Plugin)
if !ok {
return nil, fmt.Errorf("invalid item type %T", i)
}
if !s.updateChecker.IsUpdatable(ctx, p) {
return nil, nil
}
// Check if plugin has a newer version available
info, ok := s.pluginIndex[p.ID]
if !ok {
// Unable to check updates
return nil, nil
}
if s.updateChecker.CanUpdate(p.ID, p.Info.Version, info.Version, false) {
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityLow,
s.ID(),
p.Name,
p.ID,
[]advisor.CheckErrorLink{
{
Message: "Upgrade",
Url: fmt.Sprintf("/plugins/%s?page=version-history", p.ID),
},
},
)}, nil
}
return nil, nil
}

View File

@ -23,6 +23,7 @@ func TestRun(t *testing.T) {
pluginPreinstalled []string
pluginManaged []string
pluginProvisioned []string
pluginErrors []*plugins.Error
expectedFailures []advisor.CheckReportFailure
}{
{
@ -119,6 +120,69 @@ func TestRun(t *testing.T) {
pluginProvisioned: []string{"plugin5"},
expectedFailures: []advisor.CheckReportFailure{},
},
{
name: "Invalid signatures",
plugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin6", Name: "Plugin 6", Info: plugins.Info{Version: "1.0.0"}}, Signature: plugins.SignatureStatusInvalid},
{JSONData: plugins.JSONData{ID: "plugin7", Name: "Plugin 7", Info: plugins.Info{Version: "1.0.0"}}, Signature: plugins.SignatureStatusModified},
{JSONData: plugins.JSONData{ID: "plugin8", Name: "Plugin 8", Info: plugins.Info{Version: "1.0.0"}}, Signature: plugins.SignatureStatusUnsigned},
},
pluginInfo: []repo.PluginInfo{
{Status: "active", Slug: "plugin6", Version: "1.0.0"},
{Status: "active", Slug: "plugin7", Version: "1.0.0"},
{Status: "active", Slug: "plugin8", Version: "1.0.0"},
},
pluginErrors: []*plugins.Error{
{PluginID: "plugin9", ErrorCode: plugins.ErrorCodeSignatureInvalid},
{PluginID: "plugin10", ErrorCode: plugins.ErrorCodeSignatureModified},
{PluginID: "plugin11", ErrorCode: plugins.ErrorCodeSignatureMissing},
{PluginID: "plugin12", ErrorCode: plugins.ErrorCodeFailedBackendStart}, // This should be ignored atm
},
expectedFailures: []advisor.CheckReportFailure{
{
Severity: advisor.CheckReportFailureSeverityLow,
StepID: UnsignedStepID,
Item: "Plugin 6",
ItemID: "plugin6",
Links: []advisor.CheckErrorLink{{Url: "/plugins/plugin6", Message: "View plugin"}},
},
{
Severity: advisor.CheckReportFailureSeverityLow,
StepID: UnsignedStepID,
Item: "Plugin 7",
ItemID: "plugin7",
Links: []advisor.CheckErrorLink{{Url: "/plugins/plugin7", Message: "View plugin"}},
},
{
Severity: advisor.CheckReportFailureSeverityLow,
StepID: UnsignedStepID,
Item: "Plugin 8",
ItemID: "plugin8",
Links: []advisor.CheckErrorLink{{Url: "/plugins/plugin8", Message: "View plugin"}},
},
{
Severity: advisor.CheckReportFailureSeverityHigh,
StepID: UnsignedStepID,
Item: "plugin9",
ItemID: "plugin9",
Links: []advisor.CheckErrorLink{},
},
{
Severity: advisor.CheckReportFailureSeverityHigh,
StepID: UnsignedStepID,
Item: "plugin10",
ItemID: "plugin10",
Links: []advisor.CheckErrorLink{},
},
{
Severity: advisor.CheckReportFailureSeverityHigh,
StepID: UnsignedStepID,
Item: "plugin11",
ItemID: "plugin11",
Links: []advisor.CheckErrorLink{},
},
},
},
}
for _, tt := range tests {
@ -131,7 +195,8 @@ func TestRun(t *testing.T) {
managedPlugins := &mockManagedPlugins{managed: tt.pluginManaged}
provisionedPlugins := &mockProvisionedPlugins{provisioned: tt.pluginProvisioned}
updateChecker := pluginchecker.ProvideService(managedPlugins, provisionedPlugins, pluginPreinstall)
check := New(pluginStore, pluginRepo, updateChecker, "12.0.0")
pluginErrorResolver := &mockPluginErrorResolver{pluginErrors: tt.pluginErrors}
check := New(pluginStore, pluginRepo, updateChecker, pluginErrorResolver, "12.0.0")
items, err := check.Items(context.Background())
assert.NoError(t, err)
@ -148,8 +213,8 @@ func TestRun(t *testing.T) {
}
}
assert.NoError(t, err)
assert.Equal(t, len(tt.plugins), len(items))
assert.Equal(t, tt.expectedFailures, failures)
assert.Equal(t, len(tt.plugins)+len(tt.pluginErrors), len(items))
assert.ElementsMatch(t, tt.expectedFailures, failures)
})
}
}
@ -222,3 +287,21 @@ type mockProvisionedPlugins struct {
func (m *mockProvisionedPlugins) ProvisionedPlugins(ctx context.Context) ([]string, error) {
return m.provisioned, nil
}
type mockPluginErrorResolver struct {
plugins.ErrorResolver
pluginErrors []*plugins.Error
}
func (m *mockPluginErrorResolver) PluginErrors(ctx context.Context) []*plugins.Error {
return m.pluginErrors
}
func (m *mockPluginErrorResolver) PluginError(ctx context.Context, id string) *plugins.Error {
for _, err := range m.pluginErrors {
if err.PluginID == id {
return err
}
}
return nil
}

View File

@ -0,0 +1,76 @@
package plugincheck
import (
"context"
"fmt"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
)
const (
DeprecationStepID = "deprecation"
)
type deprecationStep struct {
GrafanaVersion string
updateChecker pluginchecker.PluginUpdateChecker
pluginIndex map[string]repo.PluginInfo
}
func (s *deprecationStep) Title() string {
return "Deprecation check"
}
func (s *deprecationStep) Description() string {
return "Check if any installed plugins are deprecated."
}
func (s *deprecationStep) Resolution() string {
return "Check the <a href='https://grafana.com/legal/plugin-deprecation/#a-plugin-i-use-is-deprecated-what-should-i-do'" +
"target=_blank>documentation</a> for recommended steps or delete the plugin."
}
func (s *deprecationStep) ID() string {
return DeprecationStepID
}
func (s *deprecationStep) Run(ctx context.Context, log logging.Logger, _ *advisor.CheckSpec, it any) ([]advisor.CheckReportFailure, error) {
pi, ok := it.(*pluginItem)
if !ok {
return nil, fmt.Errorf("invalid item type %T", it)
}
p := pi.Plugin
if p == nil {
return nil, nil
}
if !s.updateChecker.IsUpdatable(ctx, *p) {
return nil, nil
}
// Check if plugin is deprecated
i, ok := s.pluginIndex[p.ID]
if !ok {
// Unable to check deprecation status
return nil, nil
}
if i.Status == "deprecated" {
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityHigh,
s.ID(),
p.Name,
p.ID,
[]advisor.CheckErrorLink{
{
Message: "View plugin",
Url: fmt.Sprintf("/plugins/%s", p.ID),
},
},
)}, nil
}
return nil, nil
}

View File

@ -0,0 +1,94 @@
package plugincheck
import (
"context"
"fmt"
"slices"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/repo"
)
const (
UnsignedStepID = "unsigned"
)
type unsignedStep struct {
pluginIndex map[string]repo.PluginInfo
}
func (s *unsignedStep) Title() string {
return "Plugin signature check"
}
func (s *unsignedStep) Description() string {
return "Checks has a missing or invalid signature."
}
func (s *unsignedStep) Resolution() string {
return "For security, we recommend only installing plugins from the catalog. " +
"Review the plugin's status and verify your allowlist if appropriate."
}
func (s *unsignedStep) ID() string {
return UnsignedStepID
}
func (s *unsignedStep) Run(ctx context.Context, log logging.Logger, _ *advisor.CheckSpec, it any) ([]advisor.CheckReportFailure, error) {
pi, ok := it.(*pluginItem)
if !ok {
return nil, fmt.Errorf("invalid item type %T", it)
}
p := pi.Plugin
invalidSignatureTypes := []plugins.SignatureStatus{
plugins.SignatureStatusUnsigned,
plugins.SignatureStatusModified,
plugins.SignatureStatusInvalid,
}
if p != nil && slices.Contains(invalidSignatureTypes, p.Signature) {
// This will only happen in dev mode or if the plugin is in the unsigned allow list
links := []advisor.CheckErrorLink{}
if _, ok := s.pluginIndex[p.ID]; ok {
links = append(links, advisor.CheckErrorLink{
Message: "View plugin",
Url: fmt.Sprintf("/plugins/%s", p.ID),
})
}
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityLow,
s.ID(),
p.Name,
p.ID,
links,
)}, nil
}
pluginErr := pi.Err
invalidErrorCodeTypes := []plugins.ErrorCode{
plugins.ErrorCodeSignatureMissing,
plugins.ErrorCodeSignatureInvalid,
plugins.ErrorCodeSignatureModified,
}
if pluginErr != nil && slices.Contains(invalidErrorCodeTypes, pluginErr.ErrorCode) {
links := []advisor.CheckErrorLink{}
if _, ok := s.pluginIndex[pluginErr.PluginID]; ok {
links = append(links, advisor.CheckErrorLink{
Message: "View plugin",
Url: fmt.Sprintf("/plugins/%s", pluginErr.PluginID),
})
}
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityHigh,
s.ID(),
pluginErr.PluginID,
pluginErr.PluginID,
links,
)}, nil
}
return nil, nil
}

View File

@ -0,0 +1,76 @@
package plugincheck
import (
"context"
"fmt"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
)
const (
UpdateStepID = "update"
)
type updateStep struct {
GrafanaVersion string
updateChecker pluginchecker.PluginUpdateChecker
pluginIndex map[string]repo.PluginInfo
}
func (s *updateStep) Title() string {
return "Update check"
}
func (s *updateStep) Description() string {
return "Checks if an installed plugins has a newer version available."
}
func (s *updateStep) Resolution() string {
return "Go to the plugin admin page and upgrade to the latest version."
}
func (s *updateStep) ID() string {
return UpdateStepID
}
func (s *updateStep) Run(ctx context.Context, log logging.Logger, _ *advisor.CheckSpec, it any) ([]advisor.CheckReportFailure, error) {
pi, ok := it.(*pluginItem)
if !ok {
return nil, fmt.Errorf("invalid item type %T", it)
}
p := pi.Plugin
if p == nil {
return nil, nil
}
if !s.updateChecker.IsUpdatable(ctx, *p) {
return nil, nil
}
// Check if plugin has a newer version available
info, ok := s.pluginIndex[p.ID]
if !ok {
// Unable to check updates
return nil, nil
}
if s.updateChecker.CanUpdate(p.ID, p.Info.Version, info.Version, false) {
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityLow,
s.ID(),
p.Name,
p.ID,
[]advisor.CheckErrorLink{
{
Message: "Upgrade",
Url: fmt.Sprintf("/plugins/%s?page=version-history", p.ID),
},
},
)}, nil
}
return nil, nil
}

View File

@ -341,9 +341,9 @@ type AppDTO struct {
}
const (
errorCodeSignatureMissing ErrorCode = "signatureMissing"
errorCodeSignatureModified ErrorCode = "signatureModified"
errorCodeSignatureInvalid ErrorCode = "signatureInvalid"
ErrorCodeSignatureMissing ErrorCode = "signatureMissing"
ErrorCodeSignatureModified ErrorCode = "signatureModified"
ErrorCodeSignatureInvalid ErrorCode = "signatureInvalid"
ErrorCodeFailedBackendStart ErrorCode = "failedBackendStart"
ErrorAngular ErrorCode = "angular"
)
@ -392,11 +392,11 @@ func (e Error) AsErrorCode() ErrorCode {
switch e.SignatureStatus {
case SignatureStatusInvalid:
return errorCodeSignatureInvalid
return ErrorCodeSignatureInvalid
case SignatureStatusModified:
return errorCodeSignatureModified
return ErrorCodeSignatureModified
case SignatureStatusUnsigned:
return errorCodeSignatureMissing
return ErrorCodeSignatureMissing
case SignatureStatusInternal, SignatureStatusValid:
return ""
}
@ -411,11 +411,11 @@ func (e *Error) WithMessage(m string) *Error {
func (e Error) PublicMessage() string {
switch e.ErrorCode {
case errorCodeSignatureInvalid:
case ErrorCodeSignatureInvalid:
return "Invalid plugin signature"
case errorCodeSignatureModified:
case ErrorCodeSignatureModified:
return "Plugin signature does not match"
case errorCodeSignatureMissing:
case ErrorCodeSignatureMissing:
return "Plugin signature is missing"
case ErrorCodeFailedBackendStart:
return "Plugin failed to start"