diff --git a/apps/advisor/pkg/app/checkregistry/checkregistry.go b/apps/advisor/pkg/app/checkregistry/checkregistry.go index 724c62308c3..9fbf0ab66ef 100644 --- a/apps/advisor/pkg/app/checkregistry/checkregistry.go +++ b/apps/advisor/pkg/app/checkregistry/checkregistry.go @@ -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), diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index 30264100bd0..3d261f81b67 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -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 documentation 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 -} diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index ad3feaf43b5..60b49870af6 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -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 +} diff --git a/apps/advisor/pkg/app/checks/plugincheck/deprecation_step.go b/apps/advisor/pkg/app/checks/plugincheck/deprecation_step.go new file mode 100644 index 00000000000..b778e5d8979 --- /dev/null +++ b/apps/advisor/pkg/app/checks/plugincheck/deprecation_step.go @@ -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 documentation 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 +} diff --git a/apps/advisor/pkg/app/checks/plugincheck/unsigned_step.go b/apps/advisor/pkg/app/checks/plugincheck/unsigned_step.go new file mode 100644 index 00000000000..a0d212d0f02 --- /dev/null +++ b/apps/advisor/pkg/app/checks/plugincheck/unsigned_step.go @@ -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 +} diff --git a/apps/advisor/pkg/app/checks/plugincheck/update_step.go b/apps/advisor/pkg/app/checks/plugincheck/update_step.go new file mode 100644 index 00000000000..38eb8cf545b --- /dev/null +++ b/apps/advisor/pkg/app/checks/plugincheck/update_step.go @@ -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 +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index e4a314a9f00..965760bab50 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -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"