• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubescape / kubescape / 6991462372

25 Nov 2023 09:22PM UTC coverage: 48.16%. First build
6991462372

Pull #1516

github

web-flow
Merge 803b8dc5a into b0913b2a4
Pull Request #1516: Bug Fix and Test Addition for sanitizeYaml and revertSanitizeYaml Functions

5 of 7 new or added lines in 1 file covered. (71.43%)

5482 of 11383 relevant lines covered (48.16%)

74326.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.84
/core/pkg/fixhandler/fixhandler.go
1
package fixhandler
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "io"
8
        "os"
9
        "path"
10
        "path/filepath"
11
        "sort"
12
        "strconv"
13
        "strings"
14

15
        "github.com/armosec/armoapi-go/armotypes"
16
        metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
17

18
        logger "github.com/kubescape/go-logger"
19
        "github.com/kubescape/opa-utils/objectsenvelopes"
20
        "github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
21
        "github.com/kubescape/opa-utils/reporthandling"
22
        "github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
23
        reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
24
        "github.com/mikefarah/yq/v4/pkg/yqlib"
25
        "gopkg.in/op/go-logging.v1"
26
)
27

28
const UserValuePrefix = "YOUR_"
29

30
const windowsNewline = "\r\n"
31
const unixNewline = "\n"
32
const oldMacNewline = "\r"
33

34
func NewFixHandler(fixInfo *metav1.FixInfo) (*FixHandler, error) {
×
35
        jsonFile, err := os.Open(fixInfo.ReportFile)
×
36
        if err != nil {
×
37
                return nil, err
×
38
        }
×
39
        defer jsonFile.Close()
×
40
        byteValue, _ := io.ReadAll(jsonFile)
×
41

×
42
        var reportObj reporthandlingv2.PostureReport
×
43
        if err = json.Unmarshal(byteValue, &reportObj); err != nil {
×
44
                return nil, err
×
45
        }
×
46

47
        if err = isSupportedScanningTarget(&reportObj); err != nil {
×
48
                return nil, err
×
49
        }
×
50

51
        localPath := getLocalPath(&reportObj)
×
52
        if _, err = os.Stat(localPath); err != nil {
×
53
                return nil, err
×
54
        }
×
55

56
        backendLoggerLeveled := logging.AddModuleLevel(logging.NewLogBackend(logger.L().GetWriter(), "", 0))
×
57
        backendLoggerLeveled.SetLevel(logging.ERROR, "")
×
58
        yqlib.GetLogger().SetBackend(backendLoggerLeveled)
×
59

×
60
        return &FixHandler{
×
61
                fixInfo:       fixInfo,
×
62
                reportObj:     &reportObj,
×
63
                localBasePath: localPath,
×
64
        }, nil
×
65
}
66

67
func isSupportedScanningTarget(report *reporthandlingv2.PostureReport) error {
×
68
        scanningTarget := report.Metadata.ScanMetadata.ScanningTarget
×
69
        if scanningTarget == reporthandlingv2.GitLocal || scanningTarget == reporthandlingv2.Directory || scanningTarget == reporthandlingv2.File {
×
70
                return nil
×
71
        }
×
72

73
        return fmt.Errorf("unsupported scanning target. Supported scanning targets are: a local git repo, a directory or a file")
×
74
}
75

76
func getLocalPath(report *reporthandlingv2.PostureReport) string {
5✔
77

5✔
78
        switch report.Metadata.ScanMetadata.ScanningTarget {
5✔
79
        case reporthandlingv2.GitLocal:
1✔
80
                return report.Metadata.ContextMetadata.RepoContextMetadata.LocalRootPath
1✔
81
        case reporthandlingv2.Directory:
×
82
                return report.Metadata.ContextMetadata.DirectoryContextMetadata.BasePath
×
83
        case reporthandlingv2.File:
1✔
84
                return filepath.Dir(report.Metadata.ContextMetadata.FileContextMetadata.FilePath)
1✔
85
        default:
3✔
86
                return ""
3✔
87
        }
88
}
89

90
func (h *FixHandler) buildResourcesMap() map[string]*reporthandling.Resource {
×
91
        resourceIdToRawResource := make(map[string]*reporthandling.Resource)
×
92
        for i := range h.reportObj.Resources {
×
93
                resourceIdToRawResource[h.reportObj.Resources[i].GetID()] = &h.reportObj.Resources[i]
×
94
        }
×
95
        for i := range h.reportObj.Results {
×
96
                if h.reportObj.Results[i].RawResource == nil {
×
97
                        continue
×
98
                }
99
                resourceIdToRawResource[h.reportObj.Results[i].RawResource.GetID()] = h.reportObj.Results[i].RawResource
×
100
        }
101

102
        return resourceIdToRawResource
×
103
}
104

105
func (h *FixHandler) getPathFromRawResource(obj map[string]interface{}) string {
×
106
        if localworkload.IsTypeLocalWorkload(obj) {
×
107
                localwork := localworkload.NewLocalWorkload(obj)
×
108
                return localwork.GetPath()
×
109
        } else if objectsenvelopes.IsTypeRegoResponseVector(obj) {
×
110
                regoResponseVectorObject := objectsenvelopes.NewRegoResponseVectorObject(obj)
×
111
                relatedObjects := regoResponseVectorObject.GetRelatedObjects()
×
112
                for _, relatedObject := range relatedObjects {
×
113
                        if localworkload.IsTypeLocalWorkload(relatedObject.GetObject()) {
×
114
                                return relatedObject.(*localworkload.LocalWorkload).GetPath()
×
115
                        }
×
116
                }
117
        }
118

119
        return ""
×
120
}
121

122
func (h *FixHandler) PrepareResourcesToFix(ctx context.Context) []ResourceFixInfo {
×
123
        resourceIdToResource := h.buildResourcesMap()
×
124

×
125
        resourcesToFix := make([]ResourceFixInfo, 0)
×
126
        for _, result := range h.reportObj.Results {
×
127
                if !result.GetStatus(nil).IsFailed() {
×
128
                        continue
×
129
                }
130

131
                resourceID := result.ResourceID
×
132
                resourceObj := resourceIdToResource[resourceID]
×
133
                resourcePath := h.getPathFromRawResource(resourceObj.GetObject())
×
134
                if resourcePath == "" {
×
135
                        continue
×
136
                }
137

138
                if resourceObj.Source == nil || resourceObj.Source.FileType != reporthandling.SourceTypeYaml {
×
139
                        continue
×
140
                }
141

142
                relativePath, documentIndex, err := h.getFilePathAndIndex(resourcePath)
×
143
                if err != nil {
×
144
                        logger.L().Ctx(ctx).Warning("Skipping invalid resource path: " + resourcePath)
×
145
                        continue
×
146
                }
147

148
                absolutePath := path.Join(h.localBasePath, relativePath)
×
149
                if _, err := os.Stat(absolutePath); err != nil {
×
150
                        logger.L().Ctx(ctx).Warning("Skipping missing file: " + absolutePath)
×
151
                        continue
×
152
                }
153

154
                rfi := ResourceFixInfo{
×
155
                        FilePath:        absolutePath,
×
156
                        Resource:        resourceObj,
×
157
                        YamlExpressions: make(map[string]armotypes.FixPath, 0),
×
158
                        DocumentIndex:   documentIndex,
×
159
                }
×
160

×
161
                for i := range result.AssociatedControls {
×
162
                        if result.AssociatedControls[i].GetStatus(nil).IsFailed() {
×
163
                                rfi.addYamlExpressionsFromResourceAssociatedControl(documentIndex, &result.AssociatedControls[i], h.fixInfo.SkipUserValues)
×
164
                        }
×
165
                }
166

167
                if len(rfi.YamlExpressions) > 0 {
×
168
                        resourcesToFix = append(resourcesToFix, rfi)
×
169
                }
×
170
        }
171

172
        return resourcesToFix
×
173
}
174

175
func (h *FixHandler) PrintExpectedChanges(resourcesToFix []ResourceFixInfo) {
×
176
        var sb strings.Builder
×
177
        sb.WriteString("The following changes will be applied:\n")
×
178

×
179
        for _, resourceFixInfo := range resourcesToFix {
×
180
                sb.WriteString(fmt.Sprintf("File: %s\n", resourceFixInfo.FilePath))
×
181
                sb.WriteString(fmt.Sprintf("Resource: %s\n", resourceFixInfo.Resource.GetName()))
×
182
                sb.WriteString(fmt.Sprintf("Kind: %s\n", resourceFixInfo.Resource.GetKind()))
×
183
                sb.WriteString("Changes:\n")
×
184

×
185
                i := 1
×
186
                for _, fixPath := range resourceFixInfo.YamlExpressions {
×
187
                        sb.WriteString(fmt.Sprintf("\t%d) %s = %s\n", i, fixPath.Path, fixPath.Value))
×
188
                        i++
×
189
                }
×
190
                sb.WriteString("\n------\n")
×
191
        }
192

193
        logger.L().Info(sb.String())
×
194
}
195

196
func (h *FixHandler) ApplyChanges(ctx context.Context, resourcesToFix []ResourceFixInfo) (int, []error) {
×
197
        updatedFiles := make(map[string]bool)
×
198
        errors := make([]error, 0)
×
199

×
200
        fileYamlExpressions := h.getFileYamlExpressions(resourcesToFix)
×
201

×
202
        for filepath, yamlExpression := range fileYamlExpressions {
×
203
                fileAsString, err := GetFileString(filepath)
×
204

×
205
                if err != nil {
×
206
                        errors = append(errors, err)
×
207
                        continue
×
208
                }
209

210
                fixedYamlString, err := ApplyFixToContent(ctx, fileAsString, yamlExpression)
×
211

×
212
                if err != nil {
×
213
                        errors = append(errors, fmt.Errorf("Failed to fix file %s: %w ", filepath, err))
×
214
                        continue
×
215
                } else {
×
216
                        updatedFiles[filepath] = true
×
217
                }
×
218

219
                err = writeFixesToFile(filepath, fixedYamlString)
×
220

×
221
                if err != nil {
×
222
                        logger.L().Ctx(ctx).Warning(fmt.Sprintf("Failed to write fixes to file %s, %v", filepath, err.Error()))
×
223
                        errors = append(errors, err)
×
224
                }
×
225
        }
226

227
        return len(updatedFiles), errors
×
228
}
229

230
func (h *FixHandler) getFilePathAndIndex(filePathWithIndex string) (filePath string, documentIndex int, err error) {
×
231
        splittedPath := strings.Split(filePathWithIndex, ":")
×
232
        if len(splittedPath) <= 1 {
×
233
                return "", 0, fmt.Errorf("expected to find ':' in file path")
×
234
        }
×
235

236
        filePath = splittedPath[0]
×
237
        if documentIndex, err := strconv.Atoi(splittedPath[1]); err != nil {
×
238
                return "", 0, err
×
239
        } else {
×
240
                return filePath, documentIndex, nil
×
241
        }
×
242
}
243

244
func ApplyFixToContent(ctx context.Context, yamlAsString, yamlExpression string) (fixedString string, err error) {
23✔
245
        yamlAsString = sanitizeYaml(yamlAsString)
23✔
246
        newline := determineNewlineSeparator(yamlAsString)
23✔
247

23✔
248
        yamlLines := strings.Split(yamlAsString, newline)
23✔
249

23✔
250
        originalRootNodes, err := decodeDocumentRoots(yamlAsString)
23✔
251

23✔
252
        if err != nil {
23✔
253
                return "", err
×
254
        }
×
255

256
        fixedRootNodes, err := getFixedNodes(ctx, yamlAsString, yamlExpression)
23✔
257

23✔
258
        if err != nil {
23✔
259
                return "", err
×
260
        }
×
261

262
        fixInfo := getFixInfo(ctx, originalRootNodes, fixedRootNodes)
23✔
263

23✔
264
        fixedYamlLines := getFixedYamlLines(yamlLines, fixInfo, newline)
23✔
265

23✔
266
        fixedString = getStringFromSlice(fixedYamlLines, newline)
23✔
267
        fixedString = revertSanitizeYaml(fixedString)
23✔
268

23✔
269
        return fixedString, nil
23✔
270
}
271

272
func (h *FixHandler) getFileYamlExpressions(resourcesToFix []ResourceFixInfo) map[string]string {
×
273
        fileYamlExpressions := make(map[string]string, 0)
×
274
        for _, toPin := range resourcesToFix {
×
275
                resourceToFix := toPin
×
276

×
277
                singleExpression := reduceYamlExpressions(&resourceToFix)
×
278
                resourceFilePath := resourceToFix.FilePath
×
279

×
280
                if _, pathExistsInMap := fileYamlExpressions[resourceFilePath]; !pathExistsInMap {
×
281
                        fileYamlExpressions[resourceFilePath] = singleExpression
×
282
                } else {
×
283
                        fileYamlExpressions[resourceFilePath] = joinStrings(fileYamlExpressions[resourceFilePath], " | ", singleExpression)
×
284
                }
×
285

286
        }
287

288
        return fileYamlExpressions
×
289
}
290

291
func (rfi *ResourceFixInfo) addYamlExpressionsFromResourceAssociatedControl(documentIndex int, ac *resourcesresults.ResourceAssociatedControl, skipUserValues bool) {
×
292
        for _, rule := range ac.ResourceAssociatedRules {
×
293
                if !rule.GetStatus(nil).IsFailed() {
×
294
                        continue
×
295
                }
296

297
                for _, rulePaths := range rule.Paths {
×
298
                        if rulePaths.FixPath.Path == "" {
×
299
                                continue
×
300
                        }
301
                        if strings.HasPrefix(rulePaths.FixPath.Value, UserValuePrefix) && skipUserValues {
×
302
                                continue
×
303
                        }
304

305
                        yamlExpression := FixPathToValidYamlExpression(rulePaths.FixPath.Path, rulePaths.FixPath.Value, documentIndex)
×
306
                        rfi.YamlExpressions[yamlExpression] = rulePaths.FixPath
×
307
                }
308
        }
309
}
310

311
// reduceYamlExpressions reduces the number of yaml expressions to a single one
312
func reduceYamlExpressions(resource *ResourceFixInfo) string {
4✔
313
        expressions := make([]string, 0, len(resource.YamlExpressions))
4✔
314
        for expr := range resource.YamlExpressions {
9✔
315
                expressions = append(expressions, expr)
5✔
316
        }
5✔
317
        sort.Strings(expressions)
4✔
318
        return strings.Join(expressions, " | ")
4✔
319
}
320

321
func FixPathToValidYamlExpression(fixPath, value string, documentIndexInYaml int) string {
3✔
322
        isStringValue := true
3✔
323
        if _, err := strconv.ParseBool(value); err == nil {
4✔
324
                isStringValue = false
1✔
325
        } else if _, err := strconv.ParseFloat(value, 64); err == nil {
4✔
326
                isStringValue = false
1✔
327
        } else if _, err := strconv.Atoi(value); err == nil {
2✔
328
                isStringValue = false
×
329
        }
×
330

331
        // Strings should be quoted
332
        if isStringValue {
4✔
333
                value = fmt.Sprintf("\"%s\"", value)
1✔
334
        }
1✔
335

336
        // select document index and add a dot for the root node
337
        return fmt.Sprintf("select(di==%d).%s |= %s", documentIndexInYaml, fixPath, value)
3✔
338
}
339

340
func joinStrings(inputStrings ...string) string {
5✔
341
        return strings.Join(inputStrings, "")
5✔
342
}
5✔
343

344
func GetFileString(filepath string) (string, error) {
2✔
345
        bytes, err := os.ReadFile(filepath)
2✔
346

2✔
347
        if err != nil {
3✔
348
                return "", fmt.Errorf("Error reading file %s", filepath)
1✔
349
        }
1✔
350

351
        return string(bytes), nil
1✔
352
}
353

354
func writeFixesToFile(filepath, content string) error {
×
355
        err := os.WriteFile(filepath, []byte(content), 0644) //nolint:gosec
×
356

×
357
        if err != nil {
×
358
                return fmt.Errorf("Error writing fixes to file: %w", err)
×
359
        }
×
360

361
        return nil
×
362
}
363

364
func determineNewlineSeparator(contents string) string {
27✔
365
        switch {
27✔
366
        case strings.Contains(contents, windowsNewline):
2✔
367
                return windowsNewline
2✔
368
        default:
25✔
369
                return unixNewline
25✔
370
        }
371
}
372

373
// sanitizeYaml receives a YAML file as a string, sanitizes it and returns the result
374
//
375
// Callers should remember to call the corresponding revertSanitizeYaml function.
376
//
377
// It applies the following sanitization:
378
//
379
// - Since `yaml/v3` fails to serialize documents starting with a document
380
// separator, we comment it out to be compatible.
381
func sanitizeYaml(fileAsString string) string {
28✔
382
        if len(fileAsString) < 3 {
30✔
383
                return fileAsString
2✔
384
        }
2✔
385

386
        if fileAsString[:3] == "---" {
28✔
387
                fileAsString = "# " + fileAsString
2✔
388
        }
2✔
389
        return fileAsString
26✔
390
}
391

392
// revertSanitizeYaml receives a sanitized YAML file as a string and reverts the applied sanitization
393
//
394
// For sanitization details, refer to the sanitizeYaml() function.
395
func revertSanitizeYaml(fixedYamlString string) string {
23✔
396
        if len(fixedYamlString) < 3 {
23✔
NEW
397
                return fixedYamlString
×
NEW
398
        }
×
399

400
        if fixedYamlString[:5] == "# ---" {
24✔
401
                fixedYamlString = fixedYamlString[2:]
1✔
402
        }
1✔
403
        return fixedYamlString
23✔
404
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc