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
8 changes: 7 additions & 1 deletion central/reports/scheduler/v2/reportgenerator/csv_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ func formatCSVRow(r *ImageCVEQueryResponse) []string {
} else {
cisaKev = "Not Available"
}
csvRow = append(csvRow, cisaKev)
var knownRansomware string
if r.GetKnownRansomwareCampaign() != nil {
knownRansomware = strconv.FormatBool(*r.GetKnownRansomwareCampaign())
} else {
knownRansomware = "Not Available"
}
csvRow = append(csvRow, cisaKev, knownRansomware)
}

csvRow = append(csvRow,
Expand Down
72 changes: 55 additions & 17 deletions central/reports/scheduler/v2/reportgenerator/csv_gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/features"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -62,27 +63,64 @@ func TestFormatCSVRow(t *testing.T) {
Link: "https://nvd.nist.gov/vuln/detail/CVE-2024-1234",
}

row := formatCSVRow(r)
assert.Equal(t, len(csvHeader), len(row), "row should have same number of columns as header")
assert.Equal(t, "test-cluster", row[0])
assert.Equal(t, "test-ns", row[1])
assert.Equal(t, "test-deploy", row[2])
assert.Equal(t, "CVE-2024-1234", row[6])
assert.Equal(t, "true", row[7])
assert.Equal(t, "CRITICAL", row[9])
assert.Equal(t, "95.000", row[12])
assert.Equal(t, "Not Available", row[14]) // Image Created Date (nil → "Not Available")
assert.Equal(t, "https://nvd.nist.gov/vuln/detail/CVE-2024-1234", row[15])
t.Run("KEV disabled", func(t *testing.T) {
t.Setenv(features.KnownExploitedVulnerabilities.EnvVar(), "false")

row := formatCSVRow(r)
assert.Equal(t, len(formatCol()), len(row), "row should have same number of columns as header")
assert.Equal(t, "test-cluster", row[0])
assert.Equal(t, "test-ns", row[1])
assert.Equal(t, "test-deploy", row[2])
assert.Equal(t, "CVE-2024-1234", row[6])
assert.Equal(t, "true", row[7])
assert.Equal(t, "CRITICAL", row[9])
assert.Equal(t, "95.000", row[12])
assert.Equal(t, "Not Available", row[14]) // Image Created Date (nil → "Not Available")
assert.Equal(t, "https://nvd.nist.gov/vuln/detail/CVE-2024-1234", row[15])
})

t.Run("KEV enabled", func(t *testing.T) {
t.Setenv(features.KnownExploitedVulnerabilities.EnvVar(), "true")

// The two KEV columns are inserted after EPSS (index 12), shifting all
// trailing columns right by two.
row := formatCSVRow(r)
assert.Equal(t, len(formatCol()), len(row), "row should have same number of columns as header")
assert.Equal(t, "test-cluster", row[0])
assert.Equal(t, "95.000", row[12])
assert.Equal(t, "Not Available", row[13]) // CISA KEV (nil → "Not Available")
assert.Equal(t, "Not Available", row[14]) // Known Ransomware Campaign (nil → "Not Available")
assert.Equal(t, "Not Available", row[16]) // Image Created Date (nil → "Not Available")
assert.Equal(t, "https://nvd.nist.gov/vuln/detail/CVE-2024-1234", row[17])
})
}

func TestFormatCSVRow_NilFields(t *testing.T) {
r := &ImageCVEQueryResponse{}
row := formatCSVRow(r)
assert.Equal(t, len(csvHeader), len(row))
assert.Equal(t, "", row[0])
assert.Equal(t, "Not Available", row[12]) // EPSS Probability Percentage
assert.Equal(t, "Not Available", row[13]) // Discovered At
assert.Equal(t, "Not Available", row[14]) // Image Created Date

t.Run("KEV disabled", func(t *testing.T) {
t.Setenv(features.KnownExploitedVulnerabilities.EnvVar(), "false")

row := formatCSVRow(r)
assert.Equal(t, len(formatCol()), len(row))
assert.Equal(t, "", row[0])
assert.Equal(t, "Not Available", row[12]) // EPSS Probability Percentage
assert.Equal(t, "Not Available", row[13]) // Discovered At
assert.Equal(t, "Not Available", row[14]) // Image Created Date
})

t.Run("KEV enabled", func(t *testing.T) {
t.Setenv(features.KnownExploitedVulnerabilities.EnvVar(), "true")

row := formatCSVRow(r)
assert.Equal(t, len(formatCol()), len(row))
assert.Equal(t, "", row[0])
assert.Equal(t, "Not Available", row[12]) // EPSS Probability Percentage
assert.Equal(t, "Not Available", row[13]) // CISA KEV
assert.Equal(t, "Not Available", row[14]) // Known Ransomware Campaign
assert.Equal(t, "Not Available", row[15]) // Discovered At
assert.Equal(t, "Not Available", row[16]) // Image Created Date
})
}

func TestCsvReportName(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"encoding/csv"
"io"
"slices"
"time"

blobDS "github.com/stackrox/rox/central/blob/datastore"
Expand All @@ -19,28 +20,31 @@ import (
"github.com/stackrox/rox/pkg/uuid"
)

// CSV column indices matching csvHeader in csv_gen.go.
// CSV column indices for the fixed prefix that precedes any feature-flag-gated
// columns (see csvHeader in csv_gen.go). Columns after EPSS can shift depending
// on enabled features, so look those up with colOf instead of hardcoding them.
const (
colCluster = 0
colNamespace = 1
colDeployment = 2
colImage = 3
colComponent = 4
colCompVersion = 5
colCVE = 6
colFixable = 7
colFixedBy = 8
colSeverity = 9
colCVSS = 10
colNVDCVSS = 11
colEPSS = 12
colDiscoveredAt = 13
colImageCreated = 14
colReference = 15
colAdvName = 16
colAdvLink = 17
colCluster = 0
colNamespace = 1
colDeployment = 2
colImage = 3
colComponent = 4
colCompVersion = 5
colCVE = 6
colFixable = 7
colFixedBy = 8
colSeverity = 9
colCVSS = 10
colNVDCVSS = 11
colEPSS = 12
)

// colOf returns the index of the named column in the current CSV layout, which
// depends on which feature-flag-gated columns formatCol includes.
func colOf(name string) int {
return slices.Index(formatCol(), name)
}

// collectColumn extracts a single column from all rows.
func collectColumn(rows [][]string, col int) []string {
out := make([]string, len(rows))
Expand Down Expand Up @@ -163,7 +167,7 @@ func (s *NewDataModelEnhancedReportingTestSuite) TestGenerateReportTransaction_A

rows := s.readBlobCSV(blobStore, snap.GetReportConfigurationId(), snap.GetReportId())
s.Require().Len(rows, 9, "1 header + 8 data rows")
s.Equal(csvHeader, rows[0])
s.Equal(formatCol(), rows[0])

dataRows := rows[1:]
s.ElementsMatch([]string{
Expand Down Expand Up @@ -240,7 +244,7 @@ func (s *NewDataModelEnhancedReportingTestSuite) TestGenerateReportTransaction_E

rows := s.readBlobCSV(blobStore, snap.GetReportConfigurationId(), snap.GetReportId())
s.Require().Len(rows, 1, "only header row for empty result")
s.Equal(csvHeader, rows[0])
s.Equal(formatCol(), rows[0])
}

// TestGenerateReportTransaction_CSVFieldValues spot-checks individual CSV field
Expand Down Expand Up @@ -276,9 +280,9 @@ func (s *NewDataModelEnhancedReportingTestSuite) TestGenerateReportTransaction_C
s.Equal("9.00", row[colCVSS])
s.Equal("10.00", row[colNVDCVSS])
s.Equal("70.000", row[colEPSS])
s.NotEqual("Not Available", row[colDiscoveredAt])
s.Equal("RHSA-2025-CVE-fixable", row[colAdvName])
s.Equal("test-rhsa-link", row[colAdvLink])
s.NotEqual("Not Available", row[colOf("Discovered At")])
s.Equal("RHSA-2025-CVE-fixable", row[colOf("Advisory Name")])
s.Equal("test-rhsa-link", row[colOf("Advisory Link")])
}

// TestGenerateReportTransaction_ReportStatusUpdated verifies that the report
Expand Down
2 changes: 1 addition & 1 deletion pkg/features/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ var (
// KnownExploitedVulnerabilities enables support for CISA Known Exploited Vulnerabilities (KEV) data.
//
// This must be enabled in Central and Scanner V4 Matcher to have any effect.
KnownExploitedVulnerabilities = registerFeature("Display CISA Known Exploited Vulnerabilities (KEV) data", "ROX_CISA_KEV")
KnownExploitedVulnerabilities = registerFeature("Display CISA Known Exploited Vulnerabilities (KEV) data", "ROX_CISA_KEV", enabled)

// Display Compliance Dashboard (Deprecated) in user interface (not displayed by default starting in 4.11 release).
DeprecatedComplianceDashboard = registerFeature("Display Compliance Dashboard (Deprecated) in user interface", "ROX_DEPRECATED_COMPLIANCE_DASHBOARD")
Expand Down
Loading