Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ It is inspired by, contains code from and is designed to stay close to
* uses by default a [self-updating fork](https://github.com/yannh/kubernetes-json-schema) of the schemas registry maintained
by the kubernetes-json-schema project - which guarantees
up-to-date **schemas for all recent versions of Kubernetes**.

<details><summary><h4>Speed comparison with Kubeval</h4></summary><p>
Running on a pretty large kubeconfigs setup, on a laptop with 4 cores:

```bash
$ time kubeconform -ignore-missing-schemas -n 8 -summary preview staging production
Summary: 50714 resources found in 35139 files - Valid: 27334, Invalid: 0, Errors: 0 Skipped: 23380
Expand Down Expand Up @@ -122,7 +122,7 @@ Usage: kubeconform [OPTION]... [FILE OR FOLDER]...
-n int
number of goroutines to run concurrently (default 4)
-output string
output format - json, junit, pretty, tap, text (default "text")
output format - json, junit, pretty, tap, sarif, text (default "text")
-reject string
comma-separated list of kinds or GVKs to reject
-schema-location value
Expand Down Expand Up @@ -234,9 +234,9 @@ Here are the variables you can use in -schema-location:

### CustomResourceDefinition (CRD) Support

Because Custom Resources (CR) are not native Kubernetes objects, they are not included in the default schema.
Because Custom Resources (CR) are not native Kubernetes objects, they are not included in the default schema.
If your CRs are present in [Datree's CRDs-catalog](https://github.com/datreeio/CRDs-catalog), you can specify this project as an additional registry to lookup:

```bash
# Look in the CRDs-catalog for the desired schema/s
$ kubeconform -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' [MANIFEST]
Expand Down
14 changes: 13 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@ go 1.24

require (
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/owenrumney/go-sarif/v3 v3.2.3
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/stretchr/testify v1.11.0
golang.org/x/text v0.25.0
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/yaml v1.4.0
)

require github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
23 changes: 23 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
Expand All @@ -14,13 +19,31 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/owenrumney/go-sarif/v3 v3.2.3 h1:n6mdX5ugKwCrZInvBsf6WumXmpAe3mbmQXgkXlIq34U=
github.com/owenrumney/go-sarif/v3 v3.2.3/go.mod h1:1bV7t8SZg7pX41spaDkEUs8/yEjzk9JapztMoX1XNjg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
4 changes: 3 additions & 1 deletion pkg/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type Output interface {

func New(w io.Writer, outputFormat string, printSummary, isStdin, verbose bool) (Output, error) {
switch {
case outputFormat == "sarif":
return sarifOutput(w, printSummary, isStdin, verbose), nil
case outputFormat == "json":
return jsonOutput(w, printSummary, isStdin, verbose), nil
case outputFormat == "junit":
Expand All @@ -25,6 +27,6 @@ func New(w io.Writer, outputFormat string, printSummary, isStdin, verbose bool)
case outputFormat == "text":
return textOutput(w, printSummary, isStdin, verbose), nil
default:
return nil, fmt.Errorf("'outputFormat' must be 'json', 'junit', 'pretty', 'tap' or 'text'")
return nil, fmt.Errorf("'outputFormat' must be 'json', 'junit', 'pretty', 'tap', 'sarif' or 'text'")
}
}
205 changes: 205 additions & 0 deletions pkg/output/sarif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
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
}
Loading