kubeconform/pkg/output/sarif.go
2025-11-04 16:24:45 +07:00

205 lines
5.8 KiB
Go

package output
import (
"fmt"
"io"
"sync"
"github.com/owenrumney/go-sarif/v3/pkg/report"
"github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif"
"github.com/yannh/kubeconform/pkg/resource"
"github.com/yannh/kubeconform/pkg/validator"
)
const (
toolName = "kubeconform"
toolInfoURI = "https://github.com/yannh/kubeconform"
)
const (
ruleIDValid = "KUBE-VALID"
ruleIDInvalid = "KUBE-INVALID"
ruleIDError = "KUBE-ERROR"
ruleIDSkipped = "KUBE-SKIPPED"
)
const (
levelNote = "note"
levelError = "error"
)
var sarifReportingDescriptors = []*sarif.ReportingDescriptor{
newSarifReportingDescriptor(ruleIDValid, "ValidResource", "Resource is valid.", levelNote),
newSarifReportingDescriptor(ruleIDInvalid, "InvalidResource", "Resource is invalid against schema.", levelError),
newSarifReportingDescriptor(ruleIDError, "ProcessingError", "Error processing resource.", levelError),
newSarifReportingDescriptor(ruleIDSkipped, "SkippedResource", "Resource validation was skipped.", levelNote),
}
// newSarifReportingDescriptor creates a new SARIF reporting descriptor.
func newSarifReportingDescriptor(id, name, shortDesc, level string) *sarif.ReportingDescriptor {
shortDescMsg := sarif.NewMultiformatMessageString().WithText(shortDesc)
return sarif.NewRule(id).
WithName(name).
WithShortDescription(shortDescMsg).
WithDefaultConfiguration(sarif.NewReportingConfiguration().WithLevel(level))
}
// sarifOutputter handles the generation of SARIF format output.
// It implements the Output interface and is concurrency-safe.
type sarifOutputter struct {
mu sync.Mutex
writer io.Writer
verbose bool
results []*sarif.Result
}
// sarifOutput creates a new Outputter that formats results as SARIF.
func sarifOutput(writer io.Writer, withSummary, isStdin, verbose bool) Output {
return &sarifOutputter{
writer: writer,
verbose: verbose,
results: make([]*sarif.Result, 0),
}
}
// newSarifRun creates and initializes a new SARIF report and run
// with the standard tool and rule information.
func newSarifRun() (*sarif.Report, *sarif.Run, error) {
rep := report.NewV210Report()
if rep == nil {
return nil, nil, fmt.Errorf("failed to initialize SARIF report")
}
run := sarif.NewRunWithInformationURI(toolName, toolInfoURI)
if run == nil {
return nil, nil, fmt.Errorf("failed to initialize SARIF run")
}
if run.Tool == nil || run.Tool.Driver == nil {
return nil, nil, fmt.Errorf("SARIF run is missing required tool driver information")
}
run.Tool.Driver.WithRules(sarifReportingDescriptors)
rep.AddRun(run)
return rep, run, nil
}
// Write processes a single validation result.
// It is concurrency-safe.
func (so *sarifOutputter) Write(validationResult validator.Result) error {
so.mu.Lock()
defer so.mu.Unlock()
if validationResult.Status == validator.Empty {
return nil
}
if validationResult.Status == validator.Valid && !so.verbose {
return nil
}
signature, _ := validationResult.Resource.Signature()
if validationResult.Status == validator.Invalid && len(validationResult.ValidationErrors) > 0 {
for _, valErr := range validationResult.ValidationErrors {
sarifResult := so.newSarifResult(validationResult, signature, &valErr)
so.results = append(so.results, sarifResult)
}
} else {
sarifResult := so.newSarifResult(validationResult, signature, nil)
so.results = append(so.results, sarifResult)
}
return nil
}
// newSarifResult creates a SARIF result from a validation result.
// If valErr is provided, it populates the result with specific validation
// failure details, including the logical path.
func (so *sarifOutputter) newSarifResult(res validator.Result, sig *resource.Signature, valErr *validator.ValidationError) *sarif.Result {
result := sarif.NewResult().
AddLocation(
sarif.NewLocationWithPhysicalLocation(
sarif.NewPhysicalLocation().
WithArtifactLocation(
sarif.NewSimpleArtifactLocation(res.Resource.Path),
),
),
)
if valErr != nil {
result.Locations[0].AddLogicalLocation(
sarif.NewLogicalLocation().WithName(valErr.Path),
)
result.
WithRuleID(ruleIDInvalid).
WithLevel(levelError)
var message string
if sig.Kind != "" && sig.Name != "" {
message = fmt.Sprintf("%s %s is invalid: %s: %s", sig.Kind, sig.Name, valErr.Path, valErr.Msg)
} else {
message = fmt.Sprintf("%s is invalid: %s: %s", res.Resource.Path, valErr.Path, valErr.Msg)
}
result.WithMessage(sarif.NewTextMessage(message))
} else {
switch res.Status {
case validator.Valid:
result.
WithRuleID(ruleIDValid).
WithLevel(levelNote).
WithMessage(sarif.NewTextMessage(
fmt.Sprintf("%s %s is valid", sig.Kind, sig.Name),
))
case validator.Error:
result.
WithRuleID(ruleIDError).
WithLevel(levelError)
var message string
if sig.Kind != "" && sig.Name != "" {
message = fmt.Sprintf("%s %s failed validation: %s", sig.Kind, sig.Name, res.Err.Error())
} else {
message = fmt.Sprintf("%s failed validation: %s", res.Resource.Path, res.Err.Error())
}
result.WithMessage(sarif.NewTextMessage(message))
case validator.Skipped:
result.
WithRuleID(ruleIDSkipped).
WithLevel(levelNote).
WithMessage(sarif.NewTextMessage(
fmt.Sprintf("%s %s skipped", sig.Kind, sig.Name),
))
default:
result.
WithRuleID(ruleIDError).
WithLevel(levelError).
WithMessage(sarif.NewTextMessage(
fmt.Sprintf("Unknown validation status for %s", res.Resource.Path),
))
}
}
return result
}
// Flush generates the complete SARIF report and writes it to the output writer.
// It is concurrency-safe.
func (so *sarifOutputter) Flush() error {
so.mu.Lock()
defer so.mu.Unlock()
rep, run, err := newSarifRun()
if err != nil {
return err
}
for _, result := range so.results {
run.AddResult(result)
}
if err := rep.PrettyWrite(so.writer); err != nil {
return fmt.Errorf("failed to write SARIF report: %w", err)
}
return nil
}