feat: Add the debugging ability for scanning Helm chart (#1215)

* Fix issue 11552

Signed-off-by: MMMMMMorty <465346562@qq.com>

* Add helm chart mapping node for sarif printer

Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>

* add MappingNodes to getWorkloadFromHelmChart

Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>

* clear the code to mappingnode and parseFile

Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>

* add input to fixPathsToString

Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>

* add fixs for error message

Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>

* Add solution for multiple files in one yaml helm chart file

Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>

* Add parseFile tests

Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>

---------

Signed-off-by: MMMMMMorty <465346562@qq.com>
Signed-off-by: mmmmmmorty <mmmmmmorty@outlook.com>
This commit is contained in:
MMMMMMorty
2024-03-01 14:31:51 +02:00
committed by GitHub
parent 0033dc881e
commit bc33f10d0a
22 changed files with 971 additions and 54 deletions
+2
View File
@@ -58,6 +58,7 @@ type OPASessionObj struct {
OmitRawResources bool // omit raw resources from output
SingleResourceScan workloadinterface.IWorkload // single resource scan
TopWorkloadsByScore []reporthandling.IResource
TemplateMapping map[string]MappingNodes // Map chart obj to template (only for rendering from path)
}
func NewOPASessionObj(ctx context.Context, frameworks []reporthandling.Framework, k8sResources K8SResources, scanInfo *ScanInfo) *OPASessionObj {
@@ -74,6 +75,7 @@ func NewOPASessionObj(ctx context.Context, frameworks []reporthandling.Framework
SessionID: scanInfo.ScanID,
Metadata: scanInfoToScanMetadata(ctx, scanInfo),
OmitRawResources: scanInfo.OmitRawResources,
TemplateMapping: make(map[string]MappingNodes),
}
}
+8 -3
View File
@@ -38,7 +38,7 @@ type Chart struct {
}
// LoadResourcesFromHelmCharts scans a given path (recursively) for helm charts, renders the templates and returns a map of workloads and a map of chart names
func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[string][]workloadinterface.IMetadata, map[string]Chart) {
func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[string][]workloadinterface.IMetadata, map[string]Chart, map[string]MappingNodes) {
directories, _ := listDirs(basePath)
helmDirectories := make([]string, 0)
for _, dir := range directories {
@@ -49,14 +49,16 @@ func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[stri
sourceToWorkloads := map[string][]workloadinterface.IMetadata{}
sourceToChart := make(map[string]Chart, 0)
sourceToNodes := map[string]MappingNodes{}
for _, helmDir := range helmDirectories {
chart, err := NewHelmChart(helmDir)
if err == nil {
wls, errs := chart.GetWorkloadsWithDefaultValues()
wls, templateToNodes, errs := chart.GetWorkloadsWithDefaultValues()
if len(errs) > 0 {
logger.L().Ctx(ctx).Warning(fmt.Sprintf("Rendering of Helm chart template '%s', failed: %v", chart.GetName(), errs))
continue
}
sourceToNodes = templateToNodes
chartName := chart.GetName()
for k, v := range wls {
@@ -66,9 +68,12 @@ func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[stri
Path: helmDir,
}
}
// for k, v := range templateMappings {
// sourceToNodes[k] = v
// }
}
}
return sourceToWorkloads, sourceToChart
return sourceToWorkloads, sourceToChart, sourceToNodes
}
// If the contents at given path is a Kustomize Directory, LoadResourcesFromKustomizeDirectory will
+2 -1
View File
@@ -45,10 +45,11 @@ func TestLoadResourcesFromFiles(t *testing.T) {
}
func TestLoadResourcesFromHelmCharts(t *testing.T) {
sourceToWorkloads, sourceToChartName := LoadResourcesFromHelmCharts(context.TODO(), helmChartPath())
sourceToWorkloads, sourceToChartName, _ := LoadResourcesFromHelmCharts(context.TODO(), helmChartPath())
assert.Equal(t, 6, len(sourceToWorkloads))
for file, workloads := range sourceToWorkloads {
assert.Equalf(t, 1, len(workloads), "expected 1 workload in file %s", file)
w := workloads[0]
+66 -6
View File
@@ -1,7 +1,9 @@
package cautils
import (
"fmt"
"path/filepath"
"strconv"
"strings"
logger "github.com/kubescape/go-logger"
@@ -45,22 +47,35 @@ func (hc *HelmChart) GetDefaultValues() map[string]interface{} {
}
// GetWorkloads renders chart template using the default values and returns a map of source file to its workloads
func (hc *HelmChart) GetWorkloadsWithDefaultValues() (map[string][]workloadinterface.IMetadata, []error) {
func (hc *HelmChart) GetWorkloadsWithDefaultValues() (map[string][]workloadinterface.IMetadata, map[string]MappingNodes, []error) {
return hc.GetWorkloads(hc.GetDefaultValues())
}
// GetWorkloads renders chart template using the provided values and returns a map of source (absolute) file path to its workloads
func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]workloadinterface.IMetadata, []error) {
func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]workloadinterface.IMetadata, map[string]MappingNodes, []error) {
vals, err := helmchartutil.ToRenderValues(hc.chart, values, helmchartutil.ReleaseOptions{}, nil)
if err != nil {
return nil, []error{err}
return nil, nil, []error{err}
}
// change the chart to template with comment, only is template(.yaml added otherwise no)
hc.AddCommentToTemplate()
sourceToFile, err := helmengine.Render(hc.chart, vals)
if err != nil {
return nil, []error{err}
return nil, nil, []error{err}
}
// get the resouse and analysis and store it to the struct
fileMapping := make(map[string]MappingNodes)
err = GetTemplateMapping(sourceToFile, fileMapping)
if err != nil {
return nil, nil, []error{err}
}
// delete the comment from chart and from sourceToFile
RemoveComment(sourceToFile)
workloads := make(map[string][]workloadinterface.IMetadata, 0)
errs := []error{}
@@ -76,10 +91,14 @@ func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]w
if len(wls) == 0 {
continue
}
// separate base path and file name. We do not use the os.Separator because the paths returned from the helm engine are not OS specific (e.g. mychart/templates/myfile.yaml)
if firstPathSeparatorIndex := strings.Index(path, string("/")); firstPathSeparatorIndex != -1 {
absPath := filepath.Join(hc.path, path[firstPathSeparatorIndex:])
if nodes, ok := fileMapping[path]; ok {
fileMapping[absPath] = nodes
delete(fileMapping, path)
}
workloads[absPath] = []workloadinterface.IMetadata{}
for i := range wls {
lw := localworkload.NewLocalWorkload(wls[i].GetObject())
@@ -88,5 +107,46 @@ func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]w
}
}
}
return workloads, errs
return workloads, fileMapping, errs
}
func (hc *HelmChart) AddCommentToTemplate() {
for index, t := range hc.chart.Templates {
if IsYaml(strings.ToLower(t.Name)) {
var newLines []string
originalTemplate := string(t.Data)
lines := strings.Split(originalTemplate, "\n")
for index, line := range lines {
comment := " #This is the " + strconv.Itoa(index+1) + " line"
newLines = append(newLines, line+comment)
}
templateWithComment := strings.Join(newLines, "\n")
hc.chart.Templates[index].Data = []byte(templateWithComment)
}
}
}
func RemoveComment(sourceToFile map[string]string) {
// commentRe := regexp.MustCompile(CommentFormat)
for fileName, file := range sourceToFile {
if !IsYaml(strings.ToLower((fileName))) {
continue
}
sourceToFile[fileName] = commentRe.ReplaceAllLiteralString(file, "")
}
}
func GetTemplateMapping(sourceToFile map[string]string, fileMapping map[string]MappingNodes) error {
for fileName, fileContent := range sourceToFile {
mappingNodes, err := GetMapping(fileName, fileContent)
if err != nil {
err = fmt.Errorf("GetMapping wrong, err: %s", err.Error())
return err
}
if len(mappingNodes.Nodes) != 0 {
fileMapping[fileName] = *mappingNodes
}
}
return nil
}
+2 -2
View File
@@ -83,7 +83,7 @@ func (s *HelmChartTestSuite) TestGetWorkloadsWithOverride() {
// Override default value
values["image"].(map[string]interface{})["pullPolicy"] = "Never"
fileToWorkloads, errs := chart.GetWorkloads(values)
fileToWorkloads, _, errs := chart.GetWorkloads(values)
s.Len(errs, 0)
s.Lenf(fileToWorkloads, len(s.expectedFiles), "Expected %d files", len(s.expectedFiles))
@@ -111,7 +111,7 @@ func (s *HelmChartTestSuite) TestGetWorkloadsMissingValue() {
values := chart.GetDefaultValues()
delete(values, "image")
fileToWorkloads, errs := chart.GetWorkloads(values)
fileToWorkloads, _, errs := chart.GetWorkloads(values)
s.Nil(fileToWorkloads)
s.Len(errs, 1, "Expected an error due to missing value")
+34
View File
@@ -0,0 +1,34 @@
package cautils
type ObjectID struct {
apiVersion string
kind string
}
type MappingNode struct {
ObjectID *ObjectID
Field string
Value string
TemplateFileName string
TemplateLineNumber int
}
type MappingNodes struct {
Nodes []map[string]MappingNode //Map line number of chart to template obj map[int]MappingNode
TemplateFileName string
}
func (node *MappingNode) writeInfoToNode(objectID *ObjectID, path string, lineNumber int, value string, fileName string) {
node.Field = path
node.TemplateLineNumber = lineNumber
node.ObjectID = objectID
node.Value = value
node.TemplateFileName = fileName
}
func NewMappingNodes() *MappingNodes {
mappingNodes := new(MappingNodes)
mappingNodes.TemplateFileName = ""
return mappingNodes
}
+267
View File
@@ -0,0 +1,267 @@
package cautils
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
logger "github.com/kubescape/go-logger"
"github.com/mikefarah/yq/v4/pkg/yqlib"
"gopkg.in/op/go-logging.v1"
)
const (
CommentFormat = `#This is the (?P<line>\d*) line`
)
var apiVersionRe = regexp.MustCompile(`apiVersion: (?P<apiVersion>\S*)`)
var kindRe = regexp.MustCompile(`kind: (?P<kind>\S*)`)
var pathRe = regexp.MustCompile(`path: (?P<path>\S*)`)
var typeRe = regexp.MustCompile(`type: '(?P<type>\S*)'`)
var valueRe = regexp.MustCompile(`value: (?P<value>\[.+\]|\S*)`)
var commentRe = regexp.MustCompile(CommentFormat)
var seqRe = regexp.MustCompile(`.(?P<number>\d+)(?P<point>\.?)`)
var newSeqRe = "[${number}]${point}"
var newFileSeperator = "---"
// change to use go func
func GetMapping(fileName string, fileContent string) (*MappingNodes, error) {
node := new(MappingNode)
objectID := new(ObjectID)
subFileNodes := make(map[string]MappingNode)
mappingNodes := NewMappingNodes()
mappingNodes.TemplateFileName = fileName
lines := strings.Split(fileContent, "\n")
lastNumber := -1
reducedNumber := -1 // uses to make sure line and line in yq is the same
isApiVersionEmpty := true
isKindEmpty := true
var err error
var lineExpression = `..| select(line == %d)| {"destpath": path | join("."),"type": type,"value": .}`
for i, line := range lines {
index := i
if apiVersionRe.MatchString(line) {
isApiVersionEmpty, err = extractApiVersion(line, objectID)
if err != nil {
return nil, fmt.Errorf("extractApiVersion error: err, %s", err.Error())
}
if reducedNumber == -1 {
reducedNumber = index + reducedNumber
}
continue
} else if kindRe.MatchString(line) {
isKindEmpty, err = extractKind(line, objectID)
if err != nil {
return nil, fmt.Errorf("extractKind error: err, %s", err.Error())
}
continue
} else if strings.Contains(line, newFileSeperator) { //At least two files in one yaml
mappingNodes.Nodes = append(mappingNodes.Nodes, subFileNodes)
// Restart a subfileNode
isApiVersionEmpty = false
isKindEmpty = false
subFileNodes = make(map[string]MappingNode)
continue
}
if !isApiVersionEmpty || !isKindEmpty {
// not sure if it can go to the end
index = index - reducedNumber
expression := fmt.Sprintf(lineExpression, index)
output, err := getYamlLineInfo(expression, fileContent)
if err != nil {
return nil, fmt.Errorf("getYamlLineInfo wrong, the err is %s", err.Error())
}
path := extractParameter(pathRe, output, "$path")
//if path is empty, continue
if path != "" && path != "\"\"" {
if isApiVersionEmpty || isKindEmpty {
return nil, fmt.Errorf("there is no enough objectID info")
}
splits := strings.Split(output, "dest")
if len(splits) < 2 {
return nil, fmt.Errorf("something wrong with the length of the splits, which is %d", len(splits))
} else {
// cut the redundant one
splits = splits[1:]
lastNumber, err = writeNodes(splits, lastNumber, fileName, node, objectID, subFileNodes)
if err != nil {
return nil, fmt.Errorf("writeNodes err: %s", err.Error())
}
}
}
}
if i == len(lines)-1 {
mappingNodes.Nodes = append(mappingNodes.Nodes, subFileNodes)
}
}
return mappingNodes, nil
}
func writeNodes(splits []string, lastNumber int, fileName string, node *MappingNode, objectID *ObjectID, subFileNodes map[string]MappingNode) (int, error) {
for _, split := range splits {
path := extractPath(split)
mapMatched, err := extractMapType(split)
if err != nil {
return -1, fmt.Errorf("extractMapType err: %s", err.Error())
}
if mapMatched {
lastNumber, err = writeNoteToMapping(split, lastNumber, path, fileName, node, objectID, true, subFileNodes)
if err != nil {
return -1, fmt.Errorf("map type: writeNoteToMapping, err: %s", err.Error())
}
} else {
lastNumber, err = writeNoteToMapping(split, lastNumber, path, fileName, node, objectID, false, subFileNodes)
if err != nil {
return -1, fmt.Errorf("not map type: writeNoteToMapping, err: %s", err.Error())
}
}
}
return lastNumber, nil
}
func writeNoteToMapping(split string, lastNumber int, path string, fileName string, node *MappingNode, objectID *ObjectID, isMapType bool, subFileNodes map[string]MappingNode) (int, error) {
newlastNumber, err := writeNodeInfo(split, lastNumber, path, fileName, node, objectID, isMapType)
if err != nil {
return 0, fmt.Errorf("isMapType: %v, writeNodeInfo wrong err: %s", isMapType, err.Error())
}
if _, ok := subFileNodes[path]; !ok { // Assume the path is unique in one subfile
subFileNodes[path] = *node
}
// else {
// return 0, fmt.Errorf("isMapType: %v, %s in mapping.Nodes exists", isMapType, path)
// }
return newlastNumber, nil
}
func writeNodeInfo(split string, lastNumber int, path string, fileName string, node *MappingNode, objectID *ObjectID, isMapType bool) (int, error) {
value, lineNumber, newLastNumber, err := getInfoFromOne(split, lastNumber, isMapType)
if err != nil {
return -1, fmt.Errorf("getInfoFromOne wrong err: %s", err.Error())
}
// lastNumber = newLastNumber
node.writeInfoToNode(objectID, path, lineNumber, value, fileName)
return newLastNumber, nil
}
func getInfoFromOne(output string, lastNumber int, isMapType bool) (value string, lineNumber int, newLastNumber int, err error) {
if isMapType {
value = ""
} else {
value = extractParameter(valueRe, output, "$value")
}
number := extractParameter(commentRe, output, "$line")
if number != "" {
lineNumber, err = strconv.Atoi(number)
if err != nil {
return "", -1, -1, fmt.Errorf("strconv.Atoi err: %s", err.Error())
}
if isMapType {
lineNumber = lineNumber - 1
}
lastNumber = lineNumber
// save to structure
} else {
lineNumber = lastNumber
// use the last one number
}
newLastNumber = lineNumber
return value, lineNumber, newLastNumber, nil
}
func getYamlLineInfo(expression string, yamlFile string) (string, error) {
out, err := exectuateYq(expression, yamlFile)
if err != nil {
return "", fmt.Errorf("exectuateYq err: %s", err.Error())
}
return out, nil
}
func exectuateYq(expression string, yamlContent string) (string, error) {
backendLoggerLeveled := logging.AddModuleLevel(logging.NewLogBackend(logger.L().GetWriter(), "", 0))
backendLoggerLeveled.SetLevel(logging.ERROR, "")
yqlib.GetLogger().SetBackend(backendLoggerLeveled)
encoder := configureEncoder()
decoder := configureDecoder(false)
stringEvaluator := yqlib.NewStringEvaluator()
out, err := stringEvaluator.Evaluate(expression, yamlContent, encoder, decoder)
if err != nil {
return "", errors.New("no matches found")
}
return out, err
}
func extractApiVersion(line string, objectID *ObjectID) (bool, error) {
apiVersion := extractParameter(apiVersionRe, line, "$apiVersion")
if apiVersion == "" {
return true, fmt.Errorf("something wrong when extracting the apiVersion, the line is %s", line)
}
objectID.apiVersion = apiVersion
return false, nil
}
func extractKind(line string, objectID *ObjectID) (bool, error) {
kind := extractParameter(kindRe, line, "$kind")
if kind == "" {
return true, fmt.Errorf("something wrong when extracting the kind, the line is %s", line)
}
objectID.kind = kind
return false, nil
}
func extractPath(split string) string {
path := extractParameter(pathRe, split, "$path")
// For each match of the regex in the content.
path = seqRe.ReplaceAllString(path, newSeqRe)
return path
}
func extractMapType(split string) (bool, error) {
pathType := extractParameter(typeRe, split, "$type")
mapMatched, err := regexp.MatchString(`!!map`, pathType)
if err != nil {
err = fmt.Errorf("regexp.MatchString err: %s", err.Error())
return false, err
}
return mapMatched, nil
}
func extractParameter(re *regexp.Regexp, line string, keyword string) string {
submatch := re.FindStringSubmatchIndex(line)
result := []byte{}
result = re.ExpandString(result, keyword, line, submatch)
parameter := string(result)
return parameter
}
//yqlib configuration
func configureEncoder() yqlib.Encoder {
indent := 2
colorsEnabled := false
yqlibEncoder := yqlib.NewYamlEncoder(indent, colorsEnabled, yqlib.ConfiguredYamlPreferences)
return yqlibEncoder
}
func configureDecoder(evaluateTogether bool) yqlib.Decoder {
prefs := yqlib.ConfiguredYamlPreferences
prefs.EvaluateTogether = evaluateTogether
yqlibDecoder := yqlib.NewYamlDecoder(prefs)
return yqlibDecoder
}
+79
View File
@@ -0,0 +1,79 @@
package cautils
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/suite"
helmchartutil "helm.sh/helm/v3/pkg/chartutil"
helmengine "helm.sh/helm/v3/pkg/engine"
)
type HelmChartGetMappingSuite struct {
suite.Suite
helmChartPath string
expectedFiles []string
fileContent map[string]string
}
func TestHelmChartGetMappingSuite(t *testing.T) {
suite.Run(t, new(HelmChartGetMappingSuite))
}
func (s *HelmChartGetMappingSuite) SetupSuite() {
o, _ := os.Getwd()
s.helmChartPath = filepath.Join(filepath.Dir(o), "..", "examples", "helm_chart_mapping_node")
s.expectedFiles = []string{
filepath.Join(s.helmChartPath, "templates", "clusterrolebinding.yaml"),
filepath.Join(s.helmChartPath, "templates", "clusterrole.yaml"),
filepath.Join(s.helmChartPath, "templates", "serviceaccount.yaml"),
filepath.Join(s.helmChartPath, "templates", "rolebinding.yaml"),
filepath.Join(s.helmChartPath, "templates", "role.yaml"),
filepath.Join(s.helmChartPath, "templates", "cronjob.yaml"),
}
s.fileContent = make(map[string]string)
hc, _ := NewHelmChart(s.helmChartPath)
values := hc.GetDefaultValues()
vals, _ := helmchartutil.ToRenderValues(hc.chart, values, helmchartutil.ReleaseOptions{}, nil)
sourceToFile, _ := helmengine.Render(hc.chart, vals)
s.fileContent = sourceToFile
}
func (s *HelmChartGetMappingSuite) TestGetMapping() {
fileNodes, err := GetMapping("rolebinding.yaml", s.fileContent["kubescape/templates/rolebinding.yaml"])
s.NoError(err, "Get Mapping nodes correctly")
s.Equal(fileNodes.TemplateFileName, "rolebinding.yaml")
s.Len(fileNodes.Nodes, 1)
s.Len(fileNodes.Nodes[0], 13)
}
func (s *HelmChartGetMappingSuite) TestGetMappingFromFileContainsMultipleSubFiles() {
fileNodes, err := GetMapping("serviceaccount.yaml", s.fileContent["kubescape/templates/serviceaccount.yaml"])
s.NoError(err, "Get Mapping nodes correctly")
s.Equal(fileNodes.TemplateFileName, "serviceaccount.yaml")
s.Len(fileNodes.Nodes, 2)
s.Len(fileNodes.Nodes[0], 8)
s.Len(fileNodes.Nodes[1], 2)
}
func (s *HelmChartGetMappingSuite) TestGetMappingFromFileCWithoutKindOrApiVersion() {
fileNodes, err := GetMapping("clusterrole.yaml", s.fileContent["kubescape/templates/clusterrole.yaml"])
s.Contains(err.Error(), "there is no enough objectID info")
s.Nil(fileNodes)
}
func (s *HelmChartGetMappingSuite) TestGetMappingFromFileCWithoutApiVersion() {
fileNodes, err := GetMapping("clusterrolebinding.yaml", s.fileContent["kubescape/templates/clusterrolebinding.yaml"])
s.Contains(err.Error(), "there is no enough objectID info")
s.Nil(fileNodes)
}
+34 -15
View File
@@ -41,15 +41,16 @@ func (fileHandler *FileResourceHandler) GetResources(ctx context.Context, sessio
for path := range scanInfo.InputPatterns {
var workloadIDToSource map[string]reporthandling.Source
var workloads []workloadinterface.IMetadata
var workloadIDToMappingNodes map[string]cautils.MappingNodes
var err error
if scanInfo.ChartPath != "" && scanInfo.FilePath != "" {
workloadIDToSource, workloads, err = getWorkloadFromHelmChart(ctx, scanInfo.ChartPath, scanInfo.FilePath)
workloadIDToSource, workloads, workloadIDToMappingNodes, err = getWorkloadFromHelmChart(ctx, scanInfo.ChartPath, scanInfo.FilePath)
if err != nil {
// We should probably ignore the error so we can continue scanning other charts
}
} else {
workloadIDToSource, workloads, err = getResourcesFromPath(ctx, scanInfo.InputPatterns[path])
workloadIDToSource, workloads, workloadIDToMappingNodes, err = getResourcesFromPath(ctx, scanInfo.InputPatterns[path])
if err != nil {
return nil, allResources, nil, nil, err
}
@@ -60,6 +61,7 @@ func (fileHandler *FileResourceHandler) GetResources(ctx context.Context, sessio
for k, v := range workloadIDToSource {
sessionObj.ResourceSource[k] = v
sessionObj.TemplateMapping[k] = workloadIDToMappingNodes[k]
}
// map all resources: map["/apiVersion/version/kind"][]<k8s workloads>
@@ -105,10 +107,10 @@ func (fileHandler *FileResourceHandler) GetResources(ctx context.Context, sessio
func (fileHandler *FileResourceHandler) GetCloudProvider() string {
return ""
}
func getWorkloadFromHelmChart(ctx context.Context, helmPath, workloadPath string) (map[string]reporthandling.Source, []workloadinterface.IMetadata, error) {
func getWorkloadFromHelmChart(ctx context.Context, helmPath, workloadPath string) (map[string]reporthandling.Source, []workloadinterface.IMetadata, map[string]cautils.MappingNodes, error) {
clonedRepo, err := cloneGitRepo(&helmPath)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if clonedRepo != "" {
defer os.RemoveAll(clonedRepo)
@@ -117,7 +119,7 @@ func getWorkloadFromHelmChart(ctx context.Context, helmPath, workloadPath string
// Get repo root
repoRoot, gitRepo := extractGitRepo(helmPath)
helmSourceToWorkloads, helmSourceToChart := cautils.LoadResourcesFromHelmCharts(ctx, helmPath)
helmSourceToWorkloads, helmSourceToChart, helmSourceToNodes := cautils.LoadResourcesFromHelmCharts(ctx, helmPath)
if clonedRepo != "" {
workloadPath = clonedRepo + workloadPath
@@ -125,27 +127,34 @@ func getWorkloadFromHelmChart(ctx context.Context, helmPath, workloadPath string
wlSource, ok := helmSourceToWorkloads[workloadPath]
if !ok {
return nil, nil, fmt.Errorf("workload %s not found in chart %s", workloadPath, helmPath)
return nil, nil, nil, fmt.Errorf("workload %s not found in chart %s", workloadPath, helmPath)
}
if len(wlSource) != 1 {
return nil, nil, fmt.Errorf("workload %s found multiple times in chart %s", workloadPath, helmPath)
return nil, nil, nil, fmt.Errorf("workload %s found multiple times in chart %s", workloadPath, helmPath)
}
helmChart, ok := helmSourceToChart[workloadPath]
if !ok {
return nil, nil, fmt.Errorf("helmChart not found for workload %s", workloadPath)
return nil, nil, nil, fmt.Errorf("helmChart not found for workload %s", workloadPath)
}
templatesNodes, ok := helmSourceToNodes[workloadPath]
if !ok {
return nil, nil, nil, fmt.Errorf("templatesNodes not found for workload %s", workloadPath)
}
workloadSource := getWorkloadSourceHelmChart(repoRoot, helmPath, gitRepo, helmChart)
workloadIDToSource := make(map[string]reporthandling.Source, 1)
workloadIDToNodes := make(map[string]cautils.MappingNodes, 1)
workloadIDToSource[wlSource[0].GetID()] = workloadSource
workloadIDToNodes[wlSource[0].GetID()] = templatesNodes
workloads := []workloadinterface.IMetadata{}
workloads = append(workloads, wlSource...)
return workloadIDToSource, workloads, nil
return workloadIDToSource, workloads, workloadIDToNodes, nil
}
@@ -179,13 +188,14 @@ func getWorkloadSourceHelmChart(repoRoot string, source string, gitRepo *cautils
}
}
func getResourcesFromPath(ctx context.Context, path string) (map[string]reporthandling.Source, []workloadinterface.IMetadata, error) {
func getResourcesFromPath(ctx context.Context, path string) (map[string]reporthandling.Source, []workloadinterface.IMetadata, map[string]cautils.MappingNodes, error) {
workloadIDToSource := make(map[string]reporthandling.Source, 0)
workloadIDToNodes := make(map[string]cautils.MappingNodes)
workloads := []workloadinterface.IMetadata{}
clonedRepo, err := cloneGitRepo(&path)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if clonedRepo != "" {
defer os.RemoveAll(clonedRepo)
@@ -269,10 +279,11 @@ func getResourcesFromPath(ctx context.Context, path string) (map[string]reportha
}
// load resources from helm charts
helmSourceToWorkloads, helmSourceToChart := cautils.LoadResourcesFromHelmCharts(ctx, path)
helmSourceToWorkloads, helmSourceToChart, helmSourceToNodes := cautils.LoadResourcesFromHelmCharts(ctx, path)
for source, ws := range helmSourceToWorkloads {
workloads = append(workloads, ws...)
helmChart := helmSourceToChart[source]
templatesNodes := helmSourceToNodes[source]
if clonedRepo != "" {
url, err := gitRepo.GetRemoteUrl()
@@ -283,21 +294,29 @@ func getResourcesFromPath(ctx context.Context, path string) (map[string]reportha
helmChart.Path = strings.TrimSuffix(url, ".git")
repoRoot = ""
source = strings.TrimPrefix(source, fmt.Sprintf("%s/", clonedRepo))
templatesNodes.TemplateFileName = source
}
workloadSource := getWorkloadSourceHelmChart(repoRoot, source, gitRepo, helmChart)
for i := range ws {
workloadIDToSource[ws[i].GetID()] = workloadSource
workloadIDToNodes[ws[i].GetID()] = templatesNodes
// workloadIDToNodes[ws[i].GetID()].Nodes = templatesNodes.Nodes
// workloadIDToNodes[ws[i].GetID()].TemplateFileName = templatesNodes.TemplateFileName
// helmSourceToNodes[source]
}
}
if len(helmSourceToWorkloads) > 0 {
if len(helmSourceToWorkloads) > 0 { // && len(helmSourceToNodes) > 0
logger.L().Debug("helm templates found in local storage", helpers.Int("helmTemplates", len(helmSourceToWorkloads)), helpers.Int("workloads", len(workloads)))
} else {
workloadIDToNodes = nil
}
//patch, get value from env
// Load resources from Kustomize directory
kustomizeSourceToWorkloads, kustomizeDirectoryName := cautils.LoadResourcesFromKustomizeDirectory(ctx, path)
kustomizeSourceToWorkloads, kustomizeDirectoryName := cautils.LoadResourcesFromKustomizeDirectory(ctx, path) //?
// update workloads and workloadIDToSource with workloads from Kustomize Directory
for source, ws := range kustomizeSourceToWorkloads {
@@ -334,7 +353,7 @@ func getResourcesFromPath(ctx context.Context, path string) (map[string]reportha
}
}
return workloadIDToSource, workloads, nil
return workloadIDToSource, workloads, workloadIDToNodes, nil
}
func extractGitRepo(path string) (string, *cautils.LocalGitRepository) {
@@ -160,14 +160,18 @@ func failedPathsToString(control *resourcesresults.ResourceAssociatedControl) []
return paths
}
func fixPathsToString(control *resourcesresults.ResourceAssociatedControl) []string {
func fixPathsToString(control *resourcesresults.ResourceAssociatedControl, onlyPath bool) []string {
var paths []string
for j := range control.ResourceAssociatedRules {
for k := range control.ResourceAssociatedRules[j].Paths {
if p := control.ResourceAssociatedRules[j].Paths[k].FixPath.Path; p != "" {
v := control.ResourceAssociatedRules[j].Paths[k].FixPath.Value
paths = append(paths, fmt.Sprintf("%s=%s", p, v))
if onlyPath {
paths = append(paths, p)
} else {
v := control.ResourceAssociatedRules[j].Paths[k].FixPath.Value
paths = append(paths, fmt.Sprintf("%s=%s", p, v))
}
}
}
}
@@ -201,7 +205,7 @@ func reviewPathsToString(control *resourcesresults.ResourceAssociatedControl) []
}
func AssistedRemediationPathsToString(control *resourcesresults.ResourceAssociatedControl) []string {
paths := append(fixPathsToString(control), append(deletePathsToString(control), reviewPathsToString(control)...)...)
paths := append(fixPathsToString(control, false), append(deletePathsToString(control), reviewPathsToString(control)...)...)
// TODO - deprecate failedPaths once all controls support review/delete paths
paths = appendFailedPathsIfNotInPaths(paths, failedPathsToString(control))
return paths
@@ -254,16 +254,16 @@ func TestFixPathsToString(t *testing.T) {
}
// Test case 1: Empty ResourceAssociatedRules
actualPaths := fixPathsToString(emptyControl)
actualPaths := fixPathsToString(emptyControl, false)
assert.Nil(t, actualPaths)
// Test case 2: Single ResourceAssociatedRule and one ReviewPath
actualPaths = fixPathsToString(singleRuleControl)
actualPaths = fixPathsToString(singleRuleControl, false)
expectedPath := []string{"fix-path1=fix-path-value1"}
assert.Equal(t, expectedPath, actualPaths)
// Test case 3: Multiple ResourceAssociatedRules and multiple ReviewPaths
actualPaths = fixPathsToString(multipleRulesControl)
actualPaths = fixPathsToString(multipleRulesControl, false)
expectedPath = []string{"fix-path2=fix-path-value2", "fix-path3=fix-path-value3"}
assert.Equal(t, expectedPath, actualPaths)
}
@@ -187,8 +187,10 @@ func (sp *SARIFPrinter) printConfigurationScan(ctx context.Context, opaSessionOb
run := sarif.NewRunWithInformationURI(toolName, toolInfoURI)
basePath := getBasePathFromMetadata(*opaSessionObj)
for resourceID, result := range opaSessionObj.ResourcesResult {
for resourceID, result := range opaSessionObj.ResourcesResult { //
if result.GetStatus(nil).IsFailed() {
helmChartFileType := false
var mappingnodes []map[string]cautils.MappingNode
resourceSource := opaSessionObj.ResourceSource[resourceID]
filepath := resourceSource.RelativePath
@@ -197,9 +199,15 @@ func (sp *SARIFPrinter) printConfigurationScan(ctx context.Context, opaSessionOb
continue
}
// If the fileType is helm chart
if templateNodes, ok := opaSessionObj.TemplateMapping[resourceID]; ok {
mappingnodes = templateNodes.Nodes
helmChartFileType = true
}
rsrcAbsPath := path.Join(basePath, filepath)
locationResolver, err := locationresolver.NewFixPathLocationResolver(rsrcAbsPath)
if err != nil {
locationResolver, err := locationresolver.NewFixPathLocationResolver(rsrcAbsPath) //
if err != nil && !helmChartFileType {
logger.L().Debug("failed to create location resolver", helpers.Error(err))
continue
}
@@ -208,12 +216,24 @@ func (sp *SARIFPrinter) printConfigurationScan(ctx context.Context, opaSessionOb
ac := toPin
if ac.GetStatus(nil).IsFailed() {
var location locationresolver.Location
ctl := opaSessionObj.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, ac.GetID())
location := sp.resolveFixLocation(opaSessionObj, locationResolver, &ac, resourceID)
if helmChartFileType {
for _, subfileNodes := range mappingnodes {
// first get the failed path, then if cannot find it, use the Fix path, cui it to find the closest error.
location, split := resolveFixLocation(subfileNodes, &ac)
sp.addRule(run, ctl)
result := sp.addResult(run, ctl, filepath, location)
collectFixesFromMappingNodes(ctx, result, ac, opaSessionObj, resourceID, filepath, rsrcAbsPath, location, subfileNodes, split)
}
} else {
location = sp.resolveFixLocation(opaSessionObj, locationResolver, &ac, resourceID)
sp.addRule(run, ctl)
result := sp.addResult(run, ctl, filepath, location)
collectFixes(ctx, result, ac, opaSessionObj, resourceID, filepath, rsrcAbsPath)
}
sp.addRule(run, ctl)
result := sp.addResult(run, ctl, filepath, location)
collectFixes(ctx, result, ac, opaSessionObj, resourceID, filepath)
}
}
}
@@ -257,6 +277,56 @@ func (sp *SARIFPrinter) resolveFixLocation(opaSessionObj *cautils.OPASessionObj,
return location
}
func getFixPath(ac *resourcesresults.ResourceAssociatedControl, onlyPath bool) string {
fixPaths := failedPathsToString(ac)
if len(fixPaths) == 0 {
fixPaths = fixPathsToString(ac, onlyPath)
}
var fixPath string
if len(fixPaths) > 0 {
fixPath = fixPaths[0]
}
return fixPath
}
func resolveFixLocation(mappingnodes map[string]cautils.MappingNode, ac *resourcesresults.ResourceAssociatedControl) (locationresolver.Location, int) {
defaultLocation := locationresolver.Location{Line: 1, Column: 1}
fixPath := getFixPath(ac, true)
if fixPath == "" {
return defaultLocation, -1
}
location, split := getLocationFromMappingNodes(mappingnodes, fixPath)
return location, split
}
func getLocationFromNode(node cautils.MappingNode, path string) locationresolver.Location {
line := node.TemplateLineNumber
column := (len(strings.Split(path, "."))-1)*2 + 1 //column begins with 1 instead of 0
return locationresolver.Location{Line: line, Column: column}
}
func getLocationFromMappingNodes(mappingnodes map[string]cautils.MappingNode, fixPath string) (locationresolver.Location, int) {
var location locationresolver.Location
// If cannot match any node, return default location
location = locationresolver.Location{Line: 1, Column: 1}
split := -1
if node, ok := mappingnodes[fixPath]; ok {
location = getLocationFromNode(node, fixPath)
} else {
fields := strings.Split(fixPath, ".")
for i := len(fields) - 1; i >= 0; i-- {
field := fields[:i]
closestPath := strings.Join(field, ".")
if node, ok := mappingnodes[closestPath]; ok {
location = getLocationFromNode(node, closestPath)
split = i
break
}
}
}
return location, split
}
func addFix(result *sarif.Result, filepath string, startLine, startColumn, endLine, endColumn int, text string) {
// Create a new replacement with the specified start and end lines and columns, and the inserted text.
replacement := sarif.NewReplacement(
@@ -337,33 +407,37 @@ func collectDiffs(dmp *diffmatchpatch.DiffMatchPatch, diffs []diffmatchpatch.Dif
}
}
func collectFixes(ctx context.Context, result *sarif.Result, ac resourcesresults.ResourceAssociatedControl, opaSessionObj *cautils.OPASessionObj, resourceID string, filepath string) {
func collectFixes(ctx context.Context, result *sarif.Result, ac resourcesresults.ResourceAssociatedControl, opaSessionObj *cautils.OPASessionObj, resourceID string, filepath string, rsrcAbsPath string) {
for _, rule := range ac.ResourceAssociatedRules {
if !rule.GetStatus(nil).IsFailed() {
continue
}
for _, rulePaths := range rule.Paths {
if rulePaths.FixPath.Path == "" {
continue
}
// if strings.HasPrefix(rulePaths.FixPath.Value, fixhandler.UserValuePrefix) {
// continue
// }
documentIndex, ok := getDocIndex(opaSessionObj, resourceID)
if !ok {
fixPath := rulePaths.FixPath.Path
if fixPath == "" {
continue
}
yamlExpression := fixhandler.FixPathToValidYamlExpression(rulePaths.FixPath.Path, rulePaths.FixPath.Value, documentIndex)
fileAsString, err := fixhandler.GetFileString(filepath)
fileAsString, err := fixhandler.GetFileString(rsrcAbsPath)
if err != nil {
logger.L().Debug("failed to access "+filepath, helpers.Error(err))
continue
}
fixedYamlString, err := fixhandler.ApplyFixToContent(ctx, fileAsString, yamlExpression)
var fixedYamlString string
// if strings.HasPrefix(rulePaths.FixPath.Value, fixhandler.UserValuePrefix) {
// continue
// }
documentIndex, ok := getDocIndex(opaSessionObj, resourceID)
if !ok {
continue
}
yamlExpression := fixhandler.FixPathToValidYamlExpression(fixPath, rulePaths.FixPath.Value, documentIndex)
fixedYamlString, err = fixhandler.ApplyFixToContent(ctx, fileAsString, yamlExpression)
if err != nil {
logger.L().Debug("failed to fix "+filepath+" with "+yamlExpression, helpers.Error(err))
continue
@@ -376,6 +450,98 @@ func collectFixes(ctx context.Context, result *sarif.Result, ac resourcesresults
}
}
func collectFixesFromMappingNodes(ctx context.Context, result *sarif.Result, ac resourcesresults.ResourceAssociatedControl, opaSessionObj *cautils.OPASessionObj, resourceID string, filepath string, rsrcAbsPath string, location locationresolver.Location, subFileNodes map[string]cautils.MappingNode, split int) {
for _, rule := range ac.ResourceAssociatedRules {
if !rule.GetStatus(nil).IsFailed() {
continue
}
for _, rulePaths := range rule.Paths {
fixPath := rulePaths.FixPath.Path
if fixPath == "" {
continue
}
fileAsString, err := fixhandler.GetFileString(rsrcAbsPath)
if err != nil {
logger.L().Debug("failed to access "+filepath, helpers.Error(err))
continue
}
var fixedYamlString string
fixValue := rulePaths.FixPath.Value
if split == -1 { //replaceNode
node := subFileNodes[fixPath]
fixedYamlString = formReplaceFixedYamlString(node, fileAsString, location, fixValue, fixPath)
} else { //insertNode
maxLineNumber := getTheLocationOfAddPart(split, fixPath, subFileNodes)
fixedYamlString = applyFixToContent(split, fixPath, fileAsString, maxLineNumber, fixValue)
}
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(fileAsString, fixedYamlString, false)
collectDiffs(dmp, diffs, result, filepath, fileAsString)
}
}
}
func applyFixToContent(split int, fixPath string, fileAsString string, addLine int, value string) string {
addLines := make([]string, 0)
fields := strings.Split(fixPath, ".")
for i := split; i < len(fields); i++ {
field := fields[i]
var addedLine string
if i != len(fields)-1 {
addedLine = strings.Repeat(" ", (i*2)) + field + ":"
} else {
addedLine = strings.Repeat(" ", (i*2)) + field + ": " + value
}
addLines = append(addLines, addedLine)
}
fixedYamlString := formAddFixedYamlString(fileAsString, addLine, addLines)
return fixedYamlString
}
func formReplaceFixedYamlString(node cautils.MappingNode, fileAsString string, location locationresolver.Location, fixValue string, fixPath string) string {
replcaedValue := node.Value
yamlLines := strings.Split(fileAsString, "\n")
if replcaedValue == "" {
yamlLines[location.Line] = yamlLines[location.Line] + " # This is the suggested modification, the value for " + fixPath + " is " + fixValue + "\n"
} else {
replacedLine := "# This is the suggested modification\n" + yamlLines[location.Line]
newLine := strings.Replace(replacedLine, replcaedValue, fixValue, -1)
yamlLines[location.Line] = newLine
}
fixedYamlString := strings.Join(yamlLines, "\n")
return fixedYamlString
}
func formAddFixedYamlString(fileAsString string, addLine int, addLines []string) string {
yamlLines := strings.Split(fileAsString, "\n")
newYamlLines := append(yamlLines[:addLine], "# This is the suggested modification")
newYamlLines = append(newYamlLines, addLines...)
yamlLines = strings.Split(fileAsString, "\n")
newYamlLines = append(newYamlLines, yamlLines[addLine:]...)
fixedYamlString := strings.Join(newYamlLines, "\n")
return fixedYamlString
}
func getTheLocationOfAddPart(split int, fixPath string, mappingnodes map[string]cautils.MappingNode) int {
fields := strings.Split(fixPath, ".")
field := fields[:split]
closestPath := strings.Join(field, ".")
maxLineNumber := -1
for k, v := range mappingnodes {
if strings.Index(k, closestPath) == 0 {
if v.TemplateLineNumber > maxLineNumber {
maxLineNumber = v.TemplateLineNumber
}
}
}
return maxLineNumber
}
func getDocIndex(opaSessionObj *cautils.OPASessionObj, resourceID string) (int, bool) {
resource := opaSessionObj.AllResources[resourceID]
localworkload, ok := resource.(*localworkload.LocalWorkload)
@@ -0,0 +1,29 @@
apiVersion: v2
name: kubescape
description:
Kubescape is the first open-source tool for testing if Kubernetes is deployed securely according to multiple frameworks
regulatory, customized company policies and DevSecOps best practices, such as the [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo) and the [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) .
Kubescape scans K8s clusters, YAML files, and HELM charts, and detect misconfigurations and software vulnerabilities at early stages of the CI/CD pipeline and provides a risk score instantly and risk trends over time.
Kubescape integrates natively with other DevOps tools, including Jenkins, CircleCI and Github workflows.
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v1.0.128"
@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "kubescape.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "kubescape.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "kubescape.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "kubescape.labels" -}}
helm.sh/chart: {{ include "kubescape.chart" . }}
{{ include "kubescape.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "kubescape.selectorLabels" -}}
app.kubernetes.io/name: {{ include "kubescape.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "kubescape.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "kubescape.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -0,0 +1,10 @@
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "kubescape.fullname" . }}
labels:
{{- include "kubescape.labels" . | nindent 4 }}
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "describe"]
@@ -0,0 +1,15 @@
kind: ClusterRoleBinding
metadata:
name: {{ include "kubescape.fullname" . }}
labels:
{{- include "kubescape.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "kubescape.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "kubescape.serviceAccountName" . }}
namespace: {{ .Release.Namespace | quote }}
@@ -0,0 +1,14 @@
{{- if .Values.configMap.create -}}
kind: ConfigMap
apiVersion: v1
metadata:
name: {{ include "kubescape.fullname" . }}-configmap
labels:
{{- include "kubescape.labels" . | nindent 4 }}
data:
config.json: |
{
"customerGUID": "{{ .Values.configMap.params.customerGUID }}",
"clusterName": "{{ .Values.configMap.params.clusterName }}"
}
{{- end }}
@@ -0,0 +1,28 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "kubescape.fullname" . }}
labels:
{{- include "kubescape.labels" . | nindent 4 }}
spec:
schedule: "{{ .Values.schedule }}"
jobTemplate:
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}/{{ .Values.image.imageName }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["/bin/sh", "-c"]
args: ["kubescape scan framework nsa --submit"]
volumeMounts:
- name: kubescape-config-volume
mountPath: /root/.kubescape/config.json
subPath: config.json
restartPolicy: OnFailure
serviceAccountName: {{ include "kubescape.serviceAccountName" . }}
volumes:
- name: kubescape-config-volume
configMap:
name: {{ include "kubescape.fullname" . }}-configmap
@@ -0,0 +1,11 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "kubescape.fullname" . }}
labels:
{{- include "kubescape.labels" . | nindent 4 }}
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "describe"]
@@ -0,0 +1,16 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "kubescape.fullname" . }}
labels:
{{- include "kubescape.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "kubescape.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "kubescape.serviceAccountName" . }}
namespace: {{ .Release.Namespace | quote }}
@@ -0,0 +1,21 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "kubescape.serviceAccountName" . }}
labels:
{{- include "kubescape.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "kubescape.serviceAccountName" . }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,74 @@
# Default values for kubescape.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# -- Frequency of running the scan
# ┌────────────── timezone (optional)
# | ┌───────────── minute (0 - 59)
# | │ ┌───────────── hour (0 - 23)
# | │ │ ┌───────────── day of the month (1 - 31)
# | │ │ │ ┌───────────── month (1 - 12)
# | │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# | │ │ │ │ │ 7 is also Sunday on some systems)
# | │ │ │ │ │
# | │ │ │ │ │
# UTC * * * * *
schedule: "* * 1 * *"
# -- Image and version to deploy
image:
repository: quay.io/armosec
imageName: kubescape
pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion.
tag: latest
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# -- Service account that runs the scan and has permissions to view the cluster
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: "kubescape-discovery"
# -- ARMO customer information
configMap:
create: false
params:
customerGUID: <MyGUID>
clusterName: <MyK8sClusterName>
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# -- Default resources for running the service in cluster
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
nodeSelector: {}
tolerations: []
affinity: {}