mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-15 02:20:03 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c717a9233b | ||
|
|
e37f47de3a | ||
|
|
2ce37bd66e | ||
|
|
13c760c116 | ||
|
|
c6261e45a8 | ||
|
|
0c06b6c3e6 | ||
|
|
18a9ac3d6e | ||
|
|
2bfe2a590c | ||
|
|
fb54f4e6cf | ||
|
|
9025ba5537 | ||
|
|
0c23579db7 | ||
|
|
a755f365df | ||
|
|
15f7b9f954 | ||
|
|
92a2704fa6 | ||
|
|
a3defe3025 | ||
|
|
2be0ef48d8 | ||
|
|
c97513e4e8 | ||
|
|
1757c891aa | ||
|
|
b02410184e | ||
|
|
b4a6a18a56 | ||
|
|
13ca0027a2 | ||
|
|
93b626bb1e | ||
|
|
6b4310cd88 | ||
|
|
c883a297b3 | ||
|
|
3af351d91f | ||
|
|
93cde0f1a0 | ||
|
|
02720d32dd | ||
|
|
ebada00cf1 | ||
|
|
3b68fc94d1 | ||
|
|
10d534b5bf | ||
|
|
2d740fbf4f | ||
|
|
d0728676ee | ||
|
|
8856c84a17 | ||
|
|
0c87ff6843 | ||
|
|
a48d9be386 | ||
|
|
3c93c2c45c | ||
|
|
77e0a04c99 | ||
|
|
b8762b924c | ||
|
|
025e75213a | ||
|
|
1a3a58a309 | ||
|
|
19438e6143 | ||
|
|
284c8c737b | ||
|
|
3441a65290 | ||
|
|
773e43b1e1 | ||
|
|
ddc0b2daf2 | ||
|
|
596686602c | ||
|
|
5bb0c97f8f | ||
|
|
256db4abfb | ||
|
|
3546961a5e | ||
|
|
e6dc7c2367 | ||
|
|
07fa3b4589 | ||
|
|
d6ed4b1aca | ||
|
|
69846bb4c0 | ||
|
|
2e5ad85fe0 | ||
|
|
1025431d64 | ||
|
|
1a863473e7 | ||
|
|
28a44ac531 | ||
|
|
cf484c328b | ||
|
|
668514e08d | ||
|
|
dc45efb6ef | ||
|
|
6d3844f187 | ||
|
|
4d6e85d4c7 | ||
|
|
d336f4484c | ||
|
|
bf263d8d51 | ||
|
|
cc3cf1932c | ||
|
|
6a4dc79689 | ||
|
|
8c189f6e3c | ||
|
|
b309cfca7a | ||
|
|
c4b3ef5b80 | ||
|
|
aba978e94a | ||
|
|
a49781e9a8 | ||
|
|
3ba19f55f1 | ||
|
|
40a9b9406d | ||
|
|
d6b8f5862f | ||
|
|
09f13c05e1 | ||
|
|
b1c8872a29 | ||
|
|
22052f5869 | ||
|
|
afce43add6 | ||
|
|
4752364699 | ||
|
|
6ec974f996 | ||
|
|
ebf1486a7d | ||
|
|
4d954b2ab0 | ||
|
|
4d155a6b4f |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Keep CRLF newlines in appropriate test files to have reproducible tests
|
||||
core/pkg/fixhandler/testdata/inserts/*-crlf-newlines.yaml text eol=crlf
|
||||
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,33 +2,32 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
# Description
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
# Environment
|
||||
OS: the OS + version you’re running Kubescape on, e.g Ubuntu 22.04 LTS
|
||||
Version: the version that Kubescape reports when you run `kubescape version`
|
||||
```
|
||||
Your current version is:
|
||||
```
|
||||
|
||||
OS: ` ` <!-- the OS + version you’re running Kubescape on, e.g Ubuntu 22.04 LTS -->
|
||||
Version: ` ` <!-- the version that Kubescape reports when you run `kubescape version` -->
|
||||
|
||||
# Steps To Reproduce
|
||||
<!--
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
# Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
# Actual Behavior
|
||||
A clear and concise description of what happened. If applicable, add screenshots to help explain your problem.
|
||||
<!-- A clear and concise description of what happened. If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
# Additional context
|
||||
Add any other context about the problem here.
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,18 +2,23 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'feature'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Is your feature request related to a problem? Please describe.**</br>
|
||||
> A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like.**</br>
|
||||
> A clear and concise description of what you want to happen.
|
||||
## Overview
|
||||
<!-- A brief overview of the related current state -->
|
||||
|
||||
**Describe alternatives you've considered.**</br>
|
||||
> A clear and concise description of any alternative solutions or features you've considered.
|
||||
## Problem
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Additional context.**</br>
|
||||
> Add any other context or screenshots about the feature request here.
|
||||
## Solution
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
## Alternatives
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
## Additional context
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
||||
39
.github/PULL_REQUEST_TEMPLATE.md
vendored
39
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,13 +1,39 @@
|
||||
## Describe your changes
|
||||
## Overview
|
||||
<!-- Please provide a brief overview of the changes made in this pull request. e.g. current behavior/future behavior -->
|
||||
|
||||
## Screenshots - If Any (Optional)
|
||||
<!--
|
||||
## Additional Information
|
||||
|
||||
## This PR fixes:
|
||||
> Any additional information that may be useful for reviewers to know
|
||||
-->
|
||||
|
||||
* Resolved #
|
||||
<!--
|
||||
## How to Test
|
||||
|
||||
> Please provide instructions on how to test the changes made in this pull request
|
||||
-->
|
||||
|
||||
<!--
|
||||
## Examples/Screenshots
|
||||
|
||||
> Here you add related screenshots
|
||||
-->
|
||||
|
||||
<!--
|
||||
## Related issues/PRs:
|
||||
|
||||
Here you add related issues and PRs.
|
||||
If this resolved an issue, write "Resolved #<issue number>
|
||||
|
||||
e.g. If this PR resolves issues 1 and 2, it should look as follows:
|
||||
* Resolved #1
|
||||
* Resolved #2
|
||||
-->
|
||||
|
||||
<!--
|
||||
## Checklist before requesting a review
|
||||
<!-- put an [x] in the box to get it checked -->
|
||||
|
||||
put an [x] in the box to get it checked
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||
@@ -16,3 +42,6 @@
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
|
||||
**Please open the PR against the `dev` branch (Unless the PR contains only documentation changes)**
|
||||
|
||||
-->
|
||||
|
||||
14
.github/workflows/01-golang-lint.yaml
vendored
14
.github/workflows/01-golang-lint.yaml
vendored
@@ -5,10 +5,19 @@ on:
|
||||
- dev
|
||||
pull_request:
|
||||
types: [ edited, opened, synchronize, reopened ]
|
||||
branches: [ master, dev ]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
- 'dev'
|
||||
paths-ignore:
|
||||
- '**.yaml'
|
||||
- '**.md'
|
||||
- '**.sh'
|
||||
- 'website/*'
|
||||
- 'examples/*'
|
||||
- 'docs/*'
|
||||
- 'build/*'
|
||||
- '.github/*'
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
@@ -20,13 +29,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.19
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install libgit2
|
||||
run: make libgit2
|
||||
- name: golangci-lint
|
||||
continue-on-error: true
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
|
||||
11
.github/workflows/build.yaml
vendored
11
.github/workflows/build.yaml
vendored
@@ -2,9 +2,18 @@ name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
paths-ignore:
|
||||
- '**.yaml'
|
||||
- '**.md'
|
||||
- '**.sh'
|
||||
- 'website/*'
|
||||
- 'examples/*'
|
||||
- 'docs/*'
|
||||
- 'build/*'
|
||||
- '.github/*'
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
|
||||
8
.github/workflows/build_dev.yaml
vendored
8
.github/workflows/build_dev.yaml
vendored
@@ -4,8 +4,14 @@ on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
paths-ignore:
|
||||
# Do not run the pipeline if only Markdown files changed
|
||||
- '**.yaml'
|
||||
- '**.md'
|
||||
- '**.sh'
|
||||
- 'website/*'
|
||||
- 'examples/*'
|
||||
- 'docs/*'
|
||||
- 'build/*'
|
||||
- '.github/*'
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
|
||||
22
.github/workflows/community.yml
vendored
22
.github/workflows/community.yml
vendored
@@ -1,22 +0,0 @@
|
||||
on:
|
||||
fork:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: EddieHubCommunity/gh-action-community/src/welcome@main
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: '<h3>Hi! Welcome to Kubescape. Thank you for taking the time and reporting an issue</h3>'
|
||||
pr-message: '<h3>Hi! Welcome to Kubescape. Thank you for taking the time and contributing to the open source community</h3>'
|
||||
footer: '<h4>We will try to review as soon as possible!</h4>'
|
||||
4
.github/workflows/post-release.yaml
vendored
4
.github/workflows/post-release.yaml
vendored
@@ -3,7 +3,9 @@ name: create release digests
|
||||
on:
|
||||
release:
|
||||
types: [ published]
|
||||
branches: [ master ]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
once:
|
||||
|
||||
12
.github/workflows/pr_checks.yaml
vendored
12
.github/workflows/pr_checks.yaml
vendored
@@ -2,12 +2,20 @@ name: pr-checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master, dev ]
|
||||
types: [ edited, opened, synchronize, reopened ]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
- 'dev'
|
||||
paths-ignore:
|
||||
# Do not run the pipeline if only Markdown files changed
|
||||
- '**.yaml'
|
||||
- '**.md'
|
||||
- '**.sh'
|
||||
- 'website/*'
|
||||
- 'examples/*'
|
||||
- 'docs/*'
|
||||
- 'build/*'
|
||||
- '.github/*'
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -38,4 +38,4 @@ jobs:
|
||||
release_name: ${{ inputs.release_name }}
|
||||
draft: ${{ inputs.draft }}
|
||||
prerelease: false
|
||||
|
||||
|
||||
|
||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
@@ -73,10 +73,10 @@ jobs:
|
||||
if: matrix.os != 'windows-latest'
|
||||
|
||||
- name: Test core pkg
|
||||
run: go test -tags=static -v ./...
|
||||
run: go test "-tags=static,gitenabled" -v ./...
|
||||
|
||||
- name: Test httphandler pkg
|
||||
run: cd httphandler && go test -tags=static -v ./...
|
||||
run: cd httphandler && go test "-tags=static,gitenabled" -v ./...
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
|
||||
@@ -14,23 +14,21 @@ linters:
|
||||
- gosec
|
||||
- staticcheck
|
||||
- nolintlint
|
||||
- gofmt
|
||||
- unused
|
||||
- govet
|
||||
- bodyclose
|
||||
- typecheck
|
||||
- goimports
|
||||
- ineffassign
|
||||
- gosimple
|
||||
disable:
|
||||
# temporarily disabled
|
||||
- varcheck
|
||||
- ineffassign
|
||||
- unused
|
||||
- typecheck
|
||||
- errcheck
|
||||
- govet
|
||||
- gosimple
|
||||
- deadcode
|
||||
- gofmt
|
||||
- goimports
|
||||
- bodyclose
|
||||
- dupl
|
||||
- gocognit
|
||||
- gocritic
|
||||
- goimports
|
||||
- gocognit
|
||||
- nakedret
|
||||
- revive
|
||||
- stylecheck
|
||||
@@ -38,6 +36,7 @@ linters:
|
||||
- unparam
|
||||
#- forbidigo # <- see later
|
||||
# should remain disabled
|
||||
- deadcode # deprecated linter
|
||||
- maligned
|
||||
- lll
|
||||
- gochecknoinits
|
||||
|
||||
2
Makefile
2
Makefile
@@ -11,7 +11,7 @@ libgit2:
|
||||
cd git2go; make install-static
|
||||
|
||||
# go build tags
|
||||
TAGS = "static"
|
||||
TAGS = "gitenabled,static"
|
||||
|
||||
build:
|
||||
go build -v -tags=$(TAGS) .
|
||||
|
||||
12
build.py
12
build.py
@@ -40,7 +40,7 @@ def main():
|
||||
|
||||
client_var = "github.com/kubescape/kubescape/v2/core/cautils.Client"
|
||||
client_name = os.getenv("CLIENT")
|
||||
|
||||
|
||||
# Create build directory
|
||||
build_dir = get_build_dir()
|
||||
|
||||
@@ -56,15 +56,15 @@ def main():
|
||||
ldflags += " -X {}={}".format(build_url, release_version)
|
||||
if client_name:
|
||||
ldflags += " -X {}={}".format(client_var, client_name)
|
||||
|
||||
build_command = ["go", "build", "-buildmode=pie", "-tags=static", "-o", ks_file, "-ldflags" ,ldflags]
|
||||
|
||||
build_command = ["go", "build", "-buildmode=pie", "-tags=static,gitenabled", "-o", ks_file, "-ldflags" ,ldflags]
|
||||
|
||||
print("Building kubescape and saving here: {}".format(ks_file))
|
||||
print("Build command: {}".format(" ".join(build_command)))
|
||||
|
||||
status = subprocess.call(build_command)
|
||||
check_status(status, "Failed to build kubescape")
|
||||
|
||||
|
||||
sha256 = hashlib.sha256()
|
||||
with open(ks_file, "rb") as kube:
|
||||
sha256.update(kube.read())
|
||||
@@ -74,7 +74,7 @@ def main():
|
||||
kube_sha.write(sha256.hexdigest())
|
||||
|
||||
print("Build Done")
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
45
cmd/fix/fix.go
Normal file
45
cmd/fix/fix.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/kubescape/kubescape/v2/core/meta"
|
||||
metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var fixCmdExamples = `
|
||||
Fix command is for fixing kubernetes manifest files based on a scan command output.
|
||||
Use with caution, this command will change your files in-place.
|
||||
|
||||
# Fix kubernetes YAML manifest files based on a scan command output (output.json)
|
||||
1) kubescape scan --format json --format-version v2 --output output.json
|
||||
2) kubescape fix output.json
|
||||
|
||||
`
|
||||
|
||||
func GetFixCmd(ks meta.IKubescape) *cobra.Command {
|
||||
var fixInfo metav1.FixInfo
|
||||
|
||||
fixCmd := &cobra.Command{
|
||||
Use: "fix <report output file>",
|
||||
Short: "Fix misconfiguration in files",
|
||||
Long: ``,
|
||||
Example: fixCmdExamples,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("report output file is required")
|
||||
}
|
||||
fixInfo.ReportFile = args[0]
|
||||
|
||||
return ks.Fix(&fixInfo)
|
||||
},
|
||||
}
|
||||
|
||||
fixCmd.PersistentFlags().BoolVar(&fixInfo.NoConfirm, "no-confirm", false, "No confirmation will be given to the user before applying the fix (default false)")
|
||||
fixCmd.PersistentFlags().BoolVar(&fixInfo.DryRun, "dry-run", false, "No changes will be applied (default false)")
|
||||
fixCmd.PersistentFlags().BoolVar(&fixInfo.SkipUserValues, "skip-user-values", true, "Changes which involve user-defined values will be skipped")
|
||||
|
||||
return fixCmd
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/kubescape/kubescape/v2/cmd/config"
|
||||
"github.com/kubescape/kubescape/v2/cmd/delete"
|
||||
"github.com/kubescape/kubescape/v2/cmd/download"
|
||||
"github.com/kubescape/kubescape/v2/cmd/fix"
|
||||
"github.com/kubescape/kubescape/v2/cmd/list"
|
||||
"github.com/kubescape/kubescape/v2/cmd/scan"
|
||||
"github.com/kubescape/kubescape/v2/cmd/submit"
|
||||
@@ -78,6 +79,7 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
|
||||
rootCmd.AddCommand(version.GetVersionCmd())
|
||||
rootCmd.AddCommand(config.GetConfigCmd(ks))
|
||||
rootCmd.AddCommand(update.GetUpdateCmd())
|
||||
rootCmd.AddCommand(fix.GetFixCmd(ks))
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
|
||||
}
|
||||
|
||||
scanCmd.PersistentFlags().StringVarP(&scanInfo.Credentials.Account, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
|
||||
// scanCmd.PersistentFlags().BoolVar(&scanInfo.CreateAccount, "create-account", false, "Create a Kubescape SaaS account ID account ID is not found in cache. After creating the account, the account ID will be saved in cache. In addition, the scanning results will be uploaded to the Kubescape SaaS")
|
||||
scanCmd.PersistentFlags().BoolVar(&scanInfo.CreateAccount, "create-account", false, "Create a Kubescape SaaS account ID account ID is not found in cache. After creating the account, the account ID will be saved in cache. In addition, the scanning results will be uploaded to the Kubescape SaaS")
|
||||
scanCmd.PersistentFlags().StringVarP(&scanInfo.Credentials.ClientID, "client-id", "", "", "Kubescape SaaS client ID. Default will load client ID from cache, read more - https://hub.armosec.io/docs/authentication")
|
||||
scanCmd.PersistentFlags().StringVarP(&scanInfo.Credentials.SecretKey, "secret-key", "", "", "Kubescape SaaS secret key. Default will load secret key from cache, read more - https://hub.armosec.io/docs/authentication")
|
||||
scanCmd.PersistentFlags().StringVarP(&scanInfo.KubeContext, "kube-context", "", "", "Kube context. Default will use the current-context")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"testing"
|
||||
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
)
|
||||
|
||||
// Test_validateControlScanInfo tests how scan info is validated for the `scan control` command
|
||||
|
||||
7
cmd/version/git_native_disabled.go
Normal file
7
cmd/version/git_native_disabled.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !gitenabled
|
||||
|
||||
package version
|
||||
|
||||
func isGitEnabled() bool {
|
||||
return false
|
||||
}
|
||||
7
cmd/version/git_native_enabled.go
Normal file
7
cmd/version/git_native_enabled.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build gitenabled
|
||||
|
||||
package version
|
||||
|
||||
func isGitEnabled() bool {
|
||||
return true
|
||||
}
|
||||
@@ -16,7 +16,11 @@ func GetVersionCmd() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := cautils.NewIVersionCheckHandler()
|
||||
v.CheckLatestVersion(cautils.NewVersionCheckRequest(cautils.BuildNumber, "", "", "version"))
|
||||
fmt.Fprintln(os.Stdout, "Your current version is: "+cautils.BuildNumber)
|
||||
fmt.Fprintf(os.Stdout,
|
||||
"Your current version is: %s [git enabled in build: %t]\n",
|
||||
cautils.BuildNumber,
|
||||
isGitEnabled(),
|
||||
)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package getter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -26,8 +25,8 @@ func SaveInFile(policy interface{}, pathStr string) error {
|
||||
if os.IsNotExist(err) {
|
||||
pathDir := path.Dir(pathStr)
|
||||
// pathDir could contain subdirectories
|
||||
if err := os.MkdirAll(pathDir, 0755); err != nil {
|
||||
return err
|
||||
if erm := os.MkdirAll(pathDir, 0755); erm != nil {
|
||||
return erm
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
@@ -41,13 +40,6 @@ func SaveInFile(policy interface{}, pathStr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONDecoder returns JSON decoder for given string
|
||||
func JSONDecoder(origin string) *json.Decoder {
|
||||
dec := json.NewDecoder(strings.NewReader(origin))
|
||||
dec.UseNumber()
|
||||
return dec
|
||||
}
|
||||
|
||||
func HttpDelete(httpClient *http.Client, fullURL string, headers map[string]string) (string, error) {
|
||||
|
||||
req, err := http.NewRequest("DELETE", fullURL, nil)
|
||||
@@ -66,6 +58,7 @@ func HttpDelete(httpClient *http.Client, fullURL string, headers map[string]stri
|
||||
}
|
||||
return respStr, nil
|
||||
}
|
||||
|
||||
func HttpGetter(httpClient *http.Client, fullURL string, headers map[string]string) (string, error) {
|
||||
|
||||
req, err := http.NewRequest("GET", fullURL, nil)
|
||||
|
||||
26
core/cautils/getter/json.go
Normal file
26
core/cautils/getter/json.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
stdjson "encoding/json"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var (
|
||||
json jsoniter.API
|
||||
)
|
||||
|
||||
func init() {
|
||||
// NOTE(fredbi): attention, this configuration rounds floats down to 6 digits
|
||||
// For finer-grained config, see: https://pkg.go.dev/github.com/json-iterator/go#section-readme
|
||||
json = jsoniter.ConfigFastest
|
||||
}
|
||||
|
||||
// JSONDecoder returns JSON decoder for given string
|
||||
func JSONDecoder(origin string) *stdjson.Decoder {
|
||||
dec := stdjson.NewDecoder(strings.NewReader(origin))
|
||||
dec.UseNumber()
|
||||
return dec
|
||||
}
|
||||
32
core/cautils/getter/json_test.go
Normal file
32
core/cautils/getter/json_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJSONDecoder(t *testing.T) {
|
||||
t.Run("should decode json string", func(t *testing.T) {
|
||||
const input = `"xyz"`
|
||||
d := JSONDecoder(input)
|
||||
var receiver string
|
||||
require.NoError(t, d.Decode(&receiver))
|
||||
require.Equal(t, "xyz", receiver)
|
||||
})
|
||||
|
||||
t.Run("should decode json number", func(t *testing.T) {
|
||||
const input = `123.01`
|
||||
d := JSONDecoder(input)
|
||||
var receiver float64
|
||||
require.NoError(t, d.Decode(&receiver))
|
||||
require.Equal(t, 123.01, receiver)
|
||||
})
|
||||
|
||||
t.Run("requires json quotes", func(t *testing.T) {
|
||||
const input = `xyz`
|
||||
d := JSONDecoder(input)
|
||||
var receiver string
|
||||
require.Error(t, d.Decode(&receiver))
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package getter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -2,7 +2,6 @@ package getter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -15,7 +15,19 @@ import (
|
||||
// =======================================================================================================================
|
||||
// ============================================== LoadPolicy =============================================================
|
||||
// =======================================================================================================================
|
||||
var DefaultLocalStore = getCacheDir()
|
||||
var (
|
||||
DefaultLocalStore = getCacheDir()
|
||||
|
||||
ErrNotImplemented = errors.New("feature is currently not supported")
|
||||
ErrNotFound = errors.New("name not found")
|
||||
ErrNameRequired = errors.New("missing required input framework name")
|
||||
ErrIDRequired = errors.New("missing required input control ID")
|
||||
ErrFrameworkNotMatching = errors.New("framework from file not matching")
|
||||
ErrControlNotMatching = errors.New("framework from file not matching")
|
||||
|
||||
_ IPolicyGetter = &LoadPolicy{}
|
||||
_ IExceptionsGetter = &LoadPolicy{}
|
||||
)
|
||||
|
||||
func getCacheDir() string {
|
||||
defaultDirPath := ".kubescape"
|
||||
@@ -25,11 +37,12 @@ func getCacheDir() string {
|
||||
return defaultDirPath
|
||||
}
|
||||
|
||||
// Load policies from a local repository
|
||||
// LoadPolicy loads policies from a local repository.
|
||||
type LoadPolicy struct {
|
||||
filePaths []string
|
||||
}
|
||||
|
||||
// NewLoadPolicy builds a LoadPolicy.
|
||||
func NewLoadPolicy(filePaths []string) *LoadPolicy {
|
||||
return &LoadPolicy{
|
||||
filePaths: filePaths,
|
||||
@@ -38,118 +51,211 @@ func NewLoadPolicy(filePaths []string) *LoadPolicy {
|
||||
|
||||
// GetControl returns a control from the policy file.
|
||||
func (lp *LoadPolicy) GetControl(controlID string) (*reporthandling.Control, error) {
|
||||
control := &reporthandling.Control{}
|
||||
filePath := lp.filePath()
|
||||
if controlID == "" {
|
||||
return nil, ErrIDRequired
|
||||
}
|
||||
|
||||
// NOTE: this assumes that only the first path contains either a valid control descriptor or a framework descriptor
|
||||
filePath := lp.filePath()
|
||||
buf, err := os.ReadFile(filePath)
|
||||
|
||||
f, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(f, control); err != nil {
|
||||
return control, err
|
||||
// check if the file is a control descriptor: a ControlID field is populated.
|
||||
var control reporthandling.Control
|
||||
if err = json.Unmarshal(buf, &control); err == nil && control.ControlID != "" {
|
||||
if strings.EqualFold(controlID, control.ControlID) {
|
||||
return &control, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("controlID: %s: %w", controlID, ErrControlNotMatching)
|
||||
}
|
||||
|
||||
if controlID == "" || strings.EqualFold(controlID, control.ControlID) {
|
||||
return control, nil
|
||||
}
|
||||
|
||||
framework, err := lp.GetFramework(control.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("control from file not matching")
|
||||
// check if the file is a framework descriptor
|
||||
var framework reporthandling.Framework
|
||||
if err = json.Unmarshal(buf, &framework); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, toPin := range framework.Controls {
|
||||
ctrl := toPin
|
||||
if strings.EqualFold(ctrl.ControlID, controlID) {
|
||||
control = &ctrl
|
||||
|
||||
break
|
||||
if strings.EqualFold(ctrl.ControlID, controlID) {
|
||||
return &ctrl, nil
|
||||
}
|
||||
}
|
||||
|
||||
return control, nil
|
||||
return nil, fmt.Errorf("controlID: %s: %w", controlID, ErrControlNotMatching)
|
||||
}
|
||||
|
||||
// GetFramework retrieves a framework configuration from the policy paths.
|
||||
func (lp *LoadPolicy) GetFramework(frameworkName string) (*reporthandling.Framework, error) {
|
||||
var framework reporthandling.Framework
|
||||
var err error
|
||||
if frameworkName == "" {
|
||||
return nil, ErrNameRequired
|
||||
}
|
||||
|
||||
for _, filePath := range lp.filePaths {
|
||||
framework = reporthandling.Framework{}
|
||||
f, err := os.ReadFile(filePath)
|
||||
buf, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = json.Unmarshal(f, &framework); err != nil {
|
||||
|
||||
var framework reporthandling.Framework
|
||||
if err = json.Unmarshal(buf, &framework); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.EqualFold(frameworkName, framework.Name) {
|
||||
break
|
||||
return &framework, nil
|
||||
}
|
||||
}
|
||||
if frameworkName != "" && !strings.EqualFold(frameworkName, framework.Name) {
|
||||
|
||||
return nil, fmt.Errorf("framework from file not matching")
|
||||
}
|
||||
return &framework, err
|
||||
return nil, fmt.Errorf("framework: %s: %w", frameworkName, ErrFrameworkNotMatching)
|
||||
}
|
||||
|
||||
// GetFrameworks returns all configured framework descriptors.
|
||||
func (lp *LoadPolicy) GetFrameworks() ([]reporthandling.Framework, error) {
|
||||
frameworks := []reporthandling.Framework{}
|
||||
var err error
|
||||
return frameworks, err
|
||||
}
|
||||
frameworks := make([]reporthandling.Framework, 0, 10)
|
||||
seenFws := make(map[string]struct{})
|
||||
|
||||
func (lp *LoadPolicy) ListFrameworks() ([]string, error) {
|
||||
fwNames := []string{}
|
||||
framework := &reporthandling.Framework{}
|
||||
for _, f := range lp.filePaths {
|
||||
file, err := os.ReadFile(f)
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(file, framework); err == nil {
|
||||
if !contains(fwNames, framework.Name) {
|
||||
fwNames = append(fwNames, framework.Name)
|
||||
}
|
||||
}
|
||||
buf, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var framework reporthandling.Framework
|
||||
if err = json.Unmarshal(buf, &framework); err != nil {
|
||||
// ignore invalid framework files
|
||||
continue
|
||||
}
|
||||
|
||||
// dedupe
|
||||
_, alreadyLoaded := seenFws[framework.Name]
|
||||
if alreadyLoaded {
|
||||
continue
|
||||
}
|
||||
|
||||
seenFws[framework.Name] = struct{}{}
|
||||
frameworks = append(frameworks, framework)
|
||||
}
|
||||
return fwNames, nil
|
||||
|
||||
return frameworks, nil
|
||||
}
|
||||
|
||||
// ListFrameworks lists the names of all configured frameworks in this policy.
|
||||
func (lp *LoadPolicy) ListFrameworks() ([]string, error) {
|
||||
frameworkNames := make([]string, 0, 10)
|
||||
|
||||
for _, f := range lp.filePaths {
|
||||
buf, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var framework reporthandling.Framework
|
||||
if err := json.Unmarshal(buf, &framework); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if framework.Name == "" || contains(frameworkNames, framework.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
frameworkNames = append(frameworkNames, framework.Name)
|
||||
}
|
||||
|
||||
return frameworkNames, nil
|
||||
}
|
||||
|
||||
// ListControls returns the list of controls for this framework.
|
||||
//
|
||||
// At this moment, controls are listed for one single configured framework.
|
||||
func (lp *LoadPolicy) ListControls() ([]string, error) {
|
||||
// TODO - Support
|
||||
return []string{}, fmt.Errorf("loading controls list from file is not supported")
|
||||
}
|
||||
|
||||
func (lp *LoadPolicy) GetExceptions(clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
controlIDs := make([]string, 0, 100)
|
||||
filePath := lp.filePath()
|
||||
exception := []armotypes.PostureExceptionPolicy{}
|
||||
f, err := os.ReadFile(filePath)
|
||||
buf, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(f, &exception)
|
||||
var framework reporthandling.Framework
|
||||
if err = json.Unmarshal(buf, &framework); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ctrl := range framework.Controls {
|
||||
controlIDs = append(controlIDs, ctrl.ControlID)
|
||||
}
|
||||
|
||||
return controlIDs, nil
|
||||
}
|
||||
|
||||
// GetExceptions retrieves configured exceptions.
|
||||
//
|
||||
// NOTE: the cluster parameter is not used at this moment.
|
||||
func (lp *LoadPolicy) GetExceptions(_ /* clusterName */ string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
// NOTE: this assumes that the first path contains a valid exceptions descriptor
|
||||
filePath := lp.filePath()
|
||||
|
||||
buf, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exception := make([]armotypes.PostureExceptionPolicy, 0, 300)
|
||||
err = json.Unmarshal(buf, &exception)
|
||||
|
||||
return exception, err
|
||||
}
|
||||
|
||||
func (lp *LoadPolicy) GetControlsInputs(clusterName string) (map[string][]string, error) {
|
||||
// GetControlsInputs retrieves the map of control configs.
|
||||
//
|
||||
// NOTE: the cluster parameter is not used at this moment.
|
||||
func (lp *LoadPolicy) GetControlsInputs(_ /* clusterName */ string) (map[string][]string, error) {
|
||||
// NOTE: this assumes that only the first path contains a valid control inputs descriptor
|
||||
filePath := lp.filePath()
|
||||
accountConfig := &armotypes.CustomerConfig{}
|
||||
f, err := os.ReadFile(filePath)
|
||||
fileName := filepath.Base(filePath)
|
||||
|
||||
buf, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
formattedError := fmt.Errorf("Error opening %s file, \"controls-config\" will be downloaded from ARMO management portal", fileName)
|
||||
formattedError := fmt.Errorf(
|
||||
`Error opening %s file, "controls-config" will be downloaded from ARMO management portal`,
|
||||
fileName,
|
||||
)
|
||||
|
||||
return nil, formattedError
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(f, &accountConfig.Settings.PostureControlInputs); err == nil {
|
||||
return accountConfig.Settings.PostureControlInputs, nil
|
||||
controlInputs := make(map[string][]string, 100) // from armotypes.Settings.PostureControlInputs
|
||||
if err = json.Unmarshal(buf, &controlInputs); err != nil {
|
||||
formattedError := fmt.Errorf(
|
||||
`Error reading %s file, %v, "controls-config" will be downloaded from ARMO management portal`,
|
||||
fileName, err,
|
||||
)
|
||||
|
||||
return nil, formattedError
|
||||
}
|
||||
|
||||
formattedError := fmt.Errorf("Error reading %s file, %s, \"controls-config\" will be downloaded from ARMO management portal", fileName, err.Error())
|
||||
return controlInputs, nil
|
||||
}
|
||||
|
||||
return nil, formattedError
|
||||
// GetAttackTracks yields the attack tracks from a config file.
|
||||
func (lp *LoadPolicy) GetAttackTracks() ([]v1alpha1.AttackTrack, error) {
|
||||
attackTracks := make([]v1alpha1.AttackTrack, 0, 20)
|
||||
|
||||
buf, err := os.ReadFile(lp.filePath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(buf, &attackTracks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return attackTracks, nil
|
||||
}
|
||||
|
||||
// temporary support for a list of files
|
||||
@@ -159,18 +265,3 @@ func (lp *LoadPolicy) filePath() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (lp *LoadPolicy) GetAttackTracks() ([]v1alpha1.AttackTrack, error) {
|
||||
attackTracks := []v1alpha1.AttackTrack{}
|
||||
|
||||
f, err := os.ReadFile(lp.filePath())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(f, &attackTracks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return attackTracks, nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,409 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
"testing"
|
||||
|
||||
var mockFrameworkBasePath = filepath.Join("examples", "mocks", "frameworks")
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func MockNewLoadPolicy() *LoadPolicy {
|
||||
return &LoadPolicy{
|
||||
filePaths: []string{""},
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
testFramework = "MITRE"
|
||||
testControl = "C-0053"
|
||||
)
|
||||
|
||||
t.Run("with GetFramework", func(t *testing.T) {
|
||||
t.Run("should retrieve named framework", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
fw, err := p.GetFramework(testFramework)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fw)
|
||||
|
||||
require.Equal(t, testFramework, fw.Name)
|
||||
})
|
||||
|
||||
t.Run("should fail to retrieve framework", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
fw, err := p.GetFramework("wrong")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, fw)
|
||||
})
|
||||
|
||||
t.Run("edge case: should error on empty framework", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
fw, err := p.GetFramework("")
|
||||
require.ErrorIs(t, err, ErrNameRequired)
|
||||
require.Nil(t, fw)
|
||||
})
|
||||
|
||||
t.Run("edge case: corrupted json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidFramework = "invalid-fw"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidFramework)})
|
||||
fw, err := p.GetFramework(invalidFramework)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, fw)
|
||||
})
|
||||
|
||||
t.Run("edge case: missing json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidFramework = "nowheretobefound"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidFramework)})
|
||||
_, err := p.GetFramework(invalidFramework)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with GetControl", func(t *testing.T) {
|
||||
t.Run("should retrieve named control from framework", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
expectedControlName = "Access container service account"
|
||||
)
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
ctrl, err := p.GetControl(testControl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ctrl)
|
||||
|
||||
require.Equal(t, testControl, ctrl.ControlID)
|
||||
require.Equal(t, expectedControlName, ctrl.Name)
|
||||
})
|
||||
|
||||
t.Run("with single control descriptor", func(t *testing.T) {
|
||||
const (
|
||||
singleControl = "C-0001"
|
||||
expectedControlName = "Forbidden Container Registries"
|
||||
)
|
||||
|
||||
t.Run("should retrieve named control from control descriptor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(singleControl)})
|
||||
ctrl, err := p.GetControl(singleControl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ctrl)
|
||||
|
||||
require.Equal(t, singleControl, ctrl.ControlID)
|
||||
require.Equal(t, expectedControlName, ctrl.Name)
|
||||
})
|
||||
|
||||
t.Run("should fail to retrieve named control from control descriptor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(singleControl)})
|
||||
ctrl, err := p.GetControl("wrong")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ctrl)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with framework descriptor", func(t *testing.T) {
|
||||
t.Run("should fail to retrieve named control", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const testControl = "wrong"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
ctrl, err := p.GetControl(testControl)
|
||||
require.ErrorIs(t, err, ErrControlNotMatching)
|
||||
require.Nil(t, ctrl)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("edge case: corrupted json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidControl = "invalid-fw"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidControl)})
|
||||
_, err := p.GetControl(invalidControl)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("edge case: missing json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidControl = "nowheretobefound"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidControl)})
|
||||
_, err := p.GetControl(invalidControl)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("edge case: should error on empty control", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
ctrl, err := p.GetControl("")
|
||||
require.ErrorIs(t, err, ErrIDRequired)
|
||||
require.Nil(t, ctrl)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with ListFrameworks", func(t *testing.T) {
|
||||
t.Run("should return all frameworks in the policy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
extraFramework = "NSA"
|
||||
attackTracks = "attack-tracks"
|
||||
)
|
||||
p := NewLoadPolicy([]string{
|
||||
testFrameworkFile(testFramework),
|
||||
testFrameworkFile(extraFramework),
|
||||
testFrameworkFile(extraFramework), // should be deduped
|
||||
testFrameworkFile(attackTracks), // should be ignored
|
||||
})
|
||||
fws, err := p.ListFrameworks()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fws, 2)
|
||||
|
||||
require.Equal(t, testFramework, fws[0])
|
||||
require.Equal(t, extraFramework, fws[1])
|
||||
})
|
||||
|
||||
t.Run("should not return an empty framework", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
extraFramework = "NSA"
|
||||
attackTracks = "attack-tracks"
|
||||
controlsInputs = "controls-inputs"
|
||||
)
|
||||
p := NewLoadPolicy([]string{
|
||||
testFrameworkFile(testFramework),
|
||||
testFrameworkFile(extraFramework),
|
||||
testFrameworkFile(attackTracks), // should be ignored
|
||||
testFrameworkFile(controlsInputs), // should be ignored
|
||||
})
|
||||
fws, err := p.ListFrameworks()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fws, 2)
|
||||
require.NotContains(t, fws, "")
|
||||
|
||||
require.Equal(t, testFramework, fws[0])
|
||||
require.Equal(t, extraFramework, fws[1])
|
||||
})
|
||||
|
||||
t.Run("should fail on file error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
extraFramework = "NSA"
|
||||
nowhere = "nowheretobeseen"
|
||||
)
|
||||
p := NewLoadPolicy([]string{
|
||||
testFrameworkFile(testFramework),
|
||||
testFrameworkFile(extraFramework),
|
||||
testFrameworkFile(nowhere), // should raise an error
|
||||
})
|
||||
fws, err := p.ListFrameworks()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, fws)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("edge case: policy without path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{})
|
||||
require.Empty(t, p.filePath())
|
||||
})
|
||||
|
||||
t.Run("with GetFrameworks", func(t *testing.T) {
|
||||
const extraFramework = "NSA"
|
||||
|
||||
t.Run("should return all configured frameworks", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{
|
||||
testFrameworkFile(testFramework),
|
||||
testFrameworkFile(extraFramework),
|
||||
})
|
||||
fws, err := p.GetFrameworks()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fws, 2)
|
||||
|
||||
require.Equal(t, testFramework, fws[0].Name)
|
||||
require.Equal(t, extraFramework, fws[1].Name)
|
||||
})
|
||||
|
||||
t.Run("should return dedupe configured frameworks", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const attackTracks = "attack-tracks"
|
||||
p := NewLoadPolicy([]string{
|
||||
testFrameworkFile(testFramework),
|
||||
testFrameworkFile(extraFramework),
|
||||
testFrameworkFile(extraFramework),
|
||||
testFrameworkFile(attackTracks), // should be ignored
|
||||
})
|
||||
fws, err := p.GetFrameworks()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fws, 2)
|
||||
|
||||
require.Equal(t, testFramework, fws[0].Name)
|
||||
require.Equal(t, extraFramework, fws[1].Name)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with ListControls", func(t *testing.T) {
|
||||
t.Run("should return controls", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
controlIDs, err := p.ListControls()
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(controlIDs), 0)
|
||||
require.Equal(t, testControl, controlIDs[0])
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with GetAttackTracks", func(t *testing.T) {
|
||||
t.Run("should return attack tracks", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const attackTracks = "attack-tracks"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(attackTracks)})
|
||||
tracks, err := p.GetAttackTracks()
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(tracks), 0)
|
||||
|
||||
for _, track := range tracks {
|
||||
require.Equal(t, "AttackTrack", track.Kind)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge case: corrupted json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidTracks = "invalid-fw"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidTracks)})
|
||||
_, err := p.GetAttackTracks()
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("edge case: missing json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidTracks = "nowheretobefound"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidTracks)})
|
||||
_, err := p.GetAttackTracks()
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with GetControlsInputs", func(t *testing.T) {
|
||||
const cluster = "dummy" // unused parameter at the moment
|
||||
|
||||
t.Run("should return control inputs for a cluster", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fixture, expected := writeTempJSONControlInputs(t)
|
||||
t.Cleanup(func() {
|
||||
_ = os.Remove(fixture)
|
||||
})
|
||||
|
||||
p := NewLoadPolicy([]string{fixture})
|
||||
inputs, err := p.GetControlsInputs(cluster)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expected, inputs)
|
||||
})
|
||||
|
||||
t.Run("edge case: corrupted json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidInputs = "invalid-fw"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidInputs)})
|
||||
_, err := p.GetControlsInputs(cluster)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("edge case: missing json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidInputs = "nowheretobefound"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidInputs)})
|
||||
_, err := p.GetControlsInputs(cluster)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with GetExceptions", func(t *testing.T) {
|
||||
const cluster = "dummy" // unused parameter at the moment
|
||||
|
||||
t.Run("should return exceptions", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const exceptions = "exceptions"
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(exceptions)})
|
||||
exceptionPolicies, err := p.GetExceptions(cluster)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Greater(t, len(exceptionPolicies), 0)
|
||||
t.Logf("len=%d", len(exceptionPolicies))
|
||||
for _, policy := range exceptionPolicies {
|
||||
require.NotEmpty(t, policy.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge case: corrupted json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidInputs = "invalid-fw"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidInputs)})
|
||||
_, err := p.GetExceptions(cluster)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("edge case: missing json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const invalidInputs = "nowheretobefound"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(invalidInputs)})
|
||||
_, err := p.GetExceptions(cluster)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testFrameworkFile(framework string) string {
|
||||
return filepath.Join(".", "testdata", fmt.Sprintf("%s.json", framework))
|
||||
}
|
||||
|
||||
func writeTempJSONControlInputs(t testing.TB) (string, map[string][]string) {
|
||||
fileName := testFrameworkFile("control-inputs")
|
||||
mock := map[string][]string{
|
||||
"key1": {
|
||||
"val1", "val2",
|
||||
},
|
||||
"key2": {
|
||||
"val3", "val4",
|
||||
},
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(mock)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(fileName, buf, 0600))
|
||||
|
||||
return fileName, mock
|
||||
}
|
||||
|
||||
85
core/cautils/getter/testdata/C-0001.json
vendored
Normal file
85
core/cautils/getter/testdata/C-0001.json
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"guid": "",
|
||||
"name": "Forbidden Container Registries",
|
||||
"attributes": {
|
||||
"armoBuiltin": true,
|
||||
"attackTracks": [
|
||||
{
|
||||
"attackTrack": "container",
|
||||
"categories": [
|
||||
"Initial access"
|
||||
]
|
||||
}
|
||||
],
|
||||
"controlTypeTags": [
|
||||
"security",
|
||||
"compliance"
|
||||
],
|
||||
"microsoftMitreColumns": [
|
||||
"Initial Access"
|
||||
]
|
||||
},
|
||||
"id": "C-0001",
|
||||
"controlID": "C-0001",
|
||||
"creationTime": "",
|
||||
"description": "In cases where the Kubernetes cluster is provided by a CSP (e.g., AKS in Azure, GKE in GCP, or EKS in AWS), compromised cloud credential can lead to the cluster takeover. Attackers may abuse cloud account credentials or IAM mechanism to the cluster’s management layer.",
|
||||
"remediation": "Limit the registries from which you pull container images from",
|
||||
"rules": [
|
||||
{
|
||||
"guid": "",
|
||||
"name": "rule-identify-blocklisted-image-registries",
|
||||
"attributes": {
|
||||
"armoBuiltin": true,
|
||||
"m$K8sThreatMatrix": "Initial Access::Compromised images in registry"
|
||||
},
|
||||
"creationTime": "",
|
||||
"rule": "package armo_builtins\nimport data\n# Check for images from blocklisted repos\n\nuntrustedImageRepo[msga] {\n\tpod := input[_]\n\tk := pod.kind\n\tk == \"Pod\"\n\tcontainer := pod.spec.containers[i]\n\tpath := sprintf(\"spec.containers[%v].image\", [format_int(i, 10)])\n\timage := container.image\n untrusted_or_public_registries(image)\n\n\tmsga := {\n\t\t\"alertMessage\": sprintf(\"image '%v' in container '%s' comes from untrusted registry\", [image, container.name]),\n\t\t\"packagename\": \"armo_builtins\",\n\t\t\"alertScore\": 2,\n\t\t\"fixPaths\": [],\n\t\t\"failedPaths\": [path],\n \"alertObject\": {\n\t\t\t\"k8sApiObjects\": [pod]\n\t\t}\n }\n}\n\nuntrustedImageRepo[msga] {\n\twl := input[_]\n\tspec_template_spec_patterns := {\"Deployment\",\"ReplicaSet\",\"DaemonSet\",\"StatefulSet\",\"Job\"}\n\tspec_template_spec_patterns[wl.kind]\n\tcontainer := wl.spec.template.spec.containers[i]\n\tpath := sprintf(\"spec.template.spec.containers[%v].image\", [format_int(i, 10)])\n\timage := container.image\n untrusted_or_public_registries(image)\n\n\tmsga := {\n\t\t\"alertMessage\": sprintf(\"image '%v' in container '%s' comes from untrusted registry\", [image, container.name]),\n\t\t\"packagename\": \"armo_builtins\",\n\t\t\"alertScore\": 2,\n\t\t\"fixPaths\": [],\n\t\t\"failedPaths\": [path],\n \"alertObject\": {\n\t\t\t\"k8sApiObjects\": [wl]\n\t\t}\n }\n}\n\nuntrustedImageRepo[msga] {\n\twl := input[_]\n\twl.kind == \"CronJob\"\n\tcontainer := wl.spec.jobTemplate.spec.template.spec.containers[i]\n\tpath := sprintf(\"spec.jobTemplate.spec.template.spec.containers[%v].image\", [format_int(i, 10)])\n\timage := container.image\n untrusted_or_public_registries(image)\n\n\tmsga := {\n\t\t\"alertMessage\": sprintf(\"image '%v' in container '%s' comes from untrusted registry\", [image, container.name]),\n\t\t\"packagename\": \"armo_builtins\",\n\t\t\"alertScore\": 2,\n\t\t\"fixPaths\": [],\n\t\t\"failedPaths\": [path],\n \"alertObject\": {\n\t\t\t\"k8sApiObjects\": [wl]\n\t\t}\n }\n}\n\nuntrusted_or_public_registries(image){\n\t# see default-config-inputs.json for list values\n\tuntrusted_registries := data.postureControlInputs.untrustedRegistries\n\trepo_prefix := untrusted_registries[_]\n\tstartswith(image, repo_prefix)\n}\n\nuntrusted_or_public_registries(image){\n\t# see default-config-inputs.json for list values\n\tpublic_registries := data.postureControlInputs.publicRegistries\n\trepo_prefix := public_registries[_]\n\tstartswith(image, repo_prefix)\n}",
|
||||
"resourceEnumerator": "",
|
||||
"ruleLanguage": "Rego",
|
||||
"match": [
|
||||
{
|
||||
"apiGroups": [
|
||||
"*"
|
||||
],
|
||||
"apiVersions": [
|
||||
"*"
|
||||
],
|
||||
"resources": [
|
||||
"Pod",
|
||||
"Deployment",
|
||||
"ReplicaSet",
|
||||
"DaemonSet",
|
||||
"StatefulSet",
|
||||
"Job",
|
||||
"CronJob"
|
||||
]
|
||||
}
|
||||
],
|
||||
"ruleDependencies": [],
|
||||
"configInputs": [
|
||||
"settings.postureControlInputs.publicRegistries",
|
||||
"settings.postureControlInputs.untrustedRegistries"
|
||||
],
|
||||
"controlConfigInputs": [
|
||||
{
|
||||
"path": "settings.postureControlInputs.publicRegistries",
|
||||
"name": "Public registries",
|
||||
"description": "Kubescape checks none of these public registries are in use."
|
||||
},
|
||||
{
|
||||
"path": "settings.postureControlInputs.untrustedRegistries",
|
||||
"name": "Registries block list",
|
||||
"description": "Kubescape checks none of the following registries are in use."
|
||||
}
|
||||
],
|
||||
"description": "Identifying if pod container images are from unallowed registries",
|
||||
"remediation": "Use images from safe registry",
|
||||
"ruleQuery": "",
|
||||
"relevantCloudProviders": null
|
||||
}
|
||||
],
|
||||
"rulesIDs": [
|
||||
""
|
||||
],
|
||||
"baseScore": 7
|
||||
}
|
||||
2832
core/cautils/getter/testdata/MITRE.json
vendored
Normal file
2832
core/cautils/getter/testdata/MITRE.json
vendored
Normal file
File diff suppressed because one or more lines are too long
2249
core/cautils/getter/testdata/NSA.json
vendored
Normal file
2249
core/cautils/getter/testdata/NSA.json
vendored
Normal file
File diff suppressed because one or more lines are too long
136
core/cautils/getter/testdata/attack-tracks.json
vendored
Normal file
136
core/cautils/getter/testdata/attack-tracks.json
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
[
|
||||
{
|
||||
"apiVersion": "regolibrary.kubescape/v1alpha1",
|
||||
"kind": "AttackTrack",
|
||||
"metadata": {
|
||||
"name": "node"
|
||||
},
|
||||
"spec": {
|
||||
"data": {
|
||||
"name": "Initial access",
|
||||
"subSteps": [
|
||||
{
|
||||
"name": "Execution",
|
||||
"subSteps": [
|
||||
{
|
||||
"name": "Persistence"
|
||||
},
|
||||
{
|
||||
"name": "Credential access"
|
||||
},
|
||||
{
|
||||
"name": "Defense evasion"
|
||||
},
|
||||
{
|
||||
"name": "Discovery"
|
||||
},
|
||||
{
|
||||
"name": "Lateral movement"
|
||||
},
|
||||
{
|
||||
"name": "Impact - data theft"
|
||||
},
|
||||
{
|
||||
"name": "Impact - data destruction"
|
||||
},
|
||||
{
|
||||
"name": "Impact - service injection"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "regolibrary.kubescape/v1alpha1",
|
||||
"kind": "AttackTrack",
|
||||
"metadata": {
|
||||
"name": "kubeapi"
|
||||
},
|
||||
"spec": {
|
||||
"data": {
|
||||
"name": "Initial access",
|
||||
"subSteps": [
|
||||
{
|
||||
"name": "Persistence"
|
||||
},
|
||||
{
|
||||
"name": "Privilege escalation"
|
||||
},
|
||||
{
|
||||
"name": "Credential access"
|
||||
},
|
||||
{
|
||||
"name": "Discovery"
|
||||
},
|
||||
{
|
||||
"name": "Lateral movement"
|
||||
},
|
||||
{
|
||||
"name": "Defense evasion"
|
||||
},
|
||||
{
|
||||
"name": "Impact - data destruction"
|
||||
},
|
||||
{
|
||||
"name": "Impact - service injection"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "regolibrary.kubescape/v1alpha1",
|
||||
"kind": "AttackTrack",
|
||||
"metadata": {
|
||||
"name": "container"
|
||||
},
|
||||
"spec": {
|
||||
"data": {
|
||||
"name": "Initial access",
|
||||
"subSteps": [
|
||||
{
|
||||
"name": "Execution",
|
||||
"subSteps": [
|
||||
{
|
||||
"name": "Privilege escalation"
|
||||
},
|
||||
{
|
||||
"name": "Credential access",
|
||||
"subSteps": [
|
||||
{
|
||||
"name": "Impact - service access"
|
||||
},
|
||||
{
|
||||
"name": "Impact - K8s API access",
|
||||
"subSteps": [
|
||||
{
|
||||
"name": "Defense evasion - KubeAPI"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Discovery"
|
||||
},
|
||||
{
|
||||
"name": "Lateral movement"
|
||||
},
|
||||
{
|
||||
"name": "Impact - Data access in container"
|
||||
},
|
||||
{
|
||||
"name": "Persistence"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Impact - service destruction"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
125
core/cautils/getter/testdata/controls-inputs.json
vendored
Normal file
125
core/cautils/getter/testdata/controls-inputs.json
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"publicRegistries": [],
|
||||
"untrustedRegistries": [],
|
||||
"listOfDangerousArtifacts": [
|
||||
"bin/bash",
|
||||
"sbin/sh",
|
||||
"bin/ksh",
|
||||
"bin/tcsh",
|
||||
"bin/zsh",
|
||||
"usr/bin/scsh",
|
||||
"bin/csh",
|
||||
"bin/busybox",
|
||||
"usr/bin/busybox"
|
||||
],
|
||||
"sensitiveKeyNames": [
|
||||
"aws_access_key_id",
|
||||
"aws_secret_access_key",
|
||||
"azure_batchai_storage_account",
|
||||
"azure_batchai_storage_key",
|
||||
"azure_batch_account",
|
||||
"azure_batch_key",
|
||||
"secret",
|
||||
"key",
|
||||
"password",
|
||||
"pwd",
|
||||
"token",
|
||||
"jwt",
|
||||
"bearer",
|
||||
"credential"
|
||||
],
|
||||
"servicesNames": [
|
||||
"nifi-service",
|
||||
"argo-server",
|
||||
"minio",
|
||||
"postgres",
|
||||
"workflow-controller-metrics",
|
||||
"weave-scope-app",
|
||||
"kubernetes-dashboard"
|
||||
],
|
||||
"memory_limit_max": [],
|
||||
"cpu_request_min": [],
|
||||
"wlKnownNames": [
|
||||
"coredns",
|
||||
"kube-proxy",
|
||||
"event-exporter-gke",
|
||||
"kube-dns",
|
||||
"17-default-backend",
|
||||
"metrics-server",
|
||||
"ca-audit",
|
||||
"ca-dashboard-aggregator",
|
||||
"ca-notification-server",
|
||||
"ca-ocimage",
|
||||
"ca-oracle",
|
||||
"ca-posture",
|
||||
"ca-rbac",
|
||||
"ca-vuln-scan",
|
||||
"ca-webhook",
|
||||
"ca-websocket",
|
||||
"clair-clair"
|
||||
],
|
||||
"sensitiveInterfaces": [
|
||||
"nifi",
|
||||
"argo-server",
|
||||
"weave-scope-app",
|
||||
"kubeflow",
|
||||
"kubernetes-dashboard",
|
||||
"jenkins",
|
||||
"prometheus-deployment"
|
||||
],
|
||||
"max_high_vulnerabilities": [
|
||||
"10"
|
||||
],
|
||||
"sensitiveValues": [
|
||||
"BEGIN \\w+ PRIVATE KEY",
|
||||
"PRIVATE KEY",
|
||||
"eyJhbGciO",
|
||||
"JWT",
|
||||
"Bearer",
|
||||
"_key_",
|
||||
"_secret_"
|
||||
],
|
||||
"memory_request_max": [],
|
||||
"memory_request_min": [],
|
||||
"cpu_request_max": [],
|
||||
"cpu_limit_max": [],
|
||||
"cpu_limit_min": [],
|
||||
"insecureCapabilities": [
|
||||
"SETPCAP",
|
||||
"NET_ADMIN",
|
||||
"NET_RAW",
|
||||
"SYS_MODULE",
|
||||
"SYS_RAWIO",
|
||||
"SYS_PTRACE",
|
||||
"SYS_ADMIN",
|
||||
"SYS_BOOT",
|
||||
"MAC_OVERRIDE",
|
||||
"MAC_ADMIN",
|
||||
"PERFMON",
|
||||
"ALL",
|
||||
"BPF"
|
||||
],
|
||||
"max_critical_vulnerabilities": [
|
||||
"5"
|
||||
],
|
||||
"sensitiveValuesAllowed": [],
|
||||
"memory_limit_min": [],
|
||||
"recommendedLabels": [
|
||||
"app",
|
||||
"tier",
|
||||
"phase",
|
||||
"version",
|
||||
"owner",
|
||||
"env"
|
||||
],
|
||||
"k8sRecommendedLabels": [
|
||||
"app.kubernetes.io/name",
|
||||
"app.kubernetes.io/instance",
|
||||
"app.kubernetes.io/version",
|
||||
"app.kubernetes.io/component",
|
||||
"app.kubernetes.io/part-of",
|
||||
"app.kubernetes.io/managed-by",
|
||||
"app.kubernetes.io/created-by"
|
||||
],
|
||||
"imageRepositoryAllowList": []
|
||||
}
|
||||
6407
core/cautils/getter/testdata/exceptions.json
vendored
Normal file
6407
core/cautils/getter/testdata/exceptions.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
core/cautils/getter/testdata/invalid-fw.json
vendored
Normal file
3
core/cautils/getter/testdata/invalid-fw.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"guid": "",
|
||||
}
|
||||
22
core/cautils/git_native_disabled.go
Normal file
22
core/cautils/git_native_disabled.go
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build !gitenabled
|
||||
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/kubescape/go-git-url/apis"
|
||||
)
|
||||
|
||||
var ErrFatalNotSupportedByBuild = errors.New(`git scan not supported by this build. Build with tag "gitenabled" to enable the git scan feature`)
|
||||
|
||||
type gitRepository struct {
|
||||
}
|
||||
|
||||
func newGitRepository(root string) (*gitRepository, error) {
|
||||
return &gitRepository{}, ErrWarnNotSupportedByBuild
|
||||
}
|
||||
|
||||
func (g *gitRepository) GetFileLastCommit(filePath string) (*apis.Commit, error) {
|
||||
return nil, ErrFatalNotSupportedByBuild
|
||||
}
|
||||
11
core/cautils/git_native_disabled_test.go
Normal file
11
core/cautils/git_native_disabled_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !gitenabled
|
||||
|
||||
package cautils
|
||||
|
||||
func (s *LocalGitRepositoryTestSuite) TestGetLastCommit() {
|
||||
s.T().Log("warn: skipped testing native git functionality [GetLastCommit]")
|
||||
}
|
||||
|
||||
func (s *LocalGitRepositoryTestSuite) TestGetFileLastCommit() {
|
||||
s.T().Log("warn: skipped testing native git functionality [GetFileLastCommit]")
|
||||
}
|
||||
141
core/cautils/git_native_enabled.go
Normal file
141
core/cautils/git_native_enabled.go
Normal file
@@ -0,0 +1,141 @@
|
||||
//go:build gitenabled
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kubescape/go-git-url/apis"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
)
|
||||
|
||||
type gitRepository struct {
|
||||
git2GoRepo *git2go.Repository
|
||||
fileToLastCommit map[string]*git2go.Commit
|
||||
}
|
||||
|
||||
func newGitRepository(root string) (*gitRepository, error) {
|
||||
git2GoRepo, err := git2go.OpenRepository(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gitRepository{
|
||||
git2GoRepo: git2GoRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *gitRepository) GetFileLastCommit(filePath string) (*apis.Commit, error) {
|
||||
if len(g.fileToLastCommit) == 0 {
|
||||
filePathToCommitTime := map[string]time.Time{}
|
||||
filePathToCommit := map[string]*git2go.Commit{}
|
||||
allCommits, _ := g.getAllCommits()
|
||||
|
||||
// builds a map of all files to their last commit
|
||||
for _, commit := range allCommits {
|
||||
// Ignore merge commits (2+ parents)
|
||||
if commit.ParentCount() <= 1 {
|
||||
tree, err := commit.Tree()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// ParentCount can be either 1 or 0 (initial commit)
|
||||
// In case it's the initial commit, prevTree is nil
|
||||
var prevTree *git2go.Tree
|
||||
if commit.ParentCount() == 1 {
|
||||
prevCommit := commit.Parent(0)
|
||||
prevTree, err = prevCommit.Tree()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
diff, err := g.git2GoRepo.DiffTreeToTree(prevTree, tree, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
numDeltas, err := diff.NumDeltas()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < numDeltas; i++ {
|
||||
delta, err := diff.Delta(i)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
deltaFilePath := delta.NewFile.Path
|
||||
commitTime := commit.Author().When
|
||||
|
||||
// In case we have the commit information for the file which is not the latest - we override it
|
||||
if currentCommitTime, exists := filePathToCommitTime[deltaFilePath]; exists {
|
||||
if currentCommitTime.Before(commitTime) {
|
||||
filePathToCommitTime[deltaFilePath] = commitTime
|
||||
filePathToCommit[deltaFilePath] = commit
|
||||
}
|
||||
} else {
|
||||
filePathToCommitTime[deltaFilePath] = commitTime
|
||||
filePathToCommit[deltaFilePath] = commit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.fileToLastCommit = filePathToCommit
|
||||
}
|
||||
|
||||
if relevantCommit, exists := g.fileToLastCommit[filePath]; exists {
|
||||
return g.getCommit(relevantCommit), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to get commit information for file: %s", filePath)
|
||||
}
|
||||
|
||||
func (g *gitRepository) getAllCommits() ([]*git2go.Commit, error) {
|
||||
logItr, itrErr := g.git2GoRepo.Walk()
|
||||
if itrErr != nil {
|
||||
|
||||
return nil, itrErr
|
||||
}
|
||||
|
||||
pushErr := logItr.PushHead()
|
||||
if pushErr != nil {
|
||||
return nil, pushErr
|
||||
}
|
||||
|
||||
var allCommits []*git2go.Commit
|
||||
err := logItr.Iterate(func(commit *git2go.Commit) bool {
|
||||
if commit != nil {
|
||||
allCommits = append(allCommits, commit)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return allCommits, nil
|
||||
}
|
||||
|
||||
func (g *gitRepository) getCommit(commit *git2go.Commit) *apis.Commit {
|
||||
return &apis.Commit{
|
||||
SHA: commit.Id().String(),
|
||||
Author: apis.Committer{
|
||||
Name: commit.Author().Name,
|
||||
Email: commit.Author().Email,
|
||||
Date: commit.Author().When,
|
||||
},
|
||||
Message: commit.Message(),
|
||||
Committer: apis.Committer{},
|
||||
Files: []apis.Files{},
|
||||
}
|
||||
}
|
||||
44
core/cautils/git_native_enabled_test.go
Normal file
44
core/cautils/git_native_enabled_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build gitenabled
|
||||
package cautils
|
||||
|
||||
func (s *LocalGitRepositoryTestSuite) TestGetLastCommit() {
|
||||
if localRepo, err := NewLocalGitRepository(s.gitRepositoryPaths["localrepo"]); s.NoError(err) {
|
||||
if commit, err := localRepo.GetLastCommit(); s.NoError(err) {
|
||||
s.Equal("7e09312b8017695fadcd606882e3779f10a5c832", commit.SHA)
|
||||
s.Equal("Amir Malka", commit.Author.Name)
|
||||
s.Equal("amirm@armosec.io", commit.Author.Email)
|
||||
s.Equal("2022-05-22 19:11:57 +0300 +0300", commit.Author.Date.String())
|
||||
s.Equal("added file B\n", commit.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalGitRepositoryTestSuite) TestGetFileLastCommit() {
|
||||
s.Run("fileA", func() {
|
||||
if localRepo, err := NewLocalGitRepository(s.gitRepositoryPaths["localrepo"]); s.NoError(err) {
|
||||
|
||||
if commit, err := localRepo.GetFileLastCommit("fileA"); s.NoError(err) {
|
||||
s.Equal("9fae4be19624297947d2b605cefbff516628612d", commit.SHA)
|
||||
s.Equal("Amir Malka", commit.Author.Name)
|
||||
s.Equal("amirm@armosec.io", commit.Author.Email)
|
||||
s.Equal("2022-05-22 18:55:48 +0300 +0300", commit.Author.Date.String())
|
||||
s.Equal("added file A\n", commit.Message)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("fileB", func() {
|
||||
if localRepo, err := NewLocalGitRepository(s.gitRepositoryPaths["localrepo"]); s.NoError(err) {
|
||||
|
||||
if commit, err := localRepo.GetFileLastCommit("dirA/fileB"); s.NoError(err) {
|
||||
s.Equal("7e09312b8017695fadcd606882e3779f10a5c832", commit.SHA)
|
||||
s.Equal("Amir Malka", commit.Author.Name)
|
||||
s.Equal("amirm@armosec.io", commit.Author.Email)
|
||||
s.Equal("2022-05-22 19:11:57 +0300 +0300", commit.Author.Date.String())
|
||||
s.Equal("added file B\n", commit.Message)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gitv5 "github.com/go-git/go-git/v5"
|
||||
configv5 "github.com/go-git/go-git/v5/config"
|
||||
plumbingv5 "github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/kubescape/go-git-url/apis"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
)
|
||||
|
||||
type LocalGitRepository struct {
|
||||
goGitRepo *gitv5.Repository
|
||||
git2GoRepo *git2go.Repository
|
||||
head *plumbingv5.Reference
|
||||
config *configv5.Config
|
||||
fileToLastCommit map[string]*git2go.Commit
|
||||
*gitRepository
|
||||
goGitRepo *gitv5.Repository
|
||||
head *plumbingv5.Reference
|
||||
config *configv5.Config
|
||||
}
|
||||
|
||||
var ErrWarnNotSupportedByBuild = errors.New(`git commits retrieval not supported by this build. Build with tag "gitenabled" to enable the full git scan feature`)
|
||||
|
||||
func NewLocalGitRepository(path string) (*LocalGitRepository, error) {
|
||||
goGitRepo, err := gitv5.PlainOpenWithOptions(path, &gitv5.PlainOpenOptions{DetectDotGit: true})
|
||||
if err != nil {
|
||||
@@ -52,11 +52,12 @@ func NewLocalGitRepository(path string) (*LocalGitRepository, error) {
|
||||
}
|
||||
|
||||
if repoRoot, err := l.GetRootDir(); err == nil {
|
||||
git2GoRepo, err := git2go.OpenRepository(repoRoot)
|
||||
if err != nil {
|
||||
gitRepository, err := newGitRepository(repoRoot)
|
||||
if err != nil && !errors.Is(err, ErrWarnNotSupportedByBuild) {
|
||||
return l, err
|
||||
}
|
||||
l.git2GoRepo = git2GoRepo
|
||||
|
||||
l.gitRepository = gitRepository
|
||||
}
|
||||
|
||||
return l, nil
|
||||
@@ -72,6 +73,10 @@ func (g *LocalGitRepository) GetRemoteUrl() (string, error) {
|
||||
branchName := g.GetBranchName()
|
||||
if branchRef, branchFound := g.config.Branches[branchName]; branchFound {
|
||||
remoteName := branchRef.Remote
|
||||
// branchRef.Remote can be a reference to a config.Remotes entry or directly a gitUrl
|
||||
if _, found := g.config.Remotes[remoteName]; !found {
|
||||
return remoteName, nil
|
||||
}
|
||||
if len(g.config.Remotes[remoteName].URLs) == 0 {
|
||||
return "", fmt.Errorf("expected to find URLs for remote '%s', branch '%s'", remoteName, branchName)
|
||||
}
|
||||
@@ -79,10 +84,13 @@ func (g *LocalGitRepository) GetRemoteUrl() (string, error) {
|
||||
}
|
||||
|
||||
const defaultRemoteName string = "origin"
|
||||
if len(g.config.Remotes[defaultRemoteName].URLs) == 0 {
|
||||
defaultRemote, ok := g.config.Remotes[defaultRemoteName]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("did not find a default remote with name '%s'", defaultRemoteName)
|
||||
} else if len(defaultRemote.URLs) == 0 {
|
||||
return "", fmt.Errorf("expected to find URLs for remote '%s'", defaultRemoteName)
|
||||
}
|
||||
return g.config.Remotes[defaultRemoteName].URLs[0], nil
|
||||
return defaultRemote.URLs[0], nil
|
||||
}
|
||||
|
||||
// GetName get origin name without the .git suffix
|
||||
@@ -122,120 +130,6 @@ func (g *LocalGitRepository) GetLastCommit() (*apis.Commit, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *LocalGitRepository) getAllCommits() ([]*git2go.Commit, error) {
|
||||
logItr, itrErr := g.git2GoRepo.Walk()
|
||||
if itrErr != nil {
|
||||
|
||||
return nil, itrErr
|
||||
}
|
||||
|
||||
pushErr := logItr.PushHead()
|
||||
if pushErr != nil {
|
||||
return nil, pushErr
|
||||
}
|
||||
|
||||
var allCommits []*git2go.Commit
|
||||
err := logItr.Iterate(func(commit *git2go.Commit) bool {
|
||||
if commit != nil {
|
||||
allCommits = append(allCommits, commit)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return allCommits, nil
|
||||
}
|
||||
|
||||
func (g *LocalGitRepository) GetFileLastCommit(filePath string) (*apis.Commit, error) {
|
||||
if len(g.fileToLastCommit) == 0 {
|
||||
filePathToCommitTime := map[string]time.Time{}
|
||||
filePathToCommit := map[string]*git2go.Commit{}
|
||||
allCommits, _ := g.getAllCommits()
|
||||
|
||||
// builds a map of all files to their last commit
|
||||
for _, commit := range allCommits {
|
||||
// Ignore merge commits (2+ parents)
|
||||
if commit.ParentCount() <= 1 {
|
||||
tree, err := commit.Tree()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// ParentCount can be either 1 or 0 (initial commit)
|
||||
// In case it's the initial commit, prevTree is nil
|
||||
var prevTree *git2go.Tree
|
||||
if commit.ParentCount() == 1 {
|
||||
prevCommit := commit.Parent(0)
|
||||
prevTree, err = prevCommit.Tree()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
diff, err := g.git2GoRepo.DiffTreeToTree(prevTree, tree, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
numDeltas, err := diff.NumDeltas()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < numDeltas; i++ {
|
||||
delta, err := diff.Delta(i)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
deltaFilePath := delta.NewFile.Path
|
||||
commitTime := commit.Author().When
|
||||
|
||||
// In case we have the commit information for the file which is not the latest - we override it
|
||||
if currentCommitTime, exists := filePathToCommitTime[deltaFilePath]; exists {
|
||||
if currentCommitTime.Before(commitTime) {
|
||||
filePathToCommitTime[deltaFilePath] = commitTime
|
||||
filePathToCommit[deltaFilePath] = commit
|
||||
}
|
||||
} else {
|
||||
filePathToCommitTime[deltaFilePath] = commitTime
|
||||
filePathToCommit[deltaFilePath] = commit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
g.fileToLastCommit = filePathToCommit
|
||||
}
|
||||
|
||||
if relevantCommit, exists := g.fileToLastCommit[filePath]; exists {
|
||||
return g.getCommit(relevantCommit), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to get commit information for file: %s", filePath)
|
||||
}
|
||||
|
||||
func (g *LocalGitRepository) getCommit(commit *git2go.Commit) *apis.Commit {
|
||||
return &apis.Commit{
|
||||
SHA: commit.Id().String(),
|
||||
Author: apis.Committer{
|
||||
Name: commit.Author().Name,
|
||||
Email: commit.Author().Email,
|
||||
Date: commit.Author().When,
|
||||
},
|
||||
Message: commit.Message(),
|
||||
Committer: apis.Committer{},
|
||||
Files: []apis.Files{},
|
||||
}
|
||||
}
|
||||
|
||||
func (g *LocalGitRepository) GetRootDir() (string, error) {
|
||||
wt, err := g.goGitRepo.Worktree()
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configv5 "github.com/go-git/go-git/v5/config"
|
||||
plumbingv5 "github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@@ -26,40 +28,58 @@ func unzipFile(zipPath, destinationFolder string) (*zip.ReadCloser, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range archive.File {
|
||||
filePath := filepath.Join(destinationFolder, f.Name) //nolint:gosec
|
||||
if !strings.HasPrefix(filePath, filepath.Clean(destinationFolder)+string(os.PathSeparator)) {
|
||||
return nil, fmt.Errorf("invalid file path")
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(filePath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
if erc := copyFileInFolder(filePath, f); erc != nil {
|
||||
return nil, erc
|
||||
}
|
||||
|
||||
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileInArchive, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(dstFile, fileInArchive); err != nil { //nolint:gosec
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dstFile.Close()
|
||||
fileInArchive.Close()
|
||||
}
|
||||
|
||||
return archive, err
|
||||
}
|
||||
|
||||
func copyFileInFolder(filePath string, f *zip.File) (err error) {
|
||||
if err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = dstFile.Close()
|
||||
}()
|
||||
|
||||
fileInArchive, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = fileInArchive.Close()
|
||||
}()
|
||||
|
||||
_, err = io.Copy(dstFile, fileInArchive) //nolint:gosec
|
||||
|
||||
if err = dstFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = fileInArchive.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *LocalGitRepositoryTestSuite) SetupSuite() {
|
||||
@@ -132,44 +152,49 @@ func (s *LocalGitRepositoryTestSuite) TestGetOriginUrl() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalGitRepositoryTestSuite) TestGetLastCommit() {
|
||||
if localRepo, err := NewLocalGitRepository(s.gitRepositoryPaths["localrepo"]); s.NoError(err) {
|
||||
if commit, err := localRepo.GetLastCommit(); s.NoError(err) {
|
||||
s.Equal("7e09312b8017695fadcd606882e3779f10a5c832", commit.SHA)
|
||||
s.Equal("Amir Malka", commit.Author.Name)
|
||||
s.Equal("amirm@armosec.io", commit.Author.Email)
|
||||
s.Equal("2022-05-22 19:11:57 +0300 +0300", commit.Author.Date.String())
|
||||
s.Equal("added file B\n", commit.Message)
|
||||
}
|
||||
func TestGetRemoteUrl(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
LocalRepo LocalGitRepository
|
||||
Want string
|
||||
WantErr error
|
||||
}{
|
||||
{
|
||||
Name: "Branch with missing upstream and missing 'origin' fallback should return an error",
|
||||
LocalRepo: LocalGitRepository{
|
||||
config: &configv5.Config{
|
||||
Branches: make(map[string]*configv5.Branch),
|
||||
Remotes: make(map[string]*configv5.RemoteConfig),
|
||||
},
|
||||
head: plumbingv5.NewReferenceFromStrings("HEAD", "ref: refs/heads/v4"),
|
||||
},
|
||||
Want: "",
|
||||
WantErr: fmt.Errorf("did not find a default remote with name 'origin'"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
localRepo := LocalGitRepository{
|
||||
config: &configv5.Config{
|
||||
Branches: make(map[string]*configv5.Branch),
|
||||
Remotes: make(map[string]*configv5.RemoteConfig),
|
||||
},
|
||||
head: plumbingv5.NewReferenceFromStrings("HEAD", "ref: refs/heads/v4"),
|
||||
}
|
||||
|
||||
want := tc.Want
|
||||
wantErr := tc.WantErr
|
||||
got, gotErr := localRepo.GetRemoteUrl()
|
||||
|
||||
if got != want {
|
||||
t.Errorf("Remote URLs don’t match: got '%s', want '%s'", got, want)
|
||||
}
|
||||
|
||||
if gotErr.Error() != wantErr.Error() {
|
||||
t.Errorf("Errors don’t match: got '%v', want '%v'", gotErr, wantErr)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalGitRepositoryTestSuite) TestGetFileLastCommit() {
|
||||
s.Run("fileA", func() {
|
||||
if localRepo, err := NewLocalGitRepository(s.gitRepositoryPaths["localrepo"]); s.NoError(err) {
|
||||
|
||||
if commit, err := localRepo.GetFileLastCommit("fileA"); s.NoError(err) {
|
||||
s.Equal("9fae4be19624297947d2b605cefbff516628612d", commit.SHA)
|
||||
s.Equal("Amir Malka", commit.Author.Name)
|
||||
s.Equal("amirm@armosec.io", commit.Author.Email)
|
||||
s.Equal("2022-05-22 18:55:48 +0300 +0300", commit.Author.Date.String())
|
||||
s.Equal("added file A\n", commit.Message)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("fileB", func() {
|
||||
if localRepo, err := NewLocalGitRepository(s.gitRepositoryPaths["localrepo"]); s.NoError(err) {
|
||||
|
||||
if commit, err := localRepo.GetFileLastCommit("dirA/fileB"); s.NoError(err) {
|
||||
s.Equal("7e09312b8017695fadcd606882e3779f10a5c832", commit.SHA)
|
||||
s.Equal("Amir Malka", commit.Author.Name)
|
||||
s.Equal("amirm@armosec.io", commit.Author.Email)
|
||||
s.Equal("2022-05-22 19:11:57 +0300 +0300", commit.Author.Date.String())
|
||||
s.Equal("added file B\n", commit.Message)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
|
||||
giturl "github.com/kubescape/go-git-url"
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils/getter"
|
||||
@@ -104,6 +104,7 @@ type ScanInfo struct {
|
||||
PolicyIdentifier []PolicyIdentifier // TODO - remove from object
|
||||
UseExceptions string // Load file with exceptions configuration
|
||||
ControlsInputs string // Load file with inputs for controls
|
||||
AttackTracks string // Load file with attack tracks
|
||||
UseFrom []string // Load framework from local file (instead of download). Use when running offline
|
||||
UseDefault bool // Load framework from cached file (instead of download). Use when running offline
|
||||
UseArtifactsFrom string // Load artifacts from local path. Use when running offline
|
||||
@@ -179,6 +180,9 @@ func (scanInfo *ScanInfo) setUseArtifactsFrom() {
|
||||
scanInfo.ControlsInputs = filepath.Join(scanInfo.UseArtifactsFrom, localControlInputsFilename)
|
||||
// set exceptions
|
||||
scanInfo.UseExceptions = filepath.Join(scanInfo.UseArtifactsFrom, LocalExceptionsFilename)
|
||||
|
||||
// set attack tracks
|
||||
scanInfo.AttackTracks = filepath.Join(scanInfo.UseArtifactsFrom, LocalAttackTracksFilename)
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setUseFrom() {
|
||||
|
||||
@@ -137,7 +137,7 @@ func downloadAttackTracks(downloadInfo *metav1.DownloadInfo) error {
|
||||
var err error
|
||||
tenant := getTenantConfig(&downloadInfo.Credentials, "", "", getKubernetesApi())
|
||||
|
||||
attackTracksGetter := getAttackTracksGetter(tenant.GetAccountID(), nil)
|
||||
attackTracksGetter := getAttackTracksGetter("", tenant.GetAccountID(), nil)
|
||||
|
||||
attackTracks, err := attackTracksGetter.GetAttackTracks()
|
||||
if err != nil {
|
||||
|
||||
72
core/core/fix.go
Normal file
72
core/core/fix.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
|
||||
"github.com/kubescape/kubescape/v2/core/pkg/fixhandler"
|
||||
)
|
||||
|
||||
const NoChangesApplied = "No changes were applied."
|
||||
const NoResourcesToFix = "No issues to fix."
|
||||
const ConfirmationQuestion = "Would you like to apply the changes to the files above? [y|n]: "
|
||||
|
||||
func (ks *Kubescape) Fix(fixInfo *metav1.FixInfo) error {
|
||||
logger.L().Info("Reading report file...")
|
||||
handler, err := fixhandler.NewFixHandler(fixInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourcesToFix := handler.PrepareResourcesToFix()
|
||||
|
||||
if len(resourcesToFix) == 0 {
|
||||
logger.L().Info(NoResourcesToFix)
|
||||
return nil
|
||||
}
|
||||
|
||||
handler.PrintExpectedChanges(resourcesToFix)
|
||||
|
||||
if fixInfo.DryRun {
|
||||
logger.L().Info(NoChangesApplied)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !fixInfo.NoConfirm && !userConfirmed() {
|
||||
logger.L().Info(NoChangesApplied)
|
||||
return nil
|
||||
}
|
||||
|
||||
updatedFilesCount, errors := handler.ApplyChanges(resourcesToFix)
|
||||
logger.L().Info(fmt.Sprintf("Fixed resources in %d files.", updatedFilesCount))
|
||||
|
||||
if len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
logger.L().Error(err.Error())
|
||||
}
|
||||
return fmt.Errorf("Failed to fix some resources, check the logs for more details")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func userConfirmed() bool {
|
||||
var input string
|
||||
|
||||
for {
|
||||
fmt.Printf(ConfirmationQuestion)
|
||||
if _, err := fmt.Scanln(&input); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
input = strings.ToLower(input)
|
||||
if input == "y" || input == "yes" {
|
||||
return true
|
||||
} else if input == "n" || input == "no" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
@@ -247,7 +247,10 @@ func listFrameworksNames(policyGetter getter.IPolicyGetter) []string {
|
||||
return getter.NativeFrameworks
|
||||
}
|
||||
|
||||
func getAttackTracksGetter(accountID string, downloadReleasedPolicy *getter.DownloadReleasedPolicy) getter.IAttackTracksGetter {
|
||||
func getAttackTracksGetter(attackTracks, accountID string, downloadReleasedPolicy *getter.DownloadReleasedPolicy) getter.IAttackTracksGetter {
|
||||
if len(attackTracks) > 0 {
|
||||
return getter.NewLoadPolicy([]string{attackTracks})
|
||||
}
|
||||
if accountID != "" {
|
||||
g := getter.GetKSCloudAPIConnector() // download attack tracks from Kubescape Cloud backend
|
||||
return g
|
||||
@@ -255,6 +258,7 @@ func getAttackTracksGetter(accountID string, downloadReleasedPolicy *getter.Down
|
||||
if downloadReleasedPolicy == nil {
|
||||
downloadReleasedPolicy = getter.NewDownloadReleasedPolicy()
|
||||
}
|
||||
|
||||
if err := downloadReleasedPolicy.SetRegoObjects(); err != nil { // if failed to pull attack tracks, fallback to cache
|
||||
logger.L().Warning("failed to get attack tracks from github release, loading attack tracks from cache", helpers.Error(err))
|
||||
return getter.NewLoadPolicy([]string{getter.GetDefaultPath(cautils.LocalAttackTracksFilename)})
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils/getter"
|
||||
@@ -137,7 +137,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
|
||||
scanInfo.Getters.PolicyGetter = getPolicyGetter(scanInfo.UseFrom, interfaces.tenantConfig.GetTenantEmail(), scanInfo.FrameworkScan, downloadReleasedPolicy)
|
||||
scanInfo.Getters.ControlsInputsGetter = getConfigInputsGetter(scanInfo.ControlsInputs, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
|
||||
scanInfo.Getters.ExceptionsGetter = getExceptionsGetter(scanInfo.UseExceptions, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
|
||||
scanInfo.Getters.AttackTracksGetter = getAttackTracksGetter(interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
|
||||
scanInfo.Getters.AttackTracksGetter = getAttackTracksGetter(scanInfo.AttackTracks, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
|
||||
|
||||
// TODO - list supported frameworks/controls
|
||||
if scanInfo.ScanAll {
|
||||
|
||||
8
core/meta/datastructures/v1/fix.go
Normal file
8
core/meta/datastructures/v1/fix.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package v1
|
||||
|
||||
type FixInfo struct {
|
||||
ReportFile string // path to report file (mandatory)
|
||||
NoConfirm bool // if true, no confirmation will be given to the user before applying the fix
|
||||
SkipUserValues bool // if true, user values will not be changed
|
||||
DryRun bool // if true, no changes will be applied
|
||||
}
|
||||
@@ -25,4 +25,7 @@ type IKubescape interface {
|
||||
|
||||
// delete
|
||||
DeleteExceptions(deleteexceptions *metav1.DeleteExceptions) error
|
||||
|
||||
// fix
|
||||
Fix(fixInfo *metav1.FixInfo) error
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (pkgs *LinuxPkgs) UnmarshalJSONArray(dec *gojay.Decoder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//--------Vul fixed in----------------------------------
|
||||
// --------Vul fixed in----------------------------------
|
||||
func (fx *FixedIn) UnmarshalJSONObject(dec *gojay.Decoder, key string) (err error) {
|
||||
|
||||
switch key {
|
||||
|
||||
@@ -71,19 +71,19 @@ type PackageFile struct {
|
||||
|
||||
// types to provide unmarshalling:
|
||||
|
||||
//VulnerabilitiesList -s.e
|
||||
// VulnerabilitiesList -s.e
|
||||
type LayersList []ScanResultLayer
|
||||
|
||||
//VulnerabilitiesList -s.e
|
||||
// VulnerabilitiesList -s.e
|
||||
type VulnerabilitiesList []Vulnerability
|
||||
|
||||
//LinuxPkgs - slice of linux pkgs
|
||||
// LinuxPkgs - slice of linux pkgs
|
||||
type LinuxPkgs []LinuxPackage
|
||||
|
||||
//VulFixes - information bout when/how this vul was fixed
|
||||
// VulFixes - information bout when/how this vul was fixed
|
||||
type VulFixes []FixedIn
|
||||
|
||||
//PkgFiles - slice of files belong to specific pkg
|
||||
// PkgFiles - slice of files belong to specific pkg
|
||||
type PkgFiles []PackageFile
|
||||
|
||||
func (v *ScanResultReport) AsFNVHash() string {
|
||||
|
||||
109
core/pkg/fixhandler/datastructures.go
Normal file
109
core/pkg/fixhandler/datastructures.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package fixhandler
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/armoapi-go/armotypes"
|
||||
metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// FixHandler is a struct that holds the information of the report to be fixed
|
||||
type FixHandler struct {
|
||||
fixInfo *metav1.FixInfo
|
||||
reportObj *reporthandlingv2.PostureReport
|
||||
localBasePath string
|
||||
}
|
||||
|
||||
// ResourceFixInfo is a struct that holds the information about the resource that needs to be fixed
|
||||
type ResourceFixInfo struct {
|
||||
YamlExpressions map[string]*armotypes.FixPath
|
||||
Resource *reporthandling.Resource
|
||||
FilePath string
|
||||
DocumentIndex int
|
||||
}
|
||||
|
||||
// NodeInfo holds extra information about the node
|
||||
type nodeInfo struct {
|
||||
node *yaml.Node
|
||||
parent *yaml.Node
|
||||
|
||||
// position of the node among siblings
|
||||
index int
|
||||
}
|
||||
|
||||
// FixInfoMetadata holds the arguments "getFixInfo" function needs to pass to the
|
||||
// functions it uses
|
||||
type fixInfoMetadata struct {
|
||||
originalList *[]nodeInfo
|
||||
fixedList *[]nodeInfo
|
||||
originalListTracker int
|
||||
fixedListTracker int
|
||||
contentToAdd *[]contentToAdd
|
||||
linesToRemove *[]linesToRemove
|
||||
}
|
||||
|
||||
// contentToAdd holds the information about where to insert the new changes in the existing yaml file
|
||||
type contentToAdd struct {
|
||||
// Line where the fix should be applied to
|
||||
line int
|
||||
// Content is a string representation of the YAML node that describes a suggested fix
|
||||
content string
|
||||
}
|
||||
|
||||
func withNewline(content, targetNewline string) string {
|
||||
replaceNewlines := map[string]bool{
|
||||
unixNewline: true,
|
||||
windowsNewline: true,
|
||||
oldMacNewline: true,
|
||||
}
|
||||
replaceNewlines[targetNewline] = false
|
||||
|
||||
newlinesToReplace := make([]string, len(replaceNewlines))
|
||||
i := 0
|
||||
for k := range replaceNewlines {
|
||||
newlinesToReplace[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
// To ensure that we fully replace Windows newlines (CR LF), and not
|
||||
// corrupt them into two new newlines (CR CR or LF LF) by partially
|
||||
// replacing either CR or LF, we have to ensure we replace longer
|
||||
// Windows newlines first
|
||||
sort.Slice(newlinesToReplace, func(i int, j int) bool {
|
||||
return len(newlinesToReplace[i]) > len(newlinesToReplace[j])
|
||||
})
|
||||
|
||||
// strings.Replacer takes a flat list of (oldVal, newVal) pairs, so we
|
||||
// need to allocate twice the space and assign accordingly
|
||||
newlinesOldNew := make([]string, 2*len(replaceNewlines))
|
||||
i = 0
|
||||
for _, nl := range newlinesToReplace {
|
||||
newlinesOldNew[2*i] = nl
|
||||
newlinesOldNew[2*i+1] = targetNewline
|
||||
i++
|
||||
}
|
||||
|
||||
replacer := strings.NewReplacer(newlinesOldNew...)
|
||||
return replacer.Replace(content)
|
||||
}
|
||||
|
||||
// Content returns the content that will be added, separated by the explicitly
|
||||
// provided `targetNewline`
|
||||
func (c *contentToAdd) Content(targetNewline string) string {
|
||||
return withNewline(c.content, targetNewline)
|
||||
}
|
||||
|
||||
// LinesToRemove holds the line numbers to remove from the existing yaml file
|
||||
type linesToRemove struct {
|
||||
startLine int
|
||||
endLine int
|
||||
}
|
||||
|
||||
type fileFixInfo struct {
|
||||
contentsToAdd *[]contentToAdd
|
||||
linesToRemove *[]linesToRemove
|
||||
}
|
||||
89
core/pkg/fixhandler/datastructures_test.go
Normal file
89
core/pkg/fixhandler/datastructures_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package fixhandler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContentNewlinesMatchTarget(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
InputContent string
|
||||
TargetNewline string
|
||||
WantedContent string
|
||||
}{
|
||||
{
|
||||
"Unix to DOS",
|
||||
"first line\nsecond line\n",
|
||||
"\r\n",
|
||||
"first line\r\nsecond line\r\n",
|
||||
},
|
||||
{
|
||||
"Unix to Unix",
|
||||
"first line\nsecond line\n",
|
||||
"\n",
|
||||
"first line\nsecond line\n",
|
||||
},
|
||||
{
|
||||
"Unix to Mac",
|
||||
"first line\nsecond line\n",
|
||||
"\r",
|
||||
"first line\rsecond line\r",
|
||||
},
|
||||
{
|
||||
"DOS to Unix",
|
||||
"first line\r\nsecond line\r\n",
|
||||
"\n",
|
||||
"first line\nsecond line\n",
|
||||
},
|
||||
{
|
||||
"DOS to DOS",
|
||||
"first line\r\nsecond line\r\n",
|
||||
"\r\n",
|
||||
"first line\r\nsecond line\r\n",
|
||||
},
|
||||
{
|
||||
"DOS to OldMac",
|
||||
"first line\r\nsecond line\r\n",
|
||||
"\r",
|
||||
"first line\rsecond line\r",
|
||||
},
|
||||
{
|
||||
"Mac to DOS",
|
||||
"first line\rsecond line\r",
|
||||
"\r\n",
|
||||
"first line\r\nsecond line\r\n",
|
||||
},
|
||||
{
|
||||
"Mac to Unix",
|
||||
"first line\rsecond line\r",
|
||||
"\n",
|
||||
"first line\nsecond line\n",
|
||||
},
|
||||
{
|
||||
"DOS, Mac to Unix",
|
||||
"first line\r\n\rsecond line\r",
|
||||
"\n",
|
||||
"first line\n\nsecond line\n",
|
||||
},
|
||||
{
|
||||
"Mac, DOS to Unix",
|
||||
"first line\r\r\r\nsecond line\r",
|
||||
"\n",
|
||||
"first line\n\n\nsecond line\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
c := &contentToAdd{content: tc.InputContent}
|
||||
want := tc.WantedContent
|
||||
|
||||
got := c.Content(tc.TargetNewline)
|
||||
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
367
core/pkg/fixhandler/fixhandler.go
Normal file
367
core/pkg/fixhandler/fixhandler.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package fixhandler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/armoapi-go/armotypes"
|
||||
metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"github.com/mikefarah/yq/v4/pkg/yqlib"
|
||||
"gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
const UserValuePrefix = "YOUR_"
|
||||
|
||||
const windowsNewline = "\r\n"
|
||||
const unixNewline = "\n"
|
||||
const oldMacNewline = "\r"
|
||||
|
||||
func NewFixHandler(fixInfo *metav1.FixInfo) (*FixHandler, error) {
|
||||
jsonFile, err := os.Open(fixInfo.ReportFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer jsonFile.Close()
|
||||
byteValue, _ := ioutil.ReadAll(jsonFile)
|
||||
|
||||
var reportObj reporthandlingv2.PostureReport
|
||||
if err = json.Unmarshal(byteValue, &reportObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = isSupportedScanningTarget(&reportObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localPath := getLocalPath(&reportObj)
|
||||
if _, err = os.Stat(localPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backendLoggerLeveled := logging.AddModuleLevel(logging.NewLogBackend(logger.L().GetWriter(), "", 0))
|
||||
backendLoggerLeveled.SetLevel(logging.ERROR, "")
|
||||
yqlib.GetLogger().SetBackend(backendLoggerLeveled)
|
||||
|
||||
return &FixHandler{
|
||||
fixInfo: fixInfo,
|
||||
reportObj: &reportObj,
|
||||
localBasePath: localPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isSupportedScanningTarget(report *reporthandlingv2.PostureReport) error {
|
||||
scanningTarget := report.Metadata.ScanMetadata.ScanningTarget
|
||||
if scanningTarget == reporthandlingv2.GitLocal || scanningTarget == reporthandlingv2.Directory || scanningTarget == reporthandlingv2.File {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported scanning target. Supported scanning targets are: a local git repo, a directory or a file")
|
||||
}
|
||||
|
||||
func getLocalPath(report *reporthandlingv2.PostureReport) string {
|
||||
if report.Metadata.ScanMetadata.ScanningTarget == reporthandlingv2.GitLocal {
|
||||
return report.Metadata.ContextMetadata.RepoContextMetadata.LocalRootPath
|
||||
}
|
||||
|
||||
if report.Metadata.ScanMetadata.ScanningTarget == reporthandlingv2.Directory {
|
||||
return report.Metadata.ContextMetadata.DirectoryContextMetadata.BasePath
|
||||
}
|
||||
|
||||
if report.Metadata.ScanMetadata.ScanningTarget == reporthandlingv2.File {
|
||||
return filepath.Dir(report.Metadata.ContextMetadata.FileContextMetadata.FilePath)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *FixHandler) buildResourcesMap() map[string]*reporthandling.Resource {
|
||||
resourceIdToRawResource := make(map[string]*reporthandling.Resource)
|
||||
for i := range h.reportObj.Resources {
|
||||
resourceIdToRawResource[h.reportObj.Resources[i].GetID()] = &h.reportObj.Resources[i]
|
||||
}
|
||||
for i := range h.reportObj.Results {
|
||||
if h.reportObj.Results[i].RawResource == nil {
|
||||
continue
|
||||
}
|
||||
resourceIdToRawResource[h.reportObj.Results[i].RawResource.GetID()] = h.reportObj.Results[i].RawResource
|
||||
}
|
||||
|
||||
return resourceIdToRawResource
|
||||
}
|
||||
|
||||
func (h *FixHandler) getPathFromRawResource(obj map[string]interface{}) string {
|
||||
if localworkload.IsTypeLocalWorkload(obj) {
|
||||
localwork := localworkload.NewLocalWorkload(obj)
|
||||
return localwork.GetPath()
|
||||
} else if objectsenvelopes.IsTypeRegoResponseVector(obj) {
|
||||
regoResponseVectorObject := objectsenvelopes.NewRegoResponseVectorObject(obj)
|
||||
relatedObjects := regoResponseVectorObject.GetRelatedObjects()
|
||||
for _, relatedObject := range relatedObjects {
|
||||
if localworkload.IsTypeLocalWorkload(relatedObject.GetObject()) {
|
||||
return relatedObject.(*localworkload.LocalWorkload).GetPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *FixHandler) PrepareResourcesToFix() []ResourceFixInfo {
|
||||
resourceIdToResource := h.buildResourcesMap()
|
||||
|
||||
resourcesToFix := make([]ResourceFixInfo, 0)
|
||||
for _, result := range h.reportObj.Results {
|
||||
if !result.GetStatus(nil).IsFailed() {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceID := result.ResourceID
|
||||
resourceObj := resourceIdToResource[resourceID]
|
||||
resourcePath := h.getPathFromRawResource(resourceObj.GetObject())
|
||||
if resourcePath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if resourceObj.Source == nil || resourceObj.Source.FileType != reporthandling.SourceTypeYaml {
|
||||
continue
|
||||
}
|
||||
|
||||
relativePath, documentIndex, err := h.getFilePathAndIndex(resourcePath)
|
||||
if err != nil {
|
||||
logger.L().Error("Skipping invalid resource path: " + resourcePath)
|
||||
continue
|
||||
}
|
||||
|
||||
absolutePath := path.Join(h.localBasePath, relativePath)
|
||||
if _, err := os.Stat(absolutePath); err != nil {
|
||||
logger.L().Error("Skipping missing file: " + absolutePath)
|
||||
continue
|
||||
}
|
||||
|
||||
rfi := ResourceFixInfo{
|
||||
FilePath: absolutePath,
|
||||
Resource: resourceObj,
|
||||
YamlExpressions: make(map[string]*armotypes.FixPath, 0),
|
||||
DocumentIndex: documentIndex,
|
||||
}
|
||||
|
||||
for i := range result.AssociatedControls {
|
||||
if result.AssociatedControls[i].GetStatus(nil).IsFailed() {
|
||||
rfi.addYamlExpressionsFromResourceAssociatedControl(documentIndex, &result.AssociatedControls[i], h.fixInfo.SkipUserValues)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rfi.YamlExpressions) > 0 {
|
||||
resourcesToFix = append(resourcesToFix, rfi)
|
||||
}
|
||||
}
|
||||
|
||||
return resourcesToFix
|
||||
}
|
||||
|
||||
func (h *FixHandler) PrintExpectedChanges(resourcesToFix []ResourceFixInfo) {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("The following changes will be applied:\n")
|
||||
|
||||
for _, resourceFixInfo := range resourcesToFix {
|
||||
sb.WriteString(fmt.Sprintf("File: %s\n", resourceFixInfo.FilePath))
|
||||
sb.WriteString(fmt.Sprintf("Resource: %s\n", resourceFixInfo.Resource.GetName()))
|
||||
sb.WriteString(fmt.Sprintf("Kind: %s\n", resourceFixInfo.Resource.GetKind()))
|
||||
sb.WriteString("Changes:\n")
|
||||
|
||||
i := 1
|
||||
for _, fixPath := range resourceFixInfo.YamlExpressions {
|
||||
sb.WriteString(fmt.Sprintf("\t%d) %s = %s\n", i, (*fixPath).Path, (*fixPath).Value))
|
||||
i++
|
||||
}
|
||||
sb.WriteString("\n------\n")
|
||||
}
|
||||
|
||||
logger.L().Info(sb.String())
|
||||
}
|
||||
|
||||
func (h *FixHandler) ApplyChanges(resourcesToFix []ResourceFixInfo) (int, []error) {
|
||||
updatedFiles := make(map[string]bool)
|
||||
errors := make([]error, 0)
|
||||
|
||||
fileYamlExpressions := h.getFileYamlExpressions(resourcesToFix)
|
||||
|
||||
for filepath, yamlExpression := range fileYamlExpressions {
|
||||
fileAsString, err := getFileString(filepath)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fixedYamlString, err := h.ApplyFixToContent(fileAsString, yamlExpression)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Errorf("Failed to fix file %s: %w ", filepath, err))
|
||||
continue
|
||||
} else {
|
||||
updatedFiles[filepath] = true
|
||||
}
|
||||
|
||||
err = writeFixesToFile(filepath, fixedYamlString)
|
||||
|
||||
if err != nil {
|
||||
logger.L().Error(fmt.Sprintf("Failed to write fixes to file %s, %v", filepath, err.Error()))
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
return len(updatedFiles), errors
|
||||
}
|
||||
|
||||
func (h *FixHandler) getFilePathAndIndex(filePathWithIndex string) (filePath string, documentIndex int, err error) {
|
||||
splittedPath := strings.Split(filePathWithIndex, ":")
|
||||
if len(splittedPath) <= 1 {
|
||||
return "", 0, fmt.Errorf("expected to find ':' in file path")
|
||||
}
|
||||
|
||||
filePath = splittedPath[0]
|
||||
if documentIndex, err := strconv.Atoi(splittedPath[1]); err != nil {
|
||||
return "", 0, err
|
||||
} else {
|
||||
return filePath, documentIndex, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *FixHandler) ApplyFixToContent(yamlAsString, yamlExpression string) (fixedString string, err error) {
|
||||
newline := determineNewlineSeparator(yamlAsString)
|
||||
|
||||
yamlLines := strings.Split(yamlAsString, newline)
|
||||
|
||||
originalRootNodes, err := decodeDocumentRoots(yamlAsString)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fixedRootNodes, err := getFixedNodes(yamlAsString, yamlExpression)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileFixInfo := getFixInfo(originalRootNodes, fixedRootNodes)
|
||||
|
||||
fixedYamlLines := getFixedYamlLines(yamlLines, fileFixInfo, newline)
|
||||
|
||||
fixedString = getStringFromSlice(fixedYamlLines, newline)
|
||||
|
||||
return fixedString, nil
|
||||
}
|
||||
|
||||
func (h *FixHandler) getFileYamlExpressions(resourcesToFix []ResourceFixInfo) map[string]string {
|
||||
fileYamlExpressions := make(map[string]string, 0)
|
||||
for _, resourceToFix := range resourcesToFix {
|
||||
singleExpression := reduceYamlExpressions(&resourceToFix)
|
||||
resourceFilePath := resourceToFix.FilePath
|
||||
|
||||
if _, pathExistsInMap := fileYamlExpressions[resourceFilePath]; !pathExistsInMap {
|
||||
fileYamlExpressions[resourceFilePath] = singleExpression
|
||||
} else {
|
||||
fileYamlExpressions[resourceFilePath] = joinStrings(fileYamlExpressions[resourceFilePath], " | ", singleExpression)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return fileYamlExpressions
|
||||
}
|
||||
|
||||
func (rfi *ResourceFixInfo) addYamlExpressionsFromResourceAssociatedControl(documentIndex int, ac *resourcesresults.ResourceAssociatedControl, skipUserValues bool) {
|
||||
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, UserValuePrefix) && skipUserValues {
|
||||
continue
|
||||
}
|
||||
|
||||
yamlExpression := fixPathToValidYamlExpression(rulePaths.FixPath.Path, rulePaths.FixPath.Value, documentIndex)
|
||||
rfi.YamlExpressions[yamlExpression] = &rulePaths.FixPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reduceYamlExpressions reduces the number of yaml expressions to a single one
|
||||
func reduceYamlExpressions(resource *ResourceFixInfo) string {
|
||||
expressions := make([]string, 0, len(resource.YamlExpressions))
|
||||
for expr := range resource.YamlExpressions {
|
||||
expressions = append(expressions, expr)
|
||||
}
|
||||
|
||||
return strings.Join(expressions, " | ")
|
||||
}
|
||||
|
||||
func fixPathToValidYamlExpression(fixPath, value string, documentIndexInYaml int) string {
|
||||
isStringValue := true
|
||||
if _, err := strconv.ParseBool(value); err == nil {
|
||||
isStringValue = false
|
||||
} else if _, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
isStringValue = false
|
||||
} else if _, err := strconv.Atoi(value); err == nil {
|
||||
isStringValue = false
|
||||
}
|
||||
|
||||
// Strings should be quoted
|
||||
if isStringValue {
|
||||
value = fmt.Sprintf("\"%s\"", value)
|
||||
}
|
||||
|
||||
// select document index and add a dot for the root node
|
||||
return fmt.Sprintf("select(di==%d).%s |= %s", documentIndexInYaml, fixPath, value)
|
||||
}
|
||||
|
||||
func joinStrings(inputStrings ...string) string {
|
||||
return strings.Join(inputStrings, "")
|
||||
}
|
||||
|
||||
func getFileString(filepath string) (string, error) {
|
||||
bytes, err := ioutil.ReadFile(filepath)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error reading file %s", filepath)
|
||||
}
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func writeFixesToFile(filepath, content string) error {
|
||||
err := ioutil.WriteFile(filepath, []byte(content), 0644)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing fixes to file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func determineNewlineSeparator(contents string) string {
|
||||
switch {
|
||||
case strings.Contains(contents, windowsNewline):
|
||||
return windowsNewline
|
||||
default:
|
||||
return unixNewline
|
||||
}
|
||||
}
|
||||
253
core/pkg/fixhandler/fixhandler_test.go
Normal file
253
core/pkg/fixhandler/fixhandler_test.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package fixhandler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"github.com/mikefarah/yq/v4/pkg/yqlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type indentationTestCase struct {
|
||||
inputFile string
|
||||
yamlExpression string
|
||||
expectedFile string
|
||||
}
|
||||
|
||||
func NewFixHandlerMock() (*FixHandler, error) {
|
||||
backendLoggerLeveled := logging.AddModuleLevel(logging.NewLogBackend(logger.L().GetWriter(), "", 0))
|
||||
backendLoggerLeveled.SetLevel(logging.ERROR, "")
|
||||
yqlib.GetLogger().SetBackend(backendLoggerLeveled)
|
||||
|
||||
return &FixHandler{
|
||||
fixInfo: &metav1.FixInfo{},
|
||||
reportObj: &reporthandlingv2.PostureReport{},
|
||||
localBasePath: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTestdataPath() string {
|
||||
currentDir, _ := os.Getwd()
|
||||
return filepath.Join(currentDir, "testdata")
|
||||
}
|
||||
|
||||
func getTestCases() []indentationTestCase {
|
||||
indentationTestCases := []indentationTestCase{
|
||||
// Insertion Scenarios
|
||||
{
|
||||
"inserts/tc-01-00-input-mapping-insert-mapping.yaml",
|
||||
"select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false",
|
||||
"inserts/tc-01-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-02-00-input-mapping-insert-mapping-with-list.yaml",
|
||||
"select(di==0).spec.containers[0].securityContext.capabilities.drop += [\"NET_RAW\"]",
|
||||
"inserts/tc-02-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-03-00-input-list-append-scalar.yaml",
|
||||
"select(di==0).spec.containers[0].securityContext.capabilities.drop += [\"SYS_ADM\"]",
|
||||
"inserts/tc-03-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-04-00-input-multiple-inserts.yaml",
|
||||
|
||||
`select(di==0).spec.template.spec.securityContext.allowPrivilegeEscalation |= false |
|
||||
select(di==0).spec.template.spec.containers[0].securityContext.capabilities.drop += ["NET_RAW"] |
|
||||
select(di==0).spec.template.spec.containers[0].securityContext.seccompProfile.type |= "RuntimeDefault" |
|
||||
select(di==0).spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation |= false |
|
||||
select(di==0).spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem |= true`,
|
||||
|
||||
"inserts/tc-04-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-05-00-input-comment-blank-line-single-insert.yaml",
|
||||
"select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false",
|
||||
"inserts/tc-05-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-06-00-input-list-append-scalar-oneline.yaml",
|
||||
"select(di==0).spec.containers[0].securityContext.capabilities.drop += [\"SYS_ADM\"]",
|
||||
"inserts/tc-06-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-07-00-input-multiple-documents.yaml",
|
||||
|
||||
`select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false |
|
||||
select(di==1).spec.containers[0].securityContext.allowPrivilegeEscalation |= false`,
|
||||
|
||||
"inserts/tc-07-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-08-00-input-mapping-insert-mapping-indented.yaml",
|
||||
"select(di==0).spec.containers[0].securityContext.capabilities.drop += [\"NET_RAW\"]",
|
||||
"inserts/tc-08-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-09-00-input-list-insert-new-mapping-indented.yaml",
|
||||
`select(di==0).spec.containers += {"name": "redis", "image": "redis"}`,
|
||||
"inserts/tc-09-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-10-00-input-list-insert-new-mapping.yaml",
|
||||
`select(di==0).spec.containers += {"name": "redis", "image": "redis"}`,
|
||||
"inserts/tc-10-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"inserts/tc-11-00-input-list-insert-new-mapping-crlf-newlines.yaml",
|
||||
`select(di==0).spec.containers += {"name": "redis", "image": "redis"}`,
|
||||
"inserts/tc-11-01-expected.yaml",
|
||||
},
|
||||
|
||||
// Removal Scenarios
|
||||
{
|
||||
"removals/tc-01-00-input.yaml",
|
||||
"del(select(di==0).spec.containers[0].securityContext)",
|
||||
"removals/tc-01-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"removals/tc-02-00-input.yaml",
|
||||
"del(select(di==0).spec.containers[1])",
|
||||
"removals/tc-02-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"removals/tc-03-00-input.yaml",
|
||||
"del(select(di==0).spec.containers[0].securityContext.capabilities.drop[1])",
|
||||
"removals/tc-03-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"removes/tc-04-00-input.yaml",
|
||||
`del(select(di==0).spec.containers[0].securityContext) |
|
||||
del(select(di==1).spec.containers[1])`,
|
||||
"removes/tc-04-01-expected.yaml",
|
||||
},
|
||||
|
||||
// Replace Scenarios
|
||||
{
|
||||
"replaces/tc-01-00-input.yaml",
|
||||
"select(di==0).spec.containers[0].securityContext.runAsRoot |= false",
|
||||
"replaces/tc-01-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"replaces/tc-02-00-input.yaml",
|
||||
`select(di==0).spec.containers[0].securityContext.capabilities.drop[0] |= "SYS_ADM" |
|
||||
select(di==0).spec.containers[0].securityContext.capabilities.add[0] |= "NET_RAW"`,
|
||||
"replaces/tc-02-01-expected.yaml",
|
||||
},
|
||||
|
||||
// Hybrid Scenarios
|
||||
{
|
||||
"hybrids/tc-01-00-input.yaml",
|
||||
`del(select(di==0).spec.containers[0].securityContext) |
|
||||
select(di==0).spec.securityContext.runAsRoot |= false`,
|
||||
"hybrids/tc-01-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"hybrids/tc-02-00-input-indented-list.yaml",
|
||||
`del(select(di==0).spec.containers[0].securityContext) |
|
||||
select(di==0).spec.securityContext.runAsRoot |= false`,
|
||||
"hybrids/tc-02-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"hybrids/tc-03-00-input-comments.yaml",
|
||||
`del(select(di==0).spec.containers[0].securityContext) |
|
||||
select(di==0).spec.securityContext.runAsRoot |= false`,
|
||||
"hybrids/tc-03-01-expected.yaml",
|
||||
},
|
||||
{
|
||||
"hybrids/tc-04-00-input-separated-keys.yaml",
|
||||
`del(select(di==0).spec.containers[0].securityContext) |
|
||||
select(di==0).spec.securityContext.runAsRoot |= false`,
|
||||
"hybrids/tc-04-01-expected.yaml",
|
||||
},
|
||||
}
|
||||
|
||||
return indentationTestCases
|
||||
}
|
||||
|
||||
func TestApplyFixKeepsFormatting(t *testing.T) {
|
||||
testCases := getTestCases()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.inputFile, func(t *testing.T) {
|
||||
getTestDataPath := func(filename string) string {
|
||||
currentDir, _ := os.Getwd()
|
||||
currentFile := "testdata/" + filename
|
||||
return filepath.Join(currentDir, currentFile)
|
||||
}
|
||||
|
||||
input, _ := os.ReadFile(getTestDataPath(tc.inputFile))
|
||||
wantRaw, _ := os.ReadFile(getTestDataPath(tc.expectedFile))
|
||||
want := string(wantRaw)
|
||||
expression := tc.yamlExpression
|
||||
|
||||
h, _ := NewFixHandlerMock()
|
||||
|
||||
got, _ := h.ApplyFixToContent(string(input), expression)
|
||||
|
||||
assert.Equalf(
|
||||
t, want, got,
|
||||
"Contents of the fixed file don't match the expectation.\n"+
|
||||
"Input file: %s\n\n"+
|
||||
"Got: <%s>\n\n"+
|
||||
"Want: <%s>",
|
||||
tc.inputFile, got, want,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fixPathToValidYamlExpression(t *testing.T) {
|
||||
type args struct {
|
||||
fixPath string
|
||||
value string
|
||||
documentIndexInYaml int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "fix path with boolean value",
|
||||
args: args{
|
||||
fixPath: "spec.template.spec.containers[0].securityContext.privileged",
|
||||
value: "true",
|
||||
documentIndexInYaml: 2,
|
||||
},
|
||||
want: "select(di==2).spec.template.spec.containers[0].securityContext.privileged |= true",
|
||||
},
|
||||
{
|
||||
name: "fix path with string value",
|
||||
args: args{
|
||||
fixPath: "metadata.namespace",
|
||||
value: "YOUR_NAMESPACE",
|
||||
documentIndexInYaml: 0,
|
||||
},
|
||||
want: "select(di==0).metadata.namespace |= \"YOUR_NAMESPACE\"",
|
||||
},
|
||||
{
|
||||
name: "fix path with number",
|
||||
args: args{
|
||||
fixPath: "xxx.yyy",
|
||||
value: "123",
|
||||
documentIndexInYaml: 0,
|
||||
},
|
||||
want: "select(di==0).xxx.yyy |= 123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fixPathToValidYamlExpression(tt.args.fixPath, tt.args.value, tt.args.documentIndexInYaml); got != tt.want {
|
||||
t.Errorf("fixPathToValidYamlExpression() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
19
core/pkg/fixhandler/testdata/hybrids/tc-01-00-input.yaml
vendored
Normal file
19
core/pkg/fixhandler/testdata/hybrids/tc-01-00-input.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: true
|
||||
19
core/pkg/fixhandler/testdata/hybrids/tc-01-01-expected.yaml
vendored
Normal file
19
core/pkg/fixhandler/testdata/hybrids/tc-01-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
19
core/pkg/fixhandler/testdata/hybrids/tc-02-00-input-indented-list.yaml
vendored
Normal file
19
core/pkg/fixhandler/testdata/hybrids/tc-02-00-input-indented-list.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: true
|
||||
19
core/pkg/fixhandler/testdata/hybrids/tc-02-01-expected.yaml
vendored
Normal file
19
core/pkg/fixhandler/testdata/hybrids/tc-02-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
21
core/pkg/fixhandler/testdata/hybrids/tc-03-00-input-comments.yaml
vendored
Normal file
21
core/pkg/fixhandler/testdata/hybrids/tc-03-00-input-comments.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
# These are the container comments
|
||||
containers:
|
||||
# These are the first containers comments
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: true
|
||||
21
core/pkg/fixhandler/testdata/hybrids/tc-03-01-expected.yaml
vendored
Normal file
21
core/pkg/fixhandler/testdata/hybrids/tc-03-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
# These are the container comments
|
||||
containers:
|
||||
# These are the first containers comments
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
21
core/pkg/fixhandler/testdata/hybrids/tc-04-00-input-separated-keys.yaml
vendored
Normal file
21
core/pkg/fixhandler/testdata/hybrids/tc-04-00-input-separated-keys.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
|
||||
image: nginx
|
||||
|
||||
securityContext:
|
||||
runAsRoot: true
|
||||
21
core/pkg/fixhandler/testdata/hybrids/tc-04-01-expected.yaml
vendored
Normal file
21
core/pkg/fixhandler/testdata/hybrids/tc-04-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Fix to Apply:
|
||||
# REMOVE:
|
||||
# "del(select(di==0).spec.containers[0].securityContext)"
|
||||
|
||||
# INSERT:
|
||||
# select(di==0).spec.securityContext.runAsRoot: false
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
|
||||
12
core/pkg/fixhandler/testdata/inserts/tc-01-00-input-mapping-insert-mapping.yaml
vendored
Normal file
12
core/pkg/fixhandler/testdata/inserts/tc-01-00-input-mapping-insert-mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
14
core/pkg/fixhandler/testdata/inserts/tc-01-01-expected.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/inserts/tc-01-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
11
core/pkg/fixhandler/testdata/inserts/tc-02-00-input-mapping-insert-mapping-with-list.yaml
vendored
Normal file
11
core/pkg/fixhandler/testdata/inserts/tc-02-00-input-mapping-insert-mapping-with-list.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["NET_RAW"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
15
core/pkg/fixhandler/testdata/inserts/tc-02-01-expected.yaml
vendored
Normal file
15
core/pkg/fixhandler/testdata/inserts/tc-02-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["NET_RAW"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- NET_RAW
|
||||
15
core/pkg/fixhandler/testdata/inserts/tc-03-00-input-list-append-scalar.yaml
vendored
Normal file
15
core/pkg/fixhandler/testdata/inserts/tc-03-00-input-list-append-scalar.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["SYS_ADM"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- NET_RAW
|
||||
16
core/pkg/fixhandler/testdata/inserts/tc-03-01-expected.yaml
vendored
Normal file
16
core/pkg/fixhandler/testdata/inserts/tc-03-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["SYS_ADM"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- NET_RAW
|
||||
- SYS_ADM
|
||||
47
core/pkg/fixhandler/testdata/inserts/tc-04-00-input-multiple-inserts.yaml
vendored
Normal file
47
core/pkg/fixhandler/testdata/inserts/tc-04-00-input-multiple-inserts.yaml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# Fixes to Apply:
|
||||
# 1) select(di==0).spec.template.spec.securityContext.allowPrivilegeEscalation = false
|
||||
# 2) select(di==0).spec.template.spec.containers[0].securityContext.capabilities.drop += ["NET_RAW"]
|
||||
# 3) select(di==0).spec.template.spec.containers[0].securityContext.seccompProfile.type = RuntimeDefault
|
||||
# 4) select(di==0).spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation |= false
|
||||
# 5) select(di==0).spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem |= true
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: multiple_inserts
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: example_4
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example_4
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: example_4
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 180Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 300Mi
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:3000"]
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:3000"]
|
||||
57
core/pkg/fixhandler/testdata/inserts/tc-04-01-expected.yaml
vendored
Normal file
57
core/pkg/fixhandler/testdata/inserts/tc-04-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# Fixes to Apply:
|
||||
# 1) select(di==0).spec.template.spec.securityContext.allowPrivilegeEscalation = false
|
||||
# 2) select(di==0).spec.template.spec.containers[0].securityContext.capabilities.drop += ["NET_RAW"]
|
||||
# 3) select(di==0).spec.template.spec.containers[0].securityContext.seccompProfile.type = RuntimeDefault
|
||||
# 4) select(di==0).spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation |= false
|
||||
# 5) select(di==0).spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem |= true
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: multiple_inserts
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: example_4
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example_4
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: example_4
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 180Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 300Mi
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:3000"]
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:3000"]
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- NET_RAW
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
16
core/pkg/fixhandler/testdata/inserts/tc-05-00-input-comment-blank-line-single-insert.yaml
vendored
Normal file
16
core/pkg/fixhandler/testdata/inserts/tc-05-00-input-comment-blank-line-single-insert.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
|
||||
# Testing if comments are retained as intended
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
18
core/pkg/fixhandler/testdata/inserts/tc-05-01-expected.yaml
vendored
Normal file
18
core/pkg/fixhandler/testdata/inserts/tc-05-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
# Testing if comments are retained as intended
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
14
core/pkg/fixhandler/testdata/inserts/tc-06-00-input-list-append-scalar-oneline.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/inserts/tc-06-00-input-list-append-scalar-oneline.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["SYS_ADM"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx1
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop: [NET_RAW]
|
||||
14
core/pkg/fixhandler/testdata/inserts/tc-06-01-expected.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/inserts/tc-06-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["SYS_ADM"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx1
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop: [NET_RAW, SYS_ADM]
|
||||
27
core/pkg/fixhandler/testdata/inserts/tc-07-00-input-multiple-documents.yaml
vendored
Normal file
27
core/pkg/fixhandler/testdata/inserts/tc-07-00-input-multiple-documents.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
|
||||
---
|
||||
|
||||
# Fix to Apply:
|
||||
# "select(di==1).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
31
core/pkg/fixhandler/testdata/inserts/tc-07-01-expected.yaml
vendored
Normal file
31
core/pkg/fixhandler/testdata/inserts/tc-07-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
---
|
||||
|
||||
# Fix to Apply:
|
||||
# "select(di==1).spec.containers[0].securityContext.allowPrivilegeEscalation |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
11
core/pkg/fixhandler/testdata/inserts/tc-08-00-input-mapping-insert-mapping-indented.yaml
vendored
Normal file
11
core/pkg/fixhandler/testdata/inserts/tc-08-00-input-mapping-insert-mapping-indented.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["NET_RAW"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-parent-list-insert-list-value
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
15
core/pkg/fixhandler/testdata/inserts/tc-08-01-expected.yaml
vendored
Normal file
15
core/pkg/fixhandler/testdata/inserts/tc-08-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop += ["NET_RAW"]
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-parent-list-insert-list-value
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- NET_RAW
|
||||
11
core/pkg/fixhandler/testdata/inserts/tc-09-00-input-list-insert-new-mapping-indented.yaml
vendored
Normal file
11
core/pkg/fixhandler/testdata/inserts/tc-09-00-input-list-insert-new-mapping-indented.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers += {"name": "redis", "image": "redis"}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-parent-list-insert-list-value
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
13
core/pkg/fixhandler/testdata/inserts/tc-09-01-expected.yaml
vendored
Normal file
13
core/pkg/fixhandler/testdata/inserts/tc-09-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers += {"name": "redis", "image": "redis"}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-parent-list-insert-list-value
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
- name: redis
|
||||
image: redis
|
||||
11
core/pkg/fixhandler/testdata/inserts/tc-10-00-input-list-insert-new-mapping.yaml
vendored
Normal file
11
core/pkg/fixhandler/testdata/inserts/tc-10-00-input-list-insert-new-mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers += {"name": "redis", "image": "redis"}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-list-insert-new-object
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
13
core/pkg/fixhandler/testdata/inserts/tc-10-01-expected.yaml
vendored
Normal file
13
core/pkg/fixhandler/testdata/inserts/tc-10-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers += {"name": "redis", "image": "redis"}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-list-insert-new-object
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
- name: redis
|
||||
image: redis
|
||||
11
core/pkg/fixhandler/testdata/inserts/tc-11-00-input-list-insert-new-mapping-crlf-newlines.yaml
vendored
Normal file
11
core/pkg/fixhandler/testdata/inserts/tc-11-00-input-list-insert-new-mapping-crlf-newlines.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers += {"name": "redis", "image": "redis"}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-list-insert-new-object
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
13
core/pkg/fixhandler/testdata/inserts/tc-11-01-expected.yaml
vendored
Normal file
13
core/pkg/fixhandler/testdata/inserts/tc-11-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers += {"name": "redis", "image": "redis"}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: indented-list-insert-new-object
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
- name: redis
|
||||
image: redis
|
||||
14
core/pkg/fixhandler/testdata/removals/tc-01-00-input.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/removals/tc-01-00-input.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[0].securityContext)
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
12
core/pkg/fixhandler/testdata/removals/tc-01-01-expected.yaml
vendored
Normal file
12
core/pkg/fixhandler/testdata/removals/tc-01-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[0].securityContext)
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
15
core/pkg/fixhandler/testdata/removals/tc-02-00-input.yaml
vendored
Normal file
15
core/pkg/fixhandler/testdata/removals/tc-02-00-input.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[1])
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
|
||||
- name: container_with_security_issues
|
||||
image: image_with_security_issues
|
||||
12
core/pkg/fixhandler/testdata/removals/tc-02-01-expected.yaml
vendored
Normal file
12
core/pkg/fixhandler/testdata/removals/tc-02-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[1])
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
14
core/pkg/fixhandler/testdata/removals/tc-03-00-input.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/removals/tc-03-00-input.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[0].securityContext.capabilities.drop[1])
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx1
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop: ["NET_RAW", "SYS_ADM"]
|
||||
14
core/pkg/fixhandler/testdata/removals/tc-03-01-expected.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/removals/tc-03-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[0].securityContext.capabilities.drop[1])
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx1
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop: ["NET_RAW"]
|
||||
32
core/pkg/fixhandler/testdata/removals/tc-04-00-input.yaml
vendored
Normal file
32
core/pkg/fixhandler/testdata/removals/tc-04-00-input.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[0].securityContext)
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
|
||||
---
|
||||
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[1])
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
|
||||
- name: container_with_security_issues
|
||||
image: image_with_security_issues
|
||||
27
core/pkg/fixhandler/testdata/removals/tc-04-01-expected.yaml
vendored
Normal file
27
core/pkg/fixhandler/testdata/removals/tc-04-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[0].securityContext)
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
|
||||
---
|
||||
|
||||
# Fix to Apply:
|
||||
# del(select(di==0).spec.containers[1])
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: remove_example
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
14
core/pkg/fixhandler/testdata/replaces/tc-01-00-input.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/replaces/tc-01-00-input.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.runAsRoot |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: true
|
||||
14
core/pkg/fixhandler/testdata/replaces/tc-01-01-expected.yaml
vendored
Normal file
14
core/pkg/fixhandler/testdata/replaces/tc-01-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fix to Apply:
|
||||
# "select(di==0).spec.containers[0].securityContext.runAsRoot |= false"
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_to_mapping_node_1
|
||||
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx_container
|
||||
image: nginx
|
||||
securityContext:
|
||||
runAsRoot: false
|
||||
18
core/pkg/fixhandler/testdata/replaces/tc-02-00-input.yaml
vendored
Normal file
18
core/pkg/fixhandler/testdata/replaces/tc-02-00-input.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop[0] |= "SYS_ADM"
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.add[0] |= "NET_RAW"
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx1
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- "NET_RAW"
|
||||
add: ["SYS_ADM"]
|
||||
18
core/pkg/fixhandler/testdata/replaces/tc-02-01-expected.yaml
vendored
Normal file
18
core/pkg/fixhandler/testdata/replaces/tc-02-01-expected.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Fix to Apply:
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.drop[0] |= "SYS_ADM"
|
||||
# select(di==0).spec.containers[0].securityContext.capabilities.add[0] |= "NET_RAW"
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: insert_list
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx1
|
||||
image: nginx
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- "SYS_ADM"
|
||||
add: ["NET_RAW"]
|
||||
286
core/pkg/fixhandler/yamlhandler.go
Normal file
286
core/pkg/fixhandler/yamlhandler.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package fixhandler
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/mikefarah/yq/v4/pkg/yqlib"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// decodeDocumentRoots decodes all YAML documents stored in a given `filepath` and returns a slice of their root nodes
|
||||
func decodeDocumentRoots(yamlAsString string) ([]yaml.Node, error) {
|
||||
fileReader := strings.NewReader(yamlAsString)
|
||||
dec := yaml.NewDecoder(fileReader)
|
||||
|
||||
nodes := make([]yaml.Node, 0)
|
||||
for {
|
||||
var node yaml.Node
|
||||
err := dec.Decode(&node)
|
||||
|
||||
nodes = append(nodes, node)
|
||||
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot Decode File as YAML")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func getFixedNodes(yamlAsString, yamlExpression string) ([]yaml.Node, error) {
|
||||
preferences := yqlib.ConfiguredYamlPreferences
|
||||
preferences.EvaluateTogether = true
|
||||
decoder := yqlib.NewYamlDecoder(preferences)
|
||||
|
||||
var allDocuments = list.New()
|
||||
reader := strings.NewReader(yamlAsString)
|
||||
|
||||
fileDocuments, err := readDocuments(reader, decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allDocuments.PushBackList(fileDocuments)
|
||||
|
||||
allAtOnceEvaluator := yqlib.NewAllAtOnceEvaluator()
|
||||
|
||||
fixedCandidateNodes, err := allAtOnceEvaluator.EvaluateCandidateNodes(yamlExpression, allDocuments)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fixing YAML, %w", err)
|
||||
}
|
||||
|
||||
fixedNodes := make([]yaml.Node, 0)
|
||||
var fixedNode *yaml.Node
|
||||
for fixedCandidateNode := fixedCandidateNodes.Front(); fixedCandidateNode != nil; fixedCandidateNode = fixedCandidateNode.Next() {
|
||||
fixedNode = fixedCandidateNode.Value.(*yqlib.CandidateNode).Node
|
||||
fixedNodes = append(fixedNodes, *fixedNode)
|
||||
}
|
||||
|
||||
return fixedNodes, nil
|
||||
}
|
||||
|
||||
func flattenWithDFS(node *yaml.Node) *[]nodeInfo {
|
||||
dfsOrder := make([]nodeInfo, 0)
|
||||
flattenWithDFSHelper(node, nil, &dfsOrder, 0)
|
||||
return &dfsOrder
|
||||
}
|
||||
|
||||
func flattenWithDFSHelper(node *yaml.Node, parent *yaml.Node, dfsOrder *[]nodeInfo, index int) {
|
||||
dfsNode := nodeInfo{
|
||||
node: node,
|
||||
parent: parent,
|
||||
index: index,
|
||||
}
|
||||
*dfsOrder = append(*dfsOrder, dfsNode)
|
||||
|
||||
for idx, child := range node.Content {
|
||||
flattenWithDFSHelper(child, node, dfsOrder, idx)
|
||||
}
|
||||
}
|
||||
|
||||
func getFixInfo(originalRootNodes, fixedRootNodes []yaml.Node) fileFixInfo {
|
||||
contentToAdd := make([]contentToAdd, 0)
|
||||
linesToRemove := make([]linesToRemove, 0)
|
||||
|
||||
for idx := 0; idx < len(fixedRootNodes); idx++ {
|
||||
originalList := flattenWithDFS(&originalRootNodes[idx])
|
||||
fixedList := flattenWithDFS(&fixedRootNodes[idx])
|
||||
nodeContentToAdd, nodeLinesToRemove := getFixInfoHelper(*originalList, *fixedList)
|
||||
contentToAdd = append(contentToAdd, nodeContentToAdd...)
|
||||
linesToRemove = append(linesToRemove, nodeLinesToRemove...)
|
||||
}
|
||||
|
||||
return fileFixInfo{
|
||||
contentsToAdd: &contentToAdd,
|
||||
linesToRemove: &linesToRemove,
|
||||
}
|
||||
}
|
||||
|
||||
func getFixInfoHelper(originalList, fixedList []nodeInfo) ([]contentToAdd, []linesToRemove) {
|
||||
|
||||
// While obtaining fixedYamlNode, comments and empty lines at the top are ignored.
|
||||
// This causes a difference in Line numbers across the tree structure. In order to
|
||||
// counter this, line numbers are adjusted in fixed list.
|
||||
adjustFixedListLines(&originalList, &fixedList)
|
||||
|
||||
contentToAdd := make([]contentToAdd, 0)
|
||||
linesToRemove := make([]linesToRemove, 0)
|
||||
|
||||
originalListTracker, fixedListTracker := 0, 0
|
||||
|
||||
fixInfoMetadata := &fixInfoMetadata{
|
||||
originalList: &originalList,
|
||||
fixedList: &fixedList,
|
||||
originalListTracker: originalListTracker,
|
||||
fixedListTracker: fixedListTracker,
|
||||
contentToAdd: &contentToAdd,
|
||||
linesToRemove: &linesToRemove,
|
||||
}
|
||||
|
||||
for originalListTracker < len(originalList) && fixedListTracker < len(fixedList) {
|
||||
matchNodeResult := matchNodes(originalList[originalListTracker].node, fixedList[fixedListTracker].node)
|
||||
|
||||
fixInfoMetadata.originalListTracker = originalListTracker
|
||||
fixInfoMetadata.fixedListTracker = fixedListTracker
|
||||
|
||||
switch matchNodeResult {
|
||||
case sameNodes:
|
||||
originalListTracker += 1
|
||||
fixedListTracker += 1
|
||||
|
||||
case removedNode:
|
||||
originalListTracker, fixedListTracker = addLinesToRemove(fixInfoMetadata)
|
||||
|
||||
case insertedNode:
|
||||
originalListTracker, fixedListTracker = addLinesToInsert(fixInfoMetadata)
|
||||
|
||||
case replacedNode:
|
||||
originalListTracker, fixedListTracker = updateLinesToReplace(fixInfoMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
// Some nodes are still not visited if they are removed at the end of the list
|
||||
for originalListTracker < len(originalList) {
|
||||
fixInfoMetadata.originalListTracker = originalListTracker
|
||||
originalListTracker, _ = addLinesToRemove(fixInfoMetadata)
|
||||
}
|
||||
|
||||
// Some nodes are still not visited if they are inserted at the end of the list
|
||||
for fixedListTracker < len(fixedList) {
|
||||
// Use negative index of last node in original list as a placeholder to determine the last line number later
|
||||
fixInfoMetadata.originalListTracker = -(len(originalList) - 1)
|
||||
fixInfoMetadata.fixedListTracker = fixedListTracker
|
||||
_, fixedListTracker = addLinesToInsert(fixInfoMetadata)
|
||||
}
|
||||
|
||||
return contentToAdd, linesToRemove
|
||||
|
||||
}
|
||||
|
||||
// Adds the lines to remove and returns the updated originalListTracker
|
||||
func addLinesToRemove(fixInfoMetadata *fixInfoMetadata) (int, int) {
|
||||
isOneLine, line := isOneLineSequenceNode(fixInfoMetadata.originalList, fixInfoMetadata.originalListTracker)
|
||||
|
||||
if isOneLine {
|
||||
// Remove the entire line and replace it with the sequence node in fixed info. This way,
|
||||
// the original formatting is not lost.
|
||||
return replaceSingleLineSequence(fixInfoMetadata, line)
|
||||
}
|
||||
|
||||
currentDFSNode := (*fixInfoMetadata.originalList)[fixInfoMetadata.originalListTracker]
|
||||
|
||||
newOriginalListTracker := updateTracker(fixInfoMetadata.originalList, fixInfoMetadata.originalListTracker)
|
||||
*fixInfoMetadata.linesToRemove = append(*fixInfoMetadata.linesToRemove, linesToRemove{
|
||||
startLine: currentDFSNode.node.Line,
|
||||
endLine: getNodeLine(fixInfoMetadata.originalList, newOriginalListTracker),
|
||||
})
|
||||
|
||||
return newOriginalListTracker, fixInfoMetadata.fixedListTracker
|
||||
}
|
||||
|
||||
// Adds the lines to insert and returns the updated fixedListTracker
|
||||
func addLinesToInsert(fixInfoMetadata *fixInfoMetadata) (int, int) {
|
||||
|
||||
isOneLine, line := isOneLineSequenceNode(fixInfoMetadata.fixedList, fixInfoMetadata.fixedListTracker)
|
||||
|
||||
if isOneLine {
|
||||
return replaceSingleLineSequence(fixInfoMetadata, line)
|
||||
}
|
||||
|
||||
currentDFSNode := (*fixInfoMetadata.fixedList)[fixInfoMetadata.fixedListTracker]
|
||||
|
||||
lineToInsert := getLineToInsert(fixInfoMetadata)
|
||||
contentToInsert := getContent(currentDFSNode.parent, fixInfoMetadata.fixedList, fixInfoMetadata.fixedListTracker)
|
||||
|
||||
newFixedTracker := updateTracker(fixInfoMetadata.fixedList, fixInfoMetadata.fixedListTracker)
|
||||
|
||||
*fixInfoMetadata.contentToAdd = append(*fixInfoMetadata.contentToAdd, contentToAdd{
|
||||
line: lineToInsert,
|
||||
content: contentToInsert,
|
||||
})
|
||||
|
||||
return fixInfoMetadata.originalListTracker, newFixedTracker
|
||||
}
|
||||
|
||||
// Adds the lines to remove and insert and updates the fixedListTracker and originalListTracker
|
||||
func updateLinesToReplace(fixInfoMetadata *fixInfoMetadata) (int, int) {
|
||||
|
||||
isOneLine, line := isOneLineSequenceNode(fixInfoMetadata.fixedList, fixInfoMetadata.fixedListTracker)
|
||||
|
||||
if isOneLine {
|
||||
return replaceSingleLineSequence(fixInfoMetadata, line)
|
||||
}
|
||||
|
||||
currentDFSNode := (*fixInfoMetadata.fixedList)[fixInfoMetadata.fixedListTracker]
|
||||
|
||||
// If only the value node is changed, entire "key-value" pair is replaced
|
||||
if isValueNodeinMapping(¤tDFSNode) {
|
||||
fixInfoMetadata.originalListTracker -= 1
|
||||
fixInfoMetadata.fixedListTracker -= 1
|
||||
}
|
||||
|
||||
addLinesToRemove(fixInfoMetadata)
|
||||
updatedOriginalTracker, updatedFixedTracker := addLinesToInsert(fixInfoMetadata)
|
||||
|
||||
return updatedOriginalTracker, updatedFixedTracker
|
||||
}
|
||||
|
||||
func removeNewLinesAtTheEnd(yamlLines []string) []string {
|
||||
for idx := 1; idx < len(yamlLines); idx++ {
|
||||
if yamlLines[len(yamlLines)-idx] != "\n" {
|
||||
yamlLines = yamlLines[:len(yamlLines)-idx+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
return yamlLines
|
||||
}
|
||||
|
||||
func getFixedYamlLines(yamlLines []string, fileFixInfo fileFixInfo, newline string) (fixedYamlLines []string) {
|
||||
|
||||
// Determining last line requires original yaml lines slice. The placeholder for last line is replaced with the real last line
|
||||
assignLastLine(fileFixInfo.contentsToAdd, fileFixInfo.linesToRemove, &yamlLines)
|
||||
|
||||
removeLines(fileFixInfo.linesToRemove, &yamlLines)
|
||||
|
||||
fixedYamlLines = make([]string, 0)
|
||||
lineIdx, lineToAddIdx := 1, 0
|
||||
|
||||
// Ideally, new node is inserted at line before the next node in DFS order. But, when the previous line contains a
|
||||
// comment or empty line, we need to insert new nodes before them.
|
||||
adjustContentLines(fileFixInfo.contentsToAdd, &yamlLines)
|
||||
|
||||
for lineToAddIdx < len(*fileFixInfo.contentsToAdd) {
|
||||
for lineIdx <= (*fileFixInfo.contentsToAdd)[lineToAddIdx].line {
|
||||
// Check if the current line is not removed
|
||||
if yamlLines[lineIdx-1] != "*" {
|
||||
fixedYamlLines = append(fixedYamlLines, yamlLines[lineIdx-1])
|
||||
}
|
||||
lineIdx += 1
|
||||
}
|
||||
|
||||
content := (*fileFixInfo.contentsToAdd)[lineToAddIdx].Content(newline)
|
||||
fixedYamlLines = append(fixedYamlLines, content)
|
||||
|
||||
lineToAddIdx += 1
|
||||
}
|
||||
|
||||
for lineIdx <= len(yamlLines) {
|
||||
if yamlLines[lineIdx-1] != "*" {
|
||||
fixedYamlLines = append(fixedYamlLines, yamlLines[lineIdx-1])
|
||||
}
|
||||
lineIdx += 1
|
||||
}
|
||||
|
||||
fixedYamlLines = removeNewLinesAtTheEnd(fixedYamlLines)
|
||||
|
||||
return fixedYamlLines
|
||||
}
|
||||
406
core/pkg/fixhandler/yamlhelper.go
Normal file
406
core/pkg/fixhandler/yamlhelper.go
Normal file
@@ -0,0 +1,406 @@
|
||||
package fixhandler
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/mikefarah/yq/v4/pkg/yqlib"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type NodeRelation int
|
||||
|
||||
const (
|
||||
sameNodes NodeRelation = iota
|
||||
insertedNode
|
||||
removedNode
|
||||
replacedNode
|
||||
)
|
||||
|
||||
func matchNodes(nodeOne, nodeTwo *yaml.Node) NodeRelation {
|
||||
|
||||
isNewNode := nodeTwo.Line == 0 && nodeTwo.Column == 0
|
||||
sameLines := nodeOne.Line == nodeTwo.Line
|
||||
sameColumns := nodeOne.Column == nodeTwo.Column
|
||||
|
||||
isSameNode := isSameNode(nodeOne, nodeTwo)
|
||||
|
||||
switch {
|
||||
case isSameNode:
|
||||
return sameNodes
|
||||
case isNewNode:
|
||||
return insertedNode
|
||||
case sameLines && sameColumns:
|
||||
return replacedNode
|
||||
default:
|
||||
return removedNode
|
||||
}
|
||||
}
|
||||
|
||||
func adjustContentLines(contentToAdd *[]contentToAdd, linesSlice *[]string) {
|
||||
for contentIdx, content := range *contentToAdd {
|
||||
line := content.line
|
||||
|
||||
// Adjust line numbers such that there are no "empty lines or comment lines of next nodes" before them
|
||||
for idx := line - 1; idx >= 0; idx-- {
|
||||
if isEmptyLineOrComment((*linesSlice)[idx]) {
|
||||
(*contentToAdd)[contentIdx].line -= 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func adjustFixedListLines(originalList, fixedList *[]nodeInfo) {
|
||||
differenceAtTop := (*originalList)[0].node.Line - (*fixedList)[0].node.Line
|
||||
|
||||
if differenceAtTop <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, node := range *fixedList {
|
||||
// line numbers should not be changed for new nodes.
|
||||
if node.node.Line != 0 {
|
||||
node.node.Line += differenceAtTop
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func enocodeIntoYaml(parentNode *yaml.Node, nodeList *[]nodeInfo, tracker int) (string, error) {
|
||||
content := make([]*yaml.Node, 0)
|
||||
currentNode := (*nodeList)[tracker].node
|
||||
content = append(content, currentNode)
|
||||
|
||||
// Add the value in "key-value" pair to construct if the parent is mapping node
|
||||
if parentNode.Kind == yaml.MappingNode {
|
||||
valueNode := (*nodeList)[tracker+1].node
|
||||
content = append(content, valueNode)
|
||||
}
|
||||
|
||||
// The parent is added at the top to encode into YAML
|
||||
parentForContent := yaml.Node{
|
||||
Kind: parentNode.Kind,
|
||||
Content: content,
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
encoder := yaml.NewEncoder(buf)
|
||||
encoder.SetIndent(2)
|
||||
|
||||
errorEncoding := encoder.Encode(parentForContent)
|
||||
if errorEncoding != nil {
|
||||
return "", fmt.Errorf("Error debugging node, %v", errorEncoding.Error())
|
||||
}
|
||||
errorClosingEncoder := encoder.Close()
|
||||
if errorClosingEncoder != nil {
|
||||
return "", fmt.Errorf("Error closing encoder: %v", errorClosingEncoder.Error())
|
||||
}
|
||||
return fmt.Sprintf(`%v`, buf.String()), nil
|
||||
}
|
||||
|
||||
func getContent(parentNode *yaml.Node, nodeList *[]nodeInfo, tracker int) string {
|
||||
content, err := enocodeIntoYaml(parentNode, nodeList, tracker)
|
||||
if err != nil {
|
||||
logger.L().Fatal("Cannot Encode into YAML")
|
||||
}
|
||||
|
||||
indentationSpaces := parentNode.Column - 1
|
||||
|
||||
content = indentContent(content, indentationSpaces)
|
||||
|
||||
return strings.TrimSuffix(content, "\n")
|
||||
}
|
||||
|
||||
func indentContent(content string, indentationSpaces int) string {
|
||||
indentedContent := ""
|
||||
indentSpaces := strings.Repeat(" ", indentationSpaces)
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
indentedContent += (indentSpaces + line + "\n")
|
||||
}
|
||||
return indentedContent
|
||||
}
|
||||
|
||||
func getLineToInsert(fixInfoMetadata *fixInfoMetadata) int {
|
||||
var lineToInsert int
|
||||
// Check if lineToInsert is last line
|
||||
if fixInfoMetadata.originalListTracker < 0 {
|
||||
originalListTracker := int(math.Abs(float64(fixInfoMetadata.originalListTracker)))
|
||||
// Storing the negative value of line of last node as a placeholder to determine the last line later.
|
||||
lineToInsert = -(*fixInfoMetadata.originalList)[originalListTracker].node.Line
|
||||
} else {
|
||||
lineToInsert = (*fixInfoMetadata.originalList)[fixInfoMetadata.originalListTracker].node.Line - 1
|
||||
}
|
||||
return lineToInsert
|
||||
}
|
||||
|
||||
func assignLastLine(contentsToAdd *[]contentToAdd, linesToRemove *[]linesToRemove, linesSlice *[]string) {
|
||||
for idx, contentToAdd := range *contentsToAdd {
|
||||
if contentToAdd.line < 0 {
|
||||
currentLine := int(math.Abs(float64(contentToAdd.line)))
|
||||
(*contentsToAdd)[idx].line, _ = getLastLineOfResource(linesSlice, currentLine)
|
||||
}
|
||||
}
|
||||
|
||||
for idx, lineToRemove := range *linesToRemove {
|
||||
if lineToRemove.endLine < 0 {
|
||||
endLine, _ := getLastLineOfResource(linesSlice, lineToRemove.startLine)
|
||||
(*linesToRemove)[idx].endLine = endLine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getLastLineOfResource(linesSlice *[]string, currentLine int) (int, error) {
|
||||
// Get lastlines of all resources...
|
||||
lastLinesOfResources := make([]int, 0)
|
||||
for lineNumber, lineContent := range *linesSlice {
|
||||
if lineContent == "---" {
|
||||
for lastLine := lineNumber - 1; lastLine >= 0; lastLine-- {
|
||||
if !isEmptyLineOrComment((*linesSlice)[lastLine]) {
|
||||
lastLinesOfResources = append(lastLinesOfResources, lastLine+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastLine := len(*linesSlice)
|
||||
for lastLine >= 0 {
|
||||
if !isEmptyLineOrComment((*linesSlice)[lastLine-1]) {
|
||||
lastLinesOfResources = append(lastLinesOfResources, lastLine)
|
||||
break
|
||||
} else {
|
||||
lastLine--
|
||||
}
|
||||
}
|
||||
|
||||
// Get last line of the resource we need
|
||||
for _, endLine := range lastLinesOfResources {
|
||||
if currentLine <= endLine {
|
||||
return endLine, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("Provided line is greater than the length of YAML file")
|
||||
}
|
||||
|
||||
func getNodeLine(nodeList *[]nodeInfo, tracker int) int {
|
||||
if tracker < len(*nodeList) {
|
||||
return (*nodeList)[tracker].node.Line
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the node is value node in "key-value" pairs of mapping node
|
||||
func isValueNodeinMapping(node *nodeInfo) bool {
|
||||
if node.parent.Kind == yaml.MappingNode && node.index%2 != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Checks if the node is part of single line sequence node and returns the line
|
||||
func isOneLineSequenceNode(list *[]nodeInfo, currentTracker int) (bool, int) {
|
||||
parentNode := (*list)[currentTracker].parent
|
||||
if parentNode.Kind != yaml.SequenceNode {
|
||||
return false, -1
|
||||
}
|
||||
|
||||
var currentNode, prevNode nodeInfo
|
||||
currentTracker -= 1
|
||||
|
||||
for (*list)[currentTracker].node != parentNode {
|
||||
currentNode = (*list)[currentTracker]
|
||||
prevNode = (*list)[currentTracker-1]
|
||||
|
||||
if currentNode.node.Line != prevNode.node.Line {
|
||||
return false, -1
|
||||
}
|
||||
currentTracker -= 1
|
||||
}
|
||||
|
||||
parentNodeInfo := (*list)[currentTracker]
|
||||
|
||||
if parentNodeInfo.parent.Kind == yaml.MappingNode {
|
||||
keyNodeInfo := (*list)[currentTracker-1]
|
||||
if keyNodeInfo.node.Line == parentNode.Line {
|
||||
return true, parentNode.Line
|
||||
} else {
|
||||
return false, -1
|
||||
}
|
||||
} else {
|
||||
if parentNodeInfo.parent.Line == parentNode.Line {
|
||||
return true, parentNode.Line
|
||||
} else {
|
||||
return false, -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if nodes are of same kind, value, line and column
|
||||
func isSameNode(nodeOne, nodeTwo *yaml.Node) bool {
|
||||
sameLines := nodeOne.Line == nodeTwo.Line
|
||||
sameColumns := nodeOne.Column == nodeTwo.Column
|
||||
sameKinds := nodeOne.Kind == nodeTwo.Kind
|
||||
sameValues := nodeOne.Value == nodeTwo.Value
|
||||
|
||||
return sameKinds && sameValues && sameLines && sameColumns
|
||||
}
|
||||
|
||||
// Checks if the line is empty or a comment
|
||||
func isEmptyLineOrComment(lineContent string) bool {
|
||||
lineContent = strings.TrimSpace(lineContent)
|
||||
if lineContent == "" {
|
||||
return true
|
||||
} else if lineContent[0:1] == "#" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func readDocuments(reader io.Reader, decoder yqlib.Decoder) (*list.List, error) {
|
||||
err := decoder.Init(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error Initializing the decoder, %w", err)
|
||||
}
|
||||
inputList := list.New()
|
||||
|
||||
var currentIndex uint
|
||||
|
||||
for {
|
||||
candidateNode, errorReading := decoder.Decode()
|
||||
|
||||
if errors.Is(errorReading, io.EOF) {
|
||||
switch reader := reader.(type) {
|
||||
case *os.File:
|
||||
safelyCloseFile(reader)
|
||||
}
|
||||
return inputList, nil
|
||||
} else if errorReading != nil {
|
||||
return nil, fmt.Errorf("Error Decoding YAML file, %w", errorReading)
|
||||
}
|
||||
|
||||
candidateNode.Document = currentIndex
|
||||
candidateNode.EvaluateTogether = true
|
||||
|
||||
inputList.PushBack(candidateNode)
|
||||
|
||||
currentIndex = currentIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
func safelyCloseFile(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
logger.L().Error("Error Closing File")
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the entire line and replace it with the sequence node in fixed info. This way,
|
||||
// the original formatting is lost.
|
||||
func replaceSingleLineSequence(fixInfoMetadata *fixInfoMetadata, line int) (int, int) {
|
||||
originalListTracker := getFirstNodeInLine(fixInfoMetadata.originalList, line)
|
||||
fixedListTracker := getFirstNodeInLine(fixInfoMetadata.fixedList, line)
|
||||
|
||||
currentDFSNode := (*fixInfoMetadata.fixedList)[fixedListTracker]
|
||||
contentToInsert := getContent(currentDFSNode.parent, fixInfoMetadata.fixedList, fixedListTracker)
|
||||
|
||||
// Remove the Single line
|
||||
*fixInfoMetadata.linesToRemove = append(*fixInfoMetadata.linesToRemove, linesToRemove{
|
||||
startLine: line,
|
||||
endLine: line,
|
||||
})
|
||||
|
||||
// Encode entire Sequence Node and Insert
|
||||
*fixInfoMetadata.contentToAdd = append(*fixInfoMetadata.contentToAdd, contentToAdd{
|
||||
line: line,
|
||||
content: contentToInsert,
|
||||
})
|
||||
|
||||
originalListTracker = updateTracker(fixInfoMetadata.originalList, originalListTracker)
|
||||
fixedListTracker = updateTracker(fixInfoMetadata.fixedList, fixedListTracker)
|
||||
|
||||
return originalListTracker, fixedListTracker
|
||||
}
|
||||
|
||||
// Returns the first node in the given line that is not mapping node
|
||||
func getFirstNodeInLine(list *[]nodeInfo, line int) int {
|
||||
tracker := 0
|
||||
|
||||
currentNode := (*list)[tracker].node
|
||||
for currentNode.Line != line || currentNode.Kind == yaml.MappingNode {
|
||||
tracker += 1
|
||||
currentNode = (*list)[tracker].node
|
||||
}
|
||||
|
||||
return tracker
|
||||
}
|
||||
|
||||
// To not mess with the line number while inserting, removed lines are not deleted but replaced with "*"
|
||||
func removeLines(linesToRemove *[]linesToRemove, linesSlice *[]string) {
|
||||
var startLine, endLine int
|
||||
for _, lineToRemove := range *linesToRemove {
|
||||
startLine = lineToRemove.startLine - 1
|
||||
endLine = lineToRemove.endLine - 1
|
||||
|
||||
for line := startLine; line <= endLine; line++ {
|
||||
lineContent := (*linesSlice)[line]
|
||||
// When determining the endLine, empty lines and comments which are not intended to be removed are included.
|
||||
// To deal with that, we need to refrain from removing empty lines and comments
|
||||
if isEmptyLineOrComment(lineContent) {
|
||||
break
|
||||
}
|
||||
(*linesSlice)[line] = "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skips the current node including it's children in DFS order and returns the new tracker.
|
||||
func skipCurrentNode(node *yaml.Node, currentTracker int) int {
|
||||
updatedTracker := currentTracker + getChildrenCount(node)
|
||||
return updatedTracker
|
||||
}
|
||||
|
||||
func getChildrenCount(node *yaml.Node) int {
|
||||
totalChildren := 1
|
||||
for _, child := range node.Content {
|
||||
totalChildren += getChildrenCount(child)
|
||||
}
|
||||
return totalChildren
|
||||
}
|
||||
|
||||
// The current node along with it's children is skipped and the tracker is moved to next sibling
|
||||
// of current node. If parent is mapping node, "value" in "key-value" pairs is also skipped.
|
||||
func updateTracker(nodeList *[]nodeInfo, tracker int) int {
|
||||
currentNode := (*nodeList)[tracker]
|
||||
var updatedTracker int
|
||||
|
||||
if currentNode.parent.Kind == yaml.MappingNode {
|
||||
valueNode := (*nodeList)[tracker+1]
|
||||
updatedTracker = skipCurrentNode(valueNode.node, tracker+1)
|
||||
} else {
|
||||
updatedTracker = skipCurrentNode(currentNode.node, tracker)
|
||||
}
|
||||
|
||||
return updatedTracker
|
||||
}
|
||||
|
||||
func getStringFromSlice(yamlLines []string, newline string) (fixedYamlString string) {
|
||||
return strings.Join(yamlLines, newline)
|
||||
}
|
||||
@@ -75,6 +75,7 @@ func (hsh *HostSensorHandler) Init() error {
|
||||
// store pod names
|
||||
// make sure all pods are running, after X seconds treat has running anyway, and log an error on the pods not running yet
|
||||
logger.L().Info("Installing host scanner")
|
||||
logger.L().Debug("The host scanner is a DaemonSet that runs on each node in the cluster. The DaemonSet will be running in it's own namespace and will be deleted once the scan is completed. If you do not wish to install the host scanner, please run the scan without the --enable-host-scan flag.")
|
||||
|
||||
cautils.StartSpinner()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user