Compare commits

..

112 Commits

Author SHA1 Message Date
Rotem Refael
033e8f6b44 Merge pull request #346 from armosec/dev
returning rbac submit code + fix crash
2022-01-24 12:40:33 +02:00
Ben Hirschberg
bef40f0e6c Merge pull request #344 from slashben/dev
returning RBAC submit table
2022-01-24 09:13:28 +02:00
Ben Hirschberg
aa2f69125f returning rbac submit code 2022-01-24 09:12:13 +02:00
Ben Hirschberg
d30f3960a7 Merge pull request #341 from slashben/dev
fixing the crash around submit rbac
2022-01-23 21:28:10 +02:00
Ben Hirschberg
5f43da94ba fixing the crash around submit rbac 2022-01-23 20:17:03 +02:00
Rotem Refael
2aa8a0c935 Merge pull request #340 from Anthirian/dev
Fix typo in namespace
2022-01-23 13:43:42 +02:00
Ben Hirschberg
c02f8c6cb5 Merge pull request #338 from vfarcic/dev
Video
2022-01-23 08:18:44 +02:00
Geert Smelt
aa0be474e2 Fix typo in namespace 2022-01-21 15:40:00 +01:00
Viktor Farcic
c0161c9b33 Video 2022-01-20 19:54:57 +01:00
Rotem Refael
514da1e2db Merge pull request #332 from armosec/dev
Emergency fix: checking kube context before use
2022-01-19 16:53:14 +02:00
Ben Hirschberg
75dfceb5da Merge pull request #331 from slashben/dev
checking if local context exists before using
2022-01-19 16:38:22 +02:00
Ben Hirschberg
1ae76b4377 checking if context exists 2022-01-19 16:37:32 +02:00
Rotem Refael
b6f90cba8e Merge pull request #330 from armosec/dev
New kubescape major version
2022-01-19 15:21:52 +02:00
rrefael
62af441a1d Increase release & tag to major 2022-01-19 09:19:16 +02:00
Rotem Refael
228b8957d3 Merge pull request #317 from armosec/report-refactor
- Support report v2 (pagination)
- Scan control only once
- Support download of exceptions,control-configuration,artifacts -> kubescape download
- Support listing frameworks and controls -> kubescape list
- Adding scan framework all for scanning yaml files
- Scan other kubernetes contexts by adding the --kube-context flag
2022-01-18 21:02:56 +02:00
YiscahLevySilas1
b4ce999ab3 Merge pull request #324 from YiscahLevySilas1/dev
added download + load artifacts to readme
2022-01-18 18:02:34 +02:00
yiscah
cc06a414fe added examples of download + load artifacts 2022-01-18 17:50:28 +02:00
YiscahLevySilas1
d3c37c4e5f Merge branch 'armosec:dev' into dev 2022-01-18 17:30:36 +02:00
YiscahLevySilas1
3b448b62b1 Merge pull request #323 from YiscahLevySilas1/dev
support load artifacts from local path
2022-01-18 17:28:08 +02:00
YiscahLevySilas1
6a3f5658b1 Merge branch 'report-refactor' into dev 2022-01-18 17:19:03 +02:00
yiscah
f65e791522 fix parsing local path to artifacts 2022-01-18 17:01:53 +02:00
yiscah
d91304f9ad setUseArtifactsFrom only when flag is set 2022-01-18 16:32:01 +02:00
yiscah
61ce00108e fixed download to local path 2022-01-18 15:29:35 +02:00
Ben Hirschberg
a4eb773eee Merge pull request #322 from slashben/dev
Adding examples for cloud integrations
2022-01-18 15:05:47 +02:00
Ben Hirschberg
cfc69f5a0f adding access to container registry 2022-01-18 14:41:07 +02:00
Ben Hirschberg
a44823c3ed example cloud integration scripts 2022-01-17 13:55:43 +02:00
Ben Hirschberg
8a166e5ba5 typo fix 2022-01-17 11:54:02 +02:00
rrefael
9a7aeff870 update overview text- README 2022-01-16 15:40:34 +02:00
Lior Alafi
cb3bdb9df2 supporting both structures for score calculation 2022-01-14 17:54:39 +02:00
rrefael
0be8d57eaa add --enable-host-scan flag 2022-01-13 15:44:21 +02:00
dwertent
79b9cbf1d6 Merge branch 'report-refactor' 2022-01-13 15:16:15 +02:00
dwertent
500df8737e send small clusters once 2022-01-13 15:03:23 +02:00
David Wertenteil
b8acbd1bee fixed flag and cloud env support 2022-01-13 14:19:26 +02:00
dwertent
0bde8a65ba fixed context flag 2022-01-13 14:16:36 +02:00
dwertent
d2884b8936 update failure message 2022-01-13 13:07:29 +02:00
dwertent
e692359b47 Merge remote-tracking branch 'dwertent/master' into report-refactor 2022-01-13 12:52:34 +02:00
dwertent
473746eab0 merged with Daniels branch 2022-01-13 12:50:50 +02:00
dwertent
050878cbd6 Merge remote-tracking branch 'dwertent/master' into report-refactor 2022-01-13 11:48:13 +02:00
dwertent
e100f18bb0 fixed tests 2022-01-13 11:44:46 +02:00
dwertent
05c82fc166 Merge remote-tracking branch 'dwertent/master' into dev 2022-01-13 11:36:15 +02:00
dwertent
839c3e261f swap result and resource 2022-01-13 11:04:09 +02:00
dwertent
95b579d191 fixed resource list 2022-01-12 14:03:12 +02:00
Bezbran
8656715753 Merge pull request #314 from alegrey91/master
move kube-host-sensor manifest to indipendent yaml file with embed capability
2022-01-12 09:39:26 +02:00
dwertent
05b6394c5c send report to v2 2022-01-11 14:21:16 +02:00
Rotem Refael
d3bdbf31ac Merge pull request #315 from armosec/dev
Typo fixes
Update Kubescape logo
Adding contributors

Issues:
- Closes Spelling error Namescape should be Namespace #296
- Closes Add contributors to Readme #307
2022-01-11 09:58:36 +02:00
yiscah
995f615b10 support load artifacts from local path 2022-01-10 20:00:42 +02:00
alegrey91
392625b774 style(host-sensor): move kube-host-sensor manifest to indipendent yaml file 2022-01-10 18:39:06 +01:00
dwertent
306b9d28ca ignore skipped resources 2022-01-10 15:21:31 +02:00
dwertent
6fe87bba20 Merge branch 'dev' of github.com:dwertent/kubescape into dev 2022-01-10 13:46:19 +02:00
David Wertenteil
c0d534072d print downloaded artifacts 2022-01-10 13:45:43 +02:00
dwertent
009221aa98 report 2022-01-10 10:21:42 +02:00
yiscah
46e5aff5f9 each artifact handle print seprately 2022-01-10 09:07:06 +02:00
YiscahLevySilas1
59498361e7 Merge pull request #3 from dwertent/dev
Dev
2022-01-10 08:35:40 +02:00
dwertent
c652da130d adding download examples 2022-01-10 08:13:44 +02:00
David Wertenteil
9e524ffc34 support download artifacts 2022-01-09 17:37:26 +02:00
David Wertenteil
004cc0c469 Merge branch 'dev' into dev 2022-01-09 17:37:12 +02:00
dwertent
bd089d76af adding cluster flag - support submiting yaml file 2022-01-09 16:13:15 +02:00
yiscah
d5025b54bf handle error for each artifact download seprately 2022-01-09 15:12:03 +02:00
dwertent
740497047d cli print support v2 2022-01-09 10:33:47 +02:00
Ben Hirschberg
3f6cbd57b2 Merge pull request #311 from saiyam1814/patch-1
Adding Kubescape katacoda playground
2022-01-08 18:16:50 +02:00
Saiyam Pathak
2c9524ed45 Adding Kubescape katacoda playground 2022-01-08 19:21:33 +05:30
Ben Hirschberg
384922680a Merge pull request #309 from saiyam1814/master
Adding Contributors
2022-01-07 15:41:53 +02:00
David Wertenteil
d2e9f8f4f8 Fixes #296 Spelling error Namescape should be Namespace 2022-01-07 14:25:24 +02:00
Saiyam Pathak
b4f10f854e Adding contributors 2022-01-07 11:27:32 +05:30
Clint Modien
8ce64d2a7f Fixes #296 Spelling error Namescape should be Namespace 2022-01-06 09:45:11 -08:00
yiscah
d917e21364 support download artifacts 2022-01-06 17:44:50 +02:00
YiscahLevySilas1
32cedaf565 Merge branch 'armosec:dev' into dev 2022-01-06 17:44:23 +02:00
dwertent
4c2a5e9a11 suooirt scan all 2022-01-06 16:21:41 +02:00
dwertent
a41d2a46ff cli support list 2022-01-06 15:28:01 +02:00
dwertent
4794cbfb36 update opa version 2022-01-06 14:31:46 +02:00
rrefael
d021217cf7 add new logo 2022-01-06 14:19:13 +02:00
dwertent
4573d83831 fixed counters and skipped ctr 2022-01-06 13:05:51 +02:00
Ben Hirschberg
2bb612ca3f Merge pull request #303 from armosec/slashben-roadmap-branch
Create roadmap.md
2022-01-06 11:12:48 +02:00
Ben Hirschberg
35534112c6 Create MAINTAINERS.md 2022-01-06 11:10:21 +02:00
Ben Hirschberg
f51e531f3a fixing typos 2022-01-06 11:04:28 +02:00
Ben Hirschberg
2490856ccb Update roadmap.md 2022-01-06 11:02:37 +02:00
Ben Hirschberg
9a5a87b027 Create CODE_OF_CONDUCT.md 2022-01-06 10:44:28 +02:00
Ben Hirschberg
45b8c89865 Create roadmap.md 2022-01-06 10:30:08 +02:00
YiscahLevySilas1
e68e6dcd3d Merge pull request #1 from dwertent/master
Download policies support
2022-01-06 08:51:16 +02:00
dwertent
670ff4a15d support download 2022-01-05 20:46:56 +02:00
dwertent
b616a37800 fixed test 2022-01-05 16:45:50 +02:00
dwertent
ce488a3645 update latest fixes 2022-01-05 16:45:02 +02:00
David Wertenteil
80ace81a12 Fixing typo in the ActionSendReport error message 2022-01-05 16:16:52 +02:00
yiscah
1efdae5197 begin download config + download exceptions 2022-01-05 15:56:38 +02:00
yiscah
a4c88edfca begin download config + download exceptions 2022-01-05 15:56:24 +02:00
YiscahLevySilas1
8f38c2f627 Merge branch 'armosec:dev' into dev 2022-01-05 15:10:49 +02:00
Jonas Kint
bbf68d4ce8 Fixing typo in the ActionSendReport error message 2022-01-05 13:49:26 +01:00
Rotem Refael
fc05075817 Merge pull request #294 from armosec/dev
Minor features and improvements
2022-01-04 15:29:55 +02:00
dwertent
5bb64b634a support loading ks config in env 2022-01-04 14:42:25 +02:00
dwertent
7bc2c2be13 fliter ot reources based on owners 2022-01-03 13:36:29 +02:00
yiscah
27e2c044da update rbac-utils version for SAID2WLIDmap obj 2022-01-03 09:59:34 +02:00
Rotem Refael
803e62020e add devopsbest framework 2021-12-30 16:40:07 +02:00
Ben Hirschberg
18425c915b Merge pull request #291 from slashben/dev
adding container image vulnerability adaptor proposal
2021-12-30 10:44:57 +02:00
Benyamin Hirschberg
0de6892ddd adding container image vunerability adaptor proposal 2021-12-30 10:44:08 +02:00
David Wertenteil
dfb92ffec3 Remove RBAC deprecated objects 2021-12-29 17:49:52 +02:00
yiscah
85317f1ee1 Merge branch 'dev' of https://github.com/YiscahLevySilas1/kubescape into dev 2021-12-29 16:23:29 +02:00
yiscah
f22f60508f rbacTable and rbac struct deprecated 2021-12-29 16:23:14 +02:00
dwertent
716bdaaf38 support kind List 2021-12-29 12:06:48 +02:00
dwertent
1b0e2b87de Handle all resources failure 2021-12-28 10:47:12 +02:00
David Wertenteil
2c57b809d2 show warnings for host sensor and send kubelet cmd 2021-12-28 10:42:26 +02:00
David Wertenteil
d9c96db212 Merge branch 'dev' into master 2021-12-28 10:41:39 +02:00
Daniel-GrunbergerCA
5f7391a76b stdout to stderror 2021-12-28 09:20:05 +02:00
Daniel-GrunbergerCA
accd80eda8 rm cmdline map 2021-12-28 09:07:50 +02:00
Daniel-GrunbergerCA
e49499f085 use regoes from master 2021-12-27 08:45:50 +02:00
Rotem Refael
3fad2f3430 Merge pull request #279 from armosec/dev
Cli improvements
2021-12-22 21:16:54 +02:00
Daniel-GrunbergerCA
ad94ac7595 rm json print 2021-12-22 08:29:35 +02:00
Daniel-GrunbergerCA
cfa3993b79 print json 2021-12-21 20:31:12 +02:00
Daniel-GrunbergerCA
972793b98a print json 2021-12-21 20:27:23 +02:00
Daniel-GrunbergerCA
35682bf5b8 pull regoes from dev 2021-12-21 19:02:16 +02:00
Daniel-GrunbergerCA
b023f592aa Merge remote-tracking branch 'upstream/dev' 2021-12-21 13:37:32 +02:00
Daniel-GrunbergerCA
a1c34646f1 waning for host sensor 2021-12-21 13:34:31 +02:00
Daniel-GrunbergerCA
0cac7cb1a5 fix kubeletcmd for marshalling 2021-12-21 09:23:38 +02:00
75 changed files with 2404 additions and 484 deletions

View File

@@ -16,8 +16,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v1.0.${{ github.run_number }}
release_name: Release v1.0.${{ github.run_number }}
tag_name: v2.0.${{ github.run_number }}
release_name: Release v2.0.${{ github.run_number }}
draft: false
prerelease: false
build:
@@ -39,7 +39,7 @@ jobs:
- name: Build
env:
RELEASE: v1.0.${{ github.run_number }}
RELEASE: v2.0.${{ github.run_number }}
ArmoBEServer: api.armo.cloud
ArmoERServer: report.armo.cloud
ArmoWebsite: portal.armo.cloud
@@ -48,7 +48,7 @@ jobs:
- name: Smoke Testing
env:
RELEASE: v1.0.${{ github.run_number }}
RELEASE: v2.0.${{ github.run_number }}
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape
@@ -74,7 +74,7 @@ jobs:
- uses: actions/checkout@v2
- name: Set name
run: echo quay.io/armosec/kubescape:v1.0.${{ github.run_number }} > build_tag.txt
run: echo quay.io/armosec/kubescape:v2.0.${{ github.run_number }} > build_tag.txt
- name: Build the Docker image
run: docker build . --file build/Dockerfile --tag $(cat build_tag.txt) --build-arg run_number=${{ github.run_number }}

View File

@@ -23,7 +23,7 @@ jobs:
- name: Build
env:
RELEASE: v1.0.${{ github.run_number }}
RELEASE: v2.0.${{ github.run_number }}
ArmoBEServer: api.armo.cloud
ArmoERServer: report.euprod1.cyberarmorsoft.com
ArmoWebsite: portal.armo.cloud
@@ -32,7 +32,7 @@ jobs:
- name: Smoke Testing
env:
RELEASE: v1.0.${{ github.run_number }}
RELEASE: v2.0.${{ github.run_number }}
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape
@@ -53,7 +53,7 @@ jobs:
- uses: actions/checkout@v2
- name: Set name
run: echo quay.io/armosec/kubescape:dev-v1.0.${{ github.run_number }} > build_tag.txt
run: echo quay.io/armosec/kubescape:dev-v2.0.${{ github.run_number }} > build_tag.txt
- name: Build the Docker image
run: docker build . --file build/Dockerfile --tag $(cat build_tag.txt) --build-arg run_number=${{ github.run_number }}

View File

@@ -24,7 +24,7 @@ jobs:
- name: Build
env:
RELEASE: v1.0.${{ github.run_number }}
RELEASE: v2.0.${{ github.run_number }}
ArmoBEServer: api.armo.cloud
ArmoERServer: report.armo.cloud
ArmoWebsite: portal.armo.cloud
@@ -33,7 +33,7 @@ jobs:
- name: Smoke Testing
env:
RELEASE: v1.0.${{ github.run_number }}
RELEASE: v2.0.${{ github.run_number }}
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape

127
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,127 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders 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, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement [here](mailto:ben@armosec.io).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

9
MAINTAINERS.md Normal file
View File

@@ -0,0 +1,9 @@
# Maintainers
The following table lists Kubescape project maintainers
| Name | GitHub | Email | Organization | Repositories/Area of Expertise | Added/Renewed On |
| --- | --- | --- | --- | --- | --- |
| Ben Hirschberg | @slashben | ben@armosec.io | ARMO | Kubescape CLI | 2021-09-01 |
| Rotem Refael | @rotemamsa | rrefael@armosec.io | ARMO | Kubescape CLI | 2021-10-11 |
| David Wertenteil | @dwertent | dwertent@armosec.io | ARMO | Kubescape CLI | 2021-09-01 |

View File

@@ -3,10 +3,10 @@
[![build](https://github.com/armosec/kubescape/actions/workflows/build.yaml/badge.svg)](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/armosec/kubescape)](https://goreportcard.com/report/github.com/armosec/kubescape)
Kubescape is the first open-source tool for testing if Kubernetes is deployed securely according to multiple frameworks:
regulatory, customized company policies and DevSecOps best practices, such as the [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo) and the [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) .
Kubescape scans K8s clusters, YAML files, and HELM charts, and detect misconfigurations and software vulnerabilities at early stages of the CI/CD pipeline and provides a risk score instantly and risk trends over time.
Kubescape integrates natively with other DevOps tools, including Jenkins, CircleCI and Github workflows.
Kubescape is a K8s open-source tool providing a multi-cloud K8s single pane of glass, including risk analysis, security compliance, RBAC visualizer and image vulnerabilities scanning.
Kubescape scans K8s clusters, YAML files, and HELM charts, detecting misconfigurations according to multiple frameworks (such as the [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo) , [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/)), software vulnerabilities, and RBAC (role-based-access-control) violations at early stages of the CI/CD pipeline, calculates risk score instantly and shows risk trends over time.
It became one of the fastest-growing Kubernetes tools among developers due to its easy-to-use CLI interface, flexible output formats, and automated scanning capabilities, saving Kubernetes users and admins precious time, effort, and resources.
Kubescape integrates natively with other DevOps tools, including Jenkins, CircleCI, Github workflows, Prometheus, and Slack, and supports multi-cloud K8s deployments like EKS, GKE, and AKS.
</br>
@@ -24,7 +24,7 @@ curl -s https://raw.githubusercontent.com/armosec/kubescape/master/install.sh |
## Run:
```
kubescape scan --submit
kubescape scan --submit --enable-host-scan
```
<img src="docs/summary.png">
@@ -54,9 +54,13 @@ Want to contribute? Want to discuss something? Have an issue?
# Options and examples
## Playground
* [Kubescape playground](https://www.katacoda.com/pathaksaiyam/scenarios/kubescape)
## Tutorials
* [Overview](https://youtu.be/wdBkt_0Qhbg)
* [How To Secure Kubernetes Clusters With Kubescape And Armo](https://youtu.be/ZATGiDIDBQk)
* [Scanning Kubernetes YAML files](https://youtu.be/Ox6DaR7_4ZI)
* [Scan Kubescape on an air-gapped environment (offline support)](https://youtu.be/IGXL9s37smM)
* [Managing exceptions in the Kubescape SaaS version](https://youtu.be/OzpvxGmCR80)
@@ -94,12 +98,15 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser
| `-t`/`--fail-threshold` | `100` (do not fail) | fail command (return exit code 1) if result is above threshold | `0` -> `100` |
| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus` |
| `-o`/`--output` | print to stdout | Save scan result in file | |
| `--use-from` | | Load local framework object from specified path. If not used will download latest | |
| `--use-from` | | Load local framework object from specified path. If not used will download latest |
| `--use-artifacts-from` | | Load artifacts (frameworks, control-config, exceptions) from local directory. If not used will download them | |
| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` |
| `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json). If not set will download exceptions from Armo management portal | |
| `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json). If not set will download exceptions from Armo management portal |
| `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal | |
| `--submit` | `false` | If set, Kubescape will send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not sent | `true`/`false` |
| `--keep-local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false` |
| `--account` | | Armo portal account ID. Default will load account ID from configMap or config file | |
| `--kube-context` | current-context | Cluster context to scan | |
| `--verbose` | `false` | Display all of the input resources and not only failed resources | `true`/`false` |
@@ -189,7 +196,7 @@ It is possible to run Kubescape offline!
First download the framework and then scan with `--use-from` flag
1. Download and save in file, if file name not specified, will store save to `~/.kubescape/<framework name>.json`
1. Download and save in file, if file name not specified, will save in `~/.kubescape/<framework name>.json`
```
kubescape download framework nsa --output nsa.json
```
@@ -200,6 +207,19 @@ kubescape scan framework nsa --use-from nsa.json
```
You can also download all artifacts to a local path and then load them using `--use-artifacts-from` flag
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. Scan using the downloaded artifacts
```
kubescape scan framework nsa --use-artifacts-from path/to/local/dir
```
## Scan Periodically using Helm - Contributed by [@yonahd](https://github.com/yonahd)
You can scan your cluster periodically by adding a `CronJob` that will repeatedly trigger kubescape
@@ -263,7 +283,7 @@ go build -o kubescape .
3. Run
```
./kubescape scan framework nsa
./kubescape scan --submit --enable-host-scan
```
4. Enjoy :zany_face:
@@ -319,3 +339,9 @@ The tools retrieves Kubernetes objects from the API server and runs a set of [re
The results by default printed in a pretty "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. Were also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.
## Thanks to all the contributors ❤️
<a href = "https://github.com/armosec/kubescape/graphs/contributors">
<img src = "https://contrib.rocks/image?repo=armosec/kubescape"/>
</a>

View File

@@ -14,10 +14,7 @@ import (
corev1 "k8s.io/api/core/v1"
)
const (
configMapName = "kubescape"
configFileName = "config"
)
const configFileName = "config"
func ConfigFileFullPath() string { return getter.GetDefaultPath(configFileName + ".json") }
@@ -79,7 +76,7 @@ type LocalConfig struct {
configObj *ConfigObj
}
func NewLocalConfig(backendAPI getter.IBackend, customerGUID string) *LocalConfig {
func NewLocalConfig(backendAPI getter.IBackend, customerGUID, clusterName string) *LocalConfig {
var configObj *ConfigObj
lc := &LocalConfig{
@@ -98,6 +95,9 @@ func NewLocalConfig(backendAPI getter.IBackend, customerGUID string) *LocalConfi
if customerGUID != "" {
lc.configObj.CustomerGUID = customerGUID // override config customerGUID
}
if clusterName != "" {
lc.configObj.ClusterName = AdoptClusterName(clusterName) // override config clusterName
}
if lc.configObj.CustomerGUID != "" {
if err := lc.SetTenant(); err != nil {
fmt.Println(err)
@@ -107,10 +107,11 @@ func NewLocalConfig(backendAPI getter.IBackend, customerGUID string) *LocalConfi
return lc
}
func (lc *LocalConfig) GetConfigObj() *ConfigObj { return lc.configObj }
func (lc *LocalConfig) GetCustomerGUID() string { return lc.configObj.CustomerGUID }
func (lc *LocalConfig) GetClusterName() string { return "" }
func (lc *LocalConfig) IsConfigFound() bool { return existsConfigFile() }
func (lc *LocalConfig) GetConfigObj() *ConfigObj { return lc.configObj }
func (lc *LocalConfig) GetCustomerGUID() string { return lc.configObj.CustomerGUID }
func (lc *LocalConfig) SetCustomerGUID(customerGUID string) { lc.configObj.CustomerGUID = customerGUID }
func (lc *LocalConfig) GetClusterName() string { return lc.configObj.ClusterName }
func (lc *LocalConfig) IsConfigFound() bool { return existsConfigFile() }
func (lc *LocalConfig) SetTenant() error {
// ARMO tenant GUID
if err := getTenantConfigFromBE(lc.backendAPI, lc.configObj); err != nil {
@@ -124,7 +125,8 @@ func (lc *LocalConfig) SetTenant() error {
func getTenantConfigFromBE(backendAPI getter.IBackend, configObj *ConfigObj) error {
// get from armoBE
tenantResponse, err := backendAPI.GetCustomerGUID(configObj.CustomerGUID)
backendAPI.SetCustomerGUID(configObj.CustomerGUID)
tenantResponse, err := backendAPI.GetCustomerGUID()
if err == nil && tenantResponse != nil {
if tenantResponse.AdminMail != "" { // registered tenant
configObj.CustomerAdminEMail = tenantResponse.AdminMail
@@ -145,27 +147,40 @@ func getTenantConfigFromBE(backendAPI getter.IBackend, configObj *ConfigObj) err
// ========================== Cluster Config ============================================
// ======================================================================================
// ClusterConfig configuration of specific cluster
/*
Supported environments variables:
KS_DEFAULT_CONFIGMAP_NAME // name of configmap, if not set default is 'kubescape'
KS_DEFAULT_CONFIGMAP_NAMESPACE // configmap namespace, if not set default is 'default'
TODO - supprot:
KS_ACCOUNT // Account ID
KS_CACHE // path to cached files
*/
type ClusterConfig struct {
k8s *k8sinterface.KubernetesApi
defaultNS string
backendAPI getter.IBackend
configObj *ConfigObj
k8s *k8sinterface.KubernetesApi
configMapName string
configMapNamespace string
backendAPI getter.IBackend
configObj *ConfigObj
}
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBackend, customerGUID string) *ClusterConfig {
defaultNS := k8sinterface.GetDefaultNamespace()
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBackend, customerGUID, clusterName string) *ClusterConfig {
var configObj *ConfigObj
c := &ClusterConfig{
k8s: k8s,
backendAPI: backendAPI,
configObj: &ConfigObj{},
defaultNS: defaultNS,
k8s: k8s,
backendAPI: backendAPI,
configObj: &ConfigObj{},
configMapName: getConfigMapName(),
configMapNamespace: getConfigMapNamespace(),
}
// get from configMap
if existsConfigMap(k8s, defaultNS) {
configObj, _ = loadConfigFromConfigMap(k8s, defaultNS)
} else if existsConfigFile() { // get from file
if c.existsConfigMap() {
configObj, _ = c.loadConfigFromConfigMap()
}
if configObj == nil && existsConfigFile() { // get from file
configObj, _ = loadConfigFromFile()
}
if configObj != nil {
@@ -174,6 +189,9 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBacken
if customerGUID != "" {
c.configObj.CustomerGUID = customerGUID // override config customerGUID
}
if clusterName != "" {
c.configObj.ClusterName = AdoptClusterName(clusterName) // override config clusterName
}
if c.configObj.CustomerGUID != "" {
if err := c.SetTenant(); err != nil {
fmt.Println(err)
@@ -188,11 +206,12 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBacken
return c
}
func (c *ClusterConfig) GetConfigObj() *ConfigObj { return c.configObj }
func (c *ClusterConfig) GetDefaultNS() string { return c.defaultNS }
func (c *ClusterConfig) GetCustomerGUID() string { return c.configObj.CustomerGUID }
func (c *ClusterConfig) GetConfigObj() *ConfigObj { return c.configObj }
func (c *ClusterConfig) GetDefaultNS() string { return c.configMapNamespace }
func (c *ClusterConfig) GetCustomerGUID() string { return c.configObj.CustomerGUID }
func (c *ClusterConfig) SetCustomerGUID(customerGUID string) { c.configObj.CustomerGUID = customerGUID }
func (c *ClusterConfig) IsConfigFound() bool {
return existsConfigFile() || existsConfigMap(c.k8s, c.defaultNS)
return existsConfigFile() || c.existsConfigMap()
}
func (c *ClusterConfig) SetTenant() error {
@@ -202,7 +221,7 @@ func (c *ClusterConfig) SetTenant() error {
return err
}
// update/create config
if existsConfigMap(c.k8s, c.defaultNS) {
if c.existsConfigMap() {
c.updateConfigMap()
} else {
c.createConfigMap()
@@ -223,8 +242,8 @@ func (c *ClusterConfig) ToMapString() map[string]interface{} {
}
return m
}
func loadConfigFromConfigMap(k8s *k8sinterface.KubernetesApi, ns string) (*ConfigObj, error) {
configMap, err := k8s.KubernetesClient.CoreV1().ConfigMaps(ns).Get(context.Background(), configMapName, metav1.GetOptions{})
func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) {
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), c.configMapName, metav1.GetOptions{})
if err != nil {
return nil, err
}
@@ -235,15 +254,15 @@ func loadConfigFromConfigMap(k8s *k8sinterface.KubernetesApi, ns string) (*Confi
return nil, nil
}
func existsConfigMap(k8s *k8sinterface.KubernetesApi, ns string) bool {
_, err := k8s.KubernetesClient.CoreV1().ConfigMaps(ns).Get(context.Background(), configMapName, metav1.GetOptions{})
func (c *ClusterConfig) existsConfigMap() bool {
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), c.configMapName, metav1.GetOptions{})
// TODO - check if has customerGUID
return err == nil
}
func (c *ClusterConfig) GetValueByKeyFromConfigMap(key string) (string, error) {
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), c.configMapName, metav1.GetOptions{})
if err != nil {
return "", err
@@ -295,11 +314,11 @@ func SetKeyValueInConfigJson(key string, value string) error {
func (c *ClusterConfig) SetKeyValueInConfigmap(key string, value string) error {
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), c.configMapName, metav1.GetOptions{})
if err != nil {
configMap = &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Name: c.configMapName,
},
}
}
@@ -311,9 +330,9 @@ func (c *ClusterConfig) SetKeyValueInConfigmap(key string, value string) error {
configMap.Data[key] = value
if err != nil {
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Create(context.Background(), configMap, metav1.CreateOptions{})
} else {
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
}
return err
@@ -330,12 +349,12 @@ func (c *ClusterConfig) createConfigMap() error {
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Name: c.configMapName,
},
}
c.updateConfigData(configMap)
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Create(context.Background(), configMap, metav1.CreateOptions{})
return err
}
@@ -343,7 +362,7 @@ func (c *ClusterConfig) updateConfigMap() error {
if c.k8s == nil {
return nil
}
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), c.configMapName, metav1.GetOptions{})
if err != nil {
return err
@@ -351,7 +370,7 @@ func (c *ClusterConfig) updateConfigMap() error {
c.updateConfigData(configMap)
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
return err
}
@@ -387,21 +406,23 @@ func readConfig(dat []byte) (*ConfigObj, error) {
return nil, nil
}
configObj := &ConfigObj{}
err := json.Unmarshal(dat, configObj)
return configObj, err
if err := json.Unmarshal(dat, configObj); err != nil {
return nil, err
}
return configObj, nil
}
// Check if the customer is submitted
func (clusterConfig *ClusterConfig) IsSubmitted() bool {
return existsConfigMap(clusterConfig.k8s, clusterConfig.defaultNS) || existsConfigFile()
return clusterConfig.existsConfigMap() || existsConfigFile()
}
// Check if the customer is registered
func (clusterConfig *ClusterConfig) IsRegistered() bool {
// get from armoBE
tenantResponse, err := clusterConfig.backendAPI.GetCustomerGUID(clusterConfig.GetCustomerGUID())
clusterConfig.backendAPI.SetCustomerGUID(clusterConfig.GetCustomerGUID())
tenantResponse, err := clusterConfig.backendAPI.GetCustomerGUID()
if err == nil && tenantResponse != nil {
if tenantResponse.AdminMail != "" { // this customer already belongs to some user
return true
@@ -411,7 +432,7 @@ func (clusterConfig *ClusterConfig) IsRegistered() bool {
}
func (clusterConfig *ClusterConfig) DeleteConfig() error {
if err := DeleteConfigMap(clusterConfig.k8s); err != nil {
if err := clusterConfig.DeleteConfigMap(); err != nil {
return err
}
if err := DeleteConfigFile(); err != nil {
@@ -419,8 +440,8 @@ func (clusterConfig *ClusterConfig) DeleteConfig() error {
}
return nil
}
func DeleteConfigMap(k8s *k8sinterface.KubernetesApi) error {
return k8s.KubernetesClient.CoreV1().ConfigMaps(k8sinterface.GetDefaultNamespace()).Delete(context.Background(), configMapName, metav1.DeleteOptions{})
func (clusterConfig *ClusterConfig) DeleteConfigMap() error {
return clusterConfig.k8s.KubernetesClient.CoreV1().ConfigMaps(clusterConfig.configMapNamespace).Delete(context.Background(), clusterConfig.configMapName, metav1.DeleteOptions{})
}
func DeleteConfigFile() error {
@@ -430,3 +451,17 @@ func DeleteConfigFile() error {
func AdoptClusterName(clusterName string) string {
return strings.ReplaceAll(clusterName, "/", "-")
}
func getConfigMapName() string {
if n := os.Getenv("KS_DEFAULT_CONFIGMAP_NAME"); n != "" {
return n
}
return "kubescape"
}
func getConfigMapNamespace() string {
if n := os.Getenv("KS_DEFAULT_CONFIGMAP_NAMESPACE"); n != "" {
return n
}
return "default"
}

View File

@@ -25,8 +25,10 @@ func (policies *Policies) Set(frameworks []reporthandling.Framework, version str
compatibleRules = append(compatibleRules, frameworks[i].Controls[j].Rules[r])
}
}
frameworks[i].Controls[j].Rules = compatibleRules
policies.Controls[frameworks[i].Controls[j].ControlID] = frameworks[i].Controls[j]
if len(compatibleRules) > 0 {
frameworks[i].Controls[j].Rules = compatibleRules
policies.Controls[frameworks[i].Controls[j].ControlID] = frameworks[i].Controls[j]
}
}
}
}

View File

@@ -1,7 +1,9 @@
package cautils
type DownloadInfo struct {
Path string
FrameworkName string
ControlName string
Path string // directory to save artifact. Default is "~/.kubescape/"
FileName string // can be empty
Target string // type of artifact to download
Name string // name of artifact to download
Account string // customerGUID
}

View File

@@ -100,28 +100,41 @@ func (armoAPI *ArmoAPI) GetReportReceiverURL() string {
func (armoAPI *ArmoAPI) GetFramework(name string) (*reporthandling.Framework, error) {
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name), nil)
if err != nil {
return nil, err
return nil, nil
}
framework := &reporthandling.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return nil, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name+".json"))
SaveInFile(framework, GetDefaultPath(name+".json"))
return framework, err
}
func (armoAPI *ArmoAPI) GetFrameworks() ([]reporthandling.Framework, error) {
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getListFrameworkURL(), nil)
if err != nil {
return nil, nil
}
frameworks := []reporthandling.Framework{}
if err = JSONDecoder(respStr).Decode(&frameworks); err != nil {
return nil, err
}
// SaveInFile(framework, GetDefaultPath(name+".json"))
return frameworks, err
}
func (armoAPI *ArmoAPI) GetControl(policyName string) (*reporthandling.Control, error) {
return nil, fmt.Errorf("control api is not public")
}
func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
func (armoAPI *ArmoAPI) GetExceptions(clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
exceptions := []armotypes.PostureExceptionPolicy{}
if customerGUID == "" {
return exceptions, nil
}
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getExceptionsURL(customerGUID, clusterName), nil)
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getExceptionsURL(clusterName), nil)
if err != nil {
return nil, err
}
@@ -133,10 +146,10 @@ func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armot
return exceptions, nil
}
func (armoAPI *ArmoAPI) GetCustomerGUID(customerGUID string) (*TenantResponse, error) {
func (armoAPI *ArmoAPI) GetCustomerGUID() (*TenantResponse, error) {
url := armoAPI.getCustomerURL()
if customerGUID != "" {
url = fmt.Sprintf("%s?customerGUID=%s", url, customerGUID)
if armoAPI.customerGUID != "" {
url = fmt.Sprintf("%s?customerGUID=%s", url, armoAPI.customerGUID)
}
respStr, err := HttpGetter(armoAPI.httpClient, url, nil)
if err != nil {
@@ -151,12 +164,12 @@ func (armoAPI *ArmoAPI) GetCustomerGUID(customerGUID string) (*TenantResponse, e
}
// ControlsInputs // map[<control name>][<input arguments>]
func (armoAPI *ArmoAPI) GetAccountConfig(customerGUID, clusterName string) (*armotypes.CustomerConfig, error) {
func (armoAPI *ArmoAPI) GetAccountConfig(clusterName string) (*armotypes.CustomerConfig, error) {
accountConfig := &armotypes.CustomerConfig{}
if customerGUID == "" {
if armoAPI.customerGUID == "" {
return accountConfig, nil
}
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getAccountConfig(customerGUID, clusterName), nil)
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getAccountConfig(clusterName), nil)
if err != nil {
return nil, err
}
@@ -169,15 +182,15 @@ func (armoAPI *ArmoAPI) GetAccountConfig(customerGUID, clusterName string) (*arm
}
// ControlsInputs // map[<control name>][<input arguments>]
func (armoAPI *ArmoAPI) GetControlsInputs(customerGUID, clusterName string) (map[string][]string, error) {
accountConfig, err := armoAPI.GetAccountConfig(customerGUID, clusterName)
func (armoAPI *ArmoAPI) GetControlsInputs(clusterName string) (map[string][]string, error) {
accountConfig, err := armoAPI.GetAccountConfig(clusterName)
if err == nil {
return accountConfig.Settings.PostureControlInputs, nil
}
return nil, err
}
func (armoAPI *ArmoAPI) ListCustomFrameworks(customerGUID string) ([]string, error) {
func (armoAPI *ArmoAPI) ListCustomFrameworks() ([]string, error) {
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getListFrameworkURL(), nil)
if err != nil {
return nil, err
@@ -197,7 +210,7 @@ func (armoAPI *ArmoAPI) ListCustomFrameworks(customerGUID string) ([]string, err
return frameworkList, nil
}
func (armoAPI *ArmoAPI) ListFrameworks(customerGUID string) ([]string, error) {
func (armoAPI *ArmoAPI) ListFrameworks() ([]string, error) {
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getListFrameworkURL(), nil)
if err != nil {
return nil, err
@@ -219,6 +232,10 @@ func (armoAPI *ArmoAPI) ListFrameworks(customerGUID string) ([]string, error) {
return frameworkList, nil
}
func (armoAPI *ArmoAPI) ListControls(l ListType) ([]string, error) {
return nil, fmt.Errorf("control api is not public")
}
type TenantResponse struct {
TenantID string `json:"tenantId"`
Token string `json:"token"`

View File

@@ -5,7 +5,7 @@ import (
"strings"
)
var NativeFrameworks = []string{"nsa", "mitre", "armobest"}
var NativeFrameworks = []string{"nsa", "mitre", "armobest", "devopsbest"}
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
u := url.URL{}
@@ -36,14 +36,14 @@ func (armoAPI *ArmoAPI) getListFrameworkURL() string {
return u.String()
}
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
func (armoAPI *ArmoAPI) getExceptionsURL(clusterName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Path = "api/v1/armoPostureExceptions"
q := u.Query()
q.Add("customerGUID", customerGUID)
q.Add("customerGUID", armoAPI.customerGUID)
// if clusterName != "" { // TODO - fix customer name support in Armo BE
// q.Add("clusterName", clusterName)
// }
@@ -52,14 +52,14 @@ func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) strin
return u.String()
}
func (armoAPI *ArmoAPI) getAccountConfig(customerGUID, clusterName string) string {
func (armoAPI *ArmoAPI) getAccountConfig(clusterName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Path = "api/v1/armoCustomerConfiguration"
q := u.Query()
q.Add("customerGUID", customerGUID)
q.Add("customerGUID", armoAPI.customerGUID)
if clusterName != "" { // TODO - fix customer name support in Armo BE
q.Add("clusterName", clusterName)
}

View File

@@ -41,7 +41,28 @@ func (drp *DownloadReleasedPolicy) GetFramework(name string) (*reporthandling.Fr
return framework, err
}
func (drp *DownloadReleasedPolicy) GetControlsInputs(customerGUID, clusterName string) (map[string][]string, error) {
func (drp *DownloadReleasedPolicy) GetFrameworks() ([]reporthandling.Framework, error) {
frameworks, err := drp.gs.GetOPAFrameworks()
if err != nil {
return nil, err
}
return frameworks, err
}
func (drp *DownloadReleasedPolicy) ListFrameworks() ([]string, error) {
return drp.gs.GetOPAFrameworksNamesList()
}
func (drp *DownloadReleasedPolicy) ListControls(listType ListType) ([]string, error) {
switch listType {
case ListID:
return drp.gs.GetOPAControlsIDsList()
default:
return drp.gs.GetOPAControlsNamesList()
}
}
func (drp *DownloadReleasedPolicy) GetControlsInputs(clusterName string) (map[string][]string, error) {
defaultConfigInputs, err := drp.gs.GetDefaultConfigInputs()
if err != nil {
return nil, err

View File

@@ -5,18 +5,29 @@ import (
"github.com/armosec/opa-utils/reporthandling"
)
// supported listing
type ListType string
const ListID ListType = "id"
const ListName ListType = "name"
type IPolicyGetter interface {
GetFramework(name string) (*reporthandling.Framework, error)
GetFrameworks() ([]reporthandling.Framework, error)
GetControl(name string) (*reporthandling.Control, error)
ListFrameworks() ([]string, error)
ListControls(ListType) ([]string, error)
}
type IExceptionsGetter interface {
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
GetExceptions(clusterName string) ([]armotypes.PostureExceptionPolicy, error)
}
type IBackend interface {
GetCustomerGUID(customerGUID string) (*TenantResponse, error)
GetCustomerGUID() (*TenantResponse, error)
SetCustomerGUID(customerGUID string)
}
type IControlsInputsGetter interface {
GetControlsInputs(customerGUID, clusterName string) (map[string][]string, error)
GetControlsInputs(clusterName string) (map[string][]string, error)
}

View File

@@ -10,8 +10,6 @@ import (
"path"
"path/filepath"
"strings"
"github.com/armosec/opa-utils/reporthandling"
)
func GetDefaultPath(name string) string {
@@ -22,33 +20,8 @@ func GetDefaultPath(name string) string {
return defaultfilePath
}
// Save control as json in file
func SaveControlInFile(control *reporthandling.Control, pathStr string) error {
encodedData, err := json.Marshal(control)
if err != nil {
return err
}
err = os.WriteFile(pathStr, []byte(fmt.Sprintf("%v", string(encodedData))), 0644)
if err != nil {
if os.IsNotExist(err) {
pathDir := path.Dir(pathStr)
if err := os.Mkdir(pathDir, 0744); err != nil {
return err
}
} else {
return err
}
err = os.WriteFile(pathStr, []byte(fmt.Sprintf("%v", string(encodedData))), 0644)
if err != nil {
return err
}
}
return nil
}
func SaveFrameworkInFile(framework *reporthandling.Framework, pathStr string) error {
encodedData, err := json.Marshal(framework)
func SaveInFile(policy interface{}, pathStr string) error {
encodedData, err := json.Marshal(policy)
if err != nil {
return err
}

View File

@@ -78,7 +78,34 @@ func (lp *LoadPolicy) GetFramework(frameworkName string) (*reporthandling.Framew
return framework, err
}
func (lp *LoadPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
func (lp *LoadPolicy) GetFrameworks() ([]reporthandling.Framework, error) {
frameworks := []reporthandling.Framework{}
var err error
return frameworks, err
}
func (lp *LoadPolicy) ListFrameworks() ([]string, error) {
fwNames := []string{}
framework := &reporthandling.Framework{}
for _, f := range lp.filePaths {
file, err := os.ReadFile(f)
if err == nil {
if err := json.Unmarshal(file, framework); err == nil {
if !contains(fwNames, framework.Name) {
fwNames = append(fwNames, framework.Name)
}
}
}
}
return fwNames, nil
}
func (lp *LoadPolicy) ListControls(listType ListType) ([]string, error) {
// TODO - Support
return []string{}, fmt.Errorf("loading controls list from file is not supported")
}
func (lp *LoadPolicy) GetExceptions(clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
filePath := lp.filePath()
exception := []armotypes.PostureExceptionPolicy{}
f, err := os.ReadFile(filePath)
@@ -90,7 +117,7 @@ func (lp *LoadPolicy) GetExceptions(customerGUID, clusterName string) ([]armotyp
return exception, err
}
func (lp *LoadPolicy) GetControlsInputs(customerGUID, clusterName string) (map[string][]string, error) {
func (lp *LoadPolicy) GetControlsInputs(clusterName string) (map[string][]string, error) {
filePath := lp.filePath()
accountConfig := &armotypes.CustomerConfig{}
f, err := os.ReadFile(filePath)
@@ -98,7 +125,7 @@ func (lp *LoadPolicy) GetControlsInputs(customerGUID, clusterName string) (map[s
return nil, err
}
if err = json.Unmarshal(f, &accountConfig); err == nil {
if err = json.Unmarshal(f, &accountConfig.Settings.PostureControlInputs); err == nil {
return accountConfig.Settings.PostureControlInputs, nil
}
return nil, err

View File

@@ -1,9 +1,7 @@
package getter
import (
"os"
"path/filepath"
"testing"
)
var mockFrameworkBasePath = filepath.Join("examples", "mocks", "frameworks")
@@ -13,8 +11,3 @@ func MockNewLoadPolicy() *LoadPolicy {
filePaths: []string{""},
}
}
func TestBla(t *testing.T) {
dir, _ := os.Getwd()
t.Error(dir)
}

7
cautils/listpolicies.go Normal file
View File

@@ -0,0 +1,7 @@
package cautils
type ListPolicies struct {
Target string
ListIDs bool
Account string
}

View File

@@ -42,30 +42,38 @@ func (rbacObjects *RBACObjects) ListAllResources() (map[string]workloadinterface
func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.RbacObjects) (map[string]workloadinterface.IMetadata, error) {
allresources := map[string]workloadinterface.IMetadata{}
/*
************************************************************************************************************************
This code is adding a non valid ID ->
(github.com/armosec/rbac-utils v0.0.11): "//SA2WLIDmap/SA2WLIDmap"
(github.com/armosec/rbac-utils v0.0.12): "armo.rbac.com/v0beta1//SAID2WLIDmap/SAID2WLIDmap"
Should be investigated
************************************************************************************************************************
*/
// wrap rbac aggregated objects in IMetadata and add to allresources
rbacIMeta, err := rbacutils.RbacObjectIMetadataWrapper(resources.Rbac)
if err != nil {
return nil, err
}
allresources[rbacIMeta.GetID()] = rbacIMeta
rbacTableIMeta, err := rbacutils.RbacTableObjectIMetadataWrapper(resources.RbacT)
if err != nil {
return nil, err
}
allresources[rbacTableIMeta.GetID()] = rbacTableIMeta
// TODO - DEPRECATE SA2WLIDmap
SA2WLIDmapIMeta, err := rbacutils.SA2WLIDmapIMetadataWrapper(resources.SA2WLIDmap)
if err != nil {
return nil, err
}
allresources[SA2WLIDmapIMeta.GetID()] = SA2WLIDmapIMeta
SAID2WLIDmapIMeta, err := rbacutils.SAID2WLIDmapIMetadataWrapper(resources.SAID2WLIDmap)
if err != nil {
return nil, err
}
allresources[SAID2WLIDmapIMeta.GetID()] = SAID2WLIDmapIMeta
// convert rbac k8s resources to IMetadata and add to allresources
for _, cr := range resources.ClusterRoles.Items {
crmap, err := convertToMap(cr)
if err != nil {
return nil, err
}
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1"
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
crIMeta := workloadinterface.NewWorkloadObj(crmap)
crIMeta.SetKind("ClusterRole")
allresources[crIMeta.GetID()] = crIMeta
@@ -75,7 +83,7 @@ func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.Rbac
if err != nil {
return nil, err
}
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1"
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
crIMeta := workloadinterface.NewWorkloadObj(crmap)
crIMeta.SetKind("Role")
allresources[crIMeta.GetID()] = crIMeta
@@ -85,7 +93,7 @@ func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.Rbac
if err != nil {
return nil, err
}
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1"
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
crIMeta := workloadinterface.NewWorkloadObj(crmap)
crIMeta.SetKind("ClusterRoleBinding")
allresources[crIMeta.GetID()] = crIMeta
@@ -95,7 +103,7 @@ func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.Rbac
if err != nil {
return nil, err
}
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1"
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
crIMeta := workloadinterface.NewWorkloadObj(crmap)
crIMeta.SetKind("RoleBinding")
allresources[crIMeta.GetID()] = crIMeta

View File

@@ -1,15 +1,17 @@
package v1
package cautils
import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/opa-utils/reporthandling"
helpersv1 "github.com/armosec/opa-utils/reporthandling/helpers/v1"
"github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary"
"github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults"
"github.com/armosec/opa-utils/score"
)
func reportV2ToV1(opaSessionObj *cautils.OPASessionObj) {
func ReportV2ToV1(opaSessionObj *OPASessionObj) {
if len(opaSessionObj.PostureReport.FrameworkReports) > 0 {
return // report already converted
}
opaSessionObj.PostureReport.ClusterCloudProvider = opaSessionObj.Report.ClusterCloudProvider
@@ -34,9 +36,9 @@ func reportV2ToV1(opaSessionObj *cautils.OPASessionObj) {
frameworks = append(frameworks, fwv1)
}
// remove unused data
opaSessionObj.Report = nil
opaSessionObj.ResourcesResult = nil
// // remove unused data
// opaSessionObj.Report = nil
// opaSessionObj.ResourcesResult = nil
// setup counters and score
for f := range frameworks {
@@ -56,6 +58,7 @@ func reportV2ToV1(opaSessionObj *cautils.OPASessionObj) {
opaSessionObj.PostureReport.FrameworkReports = frameworks
// opaSessionObj.Report.SummaryDetails.Score = 0
// for i := range frameworks {
// for j := range frameworks[i].ControlReports {
// // frameworks[i].ControlReports[j].Score
@@ -73,29 +76,41 @@ func reportV2ToV1(opaSessionObj *cautils.OPASessionObj) {
// opaSessionObj.Report.SummaryDetails.Controls[frameworks[i].ControlReports[j].ControlID] = c
// }
// }
// opaSessionObj.Report.SummaryDetails.Score += opaSessionObj.PostureReport.FrameworkReports[i].Score
// }
// opaSessionObj.Report.SummaryDetails.Score /= float32(len(opaSessionObj.Report.SummaryDetails.Frameworks))
}
func controlReportV2ToV1(opaSessionObj *cautils.OPASessionObj, frameworkName string, controls map[string]reportsummary.ControlSummary) []reporthandling.ControlReport {
func controlReportV2ToV1(opaSessionObj *OPASessionObj, frameworkName string, controls map[string]reportsummary.ControlSummary) []reporthandling.ControlReport {
controlRepors := []reporthandling.ControlReport{}
for controlID, crv2 := range controls {
crv1 := reporthandling.ControlReport{}
crv1.ControlID = controlID
crv1.BaseScore = crv2.ScoreFactor
crv1.Name = crv2.GetName()
crv1.Control_ID = controlID
// crv1.Attributes = crv2.
crv1.Score = crv2.GetScore()
// TODO - add fields
crv1.Description = crv2.Description
crv1.Remediation = crv2.Remediation
rulesv1 := initializeRuleList(&crv2, opaSessionObj.ResourcesResult)
rulesv1 := map[string]reporthandling.RuleReport{}
for _, resourceID := range crv2.List().All() {
for _, resourceID := range crv2.ListResourcesIDs().All() {
if result, ok := opaSessionObj.ResourcesResult[resourceID]; ok {
for _, rulev2 := range result.ListRulesOfControl(crv2.GetID(), "") {
if _, ok := rulesv1[rulev2.GetName()]; !ok {
rulesv1[rulev2.GetName()] = reporthandling.RuleReport{
Name: rulev2.GetName(),
RuleStatus: reporthandling.RuleStatus{
Status: "success",
},
}
}
rulev1 := rulesv1[rulev2.GetName()]
status := rulev2.GetStatus(&helpersv1.Filters{FrameworkNames: []string{frameworkName}})
@@ -113,10 +128,11 @@ func controlReportV2ToV1(opaSessionObj *cautils.OPASessionObj, frameworkName str
}
if fullRessource, ok := opaSessionObj.AllResources[resourceID]; ok {
ruleResponse.AlertObject.K8SApiObjects = append(ruleResponse.AlertObject.K8SApiObjects, fullRessource.GetObject())
tmp := fullRessource.GetObject()
workloadinterface.RemoveFromMap(tmp, "spec")
ruleResponse.AlertObject.K8SApiObjects = append(ruleResponse.AlertObject.K8SApiObjects, tmp)
}
rulev1.RuleResponses = append(rulev1.RuleResponses, ruleResponse)
}
rulev1.ListInputKinds = append(rulev1.ListInputKinds, resourceID)
@@ -129,25 +145,10 @@ func controlReportV2ToV1(opaSessionObj *cautils.OPASessionObj, frameworkName str
crv1.RuleReports = append(crv1.RuleReports, rulesv1[i])
}
}
if len(crv1.RuleReports) == 0 {
crv1.RuleReports = []reporthandling.RuleReport{}
}
controlRepors = append(controlRepors, crv1)
}
return controlRepors
}
func initializeRuleList(crv2 *reportsummary.ControlSummary, resourcesResult map[string]resourcesresults.Result) map[string]reporthandling.RuleReport {
rulesv1 := map[string]reporthandling.RuleReport{} // ruleName: rules
for _, resourceID := range crv2.List().All() {
if result, ok := resourcesResult[resourceID]; ok {
for _, rulev2 := range result.ListRulesOfControl(crv2.GetID(), "") {
// add to rule
if _, ok := rulesv1[rulev2.GetName()]; !ok {
rulesv1[rulev2.GetName()] = reporthandling.RuleReport{
Name: rulev2.GetName(),
}
}
}
}
}
return rulesv1
}

View File

@@ -1,16 +1,23 @@
package cautils
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/opa-utils/reporthandling"
)
const (
ScanCluster string = "cluster"
ScanLocalFiles string = "yaml"
ScanCluster string = "cluster"
ScanLocalFiles string = "yaml"
localControlInputsFilename string = "controls-inputs.json"
localExceptionsFilename string = "exceptions.json"
)
type BoolPtrFlag struct {
@@ -52,6 +59,7 @@ type ScanInfo struct {
ControlsInputs string // Load file with inputs for controls
UseFrom []string // Load framework from local file (instead of download). Use when running offline
UseDefault bool // Load framework from cached file (instead of download). Use when running offline
UseArtifactsFrom string // Load artifacts from local path. Use when running offline
VerboseMode bool // Display all of the input resources and not only failed resources
Format string // Format results (table, json, junit ...)
Output string // Store results in an output file, Output file name
@@ -64,6 +72,7 @@ type ScanInfo struct {
HostSensor BoolPtrFlag // Deploy ARMO K8s host sensor to collect data from certain controls
Local bool // Do not submit results
Account string // account ID
KubeContext string // context name
FrameworkScan bool // false if scanning control
ScanAll bool // true if scan all frameworks
}
@@ -76,9 +85,40 @@ type Getters struct {
func (scanInfo *ScanInfo) Init() {
scanInfo.setUseFrom()
scanInfo.setUseExceptions()
scanInfo.setOutputFile()
scanInfo.setUseArtifactsFrom()
}
func (scanInfo *ScanInfo) setUseArtifactsFrom() {
if scanInfo.UseArtifactsFrom == "" {
return
}
// UseArtifactsFrom must be a path without a filename
dir, file := filepath.Split(scanInfo.UseArtifactsFrom)
if dir == "" {
scanInfo.UseArtifactsFrom = file
} else if strings.Contains(file, ".json") {
scanInfo.UseArtifactsFrom = dir
}
// set frameworks files
files, err := ioutil.ReadDir(scanInfo.UseArtifactsFrom)
if err != nil {
log.Fatal(err)
}
framework := &reporthandling.Framework{}
for _, f := range files {
filePath := filepath.Join(scanInfo.UseArtifactsFrom, f.Name())
file, err := os.ReadFile(filePath)
if err == nil {
if err := json.Unmarshal(file, framework); err == nil {
scanInfo.UseFrom = append(scanInfo.UseFrom, filepath.Join(scanInfo.UseArtifactsFrom, f.Name()))
}
}
}
// set config-inputs file
scanInfo.ControlsInputs = filepath.Join(scanInfo.UseArtifactsFrom, localControlInputsFilename)
// set exceptions
scanInfo.UseExceptions = filepath.Join(scanInfo.UseArtifactsFrom, localExceptionsFilename)
}
func (scanInfo *ScanInfo) setUseExceptions() {

176
clihandler/clidownload.go Normal file
View File

@@ -0,0 +1,176 @@
package clihandler
import (
"fmt"
"path/filepath"
"strings"
"github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
)
var downloadFunc = map[string]func(*cautils.DownloadInfo) error{
"controls-inputs": downloadConfigInputs,
"exceptions": downloadExceptions,
"control": downloadControl,
"framework": downloadFramework,
"artifacts": downloadArtifacts,
}
func DownloadSupportCommands() []string {
commands := []string{}
for k := range downloadFunc {
commands = append(commands, k)
}
return commands
}
func CliDownload(downloadInfo *cautils.DownloadInfo) error {
setPathandFilename(downloadInfo)
if err := downloadArtifact(downloadInfo, downloadFunc); err != nil {
return err
}
return nil
}
func downloadArtifact(downloadInfo *cautils.DownloadInfo, downloadArtifactFunc map[string]func(*cautils.DownloadInfo) error) error {
if f, ok := downloadArtifactFunc[downloadInfo.Target]; ok {
if err := f(downloadInfo); err != nil {
return err
}
return nil
}
return fmt.Errorf("unknown command to download")
}
func setPathandFilename(downloadInfo *cautils.DownloadInfo) {
if downloadInfo.Path == "" {
downloadInfo.Path = getter.GetDefaultPath("")
} else {
dir, file := filepath.Split(downloadInfo.Path)
if dir == "" {
downloadInfo.Path = file
} else if strings.Contains(file, ".json") {
downloadInfo.Path = dir
downloadInfo.FileName = file
}
}
}
func downloadArtifacts(downloadInfo *cautils.DownloadInfo) error {
downloadInfo.FileName = ""
var artifacts = map[string]func(*cautils.DownloadInfo) error{
"controls-inputs": downloadConfigInputs,
"exceptions": downloadExceptions,
"framework": downloadFramework,
}
for artifact := range artifacts {
if err := downloadArtifact(&cautils.DownloadInfo{Target: artifact, Path: downloadInfo.Path, FileName: fmt.Sprintf("%s.json", artifact)}, artifacts); err != nil {
fmt.Printf("error downloading %s, error: %s", artifact, err)
}
}
return nil
}
func downloadConfigInputs(downloadInfo *cautils.DownloadInfo) error {
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
controlsInputsGetter := getConfigInputsGetter(downloadInfo.Name, tenant.GetCustomerGUID(), nil)
controlInputs, err := controlsInputsGetter.GetControlsInputs(tenant.GetClusterName())
if err != nil {
return err
}
if downloadInfo.FileName == "" {
downloadInfo.FileName = fmt.Sprintf("%s.json", downloadInfo.Target)
}
// save in file
err = getter.SaveInFile(controlInputs, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
if err != nil {
return err
}
fmt.Printf("'%s' downloaded successfully and saved at: '%s'\n", downloadInfo.Target, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
return nil
}
func downloadExceptions(downloadInfo *cautils.DownloadInfo) error {
var err error
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
exceptionsGetter := getExceptionsGetter("")
exceptions := []armotypes.PostureExceptionPolicy{}
if tenant.GetCustomerGUID() != "" {
exceptions, err = exceptionsGetter.GetExceptions(tenant.GetClusterName())
if err != nil {
return err
}
}
if downloadInfo.FileName == "" {
downloadInfo.FileName = fmt.Sprintf("%s.json", downloadInfo.Target)
}
// save in file
err = getter.SaveInFile(exceptions, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
if err != nil {
return err
}
fmt.Printf("'%s' downloaded successfully and saved at: '%s'\n", downloadInfo.Target, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
return nil
}
func downloadFramework(downloadInfo *cautils.DownloadInfo) error {
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
g := getPolicyGetter(nil, tenant.GetCustomerGUID(), true, nil)
if downloadInfo.Name == "" {
// if framework name not specified - download all frameworks
frameworks, err := g.GetFrameworks()
if err != nil {
return err
}
for _, fw := range frameworks {
err = getter.SaveInFile(fw, filepath.Join(downloadInfo.Path, (strings.ToLower(fw.Name)+".json")))
if err != nil {
return err
}
fmt.Printf("'%s': '%s' downloaded successfully and saved at: '%s'\n", downloadInfo.Target, fw.Name, filepath.Join(downloadInfo.Path, (strings.ToLower(fw.Name)+".json")))
}
// return fmt.Errorf("missing framework name")
} else {
if downloadInfo.FileName == "" {
downloadInfo.FileName = fmt.Sprintf("%s.json", downloadInfo.Name)
}
framework, err := g.GetFramework(downloadInfo.Name)
if err != nil {
return err
}
err = getter.SaveInFile(framework, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
if err != nil {
return err
}
fmt.Printf("'%s' downloaded successfully and saved at: '%s'\n", downloadInfo.Target, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
}
return nil
}
func downloadControl(downloadInfo *cautils.DownloadInfo) error {
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
g := getPolicyGetter(nil, tenant.GetCustomerGUID(), false, nil)
if downloadInfo.Name == "" {
// TODO - support
return fmt.Errorf("missing control name")
}
if downloadInfo.FileName == "" {
downloadInfo.FileName = fmt.Sprintf("%s.json", downloadInfo.Name)
}
controls, err := g.GetControl(downloadInfo.Name)
if err != nil {
return err
}
err = getter.SaveInFile(controls, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
if err != nil {
return err
}
fmt.Printf("'%s' downloaded successfully and saved at: '%s'\n", downloadInfo.Target, filepath.Join(downloadInfo.Path, downloadInfo.FileName))
return nil
}

58
clihandler/clilist.go Normal file
View File

@@ -0,0 +1,58 @@
package clihandler
import (
"fmt"
"sort"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
)
var listFunc = map[string]func(*cautils.ListPolicies) ([]string, error){
"controls": listControls,
"frameworks": listFrameworks,
}
func ListSupportCommands() []string {
commands := []string{}
for k := range listFunc {
commands = append(commands, k)
}
return commands
}
func CliList(listPolicies *cautils.ListPolicies) error {
if f, ok := listFunc[listPolicies.Target]; ok {
policies, err := f(listPolicies)
if err != nil {
return err
}
sort.Strings(policies)
sep := "\n * "
usageCmd := strings.TrimSuffix(listPolicies.Target, "s")
fmt.Printf("Supported %s:%s%s\n", listPolicies.Target, sep, strings.Join(policies, sep))
fmt.Printf("\nUseage:\n")
fmt.Printf("$ kubescape scan %s \"name\"\n", usageCmd)
fmt.Printf("$ kubescape scan %s \"name-0\",\"name-1\"\n\n", usageCmd)
return nil
}
return fmt.Errorf("unknown command to download")
}
func listFrameworks(listPolicies *cautils.ListPolicies) ([]string, error) {
tenant := getTenantConfig(listPolicies.Account, "", getKubernetesApi()) // change k8sinterface
g := getPolicyGetter(nil, tenant.GetCustomerGUID(), true, nil)
return listFrameworksNames(g), nil
}
func listControls(listPolicies *cautils.ListPolicies) ([]string, error) {
tenant := getTenantConfig(listPolicies.Account, "", getKubernetesApi()) // change k8sinterface
g := getPolicyGetter(nil, tenant.GetCustomerGUID(), false, nil)
l := getter.ListName
if listPolicies.ListIDs {
l = getter.ListID
}
return g.ListControls(l)
}

View File

@@ -11,10 +11,9 @@ import (
)
var getCmd = &cobra.Command{
Use: "get <key>",
Short: "Get configuration in cluster",
Long: ``,
ValidArgs: getter.NativeFrameworks,
Use: "get <key>",
Short: "Get configuration in cluster",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 || len(args) > 1 {
return fmt.Errorf("requires one argument")
@@ -31,7 +30,7 @@ var getCmd = &cobra.Command{
key := keyValue[0]
k8s := k8sinterface.NewKubernetesApi()
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), scanInfo.Account)
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), scanInfo.Account, "")
val, err := clusterConfig.GetValueByKeyFromConfigMap(key)
if err != nil {
if err.Error() == "value does not exist." {

View File

@@ -30,7 +30,7 @@ var setCmd = &cobra.Command{
data := keyValue[1]
k8s := k8sinterface.NewKubernetesApi()
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), scanInfo.Account)
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), scanInfo.Account, "")
if err := clusterConfig.SetKeyValueInConfigmap(key, data); err != nil {
return err
}

View File

@@ -7,16 +7,34 @@ import (
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/clihandler"
"github.com/armosec/opa-utils/reporthandling"
"github.com/spf13/cobra"
)
var (
controlExample = `
# Scan the 'privileged container' control
kubescape scan control "privileged container"
# Scan list of controls separated with a comma
kubescape scan control "privileged container","allowed hostpath"
# Scan list of controls using the control ID separated with a comma
kubescape scan control C-0058,C-0057
Run 'kubescape list controls' for the list of supported controls
Control documentation:
https://hub.armo.cloud/docs/controls
`
)
// controlCmd represents the control command
var controlCmd = &cobra.Command{
Use: "control <control names list>/<control ids list>.\nExamples:\n$ kubescape scan control C-0058,C-0057 [flags]\n$ kubescape scan contol C-0058 [flags]\n$ kubescape scan control 'privileged container,allowed hostpath' [flags]",
Short: fmt.Sprintf("The control you wish to use for scan. It must be present in at least one of the following frameworks: %s", getter.NativeFrameworks),
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",
Example: controlExample,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
controls := strings.Split(args[0], ",")
@@ -35,7 +53,7 @@ var controlCmd = &cobra.Command{
scanInfo.PolicyIdentifier = []reporthandling.PolicyIdentifier{}
if len(args) == 0 {
scanInfo.SetPolicyIdentifiers(getter.NativeFrameworks, reporthandling.KindFramework)
// scanInfo.SetPolicyIdentifiers(getter.NativeFrameworks, reporthandling.KindFramework)
scanInfo.ScanAll = true
} else { // expected control or list of control sepparated by ","

View File

@@ -6,27 +6,58 @@ import (
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/clihandler"
"github.com/spf13/cobra"
)
var downloadInfo cautils.DownloadInfo
var downloadInfo = cautils.DownloadInfo{}
var (
downloadExample = `
# Download all artifacts and save them in the default path (~/.kubescape)
kubescape download artifacts
# Download all artifacts and save them in /tmp path
kubescape download artifacts --output /tmp
# Download the NSA framework. Run 'kubescape list frameworks' for all frameworks names
kubescape download frameworks nsa
# Download the "Allowed hostPath" control. Run 'kubescape list controls' for all controls names
kubescape download control "Allowed hostPath"
# Download the "C-0001" control. Run 'kubescape list controls --id' for all controls ids
kubescape download control C-0001
# Download the configured exceptions
kubescape download exceptions
# Download the configured controls-inputs
kubescape download controls-inputs
`
)
var downloadCmd = &cobra.Command{
Use: fmt.Sprintf("download framework/control <framework-name>/<control-name> [flags]\nSupported frameworks: %s", getter.NativeFrameworks),
Short: "Download framework/control",
Long: ``,
Use: "download <policy> <policy name>",
Short: fmt.Sprintf("Download %s", strings.Join(clihandler.DownloadSupportCommands(), ",")),
Long: ``,
Example: downloadExample,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("requires two arguments : framework/control <framework-name>/<control-name>")
supported := strings.Join(clihandler.DownloadSupportCommands(), ",")
if len(args) < 1 {
return fmt.Errorf("policy type required, supported: %v", supported)
}
if !strings.EqualFold(args[0], "framework") && !strings.EqualFold(args[0], "control") {
return fmt.Errorf("invalid parameter '%s'. Supported parameters: framework, control", args[0])
if cautils.StringInSlice(clihandler.DownloadSupportCommands(), args[0]) == cautils.ValueNotFound {
return fmt.Errorf("invalid parameter '%s'. Supported parameters: %s", args[0], supported)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := download(args); err != nil {
downloadInfo.Target = args[0]
if len(args) >= 2 {
downloadInfo.Name = args[1]
}
if err := clihandler.CliDownload(&downloadInfo); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
@@ -35,61 +66,10 @@ var downloadCmd = &cobra.Command{
}
func init() {
// cobra.OnInitialize(initConfig)
rootCmd.AddCommand(downloadCmd)
downloadInfo = cautils.DownloadInfo{}
downloadCmd.Flags().StringVarP(&downloadInfo.Path, "output", "o", "", "Output file. If specified, will store save to `~/.kubescape/<framework name>.json`")
}
downloadCmd.Flags().StringVarP(&downloadInfo.Path, "output", "o", "", "Output file. If not specified, will save in `~/.kubescape/<policy name>.json`")
downloadCmd.PersistentFlags().StringVarP(&downloadInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file")
func download(args []string) error {
switch strings.ToLower(args[0]) {
case "framework":
return downloadFramework(args[1])
case "control":
return downloadControl(args[1])
// case "exceptions":
// case "artifacts":
default:
return fmt.Errorf("unknown command to download")
}
}
func downloadFramework(frameworkName string) error {
downloadInfo.FrameworkName = strings.ToLower(frameworkName)
g := getter.NewDownloadReleasedPolicy()
if err := g.SetRegoObjects(); err != nil {
return err
}
if downloadInfo.Path == "" {
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName + ".json")
}
frameworks, err := g.GetFramework(downloadInfo.FrameworkName)
if err != nil {
return err
}
err = getter.SaveFrameworkInFile(frameworks, downloadInfo.Path)
if err != nil {
return err
}
return nil
}
func downloadControl(controlName string) error {
downloadInfo.ControlName = strings.ToLower(controlName)
g := getter.NewDownloadReleasedPolicy()
if err := g.SetRegoObjects(); err != nil {
return err
}
if downloadInfo.Path == "" {
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.ControlName + ".json")
}
controls, err := g.GetControl(downloadInfo.ControlName)
if err != nil {
return err
}
err = getter.SaveControlInFile(controls, downloadInfo.Path)
if err != nil {
return err
}
return nil
}

View File

@@ -7,7 +7,6 @@ import (
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/clihandler"
"github.com/armosec/opa-utils/reporthandling"
"github.com/spf13/cobra"
@@ -24,6 +23,9 @@ var (
# Scan the NSA and MITRE framework
kubescape scan framework nsa,mitre
# Scan all frameworks
kubescape scan framework all
# Scan kubernetes YAML manifest files
kubescape scan framework nsa *.yaml
@@ -35,14 +37,16 @@ var (
# Display all resources
kubescape scan --verbose
Run 'kubescape list frameworks' for the list of supported frameworks
`
)
var frameworkCmd = &cobra.Command{
Use: "framework <framework names list> [`<glob pattern>`/`-`] [flags]",
Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(getter.NativeFrameworks, ", ")),
Example: frameworkExample,
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
ValidArgs: getter.NativeFrameworks,
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",
Example: frameworkExample,
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
// ValidArgs: getter.NativeFrameworks,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
frameworks := strings.Split(args[0], ",")
@@ -61,12 +65,15 @@ var frameworkCmd = &cobra.Command{
var frameworks []string
if len(args) == 0 { // scan all frameworks
frameworks = getter.NativeFrameworks
// frameworks = getter.NativeFrameworks
scanInfo.ScanAll = true
} else {
// Read frameworks from input args
frameworks = strings.Split(args[0], ",")
if cautils.StringInSlice(frameworks, "all") != cautils.ValueNotFound {
scanInfo.ScanAll = true
frameworks = []string{}
}
if len(args) > 1 {
if len(args[1:]) == 0 || args[1] != "-" {
scanInfo.InputPatterns = args[1:]
@@ -84,6 +91,8 @@ var frameworkCmd = &cobra.Command{
}
}
}
scanInfo.FrameworkScan = true
scanInfo.SetPolicyIdentifiers(frameworks, reporthandling.KindFramework)
scanInfo.Init()

66
clihandler/cmd/list.go Normal file
View File

@@ -0,0 +1,66 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/clihandler"
"github.com/spf13/cobra"
)
var (
listExample = `
# List default supported frameworks names
kubescape list frameworks
# List all supported frameworks names
kubescape list frameworks --account <account id>
# List all supported controls names
kubescape list controls
# List all supported controls ids
kubescape list controls --id
Control documentation:
https://hub.armo.cloud/docs/controls
`
)
var listPolicies = cautils.ListPolicies{}
var listCmd = &cobra.Command{
Use: "list <policy> [flags]",
Short: "List frameworks/controls will list the supported frameworks and controls",
Long: ``,
Example: listExample,
Args: func(cmd *cobra.Command, args []string) error {
supported := strings.Join(clihandler.ListSupportCommands(), ",")
if len(args) < 1 {
return fmt.Errorf("policy type requeued, supported: %s", supported)
}
if cautils.StringInSlice(clihandler.ListSupportCommands(), args[0]) == cautils.ValueNotFound {
return fmt.Errorf("invalid parameter '%s'. Supported parameters: %s", args[0], supported)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
listPolicies.Target = args[0]
if err := clihandler.CliList(&listPolicies); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return nil
},
}
func init() {
// cobra.OnInitialize(initConfig)
rootCmd.AddCommand(listCmd)
listCmd.PersistentFlags().StringVarP(&listPolicies.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file")
listCmd.PersistentFlags().BoolVarP(&listPolicies.ListIDs, "id", "", false, "List control ID's instead of controls names")
}

View File

@@ -35,14 +35,9 @@ func init() {
flag.CommandLine.StringVar(&armoBEURLs, "environment", "", envFlagUsage)
rootCmd.PersistentFlags().StringVar(&armoBEURLs, "environment", "", envFlagUsage)
rootCmd.PersistentFlags().MarkHidden("environment")
cobra.OnInitialize(initConfig)
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
}
func InitArmoBEConnector() {
urlSlices := strings.Split(armoBEURLs, ",")
if len(urlSlices) > 3 {

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"strings"
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/spf13/cobra"
)
@@ -27,17 +27,25 @@ var scanCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
scanInfo.ScanAll = true
frameworks := getter.NativeFrameworks
frameworkArgs := []string{strings.Join(frameworks, ",")}
frameworkCmd.RunE(cmd, frameworkArgs)
// frameworks := getter.NativeFrameworks
// frameworkArgs := []string{strings.Join(frameworks, ",")}
frameworkCmd.RunE(cmd, []string{"all"})
}
},
}
func frameworkInitConfig() {
k8sinterface.SetClusterContextName(scanInfo.KubeContext)
}
func init() {
cobra.OnInitialize(frameworkInitConfig)
rootCmd.AddCommand(scanCmd)
rootCmd.PersistentFlags().StringVarP(&scanInfo.KubeContext, "kube-context", "", "", "Kube context. Default will use the current-context")
scanCmd.PersistentFlags().StringVar(&scanInfo.ControlsInputs, "controls-config", "", "Path to an controls-config obj. If not set will download controls-config from ARMO management portal")
scanCmd.PersistentFlags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from ARMO management portal")
scanCmd.PersistentFlags().StringVar(&scanInfo.UseArtifactsFrom, "use-artifacts-from", "", "Load artifacts from local directory. If not used will download them")
scanCmd.PersistentFlags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system,kube-public")
scanCmd.PersistentFlags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 100, "Failure threshold is the percent above which the command fails and returns exit code 1")
scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer"/"json"/"junit"/"prometheus"`)

View File

@@ -20,7 +20,7 @@ func init() {
}
func getSubmittedClusterConfig(k8s *k8sinterface.KubernetesApi) (*cautils.ClusterConfig, error) {
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), scanInfo.Account) // TODO - support none cluster env submit
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), scanInfo.Account, scanInfo.KubeContext) // TODO - support none cluster env submit
if clusterConfig.GetCustomerGUID() != "" {
if err := clusterConfig.SetTenant(); err != nil {
return clusterConfig, err

View File

@@ -5,6 +5,7 @@ import (
"io/fs"
"os"
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/resultshandling/printer"
printerv1 "github.com/armosec/kubescape/resultshandling/printer/v1"
@@ -34,9 +35,16 @@ type componentInterfaces struct {
func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces {
k8s := getKubernetesApi(scanInfo)
var k8s *k8sinterface.KubernetesApi
if scanInfo.GetScanningEnvironment() == cautils.ScanCluster {
k8s = getKubernetesApi()
if k8s == nil {
fmt.Println("Failed connecting to Kubernetes cluster")
os.Exit(1)
}
}
tenantConfig := getTenantConfig(scanInfo, k8s)
tenantConfig := getTenantConfig(scanInfo.Account, scanInfo.KubeContext, k8s)
// Set submit behavior AFTER loading tenant config
setSubmitBehavior(scanInfo, tenantConfig)
@@ -92,10 +100,18 @@ func ScanCliSetup(scanInfo *cautils.ScanInfo) error {
interfaces.report.SetCustomerGUID(interfaces.tenantConfig.GetCustomerGUID())
downloadReleasedPolicy := getter.NewDownloadReleasedPolicy() // download config inputs from github release
// set policy getter only after setting the customerGUID
setPolicyGetter(scanInfo, interfaces.tenantConfig.GetCustomerGUID(), downloadReleasedPolicy)
setConfigInputsGetter(scanInfo, interfaces.tenantConfig.GetCustomerGUID(), downloadReleasedPolicy)
// set policy getter only after setting the customerGUID
scanInfo.Getters.PolicyGetter = getPolicyGetter(scanInfo.UseFrom, interfaces.tenantConfig.GetCustomerGUID(), scanInfo.FrameworkScan, downloadReleasedPolicy)
scanInfo.Getters.ControlsInputsGetter = getConfigInputsGetter(scanInfo.ControlsInputs, interfaces.tenantConfig.GetCustomerGUID(), downloadReleasedPolicy)
scanInfo.Getters.ExceptionsGetter = getExceptionsGetter(scanInfo.UseExceptions)
// TODO - list supported frameworks/controls
if scanInfo.ScanAll {
scanInfo.SetPolicyIdentifiers(listFrameworksNames(scanInfo.Getters.PolicyGetter), reporthandling.KindFramework)
}
//
defer func() {
if err := interfaces.hostSensorHandler.TearDown(); err != nil {
errMsg := "failed to tear down host sensor"
@@ -129,7 +145,7 @@ func ScanCliSetup(scanInfo *cautils.ScanInfo) error {
// print report url
interfaces.report.DisplayReportURL()
if score >= float32(scanInfo.FailThreshold) {
if score > float32(scanInfo.FailThreshold) {
return fmt.Errorf("scan risk-score %.2f is above permitted threshold %d", score, scanInfo.FailThreshold)
}

View File

@@ -11,23 +11,33 @@ import (
"github.com/armosec/kubescape/resourcehandler"
"github.com/armosec/kubescape/resultshandling/reporter"
reporterv1 "github.com/armosec/kubescape/resultshandling/reporter/v1"
reporterv2 "github.com/armosec/kubescape/resultshandling/reporter/v2"
"github.com/armosec/opa-utils/reporthandling"
"github.com/armosec/rbac-utils/rbacscanner"
"github.com/golang/glog"
// reporterv2 "github.com/armosec/kubescape/resultshandling/reporter/v2"
)
func getKubernetesApi(scanInfo *cautils.ScanInfo) *k8sinterface.KubernetesApi {
if scanInfo.GetScanningEnvironment() == cautils.ScanLocalFiles {
// getKubernetesApi
func getKubernetesApi() *k8sinterface.KubernetesApi {
if !k8sinterface.IsConnectedToCluster() {
return nil
}
return k8sinterface.NewKubernetesApi()
}
func getTenantConfig(scanInfo *cautils.ScanInfo, k8s *k8sinterface.KubernetesApi) cautils.ITenantConfig {
if scanInfo.GetScanningEnvironment() == cautils.ScanLocalFiles {
return cautils.NewLocalConfig(getter.GetArmoAPIConnector(), scanInfo.Account)
func getTenantConfig(Account, clusterName string, k8s *k8sinterface.KubernetesApi) cautils.ITenantConfig {
if !k8sinterface.IsConnectedToCluster() || k8s == nil {
return cautils.NewLocalConfig(getter.GetArmoAPIConnector(), Account, clusterName)
}
return cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), Account, clusterName)
}
func getExceptionsGetter(useExceptions string) getter.IExceptionsGetter {
if useExceptions != "" {
// load exceptions from file
return getter.NewLoadPolicy([]string{useExceptions})
} else {
return getter.GetArmoAPIConnector()
}
return cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), scanInfo.Account)
}
func getRBACHandler(tenantConfig cautils.ITenantConfig, k8s *k8sinterface.KubernetesApi, submit bool) *cautils.RBACObjects {
@@ -39,12 +49,14 @@ func getRBACHandler(tenantConfig cautils.ITenantConfig, k8s *k8sinterface.Kubern
func getReporter(tenantConfig cautils.ITenantConfig, submit bool) reporter.IReport {
if submit {
return reporterv1.NewReportEventReceiver(tenantConfig.GetConfigObj())
// return reporterv1.NewReportEventReceiver(tenantConfig.GetConfigObj())
return reporterv2.NewReportEventReceiver(tenantConfig.GetConfigObj())
}
return reporterv1.NewReportMock()
}
func getResourceHandler(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantConfig, k8s *k8sinterface.KubernetesApi, hostSensorHandler hostsensorutils.IHostSensor) resourcehandler.IResourceHandler {
if scanInfo.GetScanningEnvironment() == cautils.ScanLocalFiles {
if len(scanInfo.InputPatterns) > 0 || k8s == nil {
return resourcehandler.NewFileResourceHandler(scanInfo.InputPatterns)
}
rbacObjects := getRBACHandler(tenantConfig, k8s, scanInfo.Submit)
@@ -52,18 +64,20 @@ func getResourceHandler(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenant
}
func getHostSensorHandler(scanInfo *cautils.ScanInfo, k8s *k8sinterface.KubernetesApi) hostsensorutils.IHostSensor {
if scanInfo.GetScanningEnvironment() == cautils.ScanLocalFiles {
if !k8sinterface.IsConnectedToCluster() || k8s == nil {
return &hostsensorutils.HostSensorHandlerMock{}
}
hasHostSensorControls := true
// we need to determined which controls needs host sensor
if scanInfo.HostSensor.Get() == nil && hasHostSensorControls {
scanInfo.HostSensor.SetBool(askUserForHostSensor())
cautils.WarningDisplay(os.Stderr, "Warning: Kubernetes cluster nodes scanning is disabled. This is required to collect valuable data for certain controls. You can enable it using the --enable-host-scan flag\n")
}
if hostSensorVal := scanInfo.HostSensor.Get(); hostSensorVal != nil && *hostSensorVal {
hostSensorHandler, err := hostsensorutils.NewHostSensorHandler(k8s)
if err != nil || hostSensorHandler == nil {
glog.Errorf("failed to create host sensor: %v", err)
if err != nil {
cautils.WarningDisplay(os.Stderr, fmt.Sprintf("Warning: failed to create host sensor: %v\n", err.Error()))
return &hostsensorutils.HostSensorHandlerMock{}
}
return hostSensorHandler
@@ -116,12 +130,6 @@ func setSubmitBehavior(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantC
return
}
// do not submit yaml/url scanning
if scanInfo.GetScanningEnvironment() == cautils.ScanLocalFiles {
scanInfo.Submit = false
return
}
if tenantConfig.IsConfigFound() { // config found in cache (submitted)
if !scanInfo.Local {
// Submit report
@@ -138,55 +146,64 @@ func setSubmitBehavior(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantC
}
// setPolicyGetter set the policy getter - local file/github release/ArmoAPI
func setPolicyGetter(scanInfo *cautils.ScanInfo, customerGUID string, downloadReleasedPolicy *getter.DownloadReleasedPolicy) {
if len(scanInfo.UseFrom) > 0 {
scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
} else {
if customerGUID == "" || !scanInfo.FrameworkScan {
setDownloadReleasedPolicy(scanInfo, downloadReleasedPolicy)
} else {
setGetArmoAPIConnector(scanInfo, customerGUID)
}
func getPolicyGetter(loadPoliciesFromFile []string, accountID string, frameworkScope bool, downloadReleasedPolicy *getter.DownloadReleasedPolicy) getter.IPolicyGetter {
if len(loadPoliciesFromFile) > 0 {
return getter.NewLoadPolicy(loadPoliciesFromFile)
}
if accountID != "" && frameworkScope {
g := getter.GetArmoAPIConnector() // download policy from ARMO backend
g.SetCustomerGUID(accountID)
return g
}
if downloadReleasedPolicy == nil {
downloadReleasedPolicy = getter.NewDownloadReleasedPolicy()
}
return getDownloadReleasedPolicy(downloadReleasedPolicy)
}
// func setGetArmoAPIConnector(scanInfo *cautils.ScanInfo, customerGUID string) {
// g := getter.GetArmoAPIConnector() // download policy from ARMO backend
// g.SetCustomerGUID(customerGUID)
// scanInfo.PolicyGetter = g
// if scanInfo.ScanAll {
// frameworks, err := g.ListCustomFrameworks(customerGUID)
// if err != nil {
// glog.Error("failed to get custom frameworks") // handle error
// return
// }
// scanInfo.SetPolicyIdentifiers(frameworks, reporthandling.KindFramework)
// }
// }
// setConfigInputsGetter sets the config input getter - local file/github release/ArmoAPI
func setConfigInputsGetter(scanInfo *cautils.ScanInfo, customerGUID string, downloadReleasedPolicy *getter.DownloadReleasedPolicy) {
if len(scanInfo.ControlsInputs) > 0 {
scanInfo.Getters.ControlsInputsGetter = getter.NewLoadPolicy([]string{scanInfo.ControlsInputs})
func getConfigInputsGetter(ControlsInputs string, accountID string, downloadReleasedPolicy *getter.DownloadReleasedPolicy) getter.IControlsInputsGetter {
if len(ControlsInputs) > 0 {
return getter.NewLoadPolicy([]string{ControlsInputs})
}
if accountID != "" {
g := getter.GetArmoAPIConnector() // download config from ARMO backend
g.SetCustomerGUID(accountID)
return g
}
if downloadReleasedPolicy == nil {
downloadReleasedPolicy = getter.NewDownloadReleasedPolicy()
}
if err := downloadReleasedPolicy.SetRegoObjects(); err != nil { // if failed to pull config inputs, fallback to BE
cautils.WarningDisplay(os.Stderr, "Warning: failed to get config inputs from github release, this may affect the scanning results\n")
}
return downloadReleasedPolicy
}
func getDownloadReleasedPolicy(downloadReleasedPolicy *getter.DownloadReleasedPolicy) getter.IPolicyGetter {
if err := downloadReleasedPolicy.SetRegoObjects(); err != nil { // if failed to pull policy, fallback to cache
cautils.WarningDisplay(os.Stderr, "Warning: failed to get policies from github release, loading policies from cache\n")
return getter.NewLoadPolicy(getDefaultFrameworksPaths())
} else {
if customerGUID != "" {
scanInfo.Getters.ControlsInputsGetter = getter.GetArmoAPIConnector()
} else {
if err := downloadReleasedPolicy.SetRegoObjects(); err != nil { // if failed to pull config inputs, fallback to BE
cautils.WarningDisplay(os.Stderr, "Warning: failed to get config inputs from github release, this may affect the scanning results\n")
}
scanInfo.Getters.ControlsInputsGetter = downloadReleasedPolicy
}
return downloadReleasedPolicy
}
}
func setDownloadReleasedPolicy(scanInfo *cautils.ScanInfo, downloadReleasedPolicy *getter.DownloadReleasedPolicy) {
if err := downloadReleasedPolicy.SetRegoObjects(); err != nil { // if failed to pull policy, fallback to cache
cautils.WarningDisplay(os.Stderr, "Warning: failed to get policies from github release, loading policies from cache\n")
scanInfo.PolicyGetter = getter.NewLoadPolicy(getDefaultFrameworksPaths())
} else {
scanInfo.PolicyGetter = downloadReleasedPolicy
}
}
func setGetArmoAPIConnector(scanInfo *cautils.ScanInfo, customerGUID string) {
g := getter.GetArmoAPIConnector() // download policy from ARMO backend
g.SetCustomerGUID(customerGUID)
scanInfo.PolicyGetter = g
if scanInfo.ScanAll {
frameworks, err := g.ListCustomFrameworks(customerGUID)
if err != nil {
glog.Error("failed to get custom frameworks") // handle error
return
}
scanInfo.SetPolicyIdentifiers(frameworks, reporthandling.KindFramework)
}
}
func getDefaultFrameworksPaths() []string {
fwPaths := []string{}
for i := range getter.NativeFrameworks {
@@ -194,3 +211,11 @@ func getDefaultFrameworksPaths() []string {
}
return fwPaths
}
func listFrameworksNames(policyGetter getter.IPolicyGetter) []string {
fw, err := policyGetter.ListFrameworks()
if err != nil {
fw = getDefaultFrameworksPaths()
}
return fw
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,176 @@
# Container image vulnerability adaptor interface proposal
## Rationale
source #287
### Big picture
* Kubescape team is planning to create controls which take into account image vulnerabilities, example: looking for public internet facing workloads with critical vulnerabilities. These are seriously effecting the security health of a cluster and therefore we think it is important to cover it. We think that most container registries are/will support image scanning like Harbor and therefore the ability to get information from them is important.
* There are information in the image repository which is important for existing controls as well. They are incomplete without it, example see this issue: Non-root containers check is broken #19 . These are not necessarily image vulnerability related. Can be information in the image manifest (like the issue before), but it can be the image BOM related.
### Relation to this proposal
There are multiple changes and design decisions needs to be made before Kubescape will support the before outlined controls. However, a focal point the whole picutre is the ability to access vulnerabilty databases of container images. We anticiapte that most container image repositories will support image vulnerabilty scanning, some major players are already do. Since there is no a single API available which all of these data sources support it is important to create an adaption layer within Kubescape so different datasources can serve Kubescape's goals.
## High level design of Kubescape
### Layers
* Controls and Rules: that actual control logic implementation, the "tests" themselves. Implemented in rego
* OPA engine: the [OPA](https://github.com/open-policy-agent/opa) rego interpreter
* Rules processor: Kubescape component, it enumerates and runs the controls while also preparing the all the input data that the controls need for running
* Data sources: set of different modules providing data to the Rules processor so it can run the controls with them. Examples: Kubernetes objects, cloud vendor API objects and adding in this proposal the vulnerability infomration
* Cloud Image Vulnerability adaption interface: the subject of this proposal, it gives a common interface for different registry/vulnerabilty vendors to adapt to.
* CIV adaptors: specific implementation of the CIV interface, example Harbor adaption
```
-----------------------
| Controls/Rules (rego) |
-----------------------
|
-----------------------
| OPA engine |
-----------------------
|
-----------------------
| Rules processor |
-----------------------
|
-----------------------
| Data sources |
-----------------------
|
=======================
| CIV adaption interface| <- Adding this layer in this proposal
=======================
|
-----------------------
| Specific CIV adaptors | <- will be implemented based on this proposal
-----------------------
```
## Functionalities to cover
The interface needs to cover the following functionalities:
* Authentication against the information source (abstracted login)
* Triggering image scan (if applicable, the source might store vulnerabilities for images but cannot scan alone)
* Reading image scan status (with last scan date and etc.)
* Getting vulnerability information for a given image
* Getting image information
* Image manifests
* Image BOMs (bill of material)
## Go API proposal
```
/*type ContainerImageRegistryCredentials struct {
map[string]string
Password string
Tag string
Hash string
}*/
type ContainerImageIdentifier struct {
Registry string
Repository string
Tag string
Hash string
}
type ContainerImageScanStatus struct {
ImageID ContainerImageIdentifier
IsScanAvailable bool
IsBomAvailable bool
LastScanDate time.Time
}
type ContainerImageVulnerability struct {
ImageID ContainerImageIdentifier
// TBD
}
type ContainerImageInformation struct {
ImageID ContainerImageIdentifier
Bom []string
ImageManifest Manifest // will use here Docker package definition
}
type IContainerImageVulnerabilityAdaptor interface {
// Credentials are coming from user input (CLI or configuration file) and they are abstracted at string to string map level
// so and example use would be like registry: "simpledockerregistry:80" and credentials like {"username":"joedoe","password":"abcd1234"}
Login(registry string, credentials map[string]string) error
// For "help" purposes
DescribeAdaptor() string
GetImagesScanStatus(imageIDs []ContainerImageIdentifier) ([]ContainerImageScanStatus, error)
GetImagesVulnerabilties(imageIDs []ContainerImageIdentifier) ([]ContainerImageVulnerability, error)
GetImagesInformation(imageIDs []ContainerImageIdentifier) ([]ContainerImageInformation, error)
}
```
# Integration
# Input
The objects received from the interface will be converted to an Imetadata compatible objects as following
```
{
"apiVersion": "image.vulnscan.com/v1",
"kind": "VulnScan",
"metadata": {
"name": "nginx:latest"
},
"data": {
// returned by the adaptor API (structure like our backend gives for an image
}
}
```
# Output
The rego results will be a combination of the k8s artifact and the list of relevant CVEs for the control
```
{
"apiVersion": "result.vulnscan.com/v1",
"kind": "Pod",
"metadata": {
"name": "nginx"
},
"relatedObjects": [
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "nginx"
},
"spec": {
// podSpec
},
},
{
"apiVersion": "container.vulnscan.com/v1",
"kind": "VulnScan",
"metadata": {
"name": "nginx:latest",
},
"data": {
// returned by the adaptor API (structure like our backend gives for an image
}
}
]
}
```

23
docs/roadmap.md Normal file
View File

@@ -0,0 +1,23 @@
# Kubescape project roadmap
## Proposals
* [Container registry integration](/docs/proposals/container-image-vulnerability-adaptor.md)
## Planed features
* Image vulnerablity scanning based controls
* Assited remidiation (telling where/what to fix)
* Git integration for pull requests
* Integration with container registries
* Custom controls and regos
* API server configuration validation
* Kubelet configuration validation
## Completed features
* Integration with Prometheus
* Confiugration of controls (customizing rules for a given environment)
* Installation in the cluster for continous monitoring
* Host scanner
* Cloud vendor API integration
* Custom exceptions
* Custom frameworks

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# AWS
# Attach the Kubescape service account to an AWS IAM role with the described cluster permission
# Prerequisites:
# eksctl, awscli v2
# Set environment variables
echo 'Set environment variables'
export kubescape_namespace=armo-system
export kubescape_serviceaccount=armo-kubescape-service-account
# Get current context
echo 'Get current context'
export context=$(kubectl config current-context)
# Get cluster arn
echo 'Get cluster arn'
export cluster_arn=$(kubectl config view -o jsonpath="{.contexts[?(@.name == \"$context\")].context.cluster}")
# Get cluster name
echo 'Get cluster name'
export cluster_name=$(echo "$cluster_arn" | awk -F'/' '{print $NF}')
# Get cluster region
echo 'Get cluster region'
export cluster_region=$(echo "$cluster_arn" | awk -F':' '{print $4}')
# First step, Create IAM OIDC provider for the cluster (Not required if the third step runs as is):
echo 'Create IAM OIDC provider for the cluster'
eksctl utils associate-iam-oidc-provider --cluster $cluster_name --approve
# Second step, Create a policy and service account role:
# Create a kubescape policy
echo 'Create a kubescape policy'
export kubescape_policy_arn=$(aws iam create-policy \
--output yaml \
--query 'Policy.Arn' \
--policy-name kubescape \
--policy-document \
"$(cat <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "eks:DescribeCluster",
"Resource": "$cluster_arn"
}
]
}
EOF
)")
# Create Kubernetes Kubescape service account, and AWS IAM attachment role
echo 'Create Kubernetes Kubescape service account, and AWS IAM attachment role'
eksctl create iamserviceaccount \
--name $kubescape_serviceaccount \
--namespace $kubescape_namespace \
--cluster $cluster_name \
--attach-policy-arn $kubescape_policy_arn \
--approve \
--override-existing-serviceaccounts
# Install/Upgrade Kubescape chart
echo 'Install/Upgrade Kubescape chart'
helm upgrade --install armo armo-components/ -n armo-system --create-namespace --set clusterName=$cluster_name --set cloud_provider_engine=eks --set createKubescapeServiceAccount=false --set cloudRegion=$cluster_region

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# GCP
# Attach the Kubescape service account to a GCP service account with the get cluster permission
# Prerequisites:
# gcloud
# Workload Identity enabled on the cluster.
# Node pool with --workload-metadata=GKE_METADATA where the pod can run.
# CLUSTER_NAME and CLUSTER_REGION environment variables.
[ -z $CLUSTER_NAME ] && >&2 echo "Please set the CLUSTER_NAME environment variable" && exit 1
[ -z $CLUSTER_REGION ] && >&2 echo "Please set the CLUSTER_REGION environment variable" && exit 1
# Create GCP service account
gcloud iam service-accounts create kubescape --display-name=kubescape
# Set environment variables
echo 'Set environment variables'
export kubescape_namespace=armo-system
export kubescape_serviceaccount=armo-kubescape-service-account
# Get current GCP project
echo 'Get current GCP project'
export gcp_project=$(gcloud config get-value project)
sleep 5
# Get service account email
echo 'Get service account email'
export gcp_service_account=$(gcloud iam service-accounts list --filter="email ~ kubescape@" --format="value(email)")
# Create custome cluster.get role
echo 'Create custome cluster.get role'
export custom_role_name=$(gcloud iam roles create kubescape --project=$gcp_project --title='Armo kubernetes' --description='Allow clusters.get to Kubernetes armo service account' --permissions=container.clusters.get --stage=GA --format='value(name)')
# Attach policies to the service account
echo 'Attach policies to the service account'
gcloud --quiet projects add-iam-policy-binding $gcp_project --member serviceAccount:$gcp_service_account --role $custom_role_name >/dev/null
gcloud --quiet projects add-iam-policy-binding $gcp_project --member serviceAccount:$gcp_service_account --role roles/storage.objectViewer >/dev/null
# If there are missing permissions, use this role instead
# gcloud --quiet projects add-iam-policy-binding $gcp_project --member serviceAccount:$gcp_service_account --role roles/container.clusterViewer
# Bind the GCP kubescape service account to kubescape kubernetes service account
gcloud iam service-accounts add-iam-policy-binding $gcp_service_account --role roles/iam.workloadIdentityUser --member "serviceAccount:${gcp_project}.svc.id.goog[${kubescape_namespace}/${kubescape_serviceaccount}]"
# Install/Upgrade Kubescape chart
echo 'Install/Upgrade Kubescape chart'
helm upgrade --install armo armo-components/ -n armo-system --create-namespace --set cloud_provider_engine=gke --set gke_service_account=$gcp_service_account --set cloudRegion=$CLUSTER_REGION --set clusterName=$CLUSTER_NAME --set gkeProject=$gcp_project

11
go.mod
View File

@@ -3,10 +3,10 @@ module github.com/armosec/kubescape
go 1.17
require (
github.com/armosec/armoapi-go v0.0.40
github.com/armosec/k8s-interface v0.0.50
github.com/armosec/opa-utils v0.0.88
github.com/armosec/rbac-utils v0.0.9
github.com/armosec/armoapi-go v0.0.41
github.com/armosec/k8s-interface v0.0.56
github.com/armosec/opa-utils v0.0.99
github.com/armosec/rbac-utils v0.0.12
github.com/armosec/utils-go v0.0.3
github.com/briandowns/spinner v1.18.0
github.com/enescakir/emoji v1.0.0
@@ -19,6 +19,7 @@ require (
github.com/satori/go.uuid v1.2.0
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
go.uber.org/zap v1.19.1
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
@@ -35,6 +36,7 @@ require (
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/armosec/armo-interfaces v0.0.3 // indirect
github.com/armosec/utils-k8s-go v0.0.1 // indirect
github.com/aws/aws-sdk-go v1.41.11 // indirect
github.com/coreos/go-oidc v2.2.1+incompatible // indirect
@@ -76,7 +78,6 @@ require (
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect

17
go.sum
View File

@@ -83,20 +83,23 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armosec/armo-interfaces v0.0.3 h1:kG4mJIPgWBJvQFDDy8JzdqX3ASbyl8t32IuJYqB31Pk=
github.com/armosec/armo-interfaces v0.0.3/go.mod h1:7XYefhcBCFYoF5LflCZHWuUHu+JrSJbmzk0zoNv2WlU=
github.com/armosec/armoapi-go v0.0.2/go.mod h1:vIK17yoKbJRQyZXWWLe3AqfqCRITxW8qmSkApyq5xFs=
github.com/armosec/armoapi-go v0.0.23/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw=
github.com/armosec/armoapi-go v0.0.40 h1:KQRJXFqw95s6cV7HoGgw1x8qrRZ9eNVze//yQbo24Lk=
github.com/armosec/armoapi-go v0.0.40/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw=
github.com/armosec/armoapi-go v0.0.41 h1:iMkaCsME+zhE6vnCOMaqfqc0cp7pste8QFHojeGKfGg=
github.com/armosec/armoapi-go v0.0.41/go.mod h1:exk1O3rK6V+X8SSyxc06lwb0j9ILQuKAoIdz9hs6Ndw=
github.com/armosec/k8s-interface v0.0.8/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM=
github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU=
github.com/armosec/k8s-interface v0.0.50 h1:iLPGI0j85vwKANr9QDAnba4Efjg3DyIJg15jRJdvOnc=
github.com/armosec/k8s-interface v0.0.50/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU=
github.com/armosec/k8s-interface v0.0.56 h1:7dOgc3qZaI7ReLRZcJa2JZKk0rliyYi05l1vuHc6gcE=
github.com/armosec/k8s-interface v0.0.56/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU=
github.com/armosec/opa-utils v0.0.64/go.mod h1:6tQP8UDq2EvEfSqh8vrUdr/9QVSCG4sJfju1SXQOn4c=
github.com/armosec/opa-utils v0.0.88 h1:IxIml3w7l0HFqbb+XzKuXf+Pw78DHIxPwRIkgudKQRw=
github.com/armosec/opa-utils v0.0.88/go.mod h1:ZOXYVTtuyrV4TldcfbzgRqP6F9Drlf4hB0zr210OXgM=
github.com/armosec/opa-utils v0.0.99 h1:ZuoIPg6vbgO4J09xJZDO/yIRD59odwmK2Bm55uTvkU8=
github.com/armosec/opa-utils v0.0.99/go.mod h1:BNTjeianyXlflJMz3bZM0GimBWqmzirUf1whWR6Os04=
github.com/armosec/rbac-utils v0.0.1/go.mod h1:pQ8CBiij8kSKV7aeZm9FMvtZN28VgA7LZcYyTWimq40=
github.com/armosec/rbac-utils v0.0.9 h1:rIOWp4K7BELUNX32ktSjVbb8d/0SpH7W76W6Tf+8rzw=
github.com/armosec/rbac-utils v0.0.9/go.mod h1:Ex/IdGWhGv9HZq6Hs8N/ApzCKSIvpNe/ETqDfnuyah0=
github.com/armosec/rbac-utils v0.0.12 h1:uJpMGDyLAX129PrKHp6NPNB6lVRhE0OZIwV6ywzSDrs=
github.com/armosec/rbac-utils v0.0.12/go.mod h1:Ex/IdGWhGv9HZq6Hs8N/ApzCKSIvpNe/ETqDfnuyah0=
github.com/armosec/utils-go v0.0.2/go.mod h1:itWmRLzRdsnwjpEOomL0mBWGnVNNIxSjDAdyc+b0iUo=
github.com/armosec/utils-go v0.0.3 h1:uyQI676yRciQM0sSN9uPoqHkbspTxHO0kmzXhBeE/xU=
github.com/armosec/utils-go v0.0.3/go.mod h1:itWmRLzRdsnwjpEOomL0mBWGnVNNIxSjDAdyc+b0iUo=

View File

@@ -1,6 +1,4 @@
package hostsensorutils
const hostSensorYAML = `apiVersion: v1
apiVersion: v1
kind: Namespace
metadata:
labels:
@@ -62,4 +60,4 @@ spec:
name: host-filesystem
hostNetwork: true
hostPID: true
hostIPC: true`
hostIPC: true

View File

@@ -1,6 +1,7 @@
package hostsensorutils
import (
_ "embed"
"fmt"
"io"
"strings"
@@ -18,6 +19,11 @@ import (
coreapplyv1 "k8s.io/client-go/applyconfigurations/core/v1"
)
var (
//go:embed hostsensor.yaml
hostSensorYAML string
)
type HostSensorHandler struct {
HostSensorPort int32
HostSensorPodNames map[string]string //map from pod names to node names

View File

@@ -104,7 +104,22 @@ func (hsh *HostSensorHandler) GetLinuxSecurityHardeningStatus() ([]hostsensor.Ho
// return list of KubeletCommandLine
func (hsh *HostSensorHandler) GetKubeletCommandLine() ([]hostsensor.HostSensorDataEnvelope, error) {
// loop over pods and port-forward it to each of them
return hsh.sendAllPodsHTTPGETRequest("/kubeletCommandLine", "KubeletCommandLine")
resps, err := hsh.sendAllPodsHTTPGETRequest("/kubeletCommandLine", "KubeletCommandLine")
if err != nil {
return resps, err
}
for resp := range resps {
var data = make(map[string]interface{})
data["fullCommand"] = string(resps[resp].Data)
resBytesMarshal, err := json.Marshal(data)
// TODO catch error
if err == nil {
resps[resp].Data = json.RawMessage(resBytesMarshal)
}
}
return resps, nil
}
// return list of
@@ -122,7 +137,7 @@ func (hsh *HostSensorHandler) GetOsReleaseFile() ([]hostsensor.HostSensorDataEnv
// return list of
func (hsh *HostSensorHandler) GetKubeletConfigurations() ([]hostsensor.HostSensorDataEnvelope, error) {
// loop over pods and port-forward it to each of them
res, err := hsh.sendAllPodsHTTPGETRequest("/kubeletConfigurations", "KubeletConfigurations") // empty kind, will be overridden
res, err := hsh.sendAllPodsHTTPGETRequest("/kubeletConfigurations", "KubeletConfiguration") // empty kind, will be overridden
for resIdx := range res {
jsonBytes, err := yaml.YAMLToJSON(res[resIdx].Data)
if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/armosec/kubescape/cautils"
ksscore "github.com/armosec/kubescape/score"
"github.com/armosec/opa-utils/objectsenvelopes"
"github.com/armosec/opa-utils/reporthandling"
"github.com/armosec/opa-utils/reporthandling/apis"
@@ -59,10 +60,10 @@ func (opaHandler *OPAProcessorHandler) ProcessRulesListenner() {
opaSessionObj := <-*opaHandler.processedPolicy
opap := NewOPAProcessor(opaSessionObj, opaHandler.regoDependenciesData)
ConvertFrameworksToSummaryDetails(&opap.Report.SummaryDetails, opap.Frameworks)
policies := ConvertFrameworksToPolicies(opap.Frameworks, cautils.BuildNumber)
ConvertFrameworksToSummaryDetails(&opap.Report.SummaryDetails, opap.Frameworks, policies)
// process
if err := opap.Process(policies); err != nil {
// fmt.Println(err)
@@ -71,6 +72,9 @@ func (opaHandler *OPAProcessorHandler) ProcessRulesListenner() {
// edit results
opap.updateResults()
//TODO: review this location
scorewrapper := ksscore.NewScoreWrapper(opaSessionObj)
scorewrapper.Calculate(ksscore.EPostureReportV2)
// report
*opaHandler.reportResults <- opaSessionObj
}
@@ -197,6 +201,7 @@ func (opap *OPAProcessor) processRule(rule *reporthandling.PolicyRule) (map[stri
if r, k := resources[failedResources[j].GetID()]; k {
ruleResult = r
}
ruleResult.Status = apis.StatusFailed
for j := range ruleResponses[i].FailedPaths {
ruleResult.Paths = append(ruleResult.Paths, resourcesresults.Path{FailedPath: ruleResponses[i].FailedPaths[j]})

View File

@@ -71,7 +71,7 @@ func TestProcessResourcesResult(t *testing.T) {
opaSessionObj.Frameworks = frameworks
policies := ConvertFrameworksToPolicies(opaSessionObj.Frameworks, "")
ConvertFrameworksToSummaryDetails(&opaSessionObj.Report.SummaryDetails, opaSessionObj.Frameworks)
ConvertFrameworksToSummaryDetails(&opaSessionObj.Report.SummaryDetails, opaSessionObj.Frameworks, policies)
opaSessionObj.K8SResources = &k8sResources
opaSessionObj.AllResources[deployment.GetID()] = deployment

View File

@@ -70,6 +70,7 @@ func getAllSupportedObjects(k8sResources *cautils.K8SResources, allResources map
func getKubernetesObjects(k8sResources *cautils.K8SResources, allResources map[string]workloadinterface.IMetadata, match []reporthandling.RuleMatchObjects) []workloadinterface.IMetadata {
k8sObjects := []workloadinterface.IMetadata{}
for m := range match {
for _, groups := range match[m].APIGroups {
for _, version := range match[m].APIVersions {
@@ -91,9 +92,33 @@ func getKubernetesObjects(k8sResources *cautils.K8SResources, allResources map[s
}
}
return k8sObjects
return filterOutChildResources(k8sObjects, match)
}
// filterOutChildResources filter out child resources if the parent resource is in the list
func filterOutChildResources(objects []workloadinterface.IMetadata, match []reporthandling.RuleMatchObjects) []workloadinterface.IMetadata {
response := []workloadinterface.IMetadata{}
owners := []string{}
for m := range match {
for i := range match[m].Resources {
owners = append(owners, match[m].Resources[i])
}
}
for i := range objects {
if !k8sinterface.IsTypeWorkload(objects[i].GetObject()) {
response = append(response, objects[i])
continue
}
w := workloadinterface.NewWorkloadObj(objects[i].GetObject())
ownerReferences, err := w.GetOwnerReferences()
if err != nil || len(ownerReferences) == 0 {
response = append(response, w)
} else if !k8sinterface.IsStringInSlice(owners, ownerReferences[0].Kind) {
response = append(response, w)
}
}
return response
}
func getRuleDependencies() (map[string]string, error) {
modules := resources.LoadRegoModules()
if len(modules) == 0 {
@@ -176,6 +201,7 @@ func removeSecretData(workload workloadinterface.IWorkload) {
func removePodData(workload workloadinterface.IWorkload) {
workload.RemoveAnnotation("kubectl.kubernetes.io/last-applied-configuration")
workloadinterface.RemoveFromMap(workload.GetObject(), "metadata", "managedFields")
workloadinterface.RemoveFromMap(workload.GetObject(), "status")
containers, err := workload.GetContainers()
if err != nil || len(containers) == 0 {

View File

@@ -14,7 +14,7 @@ func ConvertFrameworksToPolicies(frameworks []reporthandling.Framework, version
}
// ConvertFrameworksToSummaryDetails initialize the summary details for the report object
func ConvertFrameworksToSummaryDetails(summaryDetails *reportsummary.SummaryDetails, frameworks []reporthandling.Framework) {
func ConvertFrameworksToSummaryDetails(summaryDetails *reportsummary.SummaryDetails, frameworks []reporthandling.Framework, policies *cautils.Policies) {
if summaryDetails.Controls == nil {
summaryDetails.Controls = make(map[string]reportsummary.ControlSummary)
}
@@ -22,17 +22,19 @@ func ConvertFrameworksToSummaryDetails(summaryDetails *reportsummary.SummaryDeta
controls := map[string]reportsummary.ControlSummary{}
for j := range frameworks[i].Controls {
id := frameworks[i].Controls[j].ControlID
c := reportsummary.ControlSummary{
Name: frameworks[i].Controls[j].Name,
ControlID: id,
ScoreFactor: frameworks[i].Controls[j].BaseScore,
Description: frameworks[i].Controls[j].Description,
Remediation: frameworks[i].Controls[j].Remediation,
if _, ok := policies.Controls[id]; ok {
c := reportsummary.ControlSummary{
Name: frameworks[i].Controls[j].Name,
ControlID: id,
ScoreFactor: frameworks[i].Controls[j].BaseScore,
Description: frameworks[i].Controls[j].Description,
Remediation: frameworks[i].Controls[j].Remediation,
}
controls[frameworks[i].Controls[j].ControlID] = c
summaryDetails.Controls[id] = c
}
controls[frameworks[i].Controls[j].ControlID] = c
summaryDetails.Controls[id] = c
}
if frameworks[i].Name != "" {
if cautils.StringInSlice(policies.Frameworks, frameworks[i].Name) != cautils.ValueNotFound {
summaryDetails.Frameworks = append(summaryDetails.Frameworks, reportsummary.FrameworkSummary{
Name: frameworks[i].Name,
Controls: controls,

View File

@@ -23,7 +23,8 @@ func TestInitializeSummaryDetails(t *testing.T) {
summaryDetails := reportsummary.SummaryDetails{}
frameworks := []reporthandling.Framework{*fw0, *fw1}
ConvertFrameworksToSummaryDetails(&summaryDetails, frameworks)
policies := ConvertFrameworksToPolicies([]reporthandling.Framework{*fw0, *fw1}, "")
ConvertFrameworksToSummaryDetails(&summaryDetails, frameworks, policies)
assert.Equal(t, 2, len(summaryDetails.Frameworks))
assert.Equal(t, 3, len(summaryDetails.Controls))
}

View File

@@ -2,6 +2,7 @@ package policyhandler
import (
"fmt"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/reporthandling"
@@ -15,19 +16,19 @@ func (policyHandler *PolicyHandler) getPolicies(notification *reporthandling.Pol
return err
}
if len(frameworks) == 0 {
return fmt.Errorf("failed to download policies, please ARMO team for more information")
return fmt.Errorf("failed to download policies: '%s'. Make sure the policy exist and you spelled it correctly. For more information, please feel free to contact ARMO team", strings.Join(policyIdentifierToSlice(notification.Rules), ","))
}
policiesAndResources.Frameworks = frameworks
// get exceptions
exceptionPolicies, err := policyHandler.getters.ExceptionsGetter.GetExceptions(cautils.CustomerGUID, cautils.ClusterName)
exceptionPolicies, err := policyHandler.getters.ExceptionsGetter.GetExceptions(cautils.ClusterName)
if err == nil {
policiesAndResources.Exceptions = exceptionPolicies
}
// get account configuration
controlsInputs, err := policyHandler.getters.ControlsInputsGetter.GetControlsInputs(cautils.CustomerGUID, cautils.ClusterName)
controlsInputs, err := policyHandler.getters.ControlsInputsGetter.GetControlsInputs(cautils.ClusterName)
if err == nil {
policiesAndResources.RegoInputData.PostureControlInputs = controlsInputs
}
@@ -70,3 +71,11 @@ func (policyHandler *PolicyHandler) getScanPolicies(notification *reporthandling
}
return frameworks, nil
}
func policyIdentifierToSlice(rules []reporthandling.PolicyIdentifier) []string {
s := []string{}
for i := range rules {
s = append(s, fmt.Sprintf("%s: %s", rules[i].Kind, rules[i].Name))
}
return s
}

View File

@@ -0,0 +1,104 @@
package resourcehandler
import (
"os"
"strings"
"github.com/armosec/k8s-interface/cloudsupport"
"github.com/armosec/k8s-interface/k8sinterface"
)
var (
KS_KUBE_CLUSTER_ENV_VAR = "KS_KUBE_CLUSTER"
KS_CLOUD_PROVIDER_ENV_VAR = "KS_CLOUD_PROVIDER"
KS_CLOUD_REGION_ENV_VAR = "KS_CLOUD_REGION"
KS_GKE_PROJECT_ENV_VAR = "KS_GKE_PROJECT"
)
type ICloudProvider interface {
getKubeCluster() string
getRegion(cluster string, provider string) (string, error)
getProject(cluster string, provider string) (string, error)
getKubeClusterName() string
}
func initCloudProvider() ICloudProvider {
switch getCloudProvider() {
case "gke", "gcp":
return NewGKEProviderContext()
case "eks", "aws":
return NewEKSProviderContext()
}
return NewEmptyCloudProvider()
}
func getCloudProvider() string {
var provider string
if isEnvVars() {
provider = getCloudProviderFromEnvVar()
} else {
provider = getCloudProviderFromContext()
}
return strings.ToLower(provider)
}
func getCloudProviderFromContext() string {
return cloudsupport.GetCloudProvider(getClusterFromContext())
}
func getClusterFromContext() string {
context := k8sinterface.GetCurrentContext()
if context == nil {
return ""
}
cluster := context.Cluster
if cluster != "" {
return cluster
}
return k8sinterface.GetClusterName()
}
func getCloudProviderFromEnvVar() string {
val, present := os.LookupEnv(KS_CLOUD_PROVIDER_ENV_VAR)
if present {
return val
}
return ""
}
func isEnvVars() bool {
_, present := os.LookupEnv(KS_KUBE_CLUSTER_ENV_VAR)
if !present {
return false
}
_, present = os.LookupEnv(KS_CLOUD_PROVIDER_ENV_VAR)
if !present {
return false
}
_, present = os.LookupEnv(KS_CLOUD_REGION_ENV_VAR)
return present
}
type EmptyCloudProvider struct {
}
func NewEmptyCloudProvider() *EmptyCloudProvider {
return &EmptyCloudProvider{}
}
func (emptyCloudProvider *EmptyCloudProvider) getKubeCluster() string {
return getClusterFromContext()
}
func (emptyCloudProvider *EmptyCloudProvider) getKubeClusterName() string {
return emptyCloudProvider.getKubeCluster()
}
func (emptyCloudProvider *EmptyCloudProvider) getRegion(cluster string, provider string) (string, error) {
return "", nil
}
func (emptyCloudProvider *EmptyCloudProvider) getProject(cluster string, provider string) (string, error) {
return "", nil
}

View File

@@ -0,0 +1,95 @@
package resourcehandler
import (
"fmt"
"os"
"strings"
"github.com/armosec/k8s-interface/k8sinterface"
)
type EKSProviderEnvVar struct {
}
func NewEKSProviderEnvVar() *EKSProviderEnvVar {
return &EKSProviderEnvVar{}
}
func (eksProviderEnvVar *EKSProviderEnvVar) getKubeClusterName() string {
return eksProviderEnvVar.getKubeCluster()
}
func (eksProviderEnvVar *EKSProviderEnvVar) getKubeCluster() string {
val, present := os.LookupEnv(KS_KUBE_CLUSTER_ENV_VAR)
if present {
return val
}
return ""
}
func (eksProviderEnvVar *EKSProviderEnvVar) getRegion(cluster string, provider string) (string, error) {
return eksProviderEnvVar.getRegionForEKS(cluster)
}
func (eksProviderEnvVar *EKSProviderEnvVar) getProject(cluster string, provider string) (string, error) {
return "", nil
}
func (eksProviderEnvVar *EKSProviderEnvVar) getRegionForEKS(cluster string) (string, error) {
region, present := os.LookupEnv(KS_CLOUD_REGION_ENV_VAR)
if present {
return region, nil
}
splittedClusterContext := strings.Split(cluster, ".")
if len(splittedClusterContext) < 2 {
return "", fmt.Errorf("error: failed to get region")
}
region = splittedClusterContext[1]
return region, nil
}
// ------------------------------------- EKSProviderContext -------------------------
type EKSProviderContext struct {
}
func NewEKSProviderContext() *EKSProviderContext {
return &EKSProviderContext{}
}
func (eksProviderContext *EKSProviderContext) getKubeClusterName() string {
cluster := k8sinterface.GetCurrentContext().Cluster
var splittedCluster []string
if cluster != "" {
splittedCluster = strings.Split(cluster, ".")
if len(splittedCluster) > 1 {
return splittedCluster[0]
}
}
splittedCluster = strings.Split(k8sinterface.GetClusterName(), ".")
if len(splittedCluster) > 1 {
return splittedCluster[0]
}
return ""
}
func (eksProviderContext *EKSProviderContext) getKubeCluster() string {
cluster := k8sinterface.GetCurrentContext().Cluster
if cluster != "" {
return cluster
}
return k8sinterface.GetClusterName()
}
func (eksProviderContext *EKSProviderContext) getRegion(cluster string, provider string) (string, error) {
splittedClusterContext := strings.Split(cluster, ".")
if len(splittedClusterContext) < 2 {
return "", fmt.Errorf("error: failed to get region")
}
region := splittedClusterContext[1]
return region, nil
}
func (eksProviderContext *EKSProviderContext) getProject(cluster string, provider string) (string, error) {
return "", nil
}

View File

@@ -213,7 +213,11 @@ func readYamlFile(yamlFile []byte) ([]workloadinterface.IMetadata, []error) {
}
if obj, ok := j.(map[string]interface{}); ok {
if o := objectsenvelopes.NewObject(obj); o != nil {
yamlObjs = append(yamlObjs, o)
if o.GetKind() == "List" {
yamlObjs = append(yamlObjs, handleListObject(o)...)
} else {
yamlObjs = append(yamlObjs, o)
}
}
} else {
errs = append(errs, fmt.Errorf("failed to convert yaml file to map[string]interface, file content: %v", j))
@@ -303,3 +307,20 @@ func getFileFormat(filePath string) FileFormat {
return FileFormat(filePath)
}
}
// handleListObject handle a List manifest
func handleListObject(obj workloadinterface.IMetadata) []workloadinterface.IMetadata {
yamlObjs := []workloadinterface.IMetadata{}
if i, ok := workloadinterface.InspectMap(obj.GetObject(), "items"); ok && i != nil {
if items, ok := i.([]interface{}); ok && items != nil {
for item := range items {
if m, ok := items[item].(map[string]interface{}); ok && m != nil {
if o := objectsenvelopes.NewObject(m); o != nil {
yamlObjs = append(yamlObjs, o)
}
}
}
}
}
return yamlObjs
}

View File

@@ -0,0 +1,125 @@
package resourcehandler
import (
"fmt"
"os"
"strings"
"github.com/armosec/k8s-interface/k8sinterface"
)
type GKEProviderEnvVar struct {
}
func NewGKEProviderEnvVar() *GKEProviderEnvVar {
return &GKEProviderEnvVar{}
}
func (gkeProvider *GKEProviderEnvVar) getKubeClusterName() string {
return gkeProvider.getKubeCluster()
}
func (gkeProvider *GKEProviderEnvVar) getKubeCluster() string {
val, present := os.LookupEnv(KS_KUBE_CLUSTER_ENV_VAR)
if present {
return val
}
return ""
}
func (gkeProvider *GKEProviderEnvVar) getRegion(cluster string, provider string) (string, error) {
return gkeProvider.getRegionForGKE(cluster)
}
func (gkeProvider *GKEProviderEnvVar) getProject(cluster string, provider string) (string, error) {
return gkeProvider.getProjectForGKE(cluster)
}
func (gkeProvider *GKEProviderEnvVar) getProjectForGKE(cluster string) (string, error) {
project, present := os.LookupEnv(KS_GKE_PROJECT_ENV_VAR)
if present {
return project, nil
}
parsedName := strings.Split(cluster, "_")
if len(parsedName) < 3 {
return "", fmt.Errorf("error: failed to parse cluster name")
}
project = parsedName[1]
return project, nil
}
func (gkeProvider *GKEProviderEnvVar) getRegionForGKE(cluster string) (string, error) {
region, present := os.LookupEnv(KS_CLOUD_REGION_ENV_VAR)
if present {
return region, nil
}
parsedName := strings.Split(cluster, "_")
if len(parsedName) < 3 {
return "", fmt.Errorf("error: failed to parse cluster name")
}
region = parsedName[2]
return region, nil
}
// ------------------------------ GKEProviderContext --------------------------------------------------------
type GKEProviderContext struct {
}
func NewGKEProviderContext() *GKEProviderContext {
return &GKEProviderContext{}
}
func (gkeProviderContext *GKEProviderContext) getKubeClusterName() string {
cluster := k8sinterface.GetCurrentContext().Cluster
parsedName := strings.Split(cluster, "_")
if len(parsedName) < 3 {
return ""
}
clusterName := parsedName[3]
if clusterName != "" {
return clusterName
}
cluster = k8sinterface.GetClusterName()
parsedName = strings.Split(cluster, "_")
if len(parsedName) < 3 {
return ""
}
clusterName = parsedName[3]
return clusterName
}
func (gkeProviderContext *GKEProviderContext) getKubeCluster() string {
cluster := k8sinterface.GetCurrentContext().Cluster
if cluster != "" {
return cluster
}
return k8sinterface.GetClusterName()
}
func (gkeProviderContext *GKEProviderContext) getRegion(cluster string, provider string) (string, error) {
return gkeProviderContext.getRegionForGKE(cluster)
}
func (gkeProviderContext *GKEProviderContext) getProject(cluster string, provider string) (string, error) {
return gkeProviderContext.getProjectForGKE(cluster)
}
func (gkeProviderContext *GKEProviderContext) getProjectForGKE(cluster string) (string, error) {
parsedName := strings.Split(cluster, "_")
if len(parsedName) < 3 {
return "", fmt.Errorf("error: failed to parse cluster name")
}
project := parsedName[1]
return project, nil
}
func (gkeProviderContext *GKEProviderContext) getRegionForGKE(cluster string) (string, error) {
parsedName := strings.Split(cluster, "_")
if len(parsedName) < 3 {
return "", fmt.Errorf("error: failed to parse cluster name")
}
region := parsedName[2]
return region, nil
}

View File

@@ -61,14 +61,14 @@ func (k8sHandler *K8sResourceHandler) GetResources(frameworks []reporthandling.F
return k8sResourcesMap, allResources, err
}
if err := k8sHandler.collectHostResources(allResources, k8sResourcesMap); err != nil {
return k8sResourcesMap, allResources, err
cautils.WarningDisplay(os.Stderr, "Warning: failed to collect host sensor resources\n")
}
if err := k8sHandler.collectRbacResources(allResources); err != nil {
cautils.WarningDisplay(os.Stdout, "Warning: failed to collect rbac resources\n")
cautils.WarningDisplay(os.Stderr, "Warning: failed to collect rbac resources\n")
}
if err := getCloudProviderDescription(allResources, k8sResourcesMap); err != nil {
cautils.WarningDisplay(os.Stdout, fmt.Sprintf("Warning: %v\n", err.Error()))
cautils.WarningDisplay(os.Stderr, fmt.Sprintf("Warning: %v\n", err.Error()))
}
cautils.StopSpinner()
@@ -105,7 +105,7 @@ func (k8sHandler *K8sResourceHandler) pullResources(k8sResources *cautils.K8SRes
continue
}
// store result as []map[string]interface{}
metaObjs := ConvertMapListToMeta(k8sinterface.ConvertUnstructuredSliceToMap(k8sinterface.FilterOutOwneredResources(result)))
metaObjs := ConvertMapListToMeta(k8sinterface.ConvertUnstructuredSliceToMap(result))
for i := range metaObjs {
allResources[metaObjs[i].GetID()] = metaObjs[i]
}
@@ -193,11 +193,21 @@ func (k8sHandler *K8sResourceHandler) collectRbacResources(allResources map[stri
}
func getCloudProviderDescription(allResources map[string]workloadinterface.IMetadata, k8sResourcesMap *cautils.K8SResources) error {
if cloudsupport.IsRunningInCloudProvider() {
wl, err := cloudsupport.GetDescriptiveInfoFromCloudProvider()
cloudProvider := initCloudProvider()
cluster := cloudProvider.getKubeCluster()
clusterName := cloudProvider.getKubeClusterName()
provider := getCloudProvider()
region, err := cloudProvider.getRegion(cluster, provider)
if err != nil {
return err
}
project, err := cloudProvider.getProject(cluster, provider)
if err != nil {
return err
}
if provider != "" {
wl, err := cloudsupport.GetDescriptiveInfoFromCloudProvider(clusterName, provider, region, project)
if err != nil {
cluster := k8sinterface.GetCurrentContext().Cluster
provider := cloudsupport.GetCloudProvider(cluster)
// Return error with useful info on how to configure credentials for getting cloud provider info
switch provider {
case "gke":

View File

@@ -20,9 +20,6 @@ type IPrinter interface {
ActionPrint(opaSessionObj *cautils.OPASessionObj)
SetWriter(outputFile string)
Score(score float32)
// FinalizeData convert 'opaSessionObj' data to be ready for printing/reporting
FinalizeData(opaSessionObj *cautils.OPASessionObj)
}
func GetWriter(outputFile string) *os.File {

View File

@@ -26,6 +26,8 @@ func (jsonPrinter *JsonPrinter) Score(score float32) {
}
func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
cautils.ReportV2ToV1(opaSessionObj)
var postureReportStr []byte
var err error
@@ -41,6 +43,3 @@ func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj
}
jsonPrinter.writer.Write(postureReportStr)
}
func (jsonPrinter *JsonPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) {
reportV2ToV1(opaSessionObj)
}

View File

@@ -26,11 +26,9 @@ func (junitPrinter *JunitPrinter) Score(score float32) {
fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score))
}
func (junitPrinter *JunitPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) {
reportV2ToV1(opaSessionObj)
}
func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
cautils.ReportV2ToV1(opaSessionObj)
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
if err != nil {
fmt.Println("Failed to convert posture report object!")

View File

@@ -31,6 +31,8 @@ func NewPrettyPrinter(verboseMode bool) *PrettyPrinter {
}
func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
cautils.ReportV2ToV1(opaSessionObj)
// score := calculatePostureScore(opaSessionObj.PostureReport)
failedResources := []string{}
warningResources := []string{}
@@ -67,18 +69,15 @@ func (prettyPrinter *PrettyPrinter) SetWriter(outputFile string) {
prettyPrinter.writer = printer.GetWriter(outputFile)
}
func (prettyPrinter *PrettyPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) {
reportV2ToV1(opaSessionObj)
}
func (prettyPrinter *PrettyPrinter) Score(score float32) {
}
func (prettyPrinter *PrettyPrinter) summarySetup(fr reporthandling.FrameworkReport, allResources map[string]workloadinterface.IMetadata) {
for _, cr := range fr.ControlReports {
if len(cr.RuleReports) == 0 {
continue
}
// if len(cr.RuleReports) == 0 {
// continue
// }
workloadsSummary := listResultSummary(cr.RuleReports, allResources)
var passedWorkloads map[string][]WorkloadSummary
@@ -248,7 +247,9 @@ func (prettyPrinter *PrettyPrinter) printSummaryTable(frameworksNames []string,
func (prettyPrinter *PrettyPrinter) printFramework(frameworksNames []string, frameworkScores []float32) {
if len(frameworksNames) == 1 {
cautils.InfoTextDisplay(prettyPrinter.writer, fmt.Sprintf("FRAMEWORK %s\n", frameworksNames[0]))
if frameworksNames[0] != "" {
cautils.InfoTextDisplay(prettyPrinter.writer, fmt.Sprintf("FRAMEWORK %s\n", frameworksNames[0]))
}
} else if len(frameworksNames) > 1 {
p := "FRAMEWORKS: "
for i := 0; i < len(frameworksNames)-1; i++ {

View File

@@ -1,6 +1,9 @@
package v1
import "github.com/armosec/kubescape/resultshandling/printer"
import (
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/kubescape/resultshandling/printer/v2/controlmapping"
)
var INDENT = " "
@@ -13,6 +16,6 @@ func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter {
case printer.PrometheusFormat:
return NewPrometheusPrinter(verboseMode)
default:
return NewPrettyPrinter(verboseMode)
return controlmapping.NewPrettyPrinter(verboseMode)
}
}

View File

@@ -29,10 +29,6 @@ func (prometheusPrinter *PrometheusPrinter) Score(score float32) {
fmt.Printf("\n# Overall risk-score (0- Excellent, 100- All failed)\nkubescape_score %d\n", int(score))
}
func (prometheusPrinter *PrometheusPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) {
reportV2ToV1(opaSessionObj)
}
func (printer *PrometheusPrinter) printResources(allResources map[string]workloadinterface.IMetadata, resourcesIDs *reporthandling.ResourcesIDs, frameworkName, controlName string) {
printer.printDetails(allResources, resourcesIDs.GetFailedResources(), frameworkName, controlName, "failed")
printer.printDetails(allResources, resourcesIDs.GetWarningResources(), frameworkName, controlName, "excluded")
@@ -90,6 +86,8 @@ func (printer *PrometheusPrinter) printReports(allResources map[string]workloadi
}
func (printer *PrometheusPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
cautils.ReportV2ToV1(opaSessionObj)
err := printer.printReports(opaSessionObj.AllResources, opaSessionObj.PostureReport.FrameworkReports)
if err != nil {
fmt.Println(err)

View File

@@ -22,7 +22,7 @@ func groupByNamespaceOrKind(resources []WorkloadSummary, status func(workloadSum
case workloadinterface.TypeWorkloadObject:
ns := ""
if resources[i].resource.GetNamespace() != "" {
ns = "Namescape " + resources[i].resource.GetNamespace()
ns = "Namespace " + resources[i].resource.GetNamespace()
}
if r, ok := mapResources[ns]; ok {
r = append(r, resources[i])

View File

@@ -0,0 +1,244 @@
package controlmapping
import (
"fmt"
"os"
"sort"
"strings"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/opa-utils/objectsenvelopes"
"github.com/armosec/opa-utils/reporthandling/apis"
"github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary"
"github.com/enescakir/emoji"
"github.com/olekukonko/tablewriter"
)
type PrettyPrinter struct {
writer *os.File
verboseMode bool
sortedControlNames []string
}
func NewPrettyPrinter(verboseMode bool) *PrettyPrinter {
return &PrettyPrinter{
verboseMode: verboseMode,
}
}
func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
prettyPrinter.sortedControlNames = getSortedControlsNames(opaSessionObj.Report.SummaryDetails.Controls) // ListControls().All())
prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources)
prettyPrinter.printSummaryTable(&opaSessionObj.Report.SummaryDetails)
}
func (prettyPrinter *PrettyPrinter) SetWriter(outputFile string) {
prettyPrinter.writer = printer.GetWriter(outputFile)
}
func (prettyPrinter *PrettyPrinter) Score(score float32) {
}
func (prettyPrinter *PrettyPrinter) printResults(controls *reportsummary.ControlSummaries, allResources map[string]workloadinterface.IMetadata) {
for i := 0; i < len(prettyPrinter.sortedControlNames); i++ {
controlSummary := controls.GetControl(reportsummary.EControlCriteriaName, prettyPrinter.sortedControlNames[i]) // summaryDetails.Controls ListControls().All() Controls.GetControl(ca)
prettyPrinter.printTitle(controlSummary)
prettyPrinter.printResources(controlSummary, allResources)
if controlSummary.GetStatus().IsSkipped() {
prettyPrinter.printSummary(prettyPrinter.sortedControlNames[i], controlSummary)
}
}
}
func (prettyPrinter *PrettyPrinter) printSummary(controlName string, controlSummary reportsummary.IControlSummary) {
if controlSummary.GetStatus().IsSkipped() {
return
}
cautils.SimpleDisplay(prettyPrinter.writer, "Summary - ")
cautils.SuccessDisplay(prettyPrinter.writer, "Passed:%v ", controlSummary.NumberOfResources().Passed())
cautils.WarningDisplay(prettyPrinter.writer, "Excluded:%v ", controlSummary.NumberOfResources().Excluded())
cautils.FailureDisplay(prettyPrinter.writer, "Failed:%v ", controlSummary.NumberOfResources().Failed())
cautils.InfoDisplay(prettyPrinter.writer, "Total:%v\n", controlSummary.NumberOfResources().All())
if controlSummary.GetStatus().IsFailed() {
cautils.DescriptionDisplay(prettyPrinter.writer, "Remediation: %v\n", controlSummary.GetRemediation())
}
cautils.DescriptionDisplay(prettyPrinter.writer, "\n")
}
func (prettyPrinter *PrettyPrinter) printTitle(controlSummary reportsummary.IControlSummary) {
cautils.InfoDisplay(prettyPrinter.writer, "[control: %s - %s] ", controlSummary.GetName(), getControlURL(controlSummary.GetID()))
switch controlSummary.GetStatus().Status() {
case apis.StatusSkipped:
cautils.InfoDisplay(prettyPrinter.writer, "skipped %v\n", emoji.ConfusedFace)
case apis.StatusFailed:
cautils.FailureDisplay(prettyPrinter.writer, "failed %v\n", emoji.SadButRelievedFace)
case apis.StatusExcluded:
cautils.WarningDisplay(prettyPrinter.writer, "excluded %v\n", emoji.NeutralFace)
default:
cautils.SuccessDisplay(prettyPrinter.writer, "passed %v\n", emoji.ThumbsUp)
}
cautils.DescriptionDisplay(prettyPrinter.writer, "Description: %s\n", controlSummary.GetDescription())
}
func (prettyPrinter *PrettyPrinter) printResources(controlSummary reportsummary.IControlSummary, allResources map[string]workloadinterface.IMetadata) {
workloadsSummary := listResultSummary(controlSummary, allResources)
failedWorkloads := groupByNamespaceOrKind(workloadsSummary, workloadSummaryFailed)
excludedWorkloads := groupByNamespaceOrKind(workloadsSummary, workloadSummaryExclude)
var passedWorkloads map[string][]WorkloadSummary
if prettyPrinter.verboseMode {
passedWorkloads = groupByNamespaceOrKind(workloadsSummary, workloadSummaryPassed)
}
if len(failedWorkloads) > 0 {
cautils.FailureDisplay(prettyPrinter.writer, "Failed:\n")
prettyPrinter.printGroupedResources(failedWorkloads)
}
if len(excludedWorkloads) > 0 {
cautils.WarningDisplay(prettyPrinter.writer, "Excluded:\n")
prettyPrinter.printGroupedResources(excludedWorkloads)
}
if len(passedWorkloads) > 0 {
cautils.SuccessDisplay(prettyPrinter.writer, "Passed:\n")
prettyPrinter.printGroupedResources(passedWorkloads)
}
}
func (prettyPrinter *PrettyPrinter) printGroupedResources(workloads map[string][]WorkloadSummary) {
indent := " "
for title, rsc := range workloads {
prettyPrinter.printGroupedResource(indent, title, rsc)
}
}
func (prettyPrinter *PrettyPrinter) printGroupedResource(indent string, title string, rsc []WorkloadSummary) {
preIndent := indent
if title != "" {
cautils.SimpleDisplay(prettyPrinter.writer, "%s%s\n", indent, title)
indent += indent
}
resources := []string{}
for r := range rsc {
relatedObjectsStr := generateRelatedObjectsStr(rsc[r]) // TODO -
resources = append(resources, fmt.Sprintf("%s%s - %s %s", indent, rsc[r].resource.GetKind(), rsc[r].resource.GetName(), relatedObjectsStr))
}
sort.Strings(resources)
for i := range resources {
cautils.SimpleDisplay(prettyPrinter.writer, resources[i]+"\n")
}
indent = preIndent
}
func generateRelatedObjectsStr(workload WorkloadSummary) string {
relatedStr := ""
if workload.resource.GetObjectType() == workloadinterface.TypeWorkloadObject {
relatedObjects := objectsenvelopes.NewRegoResponseVectorObject(workload.resource.GetObject()).GetRelatedObjects()
for i, related := range relatedObjects {
if ns := related.GetNamespace(); i == 0 && ns != "" {
relatedStr += fmt.Sprintf("Namespace - %s, ", ns)
}
relatedStr += fmt.Sprintf("%s - %s, ", related.GetKind(), related.GetName())
}
}
if relatedStr != "" {
relatedStr = fmt.Sprintf(" [%s]", relatedStr[:len(relatedStr)-2])
}
return relatedStr
}
func generateRow(controlSummary reportsummary.IControlSummary) []string {
row := []string{controlSummary.GetName()}
row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed()))
row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Excluded()))
row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().All()))
if !controlSummary.GetStatus().IsSkipped() {
row = append(row, fmt.Sprintf("%d", int(controlSummary.GetScore()))+"%")
} else {
row = append(row, "skipped")
}
return row
}
func generateHeader() []string {
return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% risk-score"}
}
func generateFooter(summaryDetails *reportsummary.SummaryDetails) []string {
// Control name | # failed resources | all resources | % success
row := []string{}
row = append(row, "Resource Summary") //fmt.Sprintf(""%d", numControlers"))
row = append(row, fmt.Sprintf("%d", summaryDetails.NumberOfResources().Failed()))
row = append(row, fmt.Sprintf("%d", summaryDetails.NumberOfResources().Excluded()))
row = append(row, fmt.Sprintf("%d", summaryDetails.NumberOfResources().All()))
row = append(row, fmt.Sprintf("%.2f%s", summaryDetails.Score, "%"))
return row
}
func (prettyPrinter *PrettyPrinter) printSummaryTable(summaryDetails *reportsummary.SummaryDetails) {
// For control scan framework will be nil
prettyPrinter.printFramework(summaryDetails.ListFrameworks().All())
summaryTable := tablewriter.NewWriter(prettyPrinter.writer)
summaryTable.SetAutoWrapText(false)
summaryTable.SetHeader(generateHeader())
summaryTable.SetHeaderLine(true)
alignments := []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER}
summaryTable.SetColumnAlignment(alignments)
for i := 0; i < len(prettyPrinter.sortedControlNames); i++ {
summaryTable.Append(generateRow(summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaName, prettyPrinter.sortedControlNames[i])))
}
summaryTable.SetFooter(generateFooter(summaryDetails))
// summaryTable.SetFooter(generateFooter())
summaryTable.Render()
}
func (prettyPrinter *PrettyPrinter) printFramework(frameworks []reportsummary.IPolicies) {
if len(frameworks) == 1 {
if frameworks[0].GetName() != "" {
cautils.InfoTextDisplay(prettyPrinter.writer, fmt.Sprintf("FRAMEWORK %s\n", frameworks[0].GetName()))
}
} else if len(frameworks) > 1 {
p := "FRAMEWORKS: "
i := 0
for ; i < len(frameworks)-1; i++ {
p += fmt.Sprintf("%s (risk: %.2f), ", frameworks[i].GetName(), frameworks[i].GetScore())
}
p += fmt.Sprintf("%s (risk: %.2f)\n", frameworks[i].GetName(), frameworks[i].GetScore())
cautils.InfoTextDisplay(prettyPrinter.writer, p)
}
}
func getSortedControlsNames(controls reportsummary.ControlSummaries) []string {
controlNames := make([]string, 0, len(controls))
for k := range controls {
c := controls[k]
controlNames = append(controlNames, c.GetName())
}
sort.Strings(controlNames)
return controlNames
}
// func getSortedControlsNames(controls []reportsummary.IPolicies) []string {
// controlNames := make([]string, 0, len(controls))
// for k := range controls {
// controlNames = append(controlNames, controls[k].Get())
// }
// sort.Strings(controlNames)
// return controlNames
// }
func getControlURL(controlID string) string {
return fmt.Sprintf("https://hub.armo.cloud/docs/%s", strings.ToLower(controlID))
}

View File

@@ -0,0 +1,101 @@
package controlmapping
import (
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/opa-utils/objectsenvelopes"
"github.com/armosec/opa-utils/reporthandling/apis"
"github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary"
)
type WorkloadSummary struct {
resource workloadinterface.IMetadata
status apis.ScanningStatus
}
func workloadSummaryFailed(workloadSummary *WorkloadSummary) bool {
return workloadSummary.status == apis.StatusFailed
}
func workloadSummaryExclude(workloadSummary *WorkloadSummary) bool {
return workloadSummary.status == apis.StatusExcluded
}
func workloadSummaryPassed(workloadSummary *WorkloadSummary) bool {
return workloadSummary.status == apis.StatusPassed
}
// Group workloads by namespace - return {"namespace": <[]WorkloadSummary>}
func groupByNamespaceOrKind(resources []WorkloadSummary, status func(workloadSummary *WorkloadSummary) bool) map[string][]WorkloadSummary {
mapResources := make(map[string][]WorkloadSummary)
for i := range resources {
if !status(&resources[i]) {
continue
}
t := resources[i].resource.GetObjectType()
if t == objectsenvelopes.TypeRegoResponseVectorObject && !isKindToBeGrouped(resources[i].resource.GetKind()) {
t = workloadinterface.TypeWorkloadObject
}
switch t { // TODO - find a better way to defind the groups
case workloadinterface.TypeWorkloadObject:
ns := ""
if resources[i].resource.GetNamespace() != "" {
ns = "Namespace " + resources[i].resource.GetNamespace()
}
if r, ok := mapResources[ns]; ok {
r = append(r, resources[i])
mapResources[ns] = r
} else {
mapResources[ns] = []WorkloadSummary{resources[i]}
}
case objectsenvelopes.TypeRegoResponseVectorObject:
group := resources[i].resource.GetKind() + "s"
if r, ok := mapResources[group]; ok {
r = append(r, resources[i])
mapResources[group] = r
} else {
mapResources[group] = []WorkloadSummary{resources[i]}
}
default:
group, _ := k8sinterface.SplitApiVersion(resources[i].resource.GetApiVersion())
if r, ok := mapResources[group]; ok {
r = append(r, resources[i])
mapResources[group] = r
} else {
mapResources[group] = []WorkloadSummary{resources[i]}
}
}
}
return mapResources
}
func isKindToBeGrouped(kind string) bool {
if kind == "Group" || kind == "User" {
return true
}
return false
}
func listResultSummary(controlSummary reportsummary.IControlSummary, allResources map[string]workloadinterface.IMetadata) []WorkloadSummary {
workloadsSummary := []WorkloadSummary{}
workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, controlSummary.ListResourcesIDs().Failed(), apis.StatusFailed)...)
workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, controlSummary.ListResourcesIDs().Excluded(), apis.StatusExcluded)...)
workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, controlSummary.ListResourcesIDs().Passed(), apis.StatusPassed)...)
return workloadsSummary
}
func newListWorkloadsSummary(allResources map[string]workloadinterface.IMetadata, resourcesIDs []string, status apis.ScanningStatus) []WorkloadSummary {
workloadsSummary := []WorkloadSummary{}
for _, i := range resourcesIDs {
if r, ok := allResources[i]; ok {
workloadsSummary = append(workloadsSummary, WorkloadSummary{
resource: r,
status: status,
})
}
}
return workloadsSummary
}

View File

@@ -1,18 +1,21 @@
package v2
import "github.com/armosec/kubescape/resultshandling/printer"
import (
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/kubescape/resultshandling/printer/v2/resourcemapping"
)
var INDENT = " "
func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter {
switch printFormat {
case printer.JsonFormat:
return NewJsonPrinter()
return resourcemapping.NewJsonPrinter()
case printer.JunitResultFormat:
return NewJunitPrinter()
// case printer.PrometheusFormat:
// return NewPrometheusPrinter(verboseMode)
default:
return NewPrettyPrinter(verboseMode)
return resourcemapping.NewPrettyPrinter(verboseMode)
}
}

View File

@@ -1,4 +1,4 @@
package v2
package resourcemapping
import (
"encoding/json"
@@ -37,5 +37,5 @@ func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj
}
func (jsonPrinter *JsonPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) {
finalizeReport(opaSessionObj)
// finalizeReport(opaSessionObj)
}

View File

@@ -1,4 +1,4 @@
package v2
package resourcemapping
import (
"fmt"
@@ -89,7 +89,7 @@ func (prettyPrinter *PrettyPrinter) SetWriter(outputFile string) {
}
func (prettyPrinter *PrettyPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) {
finalizeReport(opaSessionObj)
// finalizeReport(opaSessionObj)
}
func (prettyPrinter *PrettyPrinter) Score(score float32) {
}

View File

@@ -3,8 +3,8 @@ package v2
import (
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/reporthandling"
"github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults"
reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2"
)
// finalizeV2Report finalize the results objects by copying data from map to lists
@@ -16,7 +16,7 @@ func finalizeReport(opaSessionObj *cautils.OPASessionObj) {
}
if len(opaSessionObj.Report.Resources) == 0 {
opaSessionObj.Report.Resources = make([]reporthandlingv2.Resource, len(opaSessionObj.AllResources))
opaSessionObj.Report.Resources = make([]reporthandling.Resource, len(opaSessionObj.AllResources))
finalizeResources(opaSessionObj.Report.Resources, opaSessionObj.AllResources)
opaSessionObj.AllResources = nil
}
@@ -30,13 +30,15 @@ func finalizeResults(results []resourcesresults.Result, resourcesResult map[stri
}
}
func finalizeResources(resources []reporthandlingv2.Resource, allResources map[string]workloadinterface.IMetadata) {
func finalizeResources(resources []reporthandling.Resource, allResources map[string]workloadinterface.IMetadata) {
index := 0
for resourceID := range allResources {
resources[index] = reporthandlingv2.Resource{
ResourceID: resourceID,
Object: allResources[resourceID],
if obj, ok := allResources[resourceID]; ok {
r := *reporthandling.NewResource(obj.GetObject())
r.ResourceID = resourceID
resources[index] = r
}
index++
}
}

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"os"
"strings"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils"
@@ -23,6 +24,7 @@ type ReportEventReceiver struct {
eventReceiverURL *url.URL
token string
customerAdminEMail string
message string
}
func NewReportEventReceiver(tenantConfig *cautils.ConfigObj) *ReportEventReceiver {
@@ -36,16 +38,26 @@ func NewReportEventReceiver(tenantConfig *cautils.ConfigObj) *ReportEventReceive
}
func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error {
if report.customerGUID == "" || report.clusterName == "" {
return fmt.Errorf("missing accout ID or cluster name. AccountID: '%s', Cluster name: '%s'", report.customerGUID, report.clusterName)
if opaSessionObj.PostureReport == nil && opaSessionObj.Report != nil {
cautils.ReportV2ToV1(opaSessionObj)
}
if report.customerGUID == "" {
report.message = "WARNING: Failed to publish results. Reason: Unknown accout ID. Run kubescape with the '--account <account ID>' flag. Contact ARMO team for more details"
return nil
}
if report.clusterName == "" {
report.message = "WARNING: Failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS"
return nil
}
opaSessionObj.PostureReport.ReportID = uuid.NewV4().String()
opaSessionObj.PostureReport.CustomerGUID = report.clusterName
opaSessionObj.PostureReport.ClusterName = report.customerGUID
opaSessionObj.PostureReport.CustomerGUID = report.customerGUID
opaSessionObj.PostureReport.ClusterName = report.clusterName
if err := report.prepareReport(opaSessionObj.PostureReport, opaSessionObj.AllResources); err != nil {
return err
report.message = err.Error()
return nil
}
return nil
}
@@ -74,6 +86,8 @@ func (report *ReportEventReceiver) prepareReport(postureReport *reporthandling.P
if err := report.sendResources(host, postureReport, allResources); err != nil {
return err
}
report.generateMessage()
return nil
}
@@ -112,14 +126,16 @@ func (report *ReportEventReceiver) sendReport(host string, postureReport *report
if err != nil {
return fmt.Errorf("in 'sendReport' failed to json.Marshal, reason: %v", err)
}
// fmt.Printf("\n\n%s\n\n", reqBody)
msg, err := getter.HttpPost(report.httpClient, host, nil, reqBody)
if err != nil {
return fmt.Errorf("%s, %v:%s", host, err, msg)
}
return err
return nil
}
func (report *ReportEventReceiver) DisplayReportURL() {
func (report *ReportEventReceiver) generateMessage() {
message := "You can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more by registering here:"
u := url.URL{}
@@ -127,7 +143,7 @@ func (report *ReportEventReceiver) DisplayReportURL() {
u.Host = getter.GetArmoAPIConnector().GetFrontendURL()
if report.customerAdminEMail != "" {
cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s %s/risk/%s\n(Account: %s)\n\n", message, u.String(), report.clusterName, report.customerGUID))
report.message = fmt.Sprintf("%s %s/risk/%s\n(Account: %s)", message, u.String(), report.clusterName, maskID(report.customerGUID))
return
}
u.Path = "account/sign-up"
@@ -136,5 +152,27 @@ func (report *ReportEventReceiver) DisplayReportURL() {
q.Add("customerGUID", report.customerGUID)
u.RawQuery = q.Encode()
cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s %s\n\n", message, u.String()))
report.message = fmt.Sprintf("%s %s", message, u.String())
}
func (report *ReportEventReceiver) DisplayReportURL() {
cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s\n\n", report.message))
}
func maskID(id string) string {
sep := "-"
splitted := strings.Split(id, sep)
if len(splitted) != 5 {
return ""
}
str := splitted[0][:4]
splitted[0] = splitted[0][4:]
for i := range splitted {
for j := 0; j < len(splitted[i]); j++ {
str += "X"
}
str += sep
}
return strings.TrimSuffix(str, sep)
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/armosec/kubescape/cautils/getter"
uuid "github.com/satori/go.uuid"
"github.com/armosec/opa-utils/reporthandling"
"github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults"
reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2"
)
@@ -24,6 +25,7 @@ type ReportEventReceiver struct {
eventReceiverURL *url.URL
token string
customerAdminEMail string
message string
}
func NewReportEventReceiver(tenantConfig *cautils.ConfigObj) *ReportEventReceiver {
@@ -37,16 +39,24 @@ func NewReportEventReceiver(tenantConfig *cautils.ConfigObj) *ReportEventReceive
}
func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error {
finalizeReport(opaSessionObj)
if report.customerGUID == "" || report.clusterName == "" {
return fmt.Errorf("missing accout ID or cluster name. AccountID: '%s', Cluster name: '%s'", report.customerGUID, report.clusterName)
if report.customerGUID == "" {
report.message = "WARNING: Failed to publish results. Reason: Unknown accout ID. Run kubescape with the '--account <account ID>' flag. Contact ARMO team for more details"
return nil
}
if report.clusterName == "" {
report.message = "WARNING: Failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS"
return nil
}
opaSessionObj.Report.ReportID = uuid.NewV4().String()
opaSessionObj.Report.CustomerGUID = report.clusterName
opaSessionObj.Report.ClusterName = report.customerGUID
opaSessionObj.Report.CustomerGUID = report.customerGUID
opaSessionObj.Report.ClusterName = report.clusterName
if err := report.prepareReport(opaSessionObj.Report); err != nil {
return err
report.message = err.Error()
} else {
report.generateMessage()
}
return nil
}
@@ -66,24 +76,23 @@ func (report *ReportEventReceiver) prepareReport(postureReport *reporthandlingv2
cautils.StartSpinner()
defer cautils.StopSpinner()
// send framework results
if err := report.sendReport(host, postureReport); err != nil {
return err
}
reportCounter := 0
// send resources
if err := report.sendResults(host, postureReport); err != nil {
if err := report.sendResources(host, postureReport, &reportCounter, false); err != nil {
return err
}
// reportCounter++
// // send results
// if err := report.sendResults(host, postureReport, &reportCounter, true); err != nil {
// return err
// }
// send resources
if err := report.sendResources(host, postureReport); err != nil {
return err
}
return nil
}
func (report *ReportEventReceiver) sendResources(host string, postureReport *reporthandlingv2.PostureReport) error {
func (report *ReportEventReceiver) sendResources(host string, postureReport *reporthandlingv2.PostureReport, reportCounter *int, isLastReport bool) error {
splittedPostureReport := setSubReport(postureReport)
counter := 0
@@ -96,12 +105,14 @@ func (report *ReportEventReceiver) sendResources(host string, postureReport *rep
if counter+len(r) >= MAX_REPORT_SIZE && len(splittedPostureReport.Resources) > 0 {
// send report
if err := report.sendReport(host, splittedPostureReport); err != nil {
if err := report.sendReport(host, splittedPostureReport, *reportCounter, false); err != nil {
return err
}
*reportCounter++
// delete resources
splittedPostureReport.Resources = []reporthandlingv2.Resource{}
splittedPostureReport.Resources = []reporthandling.Resource{}
splittedPostureReport.Results = []resourcesresults.Result{}
// restart counter
counter = 0
@@ -111,28 +122,23 @@ func (report *ReportEventReceiver) sendResources(host string, postureReport *rep
splittedPostureReport.Resources = append(splittedPostureReport.Resources, v)
}
return report.sendReport(host, splittedPostureReport)
}
func (report *ReportEventReceiver) sendResults(host string, postureReport *reporthandlingv2.PostureReport) error {
splittedPostureReport := setSubReport(postureReport)
counter := 0
for _, v := range postureReport.Results {
r, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("failed to unmarshal resource '%s', reason: %v", v.GetResourceID(), err)
}
if counter+len(r) >= MAX_REPORT_SIZE && len(splittedPostureReport.Resources) > 0 {
if counter+len(r) >= MAX_REPORT_SIZE && len(splittedPostureReport.Results) > 0 {
// send report
if err := report.sendReport(host, splittedPostureReport); err != nil {
if err := report.sendReport(host, splittedPostureReport, *reportCounter, false); err != nil {
return err
}
*reportCounter++
// delete results
splittedPostureReport.Results = []resourcesresults.Result{}
splittedPostureReport.Resources = []reporthandling.Resource{}
// restart counter
counter = 0
@@ -142,12 +148,46 @@ func (report *ReportEventReceiver) sendResults(host string, postureReport *repor
splittedPostureReport.Results = append(splittedPostureReport.Results, v)
}
return report.sendReport(host, splittedPostureReport)
return report.sendReport(host, splittedPostureReport, *reportCounter, true)
}
func (report *ReportEventReceiver) sendReport(host string, postureReport *reporthandlingv2.PostureReport) error {
splittedPostureReport := setSubReport(postureReport)
splittedPostureReport.SummaryDetails = postureReport.SummaryDetails
// func (report *ReportEventReceiver) sendResults(host string, postureReport *reporthandlingv2.PostureReport, reportCounter *int, isLastReport bool) error {
// splittedPostureReport := setSubReport(postureReport)
// counter := 0
// for _, v := range postureReport.Results {
// r, err := json.Marshal(v)
// if err != nil {
// return fmt.Errorf("failed to unmarshal resource '%s', reason: %v", v.GetResourceID(), err)
// }
// if counter+len(r) >= MAX_REPORT_SIZE && len(splittedPostureReport.Resources) > 0 {
// // send report
// if err := report.sendReport(host, splittedPostureReport, *reportCounter, false); err != nil {
// return err
// }
// *reportCounter++
// // delete results
// splittedPostureReport.Results = []resourcesresults.Result{}
// // restart counter
// counter = 0
// }
// counter += len(r)
// splittedPostureReport.Results = append(splittedPostureReport.Results, v)
// }
// return report.sendReport(host, splittedPostureReport, *reportCounter, isLastReport)
// }
func (report *ReportEventReceiver) sendReport(host string, postureReport *reporthandlingv2.PostureReport, counter int, isLastReport bool) error {
postureReport.PaginationInfo = reporthandlingv2.PaginationMarks{
ReportNumber: counter,
IsLastReport: isLastReport,
}
reqBody, err := json.Marshal(postureReport)
if err != nil {
return fmt.Errorf("in 'sendReport' failed to json.Marshal, reason: %v", err)
@@ -159,7 +199,7 @@ func (report *ReportEventReceiver) sendReport(host string, postureReport *report
return err
}
func (report *ReportEventReceiver) DisplayReportURL() {
func (report *ReportEventReceiver) generateMessage() {
message := "You can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more by registering here:"
u := url.URL{}
@@ -167,7 +207,7 @@ func (report *ReportEventReceiver) DisplayReportURL() {
u.Host = getter.GetArmoAPIConnector().GetFrontendURL()
if report.customerAdminEMail != "" {
cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s %s/risk/%s\n(Account: %s)\n\n", message, u.String(), report.clusterName, report.customerGUID))
report.message = fmt.Sprintf("%s %s/risk/%s\n(Account: %s)", message, u.String(), report.clusterName, maskID(report.customerGUID))
return
}
u.Path = "account/sign-up"
@@ -176,5 +216,9 @@ func (report *ReportEventReceiver) DisplayReportURL() {
q.Add("customerGUID", report.customerGUID)
u.RawQuery = q.Encode()
cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s %s\n\n", message, u.String()))
report.message = fmt.Sprintf("%s %s", message, u.String())
}
func (report *ReportEventReceiver) DisplayReportURL() {
cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s\n\n", report.message))
}

View File

@@ -15,7 +15,8 @@ func (report *ReportEventReceiver) initEventReceiverURL() {
urlObj.Scheme = "https"
urlObj.Host = getter.GetArmoAPIConnector().GetReportReceiverURL()
urlObj.Path = "/k8s/postureReport"
urlObj.Path = "/k8s/v2/postureReport"
q := urlObj.Query()
q.Add("customerGUID", uuid.FromStringOrNil(report.customerGUID).String())
q.Add("clusterName", report.clusterName)
@@ -27,7 +28,7 @@ func (report *ReportEventReceiver) initEventReceiverURL() {
func hostToString(host *url.URL, reportID string) string {
q := host.Query()
q.Add("reportID", reportID) // TODO - do we add the reportID?
q.Add("reportGUID", reportID) // TODO - do we add the reportID?
host.RawQuery = q.Encode()
return host.String()
}
@@ -38,6 +39,11 @@ func setSubReport(postureReport *reporthandlingv2.PostureReport) *reporthandling
ClusterName: postureReport.ClusterName,
ReportID: postureReport.ReportID,
ReportGenerationTime: postureReport.ReportGenerationTime,
SummaryDetails: postureReport.SummaryDetails,
Attributes: postureReport.Attributes,
ClusterCloudProvider: postureReport.ClusterCloudProvider,
JobID: postureReport.JobID,
ClusterAPIServerInfo: postureReport.ClusterAPIServerInfo,
}
}
func iMetaToResource(obj workloadinterface.IMetadata) *reporthandling.Resource {

View File

@@ -9,10 +9,10 @@ func TestHostToString(t *testing.T) {
host := url.URL{
Scheme: "https",
Host: "report.eudev3.cyberarmorsoft.com",
Path: "k8srestapi/v1/postureReport",
Path: "k8srestapi/v2/postureReport",
RawQuery: "cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af",
}
expectedHost := "https://report.eudev3.cyberarmorsoft.com/k8srestapi/v1/postureReport?cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af&reportID=ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41"
expectedHost := "https://report.eudev3.cyberarmorsoft.com/k8srestapi/v2/postureReport?cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af&reportGUID=ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41"
receivedHost := hostToString(&host, "ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41")
if receivedHost != expectedHost {
t.Errorf("%s != %s", receivedHost, expectedHost)

View File

@@ -0,0 +1,65 @@
package v2
import (
"strings"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/reporthandling"
"github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults"
)
// finalizeV2Report finalize the results objects by copying data from map to lists
func finalizeReport(opaSessionObj *cautils.OPASessionObj) {
opaSessionObj.PostureReport = nil
if len(opaSessionObj.Report.Results) == 0 {
opaSessionObj.Report.Results = make([]resourcesresults.Result, len(opaSessionObj.ResourcesResult))
finalizeResults(opaSessionObj.Report.Results, opaSessionObj.ResourcesResult)
opaSessionObj.ResourcesResult = nil
}
if len(opaSessionObj.Report.Resources) == 0 {
opaSessionObj.Report.Resources = make([]reporthandling.Resource, len(opaSessionObj.AllResources))
finalizeResources(opaSessionObj.Report.Resources, opaSessionObj.AllResources)
opaSessionObj.AllResources = nil
}
}
func finalizeResults(results []resourcesresults.Result, resourcesResult map[string]resourcesresults.Result) {
index := 0
for resourceID := range resourcesResult {
results[index] = resourcesResult[resourceID]
index++
}
}
func finalizeResources(resources []reporthandling.Resource, allResources map[string]workloadinterface.IMetadata) {
index := 0
for resourceID := range allResources {
if obj, ok := allResources[resourceID]; ok {
r := *reporthandling.NewResource(obj.GetObject())
r.ResourceID = resourceID
resources[index] = r
}
index++
}
}
func maskID(id string) string {
sep := "-"
splitted := strings.Split(id, sep)
if len(splitted) != 5 {
return ""
}
str := splitted[0][:4]
splitted[0] = splitted[0][4:]
for i := range splitted {
for j := 0; j < len(splitted[i]); j++ {
str += "X"
}
str += sep
}
return strings.TrimSuffix(str, sep)
}

View File

@@ -27,22 +27,13 @@ func (resultsHandler *ResultsHandler) HandleResults(scanInfo *cautils.ScanInfo)
opaSessionObj := <-*resultsHandler.opaSessionObj
resultsHandler.printerObj.FinalizeData(opaSessionObj)
resultsHandler.printerObj.ActionPrint(opaSessionObj)
if err := resultsHandler.reporterObj.ActionSendReport(opaSessionObj); err != nil {
fmt.Println(err)
}
// TODO - get score from table
var score float32 = 0
for i := range opaSessionObj.PostureReport.FrameworkReports {
score += opaSessionObj.PostureReport.FrameworkReports[i].Score
}
score /= float32(len(opaSessionObj.PostureReport.FrameworkReports))
resultsHandler.printerObj.Score(score)
return score
return opaSessionObj.Report.SummaryDetails.Score
}
// CalculatePostureScore calculate final score

43
score/score.go Normal file
View File

@@ -0,0 +1,43 @@
package score
import (
"fmt"
"github.com/armosec/opa-utils/score"
"github.com/armosec/kubescape/cautils"
)
/* provides a wrapper for scoreUtils, since there's no common interface between postureReportV1 and PostureReportV2
and the need of concrete objects
I've decided to create scoreWrapper that will allow calculating score regardless (as long as opaSessionObj is there)
*/
type ScoreWrapper struct {
scoreUtil *score.ScoreUtil
opaSessionObj *cautils.OPASessionObj
}
type PostureReportVersion string
const (
EPostureReportV1 PostureReportVersion = "v1"
EPostureReportV2 PostureReportVersion = "V2"
)
func (su *ScoreWrapper) Calculate(reportVersion PostureReportVersion) error {
switch reportVersion {
case EPostureReportV1:
return su.scoreUtil.Calculate(su.opaSessionObj.PostureReport.FrameworkReports)
case EPostureReportV2:
return su.scoreUtil.CalculatePostureReportV2(su.opaSessionObj.Report)
}
return fmt.Errorf("unsupported score calculator")
}
func NewScoreWrapper(opaSessionObj *cautils.OPASessionObj) *ScoreWrapper {
return &ScoreWrapper{
scoreUtil: score.NewScore(opaSessionObj.AllResources),
opaSessionObj: opaSessionObj,
}
}

1
score/score_test.go Normal file
View File

@@ -0,0 +1 @@
package score