Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
e2etests/test_helper
dist/
coverage.out
.gopath-loader/
9 changes: 9 additions & 0 deletions docs/generated/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,15 @@ minReplicas: 3
**Remediation**: Set Job.spec.ttlSecondsAfterFinished. Unset CronJob.Spec.JobTemplate.Spec.ttlSecondsAfterFinished.
**Template**: [job-ttl-seconds-after-finished](templates.md#ttlsecondsafterfinished-impact-for-standalone-and-managed-job-objects)
## kube-chainsaw-rbac
**Enabled by default**: No
**Description**: Detects RBAC privilege escalation paths, dangerous verbs, sensitive resource access, and scope confusion using graph analysis powered by kube-chainsaw
**Remediation**: Review the RBAC configuration and apply least-privilege principles. See https://ugiordan.github.io/kube-chainsaw/ for detailed remediation guidance per rule.
**Template**: [kube-chainsaw](templates.md#rbac-privilege-chain-analysis)
## latest-tag
**Enabled by default**: Yes
Expand Down
40 changes: 40 additions & 0 deletions docs/generated/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,46 @@ KubeLinter supports the following templates:
**Supported Objects**: JobLike


## RBAC Privilege Chain Analysis

**Key**: `kube-chainsaw`

**Description**: Detects RBAC privilege escalation paths, dangerous verbs, sensitive resource access, and scope confusion using graph analysis

**Supported Objects**: Any


**Parameters**:

```yaml
- arrayElemType: string
description: ""
name: rules
negationAllowed: true
regexAllowed: true
required: false
type: array
- arrayElemType: string
description: ""
name: excludeRules
negationAllowed: true
regexAllowed: true
required: false
type: array
- description: ""
name: minSeverity
negationAllowed: true
regexAllowed: true
required: false
type: string
- description: ""
name: suppressionsFile
negationAllowed: true
regexAllowed: true
required: false
type: string
```

## kubeconform

**Key**: `kubeconform`
Expand Down
15 changes: 15 additions & 0 deletions e2etests/bats-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,21 @@ get_value_from() {
[[ "${count}" == "2" ]]
}

@test "kube-chainsaw-rbac" {
tmp="tests/checks/kube-chainsaw/cluster-admin-pod.yaml"
cmd="${KUBE_LINTER_BIN} lint --include kube-chainsaw-rbac --do-not-auto-add-defaults --format json ${tmp}"
run ${cmd}

print_info "${status}" "${output}" "${cmd}" "${tmp}"
[ "$status" -eq 1 ]

message1=$(get_value_from "${lines[0]}" '.Reports[0].Diagnostic.Message')
count=$(get_value_from "${lines[0]}" '.Reports | length')

[[ "${message1}" == *"cluster-admin"* ]]
[[ "${count}" -ge "1" ]]
}

@test "latest-tag" {
tmp="tests/checks/latest-tag.yml"
cmd="${KUBE_LINTER_BIN} lint --include latest-tag --do-not-auto-add-defaults --format json ${tmp}"
Expand Down
61 changes: 61 additions & 0 deletions e2etests/kube_chainsaw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build e2e

package e2etests

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestKubeChainsawIntegration verifies that the kube-chainsaw template
// integrates correctly with kube-linter and detects RBAC issues.
func TestKubeChainsawIntegration(t *testing.T) {
kubeLinterBin := os.Getenv(kubeLinterBinEnv)
require.NotEmpty(t, kubeLinterBin, "Please set %s", kubeLinterBinEnv)

_, err := os.Stat(kubeLinterBin)
require.NoError(t, err)

// Test fixture with ServiceAccount, ClusterRoleBinding to cluster-admin, and Pod
fixtureDir := filepath.Join("..", "tests", "checks", "kube-chainsaw")

// Run kube-linter with kube-chainsaw-rbac check enabled
kubeLinterOut, err := exec.Command(
kubeLinterBin,
"lint",
fixtureDir,
"--include", "kube-chainsaw-rbac",
).CombinedOutput()

// kube-linter should exit with error code 1 when findings are detected
require.Error(t, err)
exitErr, ok := err.(*exec.ExitError)
require.True(t, ok)
outAsStr := string(kubeLinterOut)

assert.Equal(t, 1, exitErr.ExitCode(),
"unexpected exit code: %d; output from kube-linter: %v",
exitErr.ExitCode(), outAsStr)

// Verify that cluster-admin finding is detected
assert.Contains(t, outAsStr, "cluster-admin privileges",
"expected cluster-admin privileges finding in output: %s", outAsStr)

// Verify severity is critical
assert.True(t, strings.Contains(outAsStr, "CRITICAL") || strings.Contains(outAsStr, "critical"),
"expected 'critical' severity in output: %s", outAsStr)

// Verify the finding references the dangerous pod
assert.Contains(t, outAsStr, "privileged-pod",
"expected finding to reference 'privileged-pod': %s", outAsStr)

// Verify the kube-chainsaw-rbac check ran
assert.Contains(t, outAsStr, "kube-chainsaw-rbac",
"expected kube-chainsaw-rbac check in output: %s", outAsStr)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/ugiordan/kube-chainsaw v0.0.0-20260630160347-0619dd44eccb
github.com/yannh/kubeconform v0.8.0
helm.sh/helm/v3 v3.21.1
k8s.io/api v0.35.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/ugiordan/kube-chainsaw v0.0.0-20260630160347-0619dd44eccb h1:6iPNSZbGn4nIHudaGB4wp0mN0yESeTk9fcF5cCb43Mg=
github.com/ugiordan/kube-chainsaw v0.0.0-20260630160347-0619dd44eccb/go.mod h1:/24yJCO1sPpQyfzN4RCMTTH9q2xT1WJGd414OmBhOG4=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand Down
8 changes: 8 additions & 0 deletions pkg/builtinchecks/yamls/kube-chainsaw-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: "kube-chainsaw-rbac"
description: "Detects RBAC privilege escalation paths, dangerous verbs, sensitive resource access, and scope confusion using graph analysis powered by kube-chainsaw"
remediation: "Review the RBAC configuration and apply least-privilege principles. See https://ugiordan.github.io/kube-chainsaw/ for detailed remediation guidance per rule."
scope:
objectKinds:
- Any
template: "kube-chainsaw"
params: {}
16 changes: 14 additions & 2 deletions pkg/command/lint/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"strings"
"text/template"

"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand All @@ -25,15 +26,26 @@ const (
plainTemplateStr = `KubeLinter {{.Summary.KubeLinterVersion}}

{{range .Reports}}
{{- .Object.Metadata.FilePath | bold}}: (object: {{.Object.GetK8sObjectName | bold}}) {{.Diagnostic.Message | red}} (check: {{.Check | yellow}}, remediation: {{.Remediation | yellow}})
{{- .Object.Metadata.FilePath | bold}}: (object: {{.Object.GetK8sObjectName | bold}}) {{if .Diagnostic.Severity}}[{{.Diagnostic.Severity | toUpper}}] {{end}}{{.Diagnostic.Message | red}} (check: {{.Check | yellow}}, remediation: {{effectiveRemediation . | yellow}})

{{else}}No lint errors found!
{{end -}}
`
)

var (
plainTemplate = common.MustInstantiatePlainTemplate(plainTemplateStr, nil)
plainTemplate = common.MustInstantiatePlainTemplate(plainTemplateStr, template.FuncMap{
"toUpper": strings.ToUpper,
"effectiveRemediation": func(report diagnostic.WithContext) string {
// If Diagnostic.Metadata contains a remediation, use it; otherwise use the check's remediation
if report.Diagnostic.Metadata != nil {
if customRem, ok := report.Diagnostic.Metadata[diagnostic.MetaKeyRemediation]; ok && customRem != "" {
return customRem
}
}
return report.Remediation
},
})

formatters = common.Formatters{
Formatters: map[common.FormatType]common.FormatFunc{
Expand Down
162 changes: 162 additions & 0 deletions pkg/command/lint/plain_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package lint

import (
"bytes"
"strings"
"testing"
"time"

"golang.stackrox.io/kube-linter/pkg/diagnostic"
"golang.stackrox.io/kube-linter/pkg/lintcontext"
"golang.stackrox.io/kube-linter/pkg/run"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// TestPlainFormat_SeverityPrefix verifies that Diagnostic.Severity appears
// as a prefix in plain output when present.
func TestPlainFormat_SeverityPrefix(t *testing.T) {
tests := []struct {
name string
severity string
expectedPrefix string
}{
{"Critical", "critical", "[CRITICAL]"},
{"High", "high", "[HIGH]"},
{"Warning", "warning", "[WARNING]"},
{"Info", "info", "[INFO]"},
{"Empty", "", ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := run.Result{
Summary: run.Summary{
KubeLinterVersion: "test-version",
ChecksStatus: run.ChecksPassed,
CheckEndTime: time.Now().UTC(),
},
Reports: []diagnostic.WithContext{
{
Diagnostic: diagnostic.Diagnostic{
Message: "test message",
Severity: tt.severity,
},
Check: "test-check",
Remediation: "test remediation",
Object: lintcontext.Object{
Metadata: lintcontext.ObjectMetadata{
FilePath: "test.yaml",
},
K8sObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
"namespace": "default",
},
},
},
},
},
},
}

var buf bytes.Buffer
err := plainTemplate.Execute(&buf, result)
if err != nil {
t.Fatalf("plainTemplate.Execute failed: %v", err)
}

output := buf.String()
if tt.expectedPrefix != "" {
if !strings.Contains(output, tt.expectedPrefix) {
t.Errorf("expected output to contain %q, got:\n%s", tt.expectedPrefix, output)
}
}

// Always verify message is present
if !strings.Contains(output, "test message") {
t.Errorf("expected output to contain message, got:\n%s", output)
}
})
}
}

// TestPlainFormat_EffectiveRemediation verifies that when Diagnostic.Metadata
// contains a remediation, it overrides the check's default remediation.
func TestPlainFormat_EffectiveRemediation(t *testing.T) {
tests := []struct {
name string
checkRemediation string
metadataRemediation string
expectedInOutput string
}{
{
name: "UseCheckRemediation",
checkRemediation: "default remediation",
metadataRemediation: "",
expectedInOutput: "default remediation",
},
{
name: "UseMetadataRemediation",
checkRemediation: "default remediation",
metadataRemediation: "custom remediation",
expectedInOutput: "custom remediation",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diag := diagnostic.Diagnostic{
Message: "test message",
}
if tt.metadataRemediation != "" {
diag.Metadata = map[string]string{
diagnostic.MetaKeyRemediation: tt.metadataRemediation,
}
}

result := run.Result{
Summary: run.Summary{
KubeLinterVersion: "test-version",
ChecksStatus: run.ChecksPassed,
CheckEndTime: time.Now().UTC(),
},
Reports: []diagnostic.WithContext{
{
Diagnostic: diag,
Check: "test-check",
Remediation: tt.checkRemediation,
Object: lintcontext.Object{
Metadata: lintcontext.ObjectMetadata{
FilePath: "test.yaml",
},
K8sObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
"namespace": "default",
},
},
},
},
},
},
}

var buf bytes.Buffer
err := plainTemplate.Execute(&buf, result)
if err != nil {
t.Fatalf("plainTemplate.Execute failed: %v", err)
}

output := buf.String()
if !strings.Contains(output, tt.expectedInOutput) {
t.Errorf("expected output to contain %q, got:\n%s", tt.expectedInOutput, output)
}
})
}
}
Loading
Loading