diff --git a/pkg/apis/compliance/v1alpha1/profilebundle_types.go b/pkg/apis/compliance/v1alpha1/profilebundle_types.go index e16eb45b76..468212e8a1 100644 --- a/pkg/apis/compliance/v1alpha1/profilebundle_types.go +++ b/pkg/apis/compliance/v1alpha1/profilebundle_types.go @@ -19,6 +19,10 @@ const ProfileImageDigestAnnotation = "compliance.openshift.io/image-digest" // ProfileStatusAnnotation is the parsed out status from the data stream const ProfileStatusAnnotation = "compliance.openshift.io/profile-status" +// XCCDFGroupsAnnotation stores a comma-separated list of all XCCDF Group IDs +// found in the datastream. Used for re-enabling groups in TailoredProfiles. +const XCCDFGroupsAnnotation = "compliance.openshift.io/xccdf-groups" + // DataStreamStatusType is the type for the data stream status type DataStreamStatusType string diff --git a/pkg/profileparser/profileparser.go b/pkg/profileparser/profileparser.go index 6ca259f659..a944c038cc 100644 --- a/pkg/profileparser/profileparser.go +++ b/pkg/profileparser/profileparser.go @@ -55,6 +55,12 @@ func GetPrefixedName(pbName, objName string) string { } func ParseBundle(contentDom *xmlquery.Node, pb *cmpv1alpha1.ProfileBundle, pcfg *ParserConfig) error { + // Extract all XCCDF Group IDs from the datastream and store in ProfileBundle annotation + if err := extractAndStoreXCCDFGroups(contentDom, pb, pcfg); err != nil { + log.Error(err, "Failed to extract XCCDF groups") + // Don't fail the whole parse if group extraction fails + } + // One go routine per type errChan := make(chan error) done := make(chan string) @@ -950,3 +956,43 @@ func appendKeyWithSep(annotations map[string]string, key, item, sep string) { } annotations[key] = strings.Join(append(curList, item), sep) } + +// extractAndStoreXCCDFGroups extracts all XCCDF Group IDs from the datastream +// and stores them as a comma-separated list in the ProfileBundle annotation. +// This allows TailoredProfiles to re-enable all groups when extending a parent profile. +func extractAndStoreXCCDFGroups(contentDom *xmlquery.Node, pb *cmpv1alpha1.ProfileBundle, pcfg *ParserConfig) error { + // Find all Group elements in the datastream + groupNodes := xmlquery.Find(contentDom, "//xccdf-1.2:Group") + if len(groupNodes) == 0 { + log.Info("No XCCDF groups found in datastream") + return nil + } + + groupIDs := make([]string, 0, len(groupNodes)) + for _, groupNode := range groupNodes { + id := groupNode.SelectAttr("id") + if id != "" { + groupIDs = append(groupIDs, id) + } + } + + if len(groupIDs) == 0 { + return nil + } + + // Store as comma-separated list in ProfileBundle annotation + annotations := pb.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[cmpv1alpha1.XCCDFGroupsAnnotation] = strings.Join(groupIDs, ",") + pb.SetAnnotations(annotations) + + // Update the ProfileBundle with the new annotation + if err := pcfg.Client.Update(context.TODO(), pb); err != nil { + return fmt.Errorf("failed to update ProfileBundle with XCCDF groups: %w", err) + } + + log.Info("Extracted XCCDF groups", "count", len(groupIDs), "profileBundle", pb.Name) + return nil +} diff --git a/pkg/xccdf/tailoring.go b/pkg/xccdf/tailoring.go index 7c9cf85528..1f9263cfa9 100644 --- a/pkg/xccdf/tailoring.go +++ b/pkg/xccdf/tailoring.go @@ -146,8 +146,19 @@ func getSelectElementFromCRRule(rule *cmpv1alpha1.Rule, enable bool) SelectEleme } } -func getSelections(tp *cmpv1alpha1.TailoredProfile, rules map[string]*cmpv1alpha1.Rule) []SelectElement { +func getSelections(tp *cmpv1alpha1.TailoredProfile, rules map[string]*cmpv1alpha1.Rule, groupIDs []string) []SelectElement { selections := []SelectElement{} + + // When extending a profile, enable all XCCDF groups first + // This allows individual rules within deselected groups to be enabled + // Groups are enabled before rules so OpenSCAP processes them in the correct order + for _, groupID := range groupIDs { + selections = append(selections, SelectElement{ + IDRef: groupID, + Selected: true, + }) + } + for _, selection := range tp.Spec.EnableRules { rule := rules[selection.Name] selections = append(selections, getSelectElementFromCRRule(rule, true)) @@ -200,6 +211,16 @@ func getValuesFromVariables(variables []*cmpv1alpha1.Variable) []SetValueElement // TailoredProfileToXML gets an XML string from a TailoredProfile and the corresponding Profile func TailoredProfileToXML(tp *cmpv1alpha1.TailoredProfile, p *cmpv1alpha1.Profile, pb *cmpv1alpha1.ProfileBundle, rules map[string]*cmpv1alpha1.Rule, variables []*cmpv1alpha1.Variable) (string, error) { + // Extract group IDs from ProfileBundle annotation if this TP extends a profile + var groupIDs []string + if p != nil { + if pb.Annotations != nil { + if groupsStr, ok := pb.Annotations[cmpv1alpha1.XCCDFGroupsAnnotation]; ok && groupsStr != "" { + groupIDs = strings.Split(groupsStr, ",") + } + } + } + tailoring := TailoringElement{ XMLNamespaceURI: XCCDFURI, ID: getTailoringID(tp), @@ -215,7 +236,7 @@ func TailoredProfileToXML(tp *cmpv1alpha1.TailoredProfile, p *cmpv1alpha1.Profil }, Profile: ProfileElement{ ID: GetXCCDFProfileID(tp), - Selections: getSelections(tp, rules), + Selections: getSelections(tp, rules, groupIDs), Values: getValuesFromVariables(variables), }, }