Skip to content

fix(compliance): fix Control Reference for E8 and other profiles#19946

Draft
guzalv wants to merge 2 commits intomasterfrom
gualvare/ROX-31229-fix-control-ref-benchmark
Draft

fix(compliance): fix Control Reference for E8 and other profiles#19946
guzalv wants to merge 2 commits intomasterfrom
gualvare/ROX-31229-fix-control-ref-benchmark

Conversation

@guzalv
Copy link
Copy Markdown
Contributor

@guzalv guzalv commented Apr 10, 2026

Description

Fix "Data Not Available" in the Control Reference column of compliance CSV reports (and API responses) for profiles whose benchmark shortname doesn't match any standard value in rule controls.

Root cause: GetControlsForScanResults mapped profile names to benchmark shortnames via a hardcoded regex in benchmark.go, then filtered controls by that shortname. For example, ocp4-e8 mapped to E8, but the Compliance Operator never uses E8 as a standard in its rule annotations — those rules have controls under CIS-OCP, NIST-800-53, etc. The query WHERE standard = 'E8' always returned 0 rows.

Fix: Remove the benchmark filter entirely. Query all controls for the given rule names directly via a new GetControlsByRules method. Controls are already scoped to rules via CO annotations, so filtering by benchmark is redundant and lossy.

Affected profiles (confirmed): ocp4-e8 (E8), likely ocp4-pci-dss-4-0 (PCI-DSS vs PCI-DSS-4-0 mismatch).

Also fixes pre-existing SA1019 staticcheck warnings (deprecated GetByQueryGetByQueryFn) in the same file.

User-facing documentation

Testing and quality

  • the change is production ready: the change is GA, or otherwise the functionality is gated by a feature flag
  • CI results are inspected

Automated testing

  • modified existing tests

How I validated my change

  • Confirmed via direct DB query that no E8 standard exists in compliance_operator_rule_v2_controls
  • Confirmed ocp4-e8 rules have controls under CIS-OCP, NIST-800-53, BSI, PCI-DSS, etc.
  • All existing unit tests pass with updated mock expectations
  • Needs live cluster verification with ocp4-e8 profile to confirm controls appear in CSV

guzalv added 2 commits April 10, 2026 16:08
Add a new column after "Profile(version)" that shows whether the profile
is a regular "Profile" or a "Tailored Profile". The value is derived from
the profile's OperatorKind enum stored in the database.

UNSPECIFIED kind (should not occur in practice) renders as
"Data Not Available" rather than defaulting to "Profile".
GetControlsForScanResults mapped profile names to benchmark shortnames
via a hardcoded regex, then filtered controls by that shortname. Profiles
like ocp4-e8 mapped to "E8", but no rule carries controls with standard
"E8" — those rules have controls under CIS-OCP, NIST-800-53, etc.
This caused "Data Not Available" in the Control Reference column for
affected profiles.

Remove the benchmark filter: query all controls for the given rule names
directly. Controls are already scoped to rules via CO annotations, so
filtering by benchmark is redundant and lossy.

Also fix deprecated GetByQuery calls (SA1019) in the same file.
@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Apr 10, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added "Profile type" column to CSV report exports, indicating whether a profile is standard or tailored.
  • Refactor

    • Simplified control lookup mechanism by removing benchmark-dependent filtering; controls are now retrieved using rule names only.

Walkthrough

The PR simplifies the compliance rules datastore API by introducing a new GetControlsByRules method that retrieves controls by rule names without benchmark filtering, replacing previous calls to GetControlsByRulesAndBenchmarks. It also extends result rows with a ProfileType field derived from operator kind and updates CSV reporting to include profile type information.

Changes

Cohort / File(s) Summary
Datastore Interface & Implementation
central/complianceoperator/v2/rules/datastore/datastore.go, datastore_impl.go, mocks/datastore.go
Added new GetControlsByRules method to the datastore interface and implemented it in the datastore with a Postgres query that filters controls by rule names and returns results grouped by rule/control/standard. Updated mocks to support the new method.
Service Test Updates
central/complianceoperator/v2/checkresults/service/service_impl_test.go, stats/service/service_impl_test.go
Updated mock expectations across multiple test cases to call GetControlsByRules instead of GetControlsByRulesAndBenchmarks, removing benchmark arguments while preserving return values and call cardinalities.
Utility Function Refactor
central/complianceoperator/v2/checkresults/utils/utils.go
Refactored GetControlsForScanResults to use the new GetControlsByRules method, removing benchmark mapping logic and marking the profileName parameter as unused.
Result Model
central/complianceoperator/v2/report/result.go
Added new exported ProfileType string field to the ResultRow struct.
Report Formatter
central/complianceoperator/v2/report/manager/format/formatter.go, formatter_test.go
Extended CSV output schema to include "Profile type" column and updated record generation to populate it. Updated test data to include ProfileType field in fake report results.
Results Aggregator
central/complianceoperator/v2/report/manager/results/results_aggregator.go, results_aggregator_test.go
Enhanced aggregation logic to enrich result rows with ProfileType by mapping operator kind to human-readable strings. Updated mock expectations to use GetControlsByRules and removed benchmark-related test scaffolding; updated assertions to validate ProfileType population based on profile presence.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: addressing Control Reference issues for E8 and other compliance profiles by removing incorrect benchmark filtering.
Description check ✅ Passed The pull request description is comprehensive, covering root cause, fix implementation, affected profiles, and validation steps. However, CHANGELOG and documentation PR checkboxes remain unchecked without justification.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gualvare/ROX-31229-fix-control-ref-benchmark

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 50.87719% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.56%. Comparing base (38c2fdc) to head (9cefe9a).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...ianceoperator/v2/rules/datastore/datastore_impl.go 33.33% 26 Missing ⚠️
...or/v2/report/manager/results/results_aggregator.go 88.23% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #19946      +/-   ##
==========================================
- Coverage   49.56%   49.56%   -0.01%     
==========================================
  Files        2764     2764              
  Lines      208351   208405      +54     
==========================================
+ Hits       103269   103289      +20     
- Misses      97430    97464      +34     
  Partials     7652     7652              
Flag Coverage Δ
go-unit-tests 49.56% <50.87%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

🚀 Build Images Ready

Images are ready for commit 9cefe9a. To use with deploy scripts:

export MAIN_IMAGE_TAG=4.11.x-623-g9cefe9a6a5

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Now that controls are no longer filtered by benchmark, GetControlsForScanResults’s profileName parameter is unused and explicitly ignored; consider removing this parameter and updating call sites to simplify the API and avoid confusion about its role.
  • If GetControlsByRulesAndBenchmarks is no longer used after switching all callers to GetControlsByRules, consider removing the older method from the datastore interface, implementation, and mocks to reduce dead code and keep the datastore surface minimal.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Now that controls are no longer filtered by benchmark, `GetControlsForScanResults`’s `profileName` parameter is unused and explicitly ignored; consider removing this parameter and updating call sites to simplify the API and avoid confusion about its role.
- If `GetControlsByRulesAndBenchmarks` is no longer used after switching all callers to `GetControlsByRules`, consider removing the older method from the datastore interface, implementation, and mocks to reduce dead code and keep the datastore surface minimal.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
central/complianceoperator/v2/rules/datastore/datastore.go (1)

32-36: Deprecate the benchmark-filtered API to prevent regression-prone usage.

With both methods available, future callers can accidentally reintroduce the lossy benchmark filter. Please mark GetControlsByRulesAndBenchmarks as deprecated (or remove it once no callers remain) and steer usage to GetControlsByRules.

Suggested interface annotation
- // GetControlsByRulesAndBenchmarks returns controls by a list of rule names group by control, standard and rule name.
+ // Deprecated: GetControlsByRulesAndBenchmarks applies benchmark filtering and may drop valid controls.
+ // Prefer GetControlsByRules.
+ // GetControlsByRulesAndBenchmarks returns controls by a list of rule names group by control, standard and rule name.
  GetControlsByRulesAndBenchmarks(ctx context.Context, ruleNames []string, benchmarkNames []string) ([]*ControlResult, error)
As per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@central/complianceoperator/v2/rules/datastore/datastore.go` around lines 32 -
36, Mark the benchmark-filtered API GetControlsByRulesAndBenchmarks as
deprecated and steer usage to GetControlsByRules: add a clear deprecation
comment/docstring above the method declaration in the interface (mention it is
deprecated and to use GetControlsByRules instead), optionally add a //
Deprecated: ... line so static analyzers pick it up, and scan/update callers to
replace uses of GetControlsByRulesAndBenchmarks with GetControlsByRules (or
remove the method entirely once no callers remain); keep the function signature
intact for now to avoid breakage but add the deprecation note and an issue/todo
to remove it later.
central/complianceoperator/v2/report/manager/results/results_aggregator.go (1)

125-131: Replace the 4-value tuple from getProfileInfo with a typed return object.

Line 147 now returns (string, string, string, error), which is easy to mis-order at call sites (Line 125). A small struct improves safety and readability as this path evolves.

Refactor sketch
+type profileInfo struct {
+    displayName string
+    profileName string
+    profileType string
+}
-
-func (g *Aggregator) getProfileInfo(...) (string, string, string, error) {
+func (g *Aggregator) getProfileInfo(...) (*profileInfo, error) {
    ...
-   return profileInfo, profiles[0].GetName(), profileType, nil
+   return &profileInfo{
+       displayName: profileInfo,
+       profileName: profiles[0].GetName(),
+       profileType: profileType,
+   }, nil
 }
 
- profileInfo, profileName, profileType, err := g.getProfileInfo(...)
+ pInfo, err := g.getProfileInfo(...)
  if err != nil { ... }
- row.Profile = profileInfo
- row.ProfileType = profileType
+ row.Profile = pInfo.displayName
+ row.ProfileType = pInfo.profileType
- controlsInfo, err := g.getControlsInfo(ctx, checkResult, profileName)
+ controlsInfo, err := g.getControlsInfo(ctx, checkResult, pInfo.profileName)
As per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."

Also applies to: 147-161

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@central/complianceoperator/v2/report/manager/results/results_aggregator.go`
around lines 125 - 131, Change getProfileInfo to return a typed struct instead
of three separate strings and an error (e.g., type ProfileMeta struct { Name
string; Info string; Type string }), update its signature to (ProfileMeta,
error), and update all call sites including the usage in results_aggregator.go
where you currently do profileInfo, profileName, profileType, err :=
g.getProfileInfo(...) to instead receive a single value (meta, err :=
g.getProfileInfo(...)) and then set row.Profile = meta.Info and row.ProfileType
= meta.Type (and any use of profileName -> meta.Name); also update other callers
referenced around lines 147-161 to use the new struct fields. Ensure imports and
any tests are adjusted to the new type.
central/complianceoperator/v2/report/manager/results/results_aggregator_test.go (1)

356-357: Assert the rule-name slice passed into GetControlsByRules.

Using gomock.Any() here means this suite no longer checks that aggregation is actually scoping controls by the resolved rule names. That’s the regression boundary this PR is fixing, so the mock should verify the expected slice instead of only the call count.

Example tightening
 			if tcase.expectedControls != nil {
-				s.ruleDS.EXPECT().GetControlsByRules(gomock.Any(), gomock.Any()).Times(1).Return(tcase.expectedControls())
+				expRules, _ := tcase.expectedRules()
+				expRuleNames := []string{expRules[0].GetName()}
+				s.ruleDS.EXPECT().
+					GetControlsByRules(gomock.Any(), gomock.Eq(expRuleNames)).
+					Times(1).
+					Return(tcase.expectedControls())
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@central/complianceoperator/v2/report/manager/results/results_aggregator_test.go`
around lines 356 - 357, The test currently uses gomock.Any() for the rule-name
slice when setting the ruleDS.EXPECT().GetControlsByRules call, which lets the
test miss verifying that aggregation scopes by resolved rule names; change the
second gomock.Any() to assert the exact slice of rule names from the testcase
(e.g. gomock.Eq(tcase.resolvedRuleNames) or the field that holds the expected
rule-name slice on tcase) so ruleDS.EXPECT().GetControlsByRules(gomock.Any(),
gomock.Eq(tcase.resolvedRuleNames)).Times(1).Return(tcase.expectedControls())
and import/adjust any test fields as needed to provide that expected slice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@central/complianceoperator/v2/rules/datastore/datastore_impl.go`:
- Around line 97-123: GetControlsByRules currently builds and runs a query even
when the input ruleNames slice is empty; add an early return at the start of
datastoreImpl.GetControlsByRules that checks if len(ruleNames) == 0 and returns
an empty []*ControlResult and nil error to prevent unbounded or backend-specific
behavior and avoid hitting Postgres unnecessarily.

---

Nitpick comments:
In
`@central/complianceoperator/v2/report/manager/results/results_aggregator_test.go`:
- Around line 356-357: The test currently uses gomock.Any() for the rule-name
slice when setting the ruleDS.EXPECT().GetControlsByRules call, which lets the
test miss verifying that aggregation scopes by resolved rule names; change the
second gomock.Any() to assert the exact slice of rule names from the testcase
(e.g. gomock.Eq(tcase.resolvedRuleNames) or the field that holds the expected
rule-name slice on tcase) so ruleDS.EXPECT().GetControlsByRules(gomock.Any(),
gomock.Eq(tcase.resolvedRuleNames)).Times(1).Return(tcase.expectedControls())
and import/adjust any test fields as needed to provide that expected slice.

In `@central/complianceoperator/v2/report/manager/results/results_aggregator.go`:
- Around line 125-131: Change getProfileInfo to return a typed struct instead of
three separate strings and an error (e.g., type ProfileMeta struct { Name
string; Info string; Type string }), update its signature to (ProfileMeta,
error), and update all call sites including the usage in results_aggregator.go
where you currently do profileInfo, profileName, profileType, err :=
g.getProfileInfo(...) to instead receive a single value (meta, err :=
g.getProfileInfo(...)) and then set row.Profile = meta.Info and row.ProfileType
= meta.Type (and any use of profileName -> meta.Name); also update other callers
referenced around lines 147-161 to use the new struct fields. Ensure imports and
any tests are adjusted to the new type.

In `@central/complianceoperator/v2/rules/datastore/datastore.go`:
- Around line 32-36: Mark the benchmark-filtered API
GetControlsByRulesAndBenchmarks as deprecated and steer usage to
GetControlsByRules: add a clear deprecation comment/docstring above the method
declaration in the interface (mention it is deprecated and to use
GetControlsByRules instead), optionally add a // Deprecated: ... line so static
analyzers pick it up, and scan/update callers to replace uses of
GetControlsByRulesAndBenchmarks with GetControlsByRules (or remove the method
entirely once no callers remain); keep the function signature intact for now to
avoid breakage but add the deprecation note and an issue/todo to remove it
later.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 692ab720-85a6-4de7-bf45-d0fa0ca8af69

📥 Commits

Reviewing files that changed from the base of the PR and between 2948960 and 9cefe9a.

📒 Files selected for processing (11)
  • central/complianceoperator/v2/checkresults/service/service_impl_test.go
  • central/complianceoperator/v2/checkresults/stats/service/service_impl_test.go
  • central/complianceoperator/v2/checkresults/utils/utils.go
  • central/complianceoperator/v2/report/manager/format/formatter.go
  • central/complianceoperator/v2/report/manager/format/formatter_test.go
  • central/complianceoperator/v2/report/manager/results/results_aggregator.go
  • central/complianceoperator/v2/report/manager/results/results_aggregator_test.go
  • central/complianceoperator/v2/report/result.go
  • central/complianceoperator/v2/rules/datastore/datastore.go
  • central/complianceoperator/v2/rules/datastore/datastore_impl.go
  • central/complianceoperator/v2/rules/datastore/mocks/datastore.go

Comment on lines +97 to +123
func (d *datastoreImpl) GetControlsByRules(ctx context.Context, ruleNames []string) ([]*ControlResult, error) {
builder := search.NewQueryBuilder()
builder.AddSelectFields(
search.NewQuerySelect(search.ComplianceOperatorControl),
search.NewQuerySelect(search.ComplianceOperatorStandard),
search.NewQuerySelect(search.ComplianceOperatorRuleName),
)

builder.AddExactMatches(search.ComplianceOperatorRuleName, ruleNames...)

builder.AddGroupBy(
search.ComplianceOperatorRuleName,
search.ComplianceOperatorControl,
search.ComplianceOperatorStandard,
)

query := builder.ProtoQuery()
var results []*ControlResult
err := pgSearch.RunSelectRequestForSchemaFn[ControlResult](ctx, d.db, postgresSchema.ComplianceOperatorRuleV2Schema, query, func(r *ControlResult) error {
results = append(results, r)
return nil
})
if err != nil {
return nil, err
}

return results, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Short-circuit empty ruleNames before hitting Postgres.

This query now relies entirely on ComplianceOperatorRuleName for scoping. If ruleNames is empty, the method can fall through to an unbounded grouped select or backend-specific empty-match behavior. Returning early here makes the contract explicit and avoids an expensive wrong-result path.

Suggested fix
 func (d *datastoreImpl) GetControlsByRules(ctx context.Context, ruleNames []string) ([]*ControlResult, error) {
+	if len(ruleNames) == 0 {
+		return nil, nil
+	}
+
 	builder := search.NewQueryBuilder()
 	builder.AddSelectFields(
 		search.NewQuerySelect(search.ComplianceOperatorControl),
 		search.NewQuerySelect(search.ComplianceOperatorStandard),
 		search.NewQuerySelect(search.ComplianceOperatorRuleName),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (d *datastoreImpl) GetControlsByRules(ctx context.Context, ruleNames []string) ([]*ControlResult, error) {
builder := search.NewQueryBuilder()
builder.AddSelectFields(
search.NewQuerySelect(search.ComplianceOperatorControl),
search.NewQuerySelect(search.ComplianceOperatorStandard),
search.NewQuerySelect(search.ComplianceOperatorRuleName),
)
builder.AddExactMatches(search.ComplianceOperatorRuleName, ruleNames...)
builder.AddGroupBy(
search.ComplianceOperatorRuleName,
search.ComplianceOperatorControl,
search.ComplianceOperatorStandard,
)
query := builder.ProtoQuery()
var results []*ControlResult
err := pgSearch.RunSelectRequestForSchemaFn[ControlResult](ctx, d.db, postgresSchema.ComplianceOperatorRuleV2Schema, query, func(r *ControlResult) error {
results = append(results, r)
return nil
})
if err != nil {
return nil, err
}
return results, nil
func (d *datastoreImpl) GetControlsByRules(ctx context.Context, ruleNames []string) ([]*ControlResult, error) {
if len(ruleNames) == 0 {
return nil, nil
}
builder := search.NewQueryBuilder()
builder.AddSelectFields(
search.NewQuerySelect(search.ComplianceOperatorControl),
search.NewQuerySelect(search.ComplianceOperatorStandard),
search.NewQuerySelect(search.ComplianceOperatorRuleName),
)
builder.AddExactMatches(search.ComplianceOperatorRuleName, ruleNames...)
builder.AddGroupBy(
search.ComplianceOperatorRuleName,
search.ComplianceOperatorControl,
search.ComplianceOperatorStandard,
)
query := builder.ProtoQuery()
var results []*ControlResult
err := pgSearch.RunSelectRequestForSchemaFn[ControlResult](ctx, d.db, postgresSchema.ComplianceOperatorRuleV2Schema, query, func(r *ControlResult) error {
results = append(results, r)
return nil
})
if err != nil {
return nil, err
}
return results, nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@central/complianceoperator/v2/rules/datastore/datastore_impl.go` around lines
97 - 123, GetControlsByRules currently builds and runs a query even when the
input ruleNames slice is empty; add an early return at the start of
datastoreImpl.GetControlsByRules that checks if len(ruleNames) == 0 and returns
an empty []*ControlResult and nil error to prevent unbounded or backend-specific
behavior and avoid hitting Postgres unnecessarily.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant