mirror of
https://github.com/kubescape/kubescape.git
synced 2026-04-15 06:58:11 +00:00
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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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: {}
|
||||
Reference in New Issue
Block a user