* 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 openscap
import (
"encoding/xml"
"fmt"
"regexp"
"strings"
ctrl "sigs.k8s.io/controller-runtime"
compliancev1alpha1 "gitcode.com/openFuyao/compliance-operator/api/v1alpha1"
"gitcode.com/openFuyao/compliance-operator/pkg/scanner"
"gitcode.com/openFuyao/compliance-operator/pkg/unified"
)
var parserLog = ctrl.Log.WithName("openscap-parser")
type Parser struct{}
var vulnIDRegex = regexp.MustCompile(`SV-(\d+)`)
func NewParser() *Parser {
return &Parser{}
}
type xccdfBenchmark struct {
XMLName xml.Name `xml:"Benchmark"`
TestResult xccdfTestResult `xml:"TestResult"`
}
type xccdfTestResult struct {
RuleResults []xccdfRuleResult `xml:"rule-result"`
}
type xccdfRuleResult struct {
IDRef string `xml:"idref,attr"`
Time string `xml:"time,attr"`
Severity string `xml:"severity,attr"`
Version string `xml:"version,attr"`
Result string `xml:"result"`
}
func (p *Parser) Parse(rawOutput string) ([]compliancev1alpha1.UnifiedCheckResult, error) {
parserLog.V(1).Info("parsing OpenSCAP output", "inputSize", len(rawOutput))
extracted, err := extractOpenSCAPContent(rawOutput)
if err != nil {
return nil, err
}
var testResult xccdfTestResult
var benchmark xccdfBenchmark
if err := xml.Unmarshal([]byte(extracted), &benchmark); err == nil && len(benchmark.TestResult.RuleResults) > 0 {
testResult = benchmark.TestResult
} else {
if err := xml.Unmarshal([]byte(extracted), &testResult); err != nil {
parserLog.Error(err, "failed to parse XCCDF XML")
return nil, fmt.Errorf("failed to parse XCCDF XML: %w", err)
}
}
results := make([]compliancev1alpha1.UnifiedCheckResult, 0, len(testResult.RuleResults))
for _, ruleResult := range testResult.RuleResults {
unifiedResult := convertOpenSCAPResult(ruleResult)
if unifiedResult.Status != "" {
results = append(results, unifiedResult)
}
}
parserLog.V(1).Info("OpenSCAP parse complete", "resultCount", len(results))
return results, nil
}
func extractOpenSCAPContent(rawOutput string) (string, error) {
extracted, found := scanner.ExtractBetweenMarkers(rawOutput, scanner.ResultStartMarker, scanner.ResultEndMarker)
if !found {
parserLog.V(1).Info("markers not found, attempting direct XML extraction")
extracted = extractXML(rawOutput)
}
if extracted == "" {
return "", fmt.Errorf("no valid OpenSCAP output found between markers")
}
return extracted, nil
}
func convertOpenSCAPResult(rule xccdfRuleResult) compliancev1alpha1.UnifiedCheckResult {
vulnID := extractVulnID(rule.IDRef)
return compliancev1alpha1.UnifiedCheckResult{
ID: vulnID,
Status: mapOpenSCAPStatus(rule.Result),
ActualValue: rule.Result,
ExpectedValue: "pass",
}
}
func extractVulnID(idref string) string {
matches := vulnIDRegex.FindStringSubmatch(idref)
if len(matches) < 2 {
parserLog.V(1).Info("unable to extract Vuln ID from idref", "idref", idref)
return ""
}
return "V-" + matches[1]
}
var openscapStatusMap = map[string]string{
"pass": unified.StatusPass,
"fail": unified.StatusFail,
"error": unified.StatusError,
"unknown": unified.StatusWarn,
"notapplicable": unified.StatusNotApplicable,
"notchecked": unified.StatusNotChecked,
"informational": unified.StatusInfo,
"notselected": "",
}
func mapOpenSCAPStatus(result string) string {
if s, ok := openscapStatusMap[strings.ToLower(result)]; ok {
return s
}
return unified.StatusWarn
}
func extractXML(content string) string {
startIdx := strings.Index(content, "<?xml")
if startIdx == -1 {
startIdx = strings.Index(content, "<Benchmark")
}
if startIdx == -1 {
startIdx = strings.Index(content, "<TestResult")
}
if startIdx == -1 {
parserLog.V(1).Info("XML start marker not found in output")
return ""
}
endIdx := strings.LastIndex(content, "</Benchmark>")
if endIdx == -1 || endIdx <= startIdx {
endIdx = strings.LastIndex(content, "</TestResult>")
if endIdx == -1 || endIdx <= startIdx {
parserLog.V(1).Info("XML end marker not found in output")
return ""
}
return content[startIdx : endIdx+len("</TestResult>")]
}
return content[startIdx : endIdx+len("</Benchmark>")]
}