mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-15 02:20:03 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34ee347708 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# 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,32 +2,33 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Description
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
# Describe the bug
|
||||
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` -->
|
||||
|
||||
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:
|
||||
```
|
||||
|
||||
# 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,23 +2,18 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'feature'
|
||||
labels: ''
|
||||
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 [...]
|
||||
|
||||
## Overview
|
||||
<!-- A brief overview of the related current state -->
|
||||
**Describe the solution you'd like.**</br>
|
||||
> A clear and concise description of what you want to happen.
|
||||
|
||||
## Problem
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
**Describe alternatives you've considered.**</br>
|
||||
> A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
## 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. -->
|
||||
|
||||
**Additional context.**</br>
|
||||
> 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,39 +1,13 @@
|
||||
## Overview
|
||||
<!-- Please provide a brief overview of the changes made in this pull request. e.g. current behavior/future behavior -->
|
||||
## Describe your changes
|
||||
|
||||
<!--
|
||||
## Additional Information
|
||||
## Screenshots - If Any (Optional)
|
||||
|
||||
> Any additional information that may be useful for reviewers to know
|
||||
-->
|
||||
## This PR fixes:
|
||||
|
||||
<!--
|
||||
## How to Test
|
||||
* Resolved #
|
||||
|
||||
> 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
|
||||
@@ -42,6 +16,3 @@ put an [x] in the box to get it checked
|
||||
- [ ] 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,19 +5,10 @@ on:
|
||||
- dev
|
||||
pull_request:
|
||||
types: [ edited, opened, synchronize, reopened ]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
- 'dev'
|
||||
branches: [ master, 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.
|
||||
@@ -29,14 +20,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: 1.18
|
||||
- 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
|
||||
|
||||
52
.github/workflows/build.yaml
vendored
52
.github/workflows/build.yaml
vendored
@@ -2,18 +2,9 @@ name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**.yaml'
|
||||
- '**.md'
|
||||
- '**.sh'
|
||||
- 'website/*'
|
||||
- 'examples/*'
|
||||
- 'docs/*'
|
||||
- 'build/*'
|
||||
- '.github/*'
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
@@ -64,53 +55,28 @@ jobs:
|
||||
CGO_ENABLED: 1
|
||||
run: python3 --version && python3 build.py
|
||||
|
||||
- name: Upload release binaries (Windows / MacOS)
|
||||
- name: Upload release assets (Windows / MacOS)
|
||||
id: upload-release-asset-win-macos
|
||||
uses: actions/upload-release-asset@v1
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: build/${{ matrix.os }}/kubescape
|
||||
asset_name: kubescape-${{ matrix.os }}
|
||||
asset_content_type: application/octet-stream
|
||||
asset_path: build/${{ matrix.os }}/*
|
||||
if: matrix.os != 'ubuntu-20.04'
|
||||
|
||||
- name: Upload release binaries (Linux)
|
||||
- name: Upload release assets (Linux)
|
||||
id: upload-release-asset-linux
|
||||
uses: actions/upload-release-asset@v1
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: build/ubuntu-latest/kubescape
|
||||
asset_name: kubescape-ubuntu-latest
|
||||
asset_content_type: application/octet-stream
|
||||
asset_path: build/ubuntu-latest/*
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
|
||||
- name: Upload release hash (Windows / MacOS)
|
||||
id: upload-release-hash-win-macos
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: build/${{ matrix.os }}/kubescape.sha256
|
||||
asset_name: kubescape-${{ matrix.os }}-sha256
|
||||
asset_content_type: application/octet-stream
|
||||
if: matrix.os != 'ubuntu-20.04'
|
||||
|
||||
- name: Upload release hash (Linux)
|
||||
id: upload-release-hash-linux
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: build/ubuntu-latest/kubescape.sha256
|
||||
asset_name: kubescape-ubuntu-latest-sha256
|
||||
asset_content_type: application/octet-stream
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
# - name: Update new version in krew-index
|
||||
# uses: rajatjindal/krew-release-bot@v0.0.43
|
||||
|
||||
publish-image:
|
||||
uses: ./.github/workflows/build-image.yaml
|
||||
|
||||
8
.github/workflows/build_dev.yaml
vendored
8
.github/workflows/build_dev.yaml
vendored
@@ -4,14 +4,8 @@ on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
paths-ignore:
|
||||
- '**.yaml'
|
||||
# Do not run the pipeline if only Markdown files changed
|
||||
- '**.md'
|
||||
- '**.sh'
|
||||
- 'website/*'
|
||||
- 'examples/*'
|
||||
- 'docs/*'
|
||||
- 'build/*'
|
||||
- '.github/*'
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
|
||||
22
.github/workflows/community.yml
vendored
Normal file
22
.github/workflows/community.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
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,9 +3,7 @@ name: create release digests
|
||||
on:
|
||||
release:
|
||||
types: [ published]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
once:
|
||||
|
||||
12
.github/workflows/pr_checks.yaml
vendored
12
.github/workflows/pr_checks.yaml
vendored
@@ -2,20 +2,12 @@ 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
|
||||
|
||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
@@ -89,12 +89,12 @@ jobs:
|
||||
env:
|
||||
RELEASE: ${{ inputs.release }}
|
||||
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
|
||||
run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape
|
||||
run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape-${{ matrix.os }}
|
||||
if: matrix.os != 'ubuntu-20.04'
|
||||
|
||||
- name: Smoke Testing (Linux)
|
||||
env:
|
||||
RELEASE: ${{ inputs.release }}
|
||||
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
|
||||
run: python3 smoke_testing/init.py ${PWD}/build/ubuntu-latest/kubescape
|
||||
run: python3 smoke_testing/init.py ${PWD}/build/ubuntu-latest/kubescape-ubuntu-latest
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
|
||||
36
.krew.yaml
Normal file
36
.krew.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
apiVersion: krew.googlecontainertools.github.com/v1alpha2
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: kubescape
|
||||
spec:
|
||||
homepage: https://www.armosec.io/kubescape/
|
||||
shortDescription: An open-source Kubernetes security platform for your IDE, CI/CD pipelines, and clusters
|
||||
version: {{ .TagName }}
|
||||
description: |
|
||||
Kubescape is an open-source Kubernetes security platform.
|
||||
It includes risk analysis, security compliance, and misconfiguration scanning.
|
||||
Targeted at the DevSecOps practitioner or platform engineer,
|
||||
it offers an easy-to-use CLI interface, flexible output formats, and automated scanning capabilities.
|
||||
It saves Kubernetes users and admins precious time, effort, and resources.
|
||||
|
||||
Kubescape was created by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository)
|
||||
and is a [Cloud Native Computing Foundation (CNCF) sandbox project](https://www.cncf.io/sandbox-projects/).
|
||||
platforms:
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-macos-latest" .TagName }}
|
||||
bin: kubectl-kubescape
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: amd64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-ubuntu-latest" .TagName }}
|
||||
bin: kubectl-kubescape
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-windows-latest" .TagName }}
|
||||
bin: kubectl-kubescape.exe
|
||||
@@ -4,16 +4,14 @@ First, it is awesome that you are considering contributing to Kubescape! Contrib
|
||||
|
||||
When contributing, we categorize contributions into two:
|
||||
* Small code changes or fixes, whose scope is limited to a single or two files
|
||||
* Complex features and improvements, with potentially unlimited scope
|
||||
* Complex features and improvements, that are not limited
|
||||
|
||||
If you have a small change, feel free to fire up a Pull Request.
|
||||
|
||||
When planning a bigger change, please first discuss the change you wish to make via an issue,
|
||||
so the maintainers are able to help guide you and let you know if you are going in the right direction.
|
||||
When planning a bigger change, please first discuss the change you wish to make via issue,
|
||||
email, or any other method with the owners of this repository before making a change. Most likely your changes or features are great, but sometimes we might be already going in this direction (or the exact opposite ;-) ) and we don't want to waste your time.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please follow our [code of conduct](CODE_OF_CONDUCT.md) in all of your interactions within the project.
|
||||
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
@@ -24,41 +22,79 @@ Please follow our [code of conduct](CODE_OF_CONDUCT.md) in all of your interacti
|
||||
3. Open Pull Request to `dev` branch - we test the component before merging into the `master` branch
|
||||
4. We will merge the Pull Request once you have the sign-off.
|
||||
|
||||
## Developer Certificate of Origin
|
||||
## Code of Conduct
|
||||
|
||||
All commits to the project must be "signed off", which states that you agree to the terms of the [Developer Certificate of Origin](https://developercertificate.org/). This is done by adding a "Signed-off-by:" line in the commit message, with your name and email address.
|
||||
### Our Pledge
|
||||
|
||||
Commits made through the GitHub web application are automatically signed off.
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
### Configuring Git to sign off commits
|
||||
### Our Standards
|
||||
|
||||
First, configure your name and email address in Git global settings:
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
```
|
||||
$ git config --global user.name "John Doe"
|
||||
$ git config --global user.email johndoe@example.com
|
||||
```
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
You can now sign off per-commit, or configure Git to always sign off commits per repository.
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
### Sign off per-commit
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
Add [`-s`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s) to your Git command line. For example:
|
||||
We will distance those who constantly adhere to unacceptable behavior.
|
||||
|
||||
```git commit -s -m "Fix issue 64738"```
|
||||
### Our Responsibilities
|
||||
|
||||
This is tedious, and if you forget, you'll have to [amend your commit](#f)
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective actions in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
### Configure a repository to always include sign off
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
There are many ways to achieve this with Git hooks, but the simplest is to do the following:
|
||||
### Scope
|
||||
|
||||
```
|
||||
cd your-repo
|
||||
curl -Ls https://gist.githubusercontent.com/dixudx/7d7edea35b4d91e1a2a8fbf41d0954fa/raw/prepare-commit-msg -o .git/hooks/prepare-commit-msg
|
||||
chmod +x .git/hooks/prepare-commit-msg
|
||||
```
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Fixing a commit where the DCO failed
|
||||
### Enforcement
|
||||
|
||||
Check out [this guide](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md).
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
### Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
|
||||
496
README.md
496
README.md
@@ -1,94 +1,478 @@
|
||||
[](releases)
|
||||
<div align="center">
|
||||
<img src="docs/kubescape.png" width="300" alt="logo">
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/kubescape/kubescape/actions/workflows/build.yaml)
|
||||
[](https://goreportcard.com/report/github.com/kubescape/kubescape)
|
||||
[](https://gitpod.io/#https://github.com/kubescape/kubescape)
|
||||
[](https://github.com/kubescape/kubescape/blob/master/LICENSE)
|
||||
[](https://landscape.cncf.io/card-mode?project=sandbox&selected=kubescape)
|
||||
[](https://twitter.com/kubescape)
|
||||
|
||||
# Kubescape
|
||||
:sunglasses: [Want to contribute?](#being-a-part-of-the-team) :innocent:
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/cncf/artwork/master/projects/kubescape/stacked/white/kubescape-stacked-white.svg" width="150">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/cncf/artwork/master/projects/kubescape/stacked/color/kubescape-stacked-color.svg" width="150">
|
||||
<img alt="Kubescape logo" align="right" src="https://raw.githubusercontent.com/cncf/artwork/master/projects/kubescape/stacked/color/kubescape-stacked-color.svg" width="150">
|
||||
</picture>
|
||||
|
||||
_An open-source Kubernetes security platform for your IDE, CI/CD pipelines, and clusters_
|
||||
Kubescape is an open-source Kubernetes security platform. A single pane of glass access to view risk analysis, security compliance, RBAC visualization, and image vulnerability scanning.
|
||||
Kubescape scans Kubernetes clusters, YAML files, and Helm charts. It detects misconfigurations according to multiple frameworks (such as [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo/?utm_source=github&utm_medium=repository), [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) and [CIS Benchmark](https://www.armosec.io/blog/cis-kubernetes-benchmark-framework-scanning-tools-comparison/?utm_source=github&utm_medium=repository)). Kubescape also helps you find software vulnerabilities, and RBAC (role-based-access-control) violations at early stages of the CI/CD pipeline. It calculates your risk score instantly and shows risk trends over time.
|
||||
|
||||
Kubescape is an open-source Kubernetes security platform. It includes risk analysis, security compliance, and misconfiguration scanning. Targeted at the DevSecOps practitioner or platform engineer, it offers an easy-to-use CLI interface, flexible output formats, and automated scanning capabilities. It saves Kubernetes users and admins precious time, effort, and resources.
|
||||
Kubescape is one of the fastest-growing Kubernetes security tools among developers. It saves Kubernetes users and admins precious time, effort, and resources with its easy-to-use CLI interface, flexible output formats, and automated scanning capabilities.
|
||||
Kubescape integrates natively with other DevOps tools, including Jenkins, CircleCI, Github workflows, Prometheus, and Slack. It supports multi-cloud Kubernetes deployments like EKS, GKE, and AKS.
|
||||
|
||||
Kubescape scans clusters, YAML files, and Helm charts. It detects misconfigurations according to multiple frameworks (including [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo/?utm_source=github&utm_medium=repository), [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) and the [CIS Benchmark](https://www.armosec.io/blog/cis-kubernetes-benchmark-framework-scanning-tools-comparison/?utm_source=github&utm_medium=repository)).
|
||||
</br>
|
||||
|
||||
Kubescape was created by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository) and is a [Cloud Native Computing Foundation (CNCF) sandbox project](https://www.cncf.io/sandbox-projects/).
|
||||
# Kubescape CLI:
|
||||
<img src="docs/demo.gif">
|
||||
|
||||
## Demo
|
||||
<img src="docs/img/demo.gif">
|
||||
|
||||
_Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape! 😀_
|
||||
|
||||
## Getting started
|
||||
|
||||
Experimenting with Kubescape is as easy as:
|
||||
</br>
|
||||
|
||||
# TL;DR
|
||||
## Install:
|
||||
```sh
|
||||
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
|
||||
```
|
||||
|
||||
Learn more about:
|
||||
*OR:*
|
||||
|
||||
* [Installing Kubescape](docs/getting-started.md#install-kubescape)
|
||||
* [Running your first scan](docs/getting-started.md#run-your-first-scan)
|
||||
* [Usage](docs/getting-started.md#examples)
|
||||
* [Architecture](docs/architecture.md)
|
||||
* [Building Kubescape from source](docs/building.md)
|
||||
[Install on windows](#install-on-windows)
|
||||
|
||||
_Did you know you can use Kubescape in all these places?_
|
||||
[Install on macOS](#install-on-macos)
|
||||
|
||||
[Install on NixOS or Linux/macOS via nix](#install-on-nixos-or-with-nix-community)
|
||||
|
||||
## Run:
|
||||
```sh
|
||||
kubescape scan --enable-host-scan --verbose
|
||||
```
|
||||
|
||||
<img src="docs/summary.png">
|
||||
|
||||
</br>
|
||||
|
||||
> Kubescape is an open source project. We welcome your feedback and ideas for improvement. We’re also aiming to collaborate with the Kubernetes community to help make the tests more robust and complete as Kubernetes develops.
|
||||
|
||||
</br>
|
||||
|
||||
## Architecture in short
|
||||
|
||||
[Component architecture](docs/architecture.drawio.svg)
|
||||
|
||||
### [CLI](#kubescape-cli)
|
||||
<div align="center">
|
||||
<img src="docs/img/ksfromcodetodeploy.png" alt="Places you can use Kubescape: in your IDE, CI, CD, or against a running cluster.">
|
||||
<img src="docs/ks-cli-arch.png" width="300" alt="cli-diagram">
|
||||
</div>
|
||||
|
||||
## Under the hood
|
||||
### [Operator](https://github.com/kubescape/helm-charts#readme)
|
||||
<div align="center">
|
||||
<img src="docs/ks-operator-arch.png" width="300" alt="operator-diagram">
|
||||
</div>
|
||||
|
||||
Kubescape uses [Open Policy Agent](https://github.com/open-policy-agent/opa) to verify Kubernetes objects against [a library of posture controls](https://github.com/kubescape/regolibrary).
|
||||
### Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape 😀
|
||||
|
||||
By default, the results are printed in a console-friendly manner, but they can be:
|
||||
</br>
|
||||
|
||||
* exported to JSON or junit XML
|
||||
* rendered to HTML or PDF
|
||||
* submitted to a [cloud service](docs/providers.md)
|
||||
|
||||
It retrieves Kubernetes objects from the API server and runs a set of [Rego snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io?utm_source=github&utm_medium=repository).
|
||||
# Being a part of the team
|
||||
|
||||
## Community
|
||||
You are in vited to our community! We are excited about this project and want to return the love we get.
|
||||
|
||||
Kubescape is an open source project, we welcome your feedback and ideas for improvement. We are part of the Kubernetes community and are building more tests and controls as the ecosystem develops.
|
||||
We hold community meetings on [Zoom](https://us02web.zoom.us/j/84020231442) on the first Tuesday of every month at 14:00 GMT! :sunglasses:
|
||||
|
||||
We hold [community meetings](https://us02web.zoom.us/j/84020231442) on Zoom, on the first Tuesday of every month, at 14:00 GMT.
|
||||
|
||||
The Kubescape project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
Please make sure that you follow our [Code Of Conduct](https://github.com/kubescape/kubescape/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Contributions
|
||||
Want to discuss something? Have an issue? [Want to contribute?](https://github.com/kubescape/kubescape/blob/master/CONTRIBUTING.md)
|
||||
|
||||
Thanks to all our contributors! Check out our [CONTRIBUTING](CONTRIBUTING.md) file to learn how to join them.
|
||||
* Feel free to pick a task from the [issues](https://github.com/kubescape/kubescape/issues?q=is%3Aissue+is%3Aopen+label%3A%22open+for+contribution%22), [roadmap](docs/roadmap.md) or suggest a feature of your own. [Contact us](MAINTAINERS.md) directly for more information :)
|
||||
* [Open an issue](https://github.com/kubescape/kubescape/issues/new/choose) , we are trying to respond within 48 hours
|
||||
* [Join us](https://discord.com/invite/WKZRaCtBxN) in the discussion on our discord server!
|
||||
|
||||
* Feel free to pick a task from the [issues](https://github.com/kubescape/kubescape/issues?q=is%3Aissue+is%3Aopen+label%3A%22open+for+contribution%22), [roadmap](docs/roadmap.md) or suggest a feature of your own.
|
||||
* [Open an issue](https://github.com/kubescape/kubescape/issues/new/choose): we aim to respond to all issues within 48 hours.
|
||||
* [Join the CNCF Slack](https://slack.cncf.io/) and then our [users](https://cloud-native.slack.com/archives/C04EY3ZF9GE) or [developers](https://cloud-native.slack.com/archives/C04GY6H082K) channel.
|
||||
[<img src="docs/discord-banner.png" width="100" alt="logo" align="center">](https://discord.com/invite/WKZRaCtBxN)
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
# Options and examples
|
||||
|
||||
[Kubescape docs](https://hub.armosec.io/docs?utm_source=github&utm_medium=repository)
|
||||
|
||||
## Playground
|
||||
* [Kubescape playground](https://killercoda.com/saiyampathak/scenario/kubescape)
|
||||
|
||||
## Tutorials
|
||||
|
||||
* [Overview](https://youtu.be/wdBkt_0Qhbg)
|
||||
* [How To Secure Kubernetes Clusters With Kubescape And Armo](https://youtu.be/ZATGiDIDBQk)
|
||||
* [Scan Kubernetes YAML files](https://youtu.be/Ox6DaR7_4ZI)
|
||||
* [Scan container image registry](https://youtu.be/iQ_k8EnK-3s)
|
||||
* [Scan Kubescape on an air-gapped environment (offline support)](https://youtu.be/IGXL9s37smM)
|
||||
* [Managing exceptions in the Kubescape SaaS version](https://youtu.be/OzpvxGmCR80)
|
||||
* [Configure and run customized frameworks](https://youtu.be/12Sanq_rEhs)
|
||||
* Customize control configurations:
|
||||
- [Kubescape CLI](https://youtu.be/955psg6TVu4)
|
||||
- [Kubescape SaaS](https://youtu.be/lIMVSVhH33o)
|
||||
|
||||
## Install on Windows
|
||||
|
||||
<details><summary>Windows</summary>
|
||||
|
||||
**Requires powershell v5.0+**
|
||||
|
||||
``` powershell
|
||||
iwr -useb https://raw.githubusercontent.com/kubescape/kubescape/master/install.ps1 | iex
|
||||
```
|
||||
|
||||
Note: if you get an error you might need to change the execution policy (i.e. enable Powershell) with
|
||||
|
||||
``` powershell
|
||||
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
## Install on macOS
|
||||
|
||||
<details><summary>MacOS</summary>
|
||||
|
||||
1. ```sh
|
||||
brew tap kubescape/tap
|
||||
```
|
||||
2. ```sh
|
||||
brew install kubescape-cli
|
||||
```
|
||||
</details>
|
||||
|
||||
## Install on NixOS or with nix (Community)
|
||||
|
||||
<details><summary>Nix/NixOS</summary>
|
||||
|
||||
Direct issues installing `kubescape` via `nix` through the channels mentioned [here](https://nixos.wiki/wiki/Support)
|
||||
|
||||
You can use `nix` on Linux or macOS and on other platforms unofficially.
|
||||
|
||||
Try it out in an ephemeral shell: `nix-shell -p kubescape`
|
||||
|
||||
Install declarative as usual
|
||||
|
||||
NixOS:
|
||||
|
||||
```nix
|
||||
# your other config ...
|
||||
environment.systemPackages = with pkgs; [
|
||||
# your other packages ...
|
||||
kubescape
|
||||
];
|
||||
```
|
||||
|
||||
home-manager:
|
||||
|
||||
```nix
|
||||
# your other config ...
|
||||
home.packages = with pkgs; [
|
||||
# your other packages ...
|
||||
kubescape
|
||||
];
|
||||
```
|
||||
|
||||
Or to your profile (not preferred): `nix-env --install -A nixpkgs.kubescape`
|
||||
|
||||
</details>
|
||||
|
||||
## Usage & Examples
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Scan a running Kubernetes cluster
|
||||
```
|
||||
kubescape scan --enable-host-scan --verbose
|
||||
```
|
||||
|
||||
> Read [here](https://hub.armosec.io/docs/host-sensor?utm_source=github&utm_medium=repository) more about the `enable-host-scan` flag
|
||||
|
||||
#### Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
|
||||
```
|
||||
kubescape scan framework nsa
|
||||
```
|
||||
|
||||
|
||||
#### Scan a running Kubernetes cluster with [`MITRE ATT&CK®`](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) framework
|
||||
```
|
||||
kubescape scan framework mitre
|
||||
```
|
||||
|
||||
|
||||
#### Scan a running Kubernetes cluster with a specific control using the control name or control ID. [List of controls](https://hub.armosec.io/docs/controls?utm_source=github&utm_medium=repository)
|
||||
```
|
||||
kubescape scan control "Privileged container"
|
||||
```
|
||||
|
||||
#### Scan using an alternative kubeconfig file
|
||||
```
|
||||
kubescape scan --kubeconfig cluster.conf
|
||||
```
|
||||
|
||||
#### Scan specific namespaces
|
||||
```
|
||||
kubescape scan --include-namespaces development,staging,production
|
||||
```
|
||||
|
||||
#### Scan cluster and exclude some namespaces
|
||||
```
|
||||
kubescape scan --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
#### Scan local `yaml`/`json` files before deploying. [Take a look at the demonstration](https://youtu.be/Ox6DaR7_4ZI).
|
||||
```
|
||||
kubescape scan *.yaml
|
||||
```
|
||||
|
||||
#### Scan Kubernetes manifest files from a git repository
|
||||
|
||||
```
|
||||
kubescape scan https://github.com/kubescape/kubescape
|
||||
```
|
||||
|
||||
#### Display all scanned resources (including the resources which passed)
|
||||
```
|
||||
kubescape scan --verbose
|
||||
```
|
||||
|
||||
#### Output in `json` format
|
||||
|
||||
> Add the `--format-version v2` flag
|
||||
|
||||
```
|
||||
kubescape scan --format json --format-version v2 --output results.json
|
||||
```
|
||||
|
||||
#### Output in `junit xml` format
|
||||
```
|
||||
kubescape scan --format junit --output results.xml
|
||||
```
|
||||
|
||||
#### Output in `pdf` format - Contributed by [@alegrey91](https://github.com/alegrey91)
|
||||
|
||||
```
|
||||
kubescape scan --format pdf --output results.pdf
|
||||
```
|
||||
|
||||
#### Output in `prometheus` metrics format - Contributed by [@Joibel](https://github.com/Joibel)
|
||||
|
||||
```
|
||||
kubescape scan --format prometheus
|
||||
```
|
||||
|
||||
#### Output in `html` format
|
||||
|
||||
```
|
||||
kubescape scan --format html --output results.html
|
||||
```
|
||||
|
||||
#### Scan with exceptions. Objects with exceptions will be presented as `exclude` and not `fail`
|
||||
[Full documentation](examples/exceptions/README.md)
|
||||
```
|
||||
kubescape scan --exceptions examples/exceptions/exclude-kube-namespaces.json
|
||||
```
|
||||
|
||||
#### Scan Helm charts
|
||||
```
|
||||
kubescape scan </path/to/directory>
|
||||
```
|
||||
> Kubescape will load the default value file
|
||||
|
||||
#### Scan a Kustomize Directory
|
||||
```
|
||||
kubescape scan </path/to/directory>
|
||||
```
|
||||
> Kubescape will generate Kubernetes YAML objects using a 'Kustomize' file and scan them for security.
|
||||
|
||||
### Offline/Air-gapped Environment Support
|
||||
|
||||
[Video tutorial](https://youtu.be/IGXL9s37smM)
|
||||
|
||||
It is possible to run Kubescape offline!
|
||||
#### Download all artifacts
|
||||
|
||||
1. Download and save in local directory, if path not specified, will save all in `~/.kubescape`
|
||||
```
|
||||
kubescape download artifacts --output path/to/local/dir
|
||||
```
|
||||
2. Copy the downloaded artifacts to the air-gaped/offline environment
|
||||
|
||||
3. Scan using the downloaded artifacts
|
||||
```
|
||||
kubescape scan --use-artifacts-from path/to/local/dir
|
||||
```
|
||||
|
||||
#### Download a single artifact
|
||||
|
||||
You can also download a single artifact and scan with the `--use-from` flag
|
||||
|
||||
1. Download and save in a file, if the file name is not specified, will save in `~/.kubescape/<framework name>.json`
|
||||
```
|
||||
kubescape download framework nsa --output /path/nsa.json
|
||||
```
|
||||
2. Copy the downloaded artifacts to the air-gaped/offline environment
|
||||
|
||||
3. Scan using the downloaded framework
|
||||
```
|
||||
kubescape scan framework nsa --use-from /path/nsa.json
|
||||
```
|
||||
|
||||
|
||||
## Scan Periodically using Helm
|
||||
[Please follow the instructions here](https://hub.armosec.io/docs/installation-of-armo-in-cluster?utm_source=github&utm_medium=repository)
|
||||
[helm chart repo](https://github.com/armosec/armo-helm)
|
||||
|
||||
# Integrations
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
 
|
||||
|
||||
Scan the YAML files while writing them using the [VS Code extension](https://github.com/armosec/vscode-kubescape/blob/master/README.md)
|
||||
|
||||
## Lens Extension
|
||||
|
||||
View Kubescape scan results directly in [Lens IDE](https://k8slens.dev/) using kubescape [Lens extension](https://github.com/armosec/lens-kubescape/blob/master/README.md)
|
||||
|
||||
|
||||
# Building Kubescape
|
||||
|
||||
## Build on Windows
|
||||
|
||||
<details><summary>Windows</summary>
|
||||
|
||||
1. Install MSYS2 & build libgit _(needed only for the first time)_
|
||||
|
||||
```
|
||||
build.bat all
|
||||
```
|
||||
|
||||
> You can install MSYS2 separately by running `build.bat install` and build libgit2 separately by running `build.bat build`
|
||||
|
||||
2. Build kubescape
|
||||
|
||||
```
|
||||
make build
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```
|
||||
go build -tags=static .
|
||||
```
|
||||
</details>
|
||||
|
||||
## Build on Linux/MacOS
|
||||
|
||||
<details><summary>Linux / MacOS</summary>
|
||||
|
||||
1. Install libgit2 dependency _(needed only for the first time)_
|
||||
|
||||
```
|
||||
make libgit2
|
||||
```
|
||||
|
||||
> `cmake` is required to build libgit2. You can install it by running `sudo apt-get install cmake` (Linux) or `brew install cmake` (macOS)
|
||||
|
||||
2. Build kubescape
|
||||
|
||||
```
|
||||
make build
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```
|
||||
go build -tags=static .
|
||||
```
|
||||
|
||||
3. Test
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Build on pre-configured killercoda's ubuntu playground
|
||||
|
||||
* [Pre-configured Killercoda's Ubuntu Playground](https://killercoda.com/suhas-gumma/scenario/kubescape-build-for-development)
|
||||
|
||||
<details><summary> Pre-programmed actions executed by the playground </summary>
|
||||
|
||||
|
||||
* Clone the official GitHub repository of `Kubescape`.
|
||||
* [Automate the build process on Linux](https://github.com/kubescape/kubescape#build-on-linuxmacos)
|
||||
* The entire process involves executing multiple commands in order and it takes around 5-6 minutes to execute them all.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Instructions to use the playground</summary>
|
||||
|
||||
* Apply changes you wish to make to the Kubescape directory using text editors like `Vim`.
|
||||
* [Build on Linux](https://github.com/kubescape/kubescape#build-on-linuxmacos)
|
||||
* Now, you can use Kubescape like a regular user. Instead of using `kubescape`, use `./kubescape`. Make sure you are in the Kubescape directory because the command will execute the binary named `kubescape` in `kubescape directory`)
|
||||
|
||||
</details>
|
||||
|
||||
## VS Code configuration samples
|
||||
|
||||
You can use the sample files below to setup your VS Code environment for building and debugging purposes.
|
||||
|
||||
|
||||
<details><summary>.vscode/settings.json</summary>
|
||||
|
||||
```json5
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"go.testTags": "static",
|
||||
"go.buildTags": "static",
|
||||
"go.toolsEnvVars": {
|
||||
"CGO_ENABLED": "1"
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details><summary>.vscode/launch.json</summary>
|
||||
|
||||
```json5
|
||||
// .vscode/launch.json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": [
|
||||
"scan",
|
||||
"--logger",
|
||||
"debug"
|
||||
],
|
||||
"buildFlags": "-tags=static"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
# Under the hood
|
||||
|
||||
## Technology
|
||||
Kubescape is based on the [OPA engine](https://github.com/open-policy-agent/opa) and ARMO's posture controls.
|
||||
|
||||
The tools retrieve Kubernetes objects from the API server and runs a set of [Rego snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io?utm_source=github&utm_medium=repository).
|
||||
|
||||
The results by default are printed in a "console friendly" manner, but they can be retrieved in JSON format for further processing.
|
||||
|
||||
Kubescape is an open source project, we welcome your feedback and ideas for improvement. We are part of the Kubernetes community and aim to make the tests more robust and complete as Kubernetes develops.
|
||||
|
||||
## Thanks to all the contributors ❤️
|
||||
<a href = "https://github.com/kubescape/kubescape/graphs/contributors">
|
||||
<img src = "https://contrib.rocks/image?repo=kubescape/kubescape"/>
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2021-2023, the Kubescape Authors. All rights reserved. Kubescape is released under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
Kubescape is a [Cloud Native Computing Foundation (CNCF) sandbox project](https://www.cncf.io/sandbox-projects/) and was contributed by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository).
|
||||
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/cncf/artwork/master/other/cncf-sandbox/horizontal/color/cncf-sandbox-horizontal-color.svg" width="300" alt="CNCF Sandbox Project">
|
||||
</div>
|
||||
|
||||
25
build.py
25
build.py
@@ -3,9 +3,16 @@ import sys
|
||||
import hashlib
|
||||
import platform
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
BASE_GETTER_CONST = "github.com/kubescape/kubescape/v2/core/cautils/getter"
|
||||
|
||||
platformSuffixes = {
|
||||
"Windows": "windows-latest",
|
||||
"Linux": "ubuntu-latest",
|
||||
"Darwin": "macos-latest",
|
||||
}
|
||||
|
||||
def check_status(status, msg):
|
||||
if status != 0:
|
||||
sys.stderr.write(msg)
|
||||
@@ -14,20 +21,18 @@ def check_status(status, msg):
|
||||
|
||||
def get_build_dir():
|
||||
current_platform = platform.system()
|
||||
build_dir = ""
|
||||
|
||||
if current_platform == "Windows": build_dir = "windows-latest"
|
||||
elif current_platform == "Linux": build_dir = "ubuntu-latest"
|
||||
elif current_platform == "Darwin": build_dir = "macos-latest"
|
||||
else: raise OSError("Platform %s is not supported!" % (current_platform))
|
||||
if current_platform not in platformSuffixes: raise OSError("Platform %s is not supported!" % (current_platform))
|
||||
|
||||
return os.path.join("build", build_dir)
|
||||
return os.path.join("build", platformSuffixes[current_platform])
|
||||
|
||||
|
||||
def get_package_name():
|
||||
package_name = "kubescape"
|
||||
current_platform = platform.system()
|
||||
|
||||
return package_name
|
||||
if current_platform not in platformSuffixes: raise OSError("Platform %s is not supported!" % (current_platform))
|
||||
|
||||
return "kubescape-" + platformSuffixes[current_platform]
|
||||
|
||||
|
||||
def main():
|
||||
@@ -46,6 +51,7 @@ def main():
|
||||
|
||||
ks_file = os.path.join(build_dir, package_name)
|
||||
hash_file = ks_file + ".sha256"
|
||||
tar_file = ks_file + ".tar.gz"
|
||||
|
||||
if not os.path.isdir(build_dir):
|
||||
os.makedirs(build_dir)
|
||||
@@ -73,6 +79,9 @@ def main():
|
||||
print("kubescape hash: {}, file: {}".format(hash, hash_file))
|
||||
kube_sha.write(sha256.hexdigest())
|
||||
|
||||
with tarfile.open(tar_file, 'w:gz') as archive:
|
||||
archive.add(ks_file, "kubescape")
|
||||
|
||||
print("Build Done")
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ RUN ls -ltr build/ubuntu-latest
|
||||
WORKDIR /work
|
||||
RUN python build.py
|
||||
|
||||
RUN /work/build/ubuntu-latest/kubescape download artifacts -o /work/artifacts
|
||||
RUN /work/build/ubuntu-latest/kubescape-ubuntu-latest download artifacts -o /work/artifacts
|
||||
|
||||
FROM alpine:3.16.2
|
||||
|
||||
@@ -45,7 +45,7 @@ USER ks
|
||||
|
||||
WORKDIR /home/ks
|
||||
|
||||
COPY --from=builder /work/httphandler/build/ubuntu-latest/kubescape /usr/bin/ksserver
|
||||
COPY --from=builder /work/build/ubuntu-latest/kubescape /usr/bin/kubescape
|
||||
COPY --from=builder /work/httphandler/build/ubuntu-latest/kubescape-ubuntu-latest /usr/bin/ksserver
|
||||
COPY --from=builder /work/build/ubuntu-latest/kubescape-ubuntu-latest /usr/bin/kubescape
|
||||
|
||||
ENTRYPOINT ["ksserver"]
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package completion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var completionCmdExamples = `
|
||||
|
||||
var completionCmdExamples = fmt.Sprintf(`
|
||||
# Enable BASH shell autocompletion
|
||||
$ source <(kubescape completion bash)
|
||||
$ echo 'source <(kubescape completion bash)' >> ~/.bashrc
|
||||
$ source <(%[1]s completion bash)
|
||||
$ echo 'source <(%[1]s completion bash)' >> ~/.bashrc
|
||||
|
||||
# Enable ZSH shell autocompletion
|
||||
$ source <(kubectl completion zsh)
|
||||
$ echo 'source <(kubectl completion zsh)' >> "${fpath[1]}/_kubectl"
|
||||
|
||||
`
|
||||
`, cautils.ExecName())
|
||||
|
||||
func GetCompletionCmd() *cobra.Command {
|
||||
completionCmd := &cobra.Command{
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"github.com/kubescape/kubescape/v2/core/meta"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
configExample = `
|
||||
configExample = fmt.Sprintf(`
|
||||
# View cached configurations
|
||||
kubescape config view
|
||||
%[1]s config view
|
||||
|
||||
# Delete cached configurations
|
||||
kubescape config delete
|
||||
%[1]s config delete
|
||||
|
||||
# Set cached configurations
|
||||
kubescape config set --help
|
||||
`
|
||||
setConfigExample = `
|
||||
%[1]s config set --help
|
||||
`, cautils.ExecName())
|
||||
setConfigExample = fmt.Sprintf(`
|
||||
# Set account id
|
||||
kubescape config set accountID <account id>
|
||||
%[1]s config set accountID <account id>
|
||||
|
||||
# Set client id
|
||||
kubescape config set clientID <client id>
|
||||
%[1]s config set clientID <client id>
|
||||
|
||||
# Set access key
|
||||
kubescape config set secretKey <access key>
|
||||
%[1]s config set secretKey <access key>
|
||||
|
||||
# Set cloudAPIURL
|
||||
kubescape config set cloudAPIURL <cloud API URL>
|
||||
`
|
||||
%[1]s config set cloudAPIURL <cloud API URL>
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
func GetConfigCmd(ks meta.IKubescape) *cobra.Command {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"github.com/kubescape/kubescape/v2/core/meta"
|
||||
v1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var deleteExceptionsExamples = `
|
||||
var deleteExceptionsExamples = fmt.Sprintf(`
|
||||
# Delete single exception
|
||||
kubescape delete exceptions "exception name"
|
||||
%[1]s delete exceptions "exception name"
|
||||
|
||||
# Delete multiple exceptions
|
||||
kubescape delete exceptions "first exception;second exception;third exception"
|
||||
`
|
||||
%[1]s delete exceptions "first exception;second exception;third exception"
|
||||
`, cautils.ExecName())
|
||||
|
||||
func GetDeleteCmd(ks meta.IKubescape) *cobra.Command {
|
||||
var deleteInfo v1.Delete
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"github.com/kubescape/kubescape/v2/core/meta"
|
||||
v1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
func getExceptionsCmd(ks meta.IKubescape, deleteInfo *v1.Delete) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "exceptions <exception name>",
|
||||
Short: "Delete exceptions from Kubescape SaaS version. Run 'kubescape list exceptions' for all exceptions names",
|
||||
Short: fmt.Sprintf("Delete exceptions from Kubescape SaaS version. Run '%[1]s list exceptions' for all exceptions names", cautils.ExecName()),
|
||||
Example: deleteExceptionsExamples,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
|
||||
@@ -14,31 +14,31 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
downloadExample = `
|
||||
downloadExample = fmt.Sprintf(`
|
||||
# Download all artifacts and save them in the default path (~/.kubescape)
|
||||
kubescape download artifacts
|
||||
%[1]s download artifacts
|
||||
|
||||
# Download all artifacts and save them in /tmp path
|
||||
kubescape download artifacts --output /tmp
|
||||
%[1]s download artifacts --output /tmp
|
||||
|
||||
# Download the NSA framework. Run 'kubescape list frameworks' for all frameworks names
|
||||
kubescape download framework nsa
|
||||
# Download the NSA framework. Run '%[1]s list frameworks' for all frameworks names
|
||||
%[1]s download framework nsa
|
||||
|
||||
# Download the "C-0001" control. Run 'kubescape list controls --id' for all controls ids
|
||||
kubescape download control "C-0001"
|
||||
# Download the "C-0001" control. Run '%[1]s list controls --id' for all controls ids
|
||||
%[1]s download control "C-0001"
|
||||
|
||||
# Download the "C-0001" control. Run 'kubescape list controls --id' for all controls ids
|
||||
kubescape download control C-0001
|
||||
# Download the "C-0001" control. Run '%[1]s list controls --id' for all controls ids
|
||||
%[1]s download control C-0001
|
||||
|
||||
# Download the configured exceptions
|
||||
kubescape download exceptions
|
||||
%[1]s download exceptions
|
||||
|
||||
# Download the configured controls-inputs
|
||||
kubescape download controls-inputs
|
||||
%[1]s download controls-inputs
|
||||
|
||||
# Download the attack tracks
|
||||
kubescape download attack-tracks
|
||||
`
|
||||
%[1]s download attack-tracks
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
func GeDownloadCmd(ks meta.IKubescape) *cobra.Command {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -13,19 +13,19 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
listExample = `
|
||||
listExample = fmt.Sprintf(`
|
||||
# List default supported frameworks names
|
||||
kubescape list frameworks
|
||||
%[1]s list frameworks
|
||||
|
||||
# List all supported frameworks names
|
||||
kubescape list frameworks --account <account id>
|
||||
%[1]s list frameworks --account <account id>
|
||||
|
||||
# List all supported controls names with ids
|
||||
kubescape list controls
|
||||
%[1]s list controls
|
||||
|
||||
Control documentation:
|
||||
https://hub.armosec.io/docs/controls
|
||||
`
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
func GetListCmd(ks meta.IKubescape) *cobra.Command {
|
||||
@@ -65,7 +65,7 @@ func GetListCmd(ks meta.IKubescape) *cobra.Command {
|
||||
listCmd.PersistentFlags().StringVarP(&listPolicies.Credentials.ClientID, "client-id", "", "", "Kubescape SaaS client ID. Default will load client ID from cache, read more - https://hub.armosec.io/docs/authentication")
|
||||
listCmd.PersistentFlags().StringVarP(&listPolicies.Credentials.SecretKey, "secret-key", "", "", "Kubescape SaaS secret key. Default will load secret key from cache, read more - https://hub.armosec.io/docs/authentication")
|
||||
listCmd.PersistentFlags().StringVar(&listPolicies.Format, "format", "pretty-print", "output format. supported: 'pretty-print'/'json'")
|
||||
listCmd.PersistentFlags().MarkDeprecated("id", "Control ID's are included in list outpus")
|
||||
listCmd.PersistentFlags().MarkDeprecated("id", "Control ID's are included in list outputs")
|
||||
|
||||
return listCmd
|
||||
}
|
||||
|
||||
24
cmd/root.go
24
cmd/root.go
@@ -10,7 +10,6 @@ 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"
|
||||
@@ -26,19 +25,19 @@ import (
|
||||
|
||||
var rootInfo cautils.RootInfo
|
||||
|
||||
var ksExamples = `
|
||||
var ksExamples = fmt.Sprintf(`
|
||||
# Scan command
|
||||
kubescape scan
|
||||
%[1]s scan
|
||||
|
||||
# List supported frameworks
|
||||
kubescape list frameworks
|
||||
%[1]s list frameworks
|
||||
|
||||
# Download artifacts (air-gapped environment support)
|
||||
kubescape download artifacts
|
||||
%[1]s download artifacts
|
||||
|
||||
# View cached configurations
|
||||
kubescape config view
|
||||
`
|
||||
%[1]s config view
|
||||
`, cautils.ExecName())
|
||||
|
||||
func NewDefaultKubescapeCommand() *cobra.Command {
|
||||
ks := core.NewKubescape()
|
||||
@@ -53,6 +52,16 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
|
||||
Example: ksExamples,
|
||||
}
|
||||
|
||||
if cautils.IsKrewPlugin() {
|
||||
// Invoked as a kubectl plugin.
|
||||
|
||||
// Cobra doesn't have a way to specify a two word command (i.e. "kubectl kubescape"), so set a custom usage template
|
||||
// with kubectl in it. Cobra will use this template for the root and all child commands.
|
||||
oldUsageTemplate := rootCmd.UsageTemplate()
|
||||
newUsageTemplate := strings.NewReplacer("{{.UseLine}}", "kubectl {{.UseLine}}", "{{.CommandPath}}", "kubectl {{.CommandPath}}").Replace(oldUsageTemplate)
|
||||
rootCmd.SetUsageTemplate(newUsageTemplate)
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&rootInfo.KSCloudBEURLsDep, "environment", "", envFlagUsage)
|
||||
rootCmd.PersistentFlags().StringVar(&rootInfo.KSCloudBEURLs, "env", "", envFlagUsage)
|
||||
rootCmd.PersistentFlags().MarkDeprecated("environment", "use 'env' instead")
|
||||
@@ -79,7 +88,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -18,28 +18,28 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
controlExample = `
|
||||
controlExample = fmt.Sprintf(`
|
||||
# Scan the 'privileged container' control
|
||||
kubescape scan control "privileged container"
|
||||
%[1]s scan control "privileged container"
|
||||
|
||||
# Scan list of controls separated with a comma
|
||||
kubescape scan control "privileged container","HostPath mount"
|
||||
%[1]s scan control "privileged container","HostPath mount"
|
||||
|
||||
# Scan list of controls using the control ID separated with a comma
|
||||
kubescape scan control C-0058,C-0057
|
||||
%[1]s scan control C-0058,C-0057
|
||||
|
||||
Run 'kubescape list controls' for the list of supported controls
|
||||
Run '%[1]s list controls' for the list of supported controls
|
||||
|
||||
Control documentation:
|
||||
https://hub.armosec.io/docs/controls
|
||||
`
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
// controlCmd represents the control command
|
||||
func getControlCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "control <control names list>/<control ids list>",
|
||||
Short: "The controls you wish to use. Run 'kubescape list controls' for the list of supported controls",
|
||||
Short: fmt.Sprintf("The controls you wish to use. Run '%[1]s list controls' for the list of supported controls", cautils.ExecName()),
|
||||
Example: controlExample,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
@@ -67,7 +67,7 @@ func getControlCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comman
|
||||
|
||||
if len(args) == 0 {
|
||||
scanInfo.ScanAll = true
|
||||
} else { // expected control or list of control sepparated by ","
|
||||
} else { // expected control or list of control separated by ","
|
||||
|
||||
// Read controls from input args
|
||||
scanInfo.SetPolicyIdentifiers(strings.Split(args[0], ","), apisv1.KindControl)
|
||||
|
||||
@@ -20,24 +20,24 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
frameworkExample = `
|
||||
frameworkExample = fmt.Sprintf(`
|
||||
# Scan all frameworks
|
||||
kubescape scan framework all
|
||||
%[1]s scan framework all
|
||||
|
||||
# Scan the NSA framework
|
||||
kubescape scan framework nsa
|
||||
%[1]s scan framework nsa
|
||||
|
||||
# Scan the NSA and MITRE framework
|
||||
kubescape scan framework nsa,mitre
|
||||
%[1]s scan framework nsa,mitre
|
||||
|
||||
# Scan all frameworks
|
||||
kubescape scan framework all
|
||||
%[1]s scan framework all
|
||||
|
||||
# Scan kubernetes YAML manifest files (single file or glob)
|
||||
kubescape scan framework nsa .
|
||||
%[1]s scan framework nsa .
|
||||
|
||||
Run 'kubescape list frameworks' for the list of supported frameworks
|
||||
`
|
||||
Run '%[1]s list frameworks' for the list of supported frameworks
|
||||
`, cautils.ExecName())
|
||||
|
||||
ErrUnknownSeverity = errors.New("unknown severity")
|
||||
)
|
||||
@@ -46,7 +46,7 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "framework <framework names list> [`<glob pattern>`/`-`] [flags]",
|
||||
Short: "The framework you wish to use. Run 'kubescape list frameworks' for the list of supported frameworks",
|
||||
Short: fmt.Sprintf("The framework you wish to use. Run '%[1]s list frameworks' for the list of supported frameworks", cautils.ExecName()),
|
||||
Example: frameworkExample,
|
||||
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -10,25 +10,24 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var scanCmdExamples = `
|
||||
var scanCmdExamples = fmt.Sprintf(`
|
||||
Scan command is for scanning an existing cluster or kubernetes manifest files based on pre-defined frameworks
|
||||
|
||||
# Scan current cluster with all frameworks
|
||||
kubescape scan --enable-host-scan --verbose
|
||||
%[1]s scan --enable-host-scan --verbose
|
||||
|
||||
# Scan kubernetes YAML manifest files
|
||||
kubescape scan .
|
||||
%[1]s scan .
|
||||
|
||||
# Scan and save the results in the JSON format
|
||||
kubescape scan --format json --output results.json --format-version=v2
|
||||
%[1]s scan --format json --output results.json --format-version=v2
|
||||
|
||||
# Display all resources
|
||||
kubescape scan --verbose
|
||||
%[1]s scan --verbose
|
||||
|
||||
# Scan different clusters from the kubectl context
|
||||
kubescape scan --kube-context <kubernetes context>
|
||||
|
||||
`
|
||||
%[1]s scan --kube-context <kubernetes context>
|
||||
`, cautils.ExecName())
|
||||
|
||||
func GetScanCommand(ks meta.IKubescape) *cobra.Command {
|
||||
var scanInfo cautils.ScanInfo
|
||||
@@ -66,7 +65,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")
|
||||
|
||||
@@ -19,13 +19,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
rbacExamples = `
|
||||
rbacExamples = fmt.Sprintf(`
|
||||
# Submit cluster's Role-Based Access Control(RBAC)
|
||||
kubescape submit rbac
|
||||
%[1]s submit rbac
|
||||
|
||||
# Submit cluster's Role-Based Access Control(RBAC) with account ID
|
||||
kubescape submit rbac --account <account-id>
|
||||
`
|
||||
%[1]s submit rbac --account <account-id>
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
// getRBACCmd represents the RBAC command
|
||||
@@ -36,7 +36,7 @@ func getRBACCmd(ks meta.IKubescape, submitInfo *v1.Submit) *cobra.Command {
|
||||
Example: rbacExamples,
|
||||
Short: "Submit cluster's Role-Based Access Control(RBAC)",
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
|
||||
if err := flagValidationSubmit(submitInfo); err != nil {
|
||||
return err
|
||||
@@ -51,7 +51,7 @@ func getRBACCmd(ks meta.IKubescape, submitInfo *v1.Submit) *cobra.Command {
|
||||
}
|
||||
|
||||
if clusterConfig.GetAccountID() == "" {
|
||||
return fmt.Errorf("account ID is not set, run 'kubescape submit rbac --account <account-id>'")
|
||||
return fmt.Errorf("account ID is not set, run '%[1]s submit rbac --account <account-id>'", cautils.ExecName())
|
||||
}
|
||||
|
||||
// list RBAC
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
@@ -50,7 +51,7 @@ func (resultsObject *ResultsObject) ListAllResources() (map[string]workloadinter
|
||||
|
||||
func getResultsCmd(ks meta.IKubescape, submitInfo *v1.Submit) *cobra.Command {
|
||||
var resultsCmd = &cobra.Command{
|
||||
Use: "results <json file>\nExample:\n$ kubescape submit results path/to/results.json --format-version v2",
|
||||
Use: fmt.Sprintf("results <json file>\nExample:\n$ %[1]s submit results path/to/results.json --format-version v2", cautils.ExecName()),
|
||||
Short: "Submit a pre scanned results file. The file must be in json format",
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -87,7 +88,7 @@ func getResultsCmd(ks meta.IKubescape, submitInfo *v1.Submit) *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
resultsCmd.PersistentFlags().StringVar(&formatVersion, "format-version", "v1", "Output object can be differnet between versions, this is for maintaining backward and forward compatibility. Supported:'v1'/'v2'")
|
||||
resultsCmd.PersistentFlags().StringVar(&formatVersion, "format-version", "v1", "Output object can be different between versions, this is for maintaining backward and forward compatibility. Supported:'v1'/'v2'")
|
||||
|
||||
return resultsCmd
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package submit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"github.com/kubescape/kubescape/v2/core/meta"
|
||||
metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var submitCmdExamples = `
|
||||
var submitCmdExamples = fmt.Sprintf(`
|
||||
# Submit Kubescape scan results file
|
||||
kubescape submit results
|
||||
%[1]s submit results
|
||||
|
||||
# Submit exceptions file to Kubescape SaaS
|
||||
kubescape submit exceptions
|
||||
`
|
||||
%[1]s submit exceptions
|
||||
`, cautils.ExecName())
|
||||
|
||||
func GetSubmitCmd(ks meta.IKubescape) *cobra.Command {
|
||||
var submitInfo metav1.Submit
|
||||
|
||||
@@ -2,6 +2,7 @@ package getter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -40,6 +41,13 @@ 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)
|
||||
@@ -58,7 +66,6 @@ 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)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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,6 +2,7 @@ package getter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -2,6 +2,7 @@ package getter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -15,19 +15,7 @@ import (
|
||||
// =======================================================================================================================
|
||||
// ============================================== LoadPolicy =============================================================
|
||||
// =======================================================================================================================
|
||||
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{}
|
||||
)
|
||||
var DefaultLocalStore = getCacheDir()
|
||||
|
||||
func getCacheDir() string {
|
||||
defaultDirPath := ".kubescape"
|
||||
@@ -37,12 +25,11 @@ func getCacheDir() string {
|
||||
return defaultDirPath
|
||||
}
|
||||
|
||||
// LoadPolicy loads policies from a local repository.
|
||||
// Load policies from a local repository
|
||||
type LoadPolicy struct {
|
||||
filePaths []string
|
||||
}
|
||||
|
||||
// NewLoadPolicy builds a LoadPolicy.
|
||||
func NewLoadPolicy(filePaths []string) *LoadPolicy {
|
||||
return &LoadPolicy{
|
||||
filePaths: filePaths,
|
||||
@@ -51,211 +38,122 @@ func NewLoadPolicy(filePaths []string) *LoadPolicy {
|
||||
|
||||
// GetControl returns a control from the policy file.
|
||||
func (lp *LoadPolicy) GetControl(controlID string) (*reporthandling.Control, error) {
|
||||
if controlID == "" {
|
||||
return nil, ErrIDRequired
|
||||
}
|
||||
|
||||
// NOTE: this assumes that only the first path contains either a valid control descriptor or a framework descriptor
|
||||
control := &reporthandling.Control{}
|
||||
filePath := lp.filePath()
|
||||
buf, err := os.ReadFile(filePath)
|
||||
|
||||
f, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, 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 err = json.Unmarshal(f, control); err != nil {
|
||||
return control, err
|
||||
}
|
||||
|
||||
// check if the file is a framework descriptor
|
||||
var framework reporthandling.Framework
|
||||
if err = json.Unmarshal(buf, &framework); err != nil {
|
||||
return nil, err
|
||||
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")
|
||||
}
|
||||
|
||||
for _, toPin := range framework.Controls {
|
||||
ctrl := toPin
|
||||
|
||||
if strings.EqualFold(ctrl.ControlID, controlID) {
|
||||
return &ctrl, nil
|
||||
control = &ctrl
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("controlID: %s: %w", controlID, ErrControlNotMatching)
|
||||
return control, nil
|
||||
}
|
||||
|
||||
// GetFramework retrieves a framework configuration from the policy paths.
|
||||
// GetFramework retrieves a framework configuration from the policy.
|
||||
func (lp *LoadPolicy) GetFramework(frameworkName string) (*reporthandling.Framework, error) {
|
||||
if frameworkName == "" {
|
||||
return nil, ErrNameRequired
|
||||
return &reporthandling.Framework{}, nil
|
||||
}
|
||||
|
||||
for _, filePath := range lp.filePaths {
|
||||
buf, err := os.ReadFile(filePath)
|
||||
f, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var framework reporthandling.Framework
|
||||
if err = json.Unmarshal(buf, &framework); err != nil {
|
||||
var fw reporthandling.Framework
|
||||
if err = json.Unmarshal(f, &fw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.EqualFold(frameworkName, framework.Name) {
|
||||
return &framework, nil
|
||||
if strings.EqualFold(frameworkName, fw.Name) {
|
||||
return &fw, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("framework: %s: %w", frameworkName, ErrFrameworkNotMatching)
|
||||
return nil, fmt.Errorf("framework from file not matching")
|
||||
}
|
||||
|
||||
// GetFrameworks returns all configured framework descriptors.
|
||||
func (lp *LoadPolicy) GetFrameworks() ([]reporthandling.Framework, error) {
|
||||
frameworks := make([]reporthandling.Framework, 0, 10)
|
||||
seenFws := make(map[string]struct{})
|
||||
|
||||
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 {
|
||||
// ignore invalid framework files
|
||||
continue
|
||||
}
|
||||
|
||||
// dedupe
|
||||
_, alreadyLoaded := seenFws[framework.Name]
|
||||
if alreadyLoaded {
|
||||
continue
|
||||
}
|
||||
|
||||
seenFws[framework.Name] = struct{}{}
|
||||
frameworks = append(frameworks, framework)
|
||||
}
|
||||
|
||||
return frameworks, nil
|
||||
frameworks := []reporthandling.Framework{}
|
||||
var err error
|
||||
return frameworks, err
|
||||
}
|
||||
|
||||
// ListFrameworks lists the names of all configured frameworks in this policy.
|
||||
func (lp *LoadPolicy) ListFrameworks() ([]string, error) {
|
||||
frameworkNames := make([]string, 0, 10)
|
||||
fwNames := []string{}
|
||||
framework := &reporthandling.Framework{}
|
||||
|
||||
for _, f := range lp.filePaths {
|
||||
buf, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return fwNames, 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) {
|
||||
controlIDs := make([]string, 0, 100)
|
||||
filePath := lp.filePath()
|
||||
buf, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
// TODO - Support
|
||||
return []string{}, fmt.Errorf("loading controls list from file is not supported")
|
||||
}
|
||||
|
||||
// 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
|
||||
func (lp *LoadPolicy) GetExceptions(clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
filePath := lp.filePath()
|
||||
|
||||
buf, err := os.ReadFile(filePath)
|
||||
exception := []armotypes.PostureExceptionPolicy{}
|
||||
f, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exception := make([]armotypes.PostureExceptionPolicy, 0, 300)
|
||||
err = json.Unmarshal(buf, &exception)
|
||||
|
||||
err = json.Unmarshal(f, &exception)
|
||||
return exception, err
|
||||
}
|
||||
|
||||
// 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
|
||||
func (lp *LoadPolicy) GetControlsInputs(clusterName string) (map[string][]string, error) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
if err = json.Unmarshal(f, &accountConfig.Settings.PostureControlInputs); err == nil {
|
||||
return accountConfig.Settings.PostureControlInputs, nil
|
||||
}
|
||||
|
||||
return controlInputs, nil
|
||||
}
|
||||
formattedError := fmt.Errorf("Error reading %s file, %s, \"controls-config\" will be downloaded from ARMO management portal", fileName, err.Error())
|
||||
|
||||
// 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
|
||||
return nil, formattedError
|
||||
}
|
||||
|
||||
// temporary support for a list of files
|
||||
@@ -265,3 +163,18 @@ 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
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package getter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -15,13 +14,14 @@ func MockNewLoadPolicy() *LoadPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
func testFrameworkFile(framework string) string {
|
||||
return filepath.Join(".", "testdata", fmt.Sprintf("%s.json", framework))
|
||||
}
|
||||
|
||||
func TestLoadPolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
testFramework = "MITRE"
|
||||
testControl = "C-0053"
|
||||
)
|
||||
const testFramework = "MITRE"
|
||||
|
||||
t.Run("with GetFramework", func(t *testing.T) {
|
||||
t.Run("should retrieve named framework", func(t *testing.T) {
|
||||
@@ -44,13 +44,16 @@ func TestLoadPolicy(t *testing.T) {
|
||||
require.Nil(t, fw)
|
||||
})
|
||||
|
||||
t.Run("edge case: should error on empty framework", func(t *testing.T) {
|
||||
t.Run("edge case: should return empty framework", func(t *testing.T) {
|
||||
// NOTE(fredbi): this edge case corresponds to the original working of GetFramework.
|
||||
// IMHO, this is a bad request call and it should return an error.
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
fw, err := p.GetFramework("")
|
||||
require.ErrorIs(t, err, ErrNameRequired)
|
||||
require.Nil(t, fw)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fw)
|
||||
require.Empty(t, *fw)
|
||||
})
|
||||
|
||||
t.Run("edge case: corrupted json", func(t *testing.T) {
|
||||
@@ -74,10 +77,11 @@ func TestLoadPolicy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("with GetControl", func(t *testing.T) {
|
||||
t.Run("should retrieve named control from framework", func(t *testing.T) {
|
||||
t.Run("should retrieve named control", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
testControl = "C-0053"
|
||||
expectedControlName = "Access container service account"
|
||||
)
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
@@ -89,44 +93,15 @@ func TestLoadPolicy(t *testing.T) {
|
||||
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 fail to retrieve named control", func(t *testing.T) {
|
||||
// NOTE(fredbi): IMHO, this case should bubble up an error
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
const testControl = "wrong"
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
ctrl, err := p.GetControl(testControl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ctrl) // no error, but still don't get the requested control...
|
||||
})
|
||||
|
||||
t.Run("edge case: corrupted json", func(t *testing.T) {
|
||||
@@ -147,77 +122,32 @@ func TestLoadPolicy(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("edge case: should error on empty control", func(t *testing.T) {
|
||||
t.Run("edge case: should return empty control", func(t *testing.T) {
|
||||
// NOTE(fredbi): this edge case corresponds to the original working of GetFramework.
|
||||
// IMHO, this is a bad request call and it should return an error.
|
||||
t.Parallel()
|
||||
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
ctrl, err := p.GetControl("")
|
||||
require.ErrorIs(t, err, ErrIDRequired)
|
||||
require.Nil(t, ctrl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(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()
|
||||
t.Run("ListFrameworks 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])
|
||||
const extraFramework = "NSA"
|
||||
p := NewLoadPolicy([]string{
|
||||
testFrameworkFile(testFramework),
|
||||
testFrameworkFile(extraFramework),
|
||||
})
|
||||
fws, err := p.ListFrameworks()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fws, 2)
|
||||
|
||||
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)
|
||||
})
|
||||
require.Equal(t, testFramework, fws[0])
|
||||
require.Equal(t, extraFramework, fws[1])
|
||||
})
|
||||
|
||||
t.Run("edge case: policy without path", func(t *testing.T) {
|
||||
@@ -227,183 +157,20 @@ func TestLoadPolicy(t *testing.T) {
|
||||
require.Empty(t, p.filePath())
|
||||
})
|
||||
|
||||
t.Run("with GetFrameworks", func(t *testing.T) {
|
||||
const extraFramework = "NSA"
|
||||
t.Run("GetFrameworks is currently stubbed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
fws, err := p.GetFrameworks()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, fws)
|
||||
})
|
||||
|
||||
t.Run("with ListControls", func(t *testing.T) {
|
||||
t.Run("should return controls", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListControls is currently unsupported", 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)
|
||||
})
|
||||
p := NewLoadPolicy([]string{testFrameworkFile(testFramework)})
|
||||
_, err := p.ListControls()
|
||||
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
85
core/cautils/getter/testdata/C-0001.json
vendored
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
136
core/cautils/getter/testdata/attack-tracks.json
vendored
136
core/cautils/getter/testdata/attack-tracks.json
vendored
@@ -1,136 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
125
core/cautils/getter/testdata/controls-inputs.json
vendored
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"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
6407
core/cautils/getter/testdata/exceptions.json
vendored
File diff suppressed because it is too large
Load Diff
20
core/cautils/krewutils.go
Normal file
20
core/cautils/krewutils.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExecName returns the correct name to use in examples depending on how kubescape is invoked
|
||||
func ExecName() string {
|
||||
n := "kubescape"
|
||||
if IsKrewPlugin() {
|
||||
return "kubectl " + n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func IsKrewPlugin() bool {
|
||||
return strings.HasPrefix(filepath.Base(os.Args[0]), "kubectl-")
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils/getter"
|
||||
metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1"
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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,7 +25,4 @@ type IKubescape interface {
|
||||
|
||||
// delete
|
||||
DeleteExceptions(deleteexceptions *metav1.DeleteExceptions) error
|
||||
|
||||
// fix
|
||||
Fix(fixInfo *metav1.FixInfo) error
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,57 +0,0 @@
|
||||
# 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
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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]
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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]
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,286 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
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,7 +75,6 @@ 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()
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
helpersv1 "github.com/kubescape/opa-utils/reporthandling/helpers/v1"
|
||||
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
@@ -61,10 +59,7 @@ func (policyHandler *PolicyHandler) CollectResources(policyIdentifier []cautils.
|
||||
func (policyHandler *PolicyHandler) getResources(policyIdentifier []cautils.PolicyIdentifier, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) error {
|
||||
opaSessionObj.Report.ClusterAPIServerInfo = policyHandler.resourceHandler.GetClusterAPIServerInfo()
|
||||
|
||||
// set cloud metadata only when scanning a cluster
|
||||
if opaSessionObj.Metadata.ScanMetadata.ScanningTarget == reportv2.Cluster {
|
||||
setCloudMetadata(opaSessionObj)
|
||||
}
|
||||
setCloudMetadata(opaSessionObj)
|
||||
|
||||
resourcesMap, allResources, ksResources, err := policyHandler.resourceHandler.GetResources(opaSessionObj, &policyIdentifier[0].Designators)
|
||||
if err != nil {
|
||||
@@ -96,8 +91,6 @@ func setCloudMetadata(opaSessionObj *cautils.OPASessionObj) {
|
||||
opaSessionObj.Metadata.ContextMetadata.ClusterContextMetadata.CloudMetadata = cloudMetadata
|
||||
opaSessionObj.Metadata.ClusterMetadata.CloudMetadata = cloudMetadata // deprecated - fallback
|
||||
opaSessionObj.Report.ClusterCloudProvider = iCloudMetadata.Provider().ToString() // deprecated - fallback
|
||||
|
||||
logger.L().Debug("Cloud metadata", helpers.String("provider", iCloudMetadata.Provider().ToString()), helpers.String("name", iCloudMetadata.GetName()))
|
||||
}
|
||||
|
||||
// getCloudMetadata - get cloud metadata from kubeconfig or API server
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/armoapi-go/armotypes"
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/kubescape/v2/core/cautils"
|
||||
"github.com/kubescape/kubescape/v2/core/pkg/resourcehandler"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
helpersv1 "github.com/kubescape/opa-utils/reporthandling/helpers/v1"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
reportv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
fakeclientset "k8s.io/client-go/kubernetes/fake"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
@@ -28,7 +18,6 @@ var (
|
||||
kubeConfigMock string
|
||||
)
|
||||
|
||||
|
||||
func getKubeConfigMock() *clientcmdapi.Config {
|
||||
kubeConfig := clientcmdapi.Config{}
|
||||
if err := json.Unmarshal([]byte(kubeConfigMock), &kubeConfig); err != nil {
|
||||
@@ -210,53 +199,3 @@ func Test_isAKS(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type iResourceHandlerMock struct{}
|
||||
|
||||
func (*iResourceHandlerMock) GetResources(*cautils.OPASessionObj, *armotypes.PortalDesignator) (*cautils.K8SResources, map[string]workloadinterface.IMetadata, *cautils.KSResources, error) {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
func (*iResourceHandlerMock) GetClusterAPIServerInfo() *version.Info {
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://github.com/kubescape/kubescape/pull/1004
|
||||
// Cluster named .*eks.* config without a cloudconfig panics whereas we just want to scan a file
|
||||
func getResourceHandlerMock() *resourcehandler.K8sResourceHandler {
|
||||
client := fakeclientset.NewSimpleClientset()
|
||||
fakeDiscovery := client.Discovery()
|
||||
|
||||
k8s := &k8sinterface.KubernetesApi{
|
||||
KubernetesClient: client,
|
||||
DynamicClient: fake.NewSimpleDynamicClient(runtime.NewScheme()),
|
||||
DiscoveryClient: fakeDiscovery,
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
return resourcehandler.NewK8sResourceHandler(k8s, &resourcehandler.EmptySelector{}, nil, nil, nil)
|
||||
}
|
||||
func Test_getResources(t *testing.T) {
|
||||
policyHandler := &PolicyHandler{resourceHandler: getResourceHandlerMock()}
|
||||
objSession := &cautils.OPASessionObj{
|
||||
Metadata: &reporthandlingv2.Metadata{
|
||||
ScanMetadata: reporthandlingv2.ScanMetadata{
|
||||
ScanningTarget: reportv2.Cluster,
|
||||
},
|
||||
},
|
||||
Report: &reporthandlingv2.PostureReport{
|
||||
ClusterAPIServerInfo: nil,
|
||||
},
|
||||
}
|
||||
policyIdentifier := []cautils.PolicyIdentifier{{}}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
policyHandler.getResources(policyIdentifier, objSession, &cautils.ScanInfo{})
|
||||
}, "Cluster named .*eks.* without a cloud config panics on cluster scan !")
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
objSession.Metadata.ScanMetadata.ScanningTarget = reportv2.File
|
||||
policyHandler.getResources(policyIdentifier, objSession, &cautils.ScanInfo{})
|
||||
}, "Cluster named .*eks.* without a cloud config panics on non-cluster scan !")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ Expecting:
|
||||
"secretKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
```
|
||||
> **Note**
|
||||
> If you are missing the `accountID` field, set it by running `kubescape config set accountID <>`
|
||||
|
||||
For CICD, set environments variables as following:
|
||||
|
||||
@@ -88,7 +88,6 @@ func (fileHandler *FileResourceHandler) GetResources(sessionObj *cautils.OPASess
|
||||
}
|
||||
|
||||
func getResourcesFromPath(path string) (map[string]reporthandling.Source, []workloadinterface.IMetadata, error) {
|
||||
|
||||
workloadIDToSource := make(map[string]reporthandling.Source, 0)
|
||||
workloads := []workloadinterface.IMetadata{}
|
||||
|
||||
@@ -109,12 +108,6 @@ func getResourcesFromPath(path string) (map[string]reporthandling.Source, []work
|
||||
repoRoot, _ = filepath.Abs(path)
|
||||
}
|
||||
|
||||
// when scanning a single file, we consider the repository root to be
|
||||
// the directory of the scanned file
|
||||
if cautils.IsYaml(repoRoot) {
|
||||
repoRoot = filepath.Dir(repoRoot)
|
||||
}
|
||||
|
||||
// load resource from local file system
|
||||
sourceToWorkloads := cautils.LoadResourcesFromFiles(path, repoRoot)
|
||||
|
||||
@@ -124,7 +117,6 @@ func getResourcesFromPath(path string) (map[string]reporthandling.Source, []work
|
||||
workloads = append(workloads, ws...)
|
||||
|
||||
relSource, err := filepath.Rel(repoRoot, source)
|
||||
|
||||
if err == nil {
|
||||
source = relSource
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user