* Copyright (c) 2026 Huawei Technologies Co., Ltd.
* openFuyao is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package report
import (
"embed"
"fmt"
"io/fs"
"path"
"strings"
"gopkg.in/yaml.v3"
ctrl "sigs.k8s.io/controller-runtime"
"gitcode.com/openFuyao/compliance-operator/pkg/unified"
)
var cisLoaderLog = ctrl.Log.WithName("cis-loader")
var cisFS embed.FS
type yamlID string
func (id *yamlID) UnmarshalYAML(value *yaml.Node) error {
*id = yamlID(value.Value)
return nil
}
type cisControls struct {
Version string `yaml:"version"`
ID yamlID `yaml:"id"`
Text string `yaml:"text"`
Type string `yaml:"type"`
Groups []cisGroup `yaml:"groups"`
}
type cisGroup struct {
ID yamlID `yaml:"id"`
Text string `yaml:"text"`
Checks []cisCheck `yaml:"checks"`
}
type cisCheck struct {
ID yamlID `yaml:"id"`
Text string `yaml:"text"`
Type string `yaml:"type"`
Remediation string `yaml:"remediation"`
Scored bool `yaml:"scored"`
}
var cisCategoryMap = map[string]string{
"master": "Control Plane",
"node": "Worker Node",
"etcd": "etcd",
"controlplane": "Control Plane",
"policies": "Policies",
}
func loadCISRules() map[string]map[string]unified.RuleDefinition {
result := make(map[string]map[string]unified.RuleDefinition)
versions, err := fs.ReadDir(cisFS, "embed/cis")
if err != nil {
cisLoaderLog.Error(err, "failed to read CIS embed directory")
return result
}
totalRules := 0
for _, versionEntry := range versions {
if !versionEntry.IsDir() {
continue
}
version := versionEntry.Name()
rules := loadCISVersionRules(version)
if len(rules) > 0 {
result[version] = rules
totalRules += len(rules)
}
}
cisLoaderLog.Info("CIS knowledge base loaded", "versions", len(result), "totalRules", totalRules)
return result
}
func loadCISVersionRules(version string) map[string]unified.RuleDefinition {
rules := make(map[string]unified.RuleDefinition)
versionDir := path.Join("embed/cis", version)
files, err := fs.ReadDir(cisFS, versionDir)
if err != nil {
cisLoaderLog.Error(err, "failed to read CIS version directory", "version", version)
return rules
}
for _, fileEntry := range files {
if !isCISYAMLFile(fileEntry) {
continue
}
fileRules := loadCISFile(version, fileEntry.Name())
for id, rule := range fileRules {
rules[id] = rule
}
}
return rules
}
func isCISYAMLFile(entry fs.DirEntry) bool {
return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yaml")
}
func loadCISFile(version, filename string) map[string]unified.RuleDefinition {
filePath := path.Join("embed/cis", version, filename)
data, err := fs.ReadFile(cisFS, filePath)
if err != nil {
cisLoaderLog.Error(err, "failed to read CIS file", "file", filePath)
return nil
}
var controls cisControls
if err := yaml.Unmarshal(data, &controls); err != nil {
cisLoaderLog.Error(err, "failed to parse CIS YAML", "file", filePath)
return nil
}
benchmark := controls.Version
if benchmark == "" {
benchmark = fmt.Sprintf("cis-%s", version)
}
category := mapCISCategory(controls.Type)
return extractCISChecks(controls, benchmark, category)
}
func extractCISChecks(controls cisControls, benchmark, category string) map[string]unified.RuleDefinition {
rules := make(map[string]unified.RuleDefinition)
for _, group := range controls.Groups {
for _, check := range group.Checks {
ruleID := string(check.ID)
if ruleID == "" {
continue
}
severity := "info"
if check.Scored {
switch {
case strings.HasPrefix(ruleID, "1.") || strings.HasPrefix(ruleID, "2."):
severity = "high"
case strings.HasPrefix(ruleID, "3.") || strings.HasPrefix(ruleID, "4."):
severity = "medium"
default:
severity = "low"
}
}
rules[ruleID] = unified.RuleDefinition{
ID: ruleID,
Description: check.Text,
Remediation: strings.TrimSpace(check.Remediation),
Severity: severity,
Category: category,
Scored: check.Scored,
Benchmark: benchmark,
}
}
}
return rules
}
func mapCISCategory(controlType string) string {
if mapped, ok := cisCategoryMap[controlType]; ok {
return mapped
}
return "General"
}