Skip to content

CMP-3974: Propagate custom labels and annotations from Rules to ComplianceCheckResults#1091

Open
Vincent056 wants to merge 11 commits intoComplianceAsCode:masterfrom
Vincent056:custom-metadata-propagation
Open

CMP-3974: Propagate custom labels and annotations from Rules to ComplianceCheckResults#1091
Vincent056 wants to merge 11 commits intoComplianceAsCode:masterfrom
Vincent056:custom-metadata-propagation

Conversation

@Vincent056
Copy link

@Vincent056 Vincent056 commented Feb 24, 2026

Propagate user-defined labels and annotations from Rule/CustomRule objects to their resulting ComplianceCheckResults, covering both OpenSCAP and CEL scan paths. Operator-managed keys are never overridden, and user metadata survives content updates.

enhancement doc for design details.

@openshift-ci-robot
Copy link
Collaborator

@Vincent056: This pull request references CMP-3974 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the feature to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Propagate user-defined labels and annotations from Rule/CustomRule objects to their resulting ComplianceCheckResults, covering both OpenSCAP and CEL scan paths. Operator-managed keys are never overridden, and user metadata survives content updates.

See enhancement doc for design details.

Made with Cursor

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
Copy link

openshift-ci bot commented Feb 24, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: Vincent056

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@Vincent056 Vincent056 force-pushed the custom-metadata-propagation branch 2 times, most recently from ca3e70c to 5c24f2b Compare February 24, 2026 08:56
@github-actions
Copy link

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-03af3d7d4e8c97cfbfb3f66970c0a2bb99682900

@Vincent056 Vincent056 force-pushed the custom-metadata-propagation branch from 5c24f2b to 9d53f27 Compare February 24, 2026 08:58
@github-actions
Copy link

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-ca3e70cad0210b96dacd008162b27a456f7c2586

@github-actions
Copy link

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-5c24f2be95bdd098373cbec82260328bb68ac68e

@github-actions
Copy link

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-9d53f273a94d619941ec055b547cba409e32637f

@github-actions
Copy link

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-e27e8a5cd637fa2bc2e2b2c7b4fdab9e2affff67

@Vincent056 Vincent056 force-pushed the custom-metadata-propagation branch from e27e8a5 to f0893f6 Compare February 24, 2026 09:20
@github-actions
Copy link

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-f0893f68f812a6c6027323286e5218edf7d66597

@github-actions
Copy link

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-4f86f246a9490f72b17ae0dd78330aab81d00987

@taimurhafeez
Copy link
Collaborator

taimurhafeez commented Feb 24, 2026

tested on AWS OCP 4.21 and it works.

  • Custom labels and annotations on Rules and CustomRules propagate to ComplianceCheckResults
  • OpenSCAP aggregator and CEL scanner both propagate metadata correctly
  • Can filter ComplianceCheckResults by custom labels (oc get compliancecheckresults -l custom-label=value)
  • User annotations survive ProfileBundle content updates
  • Operator-managed metadata is not overridden by user values

@Vincent056
Copy link
Author

e2e failures seems unrelated, check if it will fix with #1093

Copy link
Collaborator

@rhmdnd rhmdnd left a comment

Choose a reason for hiding this comment

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

Only a handful of comments, but otherwise this is looking great.

Thanks for adding all the test coverage. It makes reviews the reviews a lot easier.


foundRule.Annotations = updatedRule.Annotations
// if the check type has changed, add an annotation to the rule
// to indicate that the rule needs to be checked in TailoredProfile validation
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment is still applicable, just further down on line 116, right?

Copy link
Author

Choose a reason for hiding this comment

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

correct, remove it from here to give more clarity

}
defer f.Client.Delete(context.TODO(), ssb)

err = f.WaitForSuiteScansStatus(testNamespace, ssbName, compv1alpha1.PhaseDone, compv1alpha1.ResultNotApplicable)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if we can update WaitForSuiteScansStatus to accept multiple result types so we can reduce the number of conditionals.

authors:
- Vincent056
reviewers:
- TBD
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can add me here.

reviewers:
- TBD
approvers:
- TBD
Copy link
Collaborator

Choose a reason for hiding this comment

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

And here.

var operatorManagedPrefixes = []string{
"compliance.openshift.io/",
"complianceoperator.openshift.io/",
"complianceascode.io/",
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about kubernetes-specific prefixes? Are those something we want to preemptively filter, too?

I'm thinking about things like kubernetes.io and k8s.io.

Copy link
Author

Choose a reason for hiding this comment

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

t.Fatalf("Failed to get ComplianceCheckResult: %v", err)
}

if checkResult.Labels["business-unit"] != "payments" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We'd then condense these into a loop over the customLabels and customAnnotations. If we need to add to or update the customLabels, we only need to do it in one place.

t.Errorf("operator-managed status label should not be overridden, got %q", checkResult.Labels[compv1alpha1.ComplianceCheckResultStatusLabel])
}

t.Log("Test completed successfully. Custom metadata propagation verified:")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder how much we want to log successful cases since Go kind of does this already with it's native test runner.

t.Errorf("operator-managed scan label should not be overridden, got %q", checkResult.Labels[compv1alpha1.ComplianceScanLabel])
}

t.Log("OpenSCAP metadata propagation verified:")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar comment here about logging successful test cases.

if rule.Labels == nil {
rule.Labels = make(map[string]string)
}
rule.Labels["e2e-business-unit"] = "security-ops"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar comment here as above about putting these in a variable.

if !exists {
cmdLog.Info("Creating object", "kind", kind, "name", name)
annotations = setTimestampAnnotations(owner, annotations)
if annotations != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this potentially overwrite the list of custom labels and annotations we curated above? This is coming for a CheckResult and not the Rule?

Copy link
Author

Choose a reason for hiding this comment

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

we add https://github.com/ComplianceAsCode/compliance-operator/pull/1091/changes#diff-2f2ea3af9b0b84d07bf17a91e568468b4dce17832cc0971d2523ba469613d36eR377 custom labels to compResult which is items in evalResultList. It should contain custom labels here

Copy link
Collaborator

Choose a reason for hiding this comment

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

What if we add an annotation that isn't prefixed? The user supplied one would overwrite it, wouldn't it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If I understand the SCAP result path, the aggregator will only add in the labels/annotations if they don't already exist. For the CEL path, it looks like it filters the operator annotations, but only based on the prefix, and doesn't use the same merge logic as the SCAP path? Is my understand correct there?

t.Logf(" - Validated that rule %s has FAIL status", customRuleName)
}

func TestCustomRuleMetadataPropagation(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens if a user

  1. annotates a rule
  2. runs a scan
  3. observes the result has the annotation
  4. removes the annotation from the rule
  5. rescan the cluster

Does the logic remove that annotation from the existing check result, or is it sticky from the last (stale) check result?

Copy link
Author

Choose a reason for hiding this comment

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

Custom metadata is not sticky. On rescan, results are created fresh from the current Rule metadata. If a user removes an annotation from the Rule, the next scan's results will not have it.

Introduce RuleMetadataCache and helper functions to extract, cache,
and merge user-defined labels and annotations from Rule objects while
preserving operator-managed keys.
Merge annotations instead of replacing them in the profileparser so
custom labels and annotations added by users survive ProfileBundle
content refreshes.
Build a RuleMetadataCache at scan result aggregation time and merge
custom labels/annotations from Rule objects into each
ComplianceCheckResult for OpenSCAP-based scans.
Extract custom labels/annotations from CustomRule objects and set them
on the resulting ComplianceCheckResult for CEL-based scans.
Verify that user-defined labels and annotations on a CustomRule are
propagated to the resulting ComplianceCheckResult after a CEL scan,
and that operator-managed keys are not overridden.
Document the design, motivation, and usage examples for propagating
user-defined labels and annotations from Rule/CustomRule objects to
ComplianceCheckResults.
The RuleMetadataCache needs to list Rules in the namespace to build
the custom metadata lookup table. Add the missing list verb for
rules to the remediation-aggregator Role and regenerate bundle.
Verify that custom labels and annotations on an existing Rule are
propagated to the ComplianceCheckResult through the aggregator path
after an OpenSCAP scan completes.
@Vincent056 Vincent056 force-pushed the custom-metadata-propagation branch from 4f86f24 to c163361 Compare March 3, 2026 14:38
@github-actions
Copy link

github-actions bot commented Mar 3, 2026

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-c163361404c1d331071d49563db292793cc3d2aa

Filter kubernetes.io/ and k8s.io/ reserved keys from custom metadata,
refactor e2e tests to use loops and named variables, add
WaitForSuiteScansStatusAnyResult helper, and update enhancement doc
reviewers.
@github-actions
Copy link

github-actions bot commented Mar 3, 2026

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-2e054b0a0fa4a12ea47a677bb490cba57edab171

@xiaojiey
Copy link
Collaborator

xiaojiey commented Mar 4, 2026

I can see both the label and annotations work for customrules. I will double check whether it works for rules.

$ oc get customrules
NAME                            STATUS   AGE
allowed-registries-configured   Ready    63s
cluster-admin-allow-list        Ready    3m6s
disallow-shadow-databases       Ready    54s
$   oc label customrule cluster-admin-allow-list  business-unit=payments risk-tier=critical
customrule.compliance.openshift.io/cluster-admin-allow-list labeled
$ oc apply -f ~/func/tp.yaml 
tailoredprofile.compliance.openshift.io/custom-security-checks created
$ oc-compliance bind -N -S default tailoredprofile/custom-security-checks
Error: Missing object type
$ oc-compliance bind -N test -S default tailoredprofile/custom-security-checks
Creating ScanSettingBinding test
$ oc get scan -w
NAME                     PHASE     RESULT
custom-security-checks   RUNNING   NOT-AVAILABLE
custom-security-checks   AGGREGATING   NOT-AVAILABLE
custom-security-checks   AGGREGATING   NOT-AVAILABLE
custom-security-checks   DONE          NON-COMPLIANT
^C$ oc get compliancecheckresults -l business-unit=payments
NAME                                              STATUS   SEVERITY
custom-security-checks-cluster-admin-allow-list   PASS     high
$ oc annotate customrule cluster-admin-allow-list     internal-id=RH-123  exception-ticket=JIRA-123
customrule.compliance.openshift.io/cluster-admin-allow-list annotated
$ oc-compliance rerun-now scansettingbinding  test
Rerunning scans from 'test': custom-security-checks
Re-running scan 'openshift-compliance/custom-security-checks'
$ oc get scan
NAME                     PHASE   RESULT
custom-security-checks   DONE    NON-COMPLIANT
$ oc get ccr custom-security-checks-cluster-admin-allow-list -ojsonpath='{.metadata.annotations}' | jq -r
{
  "compliance.openshift.io/last-scanned-timestamp": "2026-03-04T12:42:19Z",
  "compliance.openshift.io/rule": "cluster-admin-allow-list",
  "exception-ticket": "JIRA-123",
  "internal-id": "RH-123"
}

@xiaojiey
Copy link
Collaborator

xiaojiey commented Mar 5, 2026

I tested below scenarios and all scenarios wok as expected. One reminder is: Don't update the annotations/labels for compliance.openshift.io/rule, otherwise the propagate process will fail.

  1. Add annotations and labels for customrules without special prefix - PASS
  2. Add annotations and labels for regular rules without special prefix - PASS
  3. Add annotations/lables for customrules and regular rules without special prefix, check the ccr labels, ccr annotations; update the pb image, rerun the ssb, check the ccr labels, ccr annotations - PASS
  4. Add annotations/lables for customrules and regular rules with special prefix, check the ccr labels, ccr annotations; update the pb image, rerun the ssb, check the ccr labels, ccr annotations - PASS
  5. Add annotations/lables for customrules and regular rules with special prefix, check the ccr labels, ccr annotations; update the pb image, rerun the ssb, check the ccr labels, ccr annotations; add more annotations/lables for customrules and regular rules with special prefix - PASS
$ oc label rule ocp4-scc-limit-privilege-escalation  business-unit=cis-benchmark risk-tier=critical compliance.openshift.io/check-status=ERROR compliance.openshift.io/profile-bundle=ocp4 --overwrite=true
rule.compliance.openshift.io/ocp4-scc-limit-privilege-escalation labeled
$ oc annotate rule ocp4-scc-limit-privilege-escalation  business-unit=cis-benchmark risk-tier=critical compliance.openshift.io/check-status=ERROR compliance.openshift.io/profile-bundle=ocp4 --overwrite=true
rule.compliance.openshift.io/ocp4-scc-limit-privilege-escalation annotated
$ oc label customrule disallow-shadow-databases business-unit=payment-benchmark risk-tier=critical compliance.openshift.io/check-status=ERROR compliance.openshift.io/profile-bundle=ocp4 compliance.openshift.io/rule=ocp4-api-server-anonymous-auth --overwrite=true
customrule.compliance.openshift.io/disallow-shadow-databases labeled
$ oc annotate customrule disallow-shadow-databases   internal-id=RH-123  exception-ticket=JIRA-123 compliance.openshift.io/check-status=ERROR compliance.openshift.io/profile-bundle=rhcos4 compliance.openshift.io/rule=ocp4-api-server-admission-control-plugin-alwaysadmit --overwrite=true
customrule.compliance.openshift.io/disallow-shadow-databases annotated
$ oc get rule ocp4-scc-limit-privilege-escalation --show-labels 
NAME                                  AGE     LABELS
ocp4-scc-limit-privilege-escalation   5h25m   business-unit=cis-benchmark,compliance.openshift.io/check-status=ERROR,compliance.openshift.io/profile-bundle=ocp4,risk-tier=critical
$ oc get customrules.compliance.openshift.io disallow-shadow-databases  --show-labels 
NAME                        STATUS   AGE     LABELS
disallow-shadow-databases   Ready    4h21m   business-unit=payment-benchmark,compliance.openshift.io/check-status=ERROR,compliance.openshift.io/profile-bundle=ocp4,compliance.openshift.io/rule=ocp4-api-server-anonymous-auth,risk-tier=critical
$ oc get ccr ocp4-cis-tp-scc-limit-privilege-escalation --show-labels 
NAME                                         STATUS   SEVERITY   LABELS
ocp4-cis-tp-scc-limit-privilege-escalation   MANUAL   medium     business-unit=cis-benchmark,compliance.openshift.io/check-severity=medium,compliance.openshift.io/check-status=MANUAL,compliance.openshift.io/profile-guid=71f11afa-5066-5f21-9453-5de0c15a9106,compliance.openshift.io/scan-name=ocp4-cis-tp,compliance.openshift.io/suite=test-tp,risk-tier=critical
$ oc get ccr ocp4-cis-scc-limit-privilege-escalation --show-labels 
NAME                                      STATUS   SEVERITY   LABELS
ocp4-cis-scc-limit-privilege-escalation   MANUAL   medium     business-unit=cis-benchmark,compliance.openshift.io/check-severity=medium,compliance.openshift.io/check-status=MANUAL,compliance.openshift.io/profile-guid=a230315d-3e4a-5b58-b00f-f96f1553e036,compliance.openshift.io/scan-name=ocp4-cis,compliance.openshift.io/suite=test,risk-tier=critical
$ oc get ccr ocp4-cis-tp-scc-limit-privilege-escalation -o=jsonpath={.metadata.annotations.business-unit}
cis-benchmark$
$ oc get ccr ocp4-cis-tp-scc-limit-privilege-escalation -o=jsonpath={.metadata.annotations.risk-tier}
critical$
$ oc get ccr custom-security-checks-disallow-shadow-databases --show-labels 
NAME                                               STATUS   SEVERITY   LABELS
custom-security-checks-disallow-shadow-databases   PASS     high       business-unit=payment-benchmark,compliance.openshift.io/check-severity=high,compliance.openshift.io/check-status=PASS,compliance.openshift.io/profile-guid=74aa488b-02c1-570f-9ace-f7bb3c7d2e86,compliance.openshift.io/scan-name=custom-security-checks,compliance.openshift.io/suite=test-custom,risk-tier=critical
$ oc get ccr custom-security-checks-disallow-shadow-databases -o=jsonpath={.metadata.annotations} | jq -r
{
  "compliance.openshift.io/last-scanned-timestamp": "2026-03-05T06:39:11Z",
  "compliance.openshift.io/rule": "disallow-shadow-databases",
  "exception-ticket": "JIRA-123",
  "internal-id": "RH-123"
}
$ oc get rule ocp4-scc-limit-privilege-escalation -o=jsonpath={.metadata.annotations} | jq -r
{
  "business-unit": "cis-benchmark",
  "compliance.openshift.io/check-status": "ERROR",
  "compliance.openshift.io/image-digest": "pb-ocp4n89s8",
  "compliance.openshift.io/profile-bundle": "ocp4",
  "compliance.openshift.io/profiles": "ocp4-bsi-2022,ocp4-pci-dss-3-2,ocp4-moderate-rev-4,ocp4-high,ocp4-cis-1-7,ocp4-e8,ocp4-high-rev-4,ocp4-pci-dss-4-0,ocp4-pci-dss,ocp4-moderate,ocp4-cis,ocp4-nerc-cip,ocp4-bsi",
  "compliance.openshift.io/rule": "scc-limit-privilege-escalation",
  "control.compliance.openshift.io/BSI": "APP.4.4.A9;SYS.1.6.A16;SYS.1.6.A17",
  "control.compliance.openshift.io/CIS-OCP": "5.2.5",
  "control.compliance.openshift.io/NERC-CIP": "CIP-003-8 R6;CIP-004-6 R3;CIP-007-3 R6.1",
  "control.compliance.openshift.io/NIST-800-53": "CM-6;CM-6(1)",
  "control.compliance.openshift.io/PCI-DSS": "Req-2.2",
  "control.compliance.openshift.io/PCI-DSS-4-0": "2.2.1;2.2",
  "control.compliance.openshift.io/STIG": "SRG-APP-000516-CTR-001325",
  "policies.open-cluster-management.io/controls": "CIP-003-8 R6,CIP-004-6 R3,CIP-007-3 R6.1,CM-6,CM-6(1),Req-2.2,5.2.5,APP.4.4.A9,SYS.1.6.A16,SYS.1.6.A17,2.2.1,2.2",
  "policies.open-cluster-management.io/standards": "NERC-CIP,NIST-800-53,PCI-DSS,CIS-OCP,BSI,PCI-DSS-4-0",
  "risk-tier": "critical"
}

@xiaojiey
Copy link
Collaborator

xiaojiey commented Mar 5, 2026

/label qe-approved

return f.waitForSuiteScansStatusMulti(namespace, name, targetStatus, acceptableResults...)
}

func (f *Framework) waitForSuiteScansStatusMulti(namespace, name string, targetStatus compv1alpha1.ComplianceScanStatusPhase, acceptableResults ...compv1alpha1.ComplianceScanStatusResult) error {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@Anna-Koudelkova and @taimurhafeez are both adding variations of this in PRs for testing.

#1094 (comment)

Unless one of those PRs is ready to land soon, we could break it out and merge it so that all three of you could use it on a rebase.

if !exists {
cmdLog.Info("Creating object", "kind", kind, "name", name)
annotations = setTimestampAnnotations(owner, annotations)
if annotations != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if we add an annotation that isn't prefixed? The user supplied one would overwrite it, wouldn't it?

if !exists {
cmdLog.Info("Creating object", "kind", kind, "name", name)
annotations = setTimestampAnnotations(owner, annotations)
if annotations != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I understand the SCAP result path, the aggregator will only add in the labels/annotations if they don't already exist. For the CEL path, it looks like it filters the operator annotations, but only based on the prefix, and doesn't use the same merge logic as the SCAP path? Is my understand correct there?

Align the CEL scanner path with the OpenSCAP aggregator by deferring
custom metadata merge until after operator-managed labels/annotations
are set. This uses MergeCustomMetadata so operator keys always win,
preventing user-defined metadata from overwriting operator keys.
@github-actions
Copy link

github-actions bot commented Mar 6, 2026

🤖 To deploy this PR, run the following command:

make catalog-deploy CATALOG_IMG=ghcr.io/complianceascode/compliance-operator-catalog:1091-c75dbba0c724c3c4da4bf7001a2c22dfc34d58f1

@Vincent056
Copy link
Author

/retest

@openshift-ci
Copy link

openshift-ci bot commented Mar 6, 2026

@Vincent056: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-rosa c75dbba link true /test e2e-rosa

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

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.

5 participants