mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-13 15:09:22 +00:00
205 lines
5.8 KiB
Go
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
|
|
}
|