Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Changes should still be described appropriately in JIRA/doc input pages, for inc
- ROX-34997: The Central CR now supports `spec.central.rolloutStrategy` (`Recreate` or `RollingUpdate`) to configure the central deployment rollout strategy. Default remains `Recreate`.

- ROX-35181: Administrative events are now exposed as configurable custom Prometheus metrics (`rox_central_admin_event_*`), aggregated by Type, Level, Domain, ResourceType, and ResourceName. Requires permission to read Administration resource, globally scoped.
- ROX-35938: The `rox_central_cert_exp_hours` Prometheus metric now also tracks Secured Cluster (Sensor) certificate expiry. These entries use the `SECURED_CLUSTER` component type and include the cluster name in a new `Name` label.
- ROX-35545: Added ACL change as a file access operation for runtime policies.

### Removed Features
Expand Down
2 changes: 2 additions & 0 deletions central/metrics/custom/expiry/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (

var LazyLabels = tracker.LazyLabelGetters[*finding]{
"Component": func(f *finding) string { return f.component },
"Name": func(f *finding) string { return f.name },
}

type finding struct {
component string
name string
hoursUntilExpiration int
}

Expand Down
89 changes: 61 additions & 28 deletions central/metrics/custom/expiry/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,84 @@ import (
"context"
"time"

"github.com/stackrox/rox/central/credentialexpiry/service"
clusterDS "github.com/stackrox/rox/central/cluster/datastore"
creds "github.com/stackrox/rox/central/credentialexpiry/service"
"github.com/stackrox/rox/central/metrics"
"github.com/stackrox/rox/central/metrics/custom/tracker"
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/logging"
)

func New(s service.Service) *tracker.TrackerBase[*finding] {
return tracker.MakeGlobalTrackerBase(
// securedClusterComponent is a synthetic component, not part of the
// v1.GetCertExpiry_Component enum: unlike Central/Scanner/Central DB/Scanner
// V4, there can be any number of secured clusters, each identified by the
// "Name" label rather than by a fixed enum value.
const securedClusterComponent = "SECURED_CLUSTER"

func New(s creds.Service, clusters clusterDS.DataStore) *tracker.TrackerBase[*finding] {
return tracker.MakeTrackerBase(
metrics.Expiry,
"hours before certificate expires",
LazyLabels,
func(ctx context.Context, _ tracker.MetricDescriptors) tracker.FindingErrorSequence[*finding] {
return track(ctx, s)
return track(ctx, s, clusters)
},
)
}

func track(ctx context.Context, s service.Service) tracker.FindingErrorSequence[*finding] {
func track(ctx context.Context, s creds.Service, clusters clusterDS.DataStore) tracker.FindingErrorSequence[*finding] {
return func(yield func(*finding, error) bool) {
if s == nil {
return
}
var f finding
for i, component := range v1.GetCertExpiry_Component_name {
if v1.GetCertExpiry_Component(i) == v1.GetCertExpiry_UNKNOWN {
continue
}
result, err := s.GetCertExpiry(ctx, &v1.GetCertExpiry_Request{
Component: v1.GetCertExpiry_Component(i),
})
if err != nil {
// Ignore particular component errors, as they do not affect
// other components metrics.
logging.LoggerForModule().Errorw("Failed to get certificate expiry",
logging.String("component", component), logging.Err(err))
continue
}
f.component = component
if result != nil {
f.hoursUntilExpiration = int(time.Until(result.GetExpiry().AsTime()).Hours())
}
if !yield(&f, nil) {
if s != nil {
if !fromCredsService(ctx, s, yield) {
return
}
}
if clusters == nil {
return
}
fromClustersDS(ctx, clusters, yield)
}
}

func fromCredsService(ctx context.Context, s creds.Service, yield func(*finding, error) bool) bool {
for i, component := range v1.GetCertExpiry_Component_name {
if v1.GetCertExpiry_Component(i) == v1.GetCertExpiry_UNKNOWN {
continue
}
result, err := s.GetCertExpiry(ctx, &v1.GetCertExpiry_Request{
Component: v1.GetCertExpiry_Component(i),
})
if err != nil {
// Ignore particular component errors, as they do not affect
// other components metrics.
logging.LoggerForModule().Errorw("Failed to get certificate expiry",
logging.String("component", component), logging.Err(err))
continue
}
f := &finding{component: component}
if result != nil {
f.hoursUntilExpiration = int(time.Until(result.GetExpiry().AsTime()).Hours())
}
if !yield(f, nil) {
return false
}
}
return true
}

func fromClustersDS(ctx context.Context, clusters clusterDS.DataStore, yield func(*finding, error) bool) {
collector := tracker.NewFindingCollector(yield)
collector.Finally(clusters.WalkClusters(ctx, func(cluster *storage.Cluster) error {
expiry := cluster.GetStatus().GetCertExpiryStatus().GetSensorCertExpiry()
if expiry == nil {
// The cluster hasn't connected yet, so there is no cert to report on.
return nil
}
return collector.Yield(&finding{
component: securedClusterComponent,
name: cluster.GetName(),
hoursUntilExpiration: int(time.Until(expiry.AsTime()).Hours()),
})
}))
}
49 changes: 48 additions & 1 deletion central/metrics/custom/expiry/tracker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import (
"context"
"errors"
"testing"
"time"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
clusterDSMocks "github.com/stackrox/rox/central/cluster/datastore/mocks"
"github.com/stackrox/rox/central/credentialexpiry/service"
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
)

var errTest = errors.New("test")
Expand Down Expand Up @@ -39,11 +44,53 @@ var _ service.Service = (*mockService)(nil)

func Test_track(t *testing.T) {
var s mockService
ctrl := gomock.NewController(t)
clusters := clusterDSMocks.NewMockDataStore(ctrl)
clusters.EXPECT().WalkClusters(gomock.Any(), gomock.Any()).Return(nil)

components := make([]string, 0, len(v1.GetCertExpiry_Component_name))
for f := range track(context.Background(), &s) {
for f := range track(context.Background(), &s, clusters) {
components = append(components, f.component)
}
assert.ElementsMatch(t, []string{"SCANNER", "CENTRAL_DB", "CENTRAL"}, components,
"should have no UNKNOWN and SCANNER_V4")
}

func Test_track_securedClusters(t *testing.T) {
var s mockService
ctrl := gomock.NewController(t)
clusters := clusterDSMocks.NewMockDataStore(ctrl)

connected := &storage.Cluster{
Name: "connected-cluster",
Status: &storage.ClusterStatus{
CertExpiryStatus: &storage.ClusterCertExpiryStatus{
SensorCertExpiry: timestamppb.New(time.Now().Add(24 * time.Hour)),
},
},
}
neverConnected := &storage.Cluster{Name: "never-connected-cluster"}

clusters.EXPECT().WalkClusters(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, fn func(*storage.Cluster) error) error {
for _, cluster := range []*storage.Cluster{connected, neverConnected} {
if err := fn(cluster); err != nil {
return err
}
}
return nil
})

var securedClusterFindings []*finding
for f, err := range track(context.Background(), &s, clusters) {
assert.NoError(t, err)
if f.component == securedClusterComponent {
securedClusterFindings = append(securedClusterFindings, f)
}
}

if assert.Len(t, securedClusterFindings, 1, "the never-connected cluster should not produce a finding") {
assert.Equal(t, "connected-cluster", securedClusterFindings[0].name)
assert.InDelta(t, 24, securedClusterFindings[0].hoursUntilExpiration, 1)
}
}
2 changes: 1 addition & 1 deletion central/metrics/custom/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func makeRunner(ds *runnerDatastores) trackerRunner {
"total_policies": policies.LazyLabels.GetLabels(),
}),
}, {
expiry.New(ds.expiry),
expiry.New(ds.expiry, ds.clusters),
withHardcodedConfiguration(60, map[string][]string{
// rox_central_cert_exp_hours
"hours": expiry.LazyLabels.GetLabels(),
Expand Down
Loading