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
3 changes: 3 additions & 0 deletions central/complianceoperator/v2/report/datastore/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type DataStore interface {

// DeleteSnapshot removes a report snapshot object from the database
DeleteSnapshot(ctx context.Context, id string) error

// GetLastSnapshotFromScanConfig returns the last snapshot associated with a ScanConfiguration
GetLastSnapshotFromScanConfig(ctx context.Context, scanConfigID string) (*storage.ComplianceOperatorReportSnapshotV2, error)
}

// New returns an instance of DataStore.
Expand Down
17 changes: 17 additions & 0 deletions central/complianceoperator/v2/report/datastore/datastore_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/errorhelpers"
types "github.com/stackrox/rox/pkg/protocompat"
"github.com/stackrox/rox/pkg/search"
)

Expand Down Expand Up @@ -75,3 +76,19 @@
}
return errList.ToError()
}

func (d *datastoreImpl) GetLastSnapshotFromScanConfig(ctx context.Context, scanConfigID string) (*storage.ComplianceOperatorReportSnapshotV2, error) {
query := search.NewQueryBuilder().
AddExactMatches(search.ComplianceOperatorScanConfig, scanConfigID).ProtoQuery()
snapshots, err := d.SearchSnapshots(ctx, query)
if err != nil {
return nil, err
}

Check warning on line 86 in central/complianceoperator/v2/report/datastore/datastore_impl.go

View check run for this annotation

Codecov / codecov/patch

central/complianceoperator/v2/report/datastore/datastore_impl.go#L85-L86

Added lines #L85 - L86 were not covered by tests
var lastSnapshot *storage.ComplianceOperatorReportSnapshotV2
for _, snapshot := range snapshots {
if types.CompareTimestamps(snapshot.GetReportStatus().GetCompletedAt(), lastSnapshot.GetReportStatus().GetCompletedAt()) > 0 {
lastSnapshot = snapshot
}
}
return lastSnapshot, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"context"
"fmt"
"testing"
"time"

"github.com/google/uuid"
reportStorage "github.com/stackrox/rox/central/complianceoperator/v2/report/store/postgres"
scanConfigDS "github.com/stackrox/rox/central/complianceoperator/v2/scanconfigurations/datastore"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/features"
"github.com/stackrox/rox/pkg/postgres/pgtest"
"github.com/stackrox/rox/pkg/protocompat"
"github.com/stackrox/rox/pkg/sac"
"github.com/stackrox/rox/pkg/sac/resources"
"github.com/stackrox/rox/pkg/search"
Expand Down Expand Up @@ -255,6 +257,34 @@ func (s *complianceReportSnapshotDataStoreSuite) TestDeleteOrphaned() {
}
}

func (s *complianceReportSnapshotDataStoreSuite) TestGetLastSnapshot() {
// make sure we have nothing
reportIDs, err := s.storage.GetIDs(s.hasReadCtx)
s.Require().NoError(err)
s.Require().Empty(reportIDs)

timeNow := time.Now()
oldTime := timeNow.Add(-time.Hour)
timestampNow, err := protocompat.ConvertTimeToTimestampOrError(timeNow)
s.Require().NoError(err)
oldTimestamp, err := protocompat.ConvertTimeToTimestampOrError(oldTime)
s.Require().NoError(err)

status1 := getStatus(storage.ComplianceOperatorReportStatus_PREPARING, oldTimestamp, timestampNow, "", storage.ComplianceOperatorReportStatus_SCHEDULED, storage.ComplianceOperatorReportStatus_EMAIL)
status2 := getStatus(storage.ComplianceOperatorReportStatus_PREPARING, oldTimestamp, oldTimestamp, "", storage.ComplianceOperatorReportStatus_SCHEDULED, storage.ComplianceOperatorReportStatus_EMAIL)
user := getUser("u-1", "user-1")
reports := []*storage.ComplianceOperatorReportSnapshotV2{
getTestReport(uuidStub1, uuidScanConfigStub1, status1, user),
getTestReport(uuidStub2, uuidScanConfigStub1, status2, user),
}
for _, r := range reports {
s.Require().NoError(s.storage.Upsert(s.hasWriteCtx, r))
}
snapshot, err := s.datastore.GetLastSnapshotFromScanConfig(s.hasReadCtx, uuidScanConfigStub1)
s.Assert().NoError(err)
s.Assert().Equal(uuidStub1, snapshot.GetReportId())
}

func getTestReport(id string, scanConfigID string, status *storage.ComplianceOperatorReportStatus, user *storage.SlimUser) *storage.ComplianceOperatorReportSnapshotV2 {
return &storage.ComplianceOperatorReportSnapshotV2{
ReportId: id,
Expand Down
15 changes: 15 additions & 0 deletions central/complianceoperator/v2/report/datastore/mocks/datastore.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 23 additions & 24 deletions central/complianceoperator/v2/report/manager/format/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
"bytes"
"fmt"
"io"
"strings"

"github.com/pkg/errors"
"github.com/stackrox/rox/central/complianceoperator/v2/report"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/csv"
"google.golang.org/protobuf/types/known/timestamppb"
)

const (
Expand Down Expand Up @@ -67,7 +66,7 @@
// If a cluster fails, the generated CSV file will contain the reason for the reason but (no check results).
// If a cluster success, the generated CSV file will contain all the check results with enhanced information (e.g. remediation, associated profile, etc)
// The results parameter is expected to contain the clusters that succeed (no failed clusters should be passed in results).
func (f *FormatterImpl) FormatCSVReport(results map[string][]*report.ResultRow, failedClusters map[string]*storage.ComplianceOperatorReportSnapshotV2_FailedCluster) (buffRet *bytes.Buffer, errRet error) {
func (f *FormatterImpl) FormatCSVReport(results map[string][]*report.ResultRow, clusters map[string]*report.ClusterData) (buffRet *bytes.Buffer, errRet error) {
var buf bytes.Buffer
zipWriter := f.newZipWriter(&buf)
defer func() {
Expand All @@ -76,20 +75,22 @@
errRet = errors.Wrap(err, "unable to create a zip file of the compliance report")
}
}()
for clusterID, failedCluster := range failedClusters {
fileName := fmt.Sprintf(failedClusterFmt, clusterID)
if err := f.createFailedClusterFileInZip(zipWriter, fileName, failedCluster); err != nil {
return nil, errors.Wrap(err, "error creating failed cluster report")
timestamp := timestamppb.Now()
for clusterID, cluster := range clusters {
if cluster.FailedInfo != nil {
fileName := getFileName(failedClusterFmt, cluster.ClusterName, timestamp)
if err := f.createFailedClusterFileInZip(zipWriter, fileName, cluster.FailedInfo); err != nil {
return nil, errors.Wrap(err, "error creating failed cluster report")
}
}
}
for clusterID, res := range results {
// We should not receive results from a failed cluster
if _, ok := failedClusters[clusterID]; ok {
if len(results[clusterID]) == 0 && cluster.FailedInfo != nil {
continue
}
fileName := fmt.Sprintf(successfulClusterFmt, clusterID)
err := f.createCSVInZip(zipWriter, fileName, res)
if err != nil {
if _, ok := results[clusterID]; !ok {
return nil, errors.Errorf("found no results for cluster %q", clusterID)
}

Check warning on line 91 in central/complianceoperator/v2/report/manager/format/formatter.go

View check run for this annotation

Codecov / codecov/patch

central/complianceoperator/v2/report/manager/format/formatter.go#L90-L91

Added lines #L90 - L91 were not covered by tests
fileName := getFileName(successfulClusterFmt, cluster.ClusterName, timestamp)
if err := f.createCSVInZip(zipWriter, fileName, results[clusterID]); err != nil {
return nil, errors.Wrap(err, "error creating csv report")
}
}
Expand Down Expand Up @@ -127,24 +128,22 @@
}
}

func (f *FormatterImpl) createFailedClusterFileInZip(zipWriter ZipWriter, filename string, failedCluster *storage.ComplianceOperatorReportSnapshotV2_FailedCluster) error {
func (f *FormatterImpl) createFailedClusterFileInZip(zipWriter ZipWriter, filename string, failedCluster *report.FailedCluster) error {
w, err := zipWriter.Create(filename)
if err != nil {
return err
}
csvWriter := f.newCSVWriter(failedClusterCSVHeader, true)
csvWriter.AddValue(generateFailRecord(failedCluster))
for _, reason := range failedCluster.Reasons {
// The order in the slice needs to match the order defined in `failedClusterCSVHeader`
csvWriter.AddValue([]string{failedCluster.ClusterId, failedCluster.ClusterName, reason, failedCluster.OperatorVersion})
}
return csvWriter.WriteCSV(w)
}

func generateFailRecord(failedCluster *storage.ComplianceOperatorReportSnapshotV2_FailedCluster) []string {
// The order in the slice needs to match the order defined in `failedClusterCSVHeader`
return []string{
failedCluster.GetClusterId(),
failedCluster.GetClusterName(),
strings.Join(failedCluster.GetReasons(), ", "),
failedCluster.GetOperatorVersion(),
}
func getFileName(format string, clusterName string, timestamp *timestamppb.Timestamp) string {
year, month, day := timestamp.AsTime().Date()
return fmt.Sprintf(format, fmt.Sprintf("%s_%d-%d-%d", clusterName, year, month, day))
}

func createNewZipWriter(buf *bytes.Buffer) ZipWriter {
Expand Down
Loading
Loading