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
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 v1.0.0
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 v1.0.0 h1:m6m11HZfaMq2+liCQa9HEia1hCS93nj7zaH39fFfYas=
github.com/ugiordan/kube-chainsaw v1.0.0/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