Skip to content

Commit

Permalink
Improving rule r0006
Browse files Browse the repository at this point in the history
Signed-off-by: Amit Schendel <[email protected]>
  • Loading branch information
amitschendel committed Nov 21, 2024
1 parent 4fb52bf commit 82c1608
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 78 deletions.
143 changes: 106 additions & 37 deletions pkg/ruleengine/v1/r0006_unexpected_service_account_token_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ruleengine

import (
"fmt"
"path/filepath"
"strings"

"github.com/kubescape/node-agent/pkg/objectcache"
Expand All @@ -10,7 +11,6 @@ import (

apitypes "github.com/armosec/armoapi-go/armotypes"
traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types"

"github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
)

Expand All @@ -19,8 +19,7 @@ const (
R0006Name = "Unexpected Service Account Token Access"
)

// ServiceAccountTokenPathsPrefixs is a list because of symlinks.
var serviceAccountTokenPathsPrefix = []string{
var serviceAccountTokenPathsPrefixes = []string{
"/run/secrets/kubernetes.io/serviceaccount",
"/var/run/secrets/kubernetes.io/serviceaccount",
"/run/secrets/eks.amazonaws.com/serviceaccount",
Expand All @@ -31,7 +30,7 @@ var R0006UnexpectedServiceAccountTokenAccessRuleDescriptor = ruleengine.RuleDesc
ID: R0006ID,
Name: R0006Name,
Description: "Detecting unexpected access to service account token.",
Tags: []string{"token", "malicious", "whitelisted"},
Tags: []string{"token", "malicious", "security", "kubernetes"},
Priority: RulePriorityHigh,
Requirements: &RuleRequirements{
EventTypes: []utils.EventType{
Expand All @@ -42,15 +41,68 @@ var R0006UnexpectedServiceAccountTokenAccessRuleDescriptor = ruleengine.RuleDesc
return CreateRuleR0006UnexpectedServiceAccountTokenAccess()
},
}
var _ ruleengine.RuleEvaluator = (*R0006UnexpectedServiceAccountTokenAccess)(nil)

type R0006UnexpectedServiceAccountTokenAccess struct {
BaseRule
}

// getTokenBasePath returns the base service account token path if the path is a token path,
// otherwise returns an empty string. Using a single iteration through prefixes.
func getTokenBasePath(path string) string {
for _, prefix := range serviceAccountTokenPathsPrefixes {
if strings.HasPrefix(path, prefix) {
return prefix
}
}
return ""
}

// normalizeTokenPath removes timestamp directories from the path while maintaining
// the essential structure. Optimized for minimal allocations.
func normalizeTokenPath(path string) string {
// Get the base path - if not a token path, return original
basePath := getTokenBasePath(path)
if basePath == "" {
return path
}

// Get the final component (usually "token", "ca.crt", etc.)
finalComponent := filepath.Base(path)

// Split the middle part (between base path and final component)
middle := strings.TrimPrefix(filepath.Dir(path), basePath)
if middle == "" {
return filepath.Join(basePath, finalComponent)
}

// Process middle parts
var normalizedMiddle strings.Builder
parts := strings.Split(middle, "/")
for _, part := range parts {
if part == "" {
continue
}
// Skip timestamp directories (starting with ".." and containing "_")
if strings.HasPrefix(part, "..") && strings.Contains(part, "_") {
continue
}
normalizedMiddle.WriteString("/")
normalizedMiddle.WriteString(part)
}

// If no middle parts remain, join base and final
if normalizedMiddle.Len() == 0 {
return filepath.Join(basePath, finalComponent)
}

// Join all parts
return basePath + normalizedMiddle.String() + "/" + finalComponent
}

func CreateRuleR0006UnexpectedServiceAccountTokenAccess() *R0006UnexpectedServiceAccountTokenAccess {
return &R0006UnexpectedServiceAccountTokenAccess{}
}

func (rule *R0006UnexpectedServiceAccountTokenAccess) Name() string {
return R0006Name
}
Expand All @@ -59,24 +111,36 @@ func (rule *R0006UnexpectedServiceAccountTokenAccess) ID() string {
return R0006ID
}

func (rule *R0006UnexpectedServiceAccountTokenAccess) DeleteRule() {
}
func (rule *R0006UnexpectedServiceAccountTokenAccess) DeleteRule() {}

func (rule *R0006UnexpectedServiceAccountTokenAccess) generatePatchCommand(event *traceropentype.Event, ap *v1beta1.ApplicationProfile) string {
flagList := "["
for _, arg := range event.Flags {
flagList += "\"" + arg + "\","
if len(event.Flags) == 0 {
return fmt.Sprintf(
"kubectl patch applicationprofile %s --namespace %s --type merge -p '{\"spec\": {\"containers\": [{\"name\": \"%s\", \"opens\": [{\"path\": \"%s\"}]}]}}'",
ap.GetName(),
ap.GetNamespace(),
event.GetContainer(),
event.FullPath,
)
}
// remove the last comma
if len(flagList) > 1 {
flagList = flagList[:len(flagList)-1]

flagList := make([]string, len(event.Flags))
for i, flag := range event.Flags {
flagList[i] = fmt.Sprintf("%q", flag)
}
baseTemplate := "kubectl patch applicationprofile %s --namespace %s --type merge -p '{\"spec\": {\"containers\": [{\"name\": \"%s\", \"opens\": [{\"path\": \"%s\", \"flags\": %s}]}]}}'"
return fmt.Sprintf(baseTemplate, ap.GetName(), ap.GetNamespace(),
event.GetContainer(), event.FullPath, flagList)

return fmt.Sprintf(
"kubectl patch applicationprofile %s --namespace %s --type merge -p '{\"spec\": {\"containers\": [{\"name\": \"%s\", \"opens\": [{\"path\": \"%s\", \"flags\": [%s]}]}]}}'",
ap.GetName(),
ap.GetNamespace(),
event.GetContainer(),
event.FullPath,
strings.Join(flagList, ","),
)
}

func (rule *R0006UnexpectedServiceAccountTokenAccess) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objCache objectcache.ObjectCache) ruleengine.RuleFailure {
// Quick type checks first
if eventType != utils.OpenEventType {
return nil
}
Expand All @@ -86,19 +150,12 @@ func (rule *R0006UnexpectedServiceAccountTokenAccess) ProcessEvent(eventType uti
return nil
}

shouldCheckEvent := false

for _, prefix := range serviceAccountTokenPathsPrefix {
if strings.HasPrefix(openEvent.FullPath, prefix) {
shouldCheckEvent = true
break
}
}

if !shouldCheckEvent {
// Check if this is a token path - using optimized check
if getTokenBasePath(openEvent.FullPath) == "" {
return nil
}

// Get the application profile
ap := objCache.ApplicationProfileCache().GetApplicationProfile(openEvent.Runtime.ContainerID)
if ap == nil {
return nil
Expand All @@ -109,24 +166,33 @@ func (rule *R0006UnexpectedServiceAccountTokenAccess) ProcessEvent(eventType uti
return nil
}

// Normalize the accessed path once
normalizedAccessedPath := normalizeTokenPath(openEvent.FullPath)

// Check against whitelisted paths
for _, open := range appProfileOpenList.Opens {
for _, prefix := range serviceAccountTokenPathsPrefix {
if strings.HasPrefix(open.Path, prefix) {
return nil
}
if normalizedAccessedPath == normalizeTokenPath(open.Path) {
return nil
}
}

ruleFailure := GenericRuleFailure{
// If we get here, the access was not whitelisted - create an alert
return &GenericRuleFailure{
BaseRuntimeAlert: apitypes.BaseRuntimeAlert{
AlertName: rule.Name(),
Arguments: map[string]interface{}{
"path": openEvent.FullPath,
"flags": openEvent.Flags,
},
InfectedPID: openEvent.Pid,
FixSuggestions: fmt.Sprintf("If this is a valid behavior, please add the open call \"%s\" to the whitelist in the application profile for the Pod \"%s\". You can use the following command: %s", openEvent.FullPath, openEvent.GetPod(), rule.generatePatchCommand(openEvent, ap)),
Severity: R0006UnexpectedServiceAccountTokenAccessRuleDescriptor.Priority,
InfectedPID: openEvent.Pid,
FixSuggestions: fmt.Sprintf(
"If this is a valid behavior, please add the open call \"%s\" to the whitelist in the application profile for the Pod \"%s\". "+
"You can use the following command:\n%s",
openEvent.FullPath,
openEvent.GetPod(),
rule.generatePatchCommand(openEvent, ap),
),
Severity: R0006UnexpectedServiceAccountTokenAccessRuleDescriptor.Priority,
},
RuntimeProcessDetails: apitypes.ProcessTree{
ProcessTree: apitypes.Process{
Expand All @@ -139,16 +205,19 @@ func (rule *R0006UnexpectedServiceAccountTokenAccess) ProcessEvent(eventType uti
},
TriggerEvent: openEvent.Event,
RuleAlert: apitypes.RuleAlert{
RuleDescription: fmt.Sprintf("Unexpected access to service account token: %s with flags: %s in: %s", openEvent.FullPath, strings.Join(openEvent.Flags, ","), openEvent.GetContainer()),
RuleDescription: fmt.Sprintf(
"Unexpected access to service account token: %s with flags: %s in: %s",
openEvent.FullPath,
strings.Join(openEvent.Flags, ","),
openEvent.GetContainer(),
),
},
RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{
PodName: openEvent.GetPod(),
PodLabels: openEvent.K8s.PodLabels,
},
RuleID: rule.ID(),
}

return &ruleFailure
}

func (rule *R0006UnexpectedServiceAccountTokenAccess) Requirements() ruleengine.RuleSpec {
Expand Down
Loading

0 comments on commit 82c1608

Please sign in to comment.