mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-25 23:33:51 +00:00
Compare commits
236 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
906c69a86d | ||
|
|
9c65aadcc7 | ||
|
|
89a05d247b | ||
|
|
7faf24cf88 | ||
|
|
7bebc7a814 | ||
|
|
8647a087dd | ||
|
|
78670665c4 | ||
|
|
1dd587cd83 | ||
|
|
d69cccf821 | ||
|
|
8ae3b9c28f | ||
|
|
9b6ad102b1 | ||
|
|
e6787b77fb | ||
|
|
46449045a6 | ||
|
|
d81984b4c6 | ||
|
|
22b463f306 | ||
|
|
475e45b848 | ||
|
|
32fac97d21 | ||
|
|
f9f32a1062 | ||
|
|
c90c3bbd05 | ||
|
|
cbae9a087b | ||
|
|
4da199c43e | ||
|
|
00d8660a91 | ||
|
|
76c2f6afe0 | ||
|
|
efa53bd83c | ||
|
|
01f6a1e1c0 | ||
|
|
9f78703dee | ||
|
|
1a0e96338e | ||
|
|
eff4690e0e | ||
|
|
266480c234 | ||
|
|
a922d01005 | ||
|
|
0d9711c8bb | ||
|
|
edab68f4fb | ||
|
|
08f04e19ef | ||
|
|
e62234a6ac | ||
|
|
5499c7a96f | ||
|
|
7f9c5c25ae | ||
|
|
b1276d56f7 | ||
|
|
b53bf320a6 | ||
|
|
81a4c168ed | ||
|
|
512a1a806e | ||
|
|
c95ef05177 | ||
|
|
563bd8a6a3 | ||
|
|
b444542f4d | ||
|
|
6eded41eee | ||
|
|
de91ce182d | ||
|
|
afc7f85460 | ||
|
|
c1b4d7de39 | ||
|
|
cde5b83bca | ||
|
|
f00106a502 | ||
|
|
3ae2742717 | ||
|
|
8deac19945 | ||
|
|
2ad469a5f4 | ||
|
|
c67b111c77 | ||
|
|
ee770e7429 | ||
|
|
197a3adf6a | ||
|
|
269d39497b | ||
|
|
0c3a7ac02b | ||
|
|
07443548c9 | ||
|
|
e561f78ada | ||
|
|
bb88272251 | ||
|
|
ce2f3dafae | ||
|
|
488c67056c | ||
|
|
351e51208d | ||
|
|
0c79df7299 | ||
|
|
b9a5c5af35 | ||
|
|
59741b84d1 | ||
|
|
25efcdf750 | ||
|
|
49dfa6c2a6 | ||
|
|
659647353e | ||
|
|
0eba51c80b | ||
|
|
35996d11f4 | ||
|
|
e0e0a811eb | ||
|
|
76f400d0aa | ||
|
|
8e950e0f54 | ||
|
|
0adb9dd540 | ||
|
|
5825555534 | ||
|
|
306a8c6201 | ||
|
|
123b620085 | ||
|
|
831e7814be | ||
|
|
7c85199ac2 | ||
|
|
efec8e4f2f | ||
|
|
af4faef9cf | ||
|
|
90052ad9e3 | ||
|
|
35c7b16e4a | ||
|
|
7ac75acfc2 | ||
|
|
22662fddcd | ||
|
|
6df1cdf5d8 | ||
|
|
2287c51d73 | ||
|
|
372942a14f | ||
|
|
6362246da4 | ||
|
|
9986d69215 | ||
|
|
21644e5cba | ||
|
|
ad93217bf6 | ||
|
|
ca49bc1ddd | ||
|
|
1229c73ddc | ||
|
|
7416202555 | ||
|
|
a0ba683eea | ||
|
|
89654eb26f | ||
|
|
9d1736a141 | ||
|
|
eaa4ed3da5 | ||
|
|
0db3f65312 | ||
|
|
1ea0a3ccc5 | ||
|
|
16cd30bea8 | ||
|
|
075ba4c603 | ||
|
|
2d898822df | ||
|
|
25b8ec82e8 | ||
|
|
44b74e2681 | ||
|
|
485e171008 | ||
|
|
c12eb83b4b | ||
|
|
84060e7823 | ||
|
|
d80d50b59d | ||
|
|
f11f054fea | ||
|
|
4c1d491d5a | ||
|
|
2b67cc520c | ||
|
|
2a5712bd3c | ||
|
|
ccbc11408b | ||
|
|
22bae315be | ||
|
|
740ab7cb46 | ||
|
|
cdaa9aa1b0 | ||
|
|
875f82dcbb | ||
|
|
e8f6bdd64a | ||
|
|
25247491ee | ||
|
|
57ae3dc3a7 | ||
|
|
27d00b58d7 | ||
|
|
263821ce67 | ||
|
|
3c12247b00 | ||
|
|
56d41596f6 | ||
|
|
f0cd1965b4 | ||
|
|
3a4c06a818 | ||
|
|
fd8dd7ab8a | ||
|
|
0434f6a935 | ||
|
|
b51a26442c | ||
|
|
6462ac0f0e | ||
|
|
fc01dbbac9 | ||
|
|
df8576c066 | ||
|
|
ca8adf28cf | ||
|
|
f70c6c566e | ||
|
|
056d4411b7 | ||
|
|
602b83b0e5 | ||
|
|
1cfcc6d930 | ||
|
|
8c6f618743 | ||
|
|
6adf1c3162 | ||
|
|
9f5f4f1832 | ||
|
|
9f49cc83e9 | ||
|
|
67972199ce | ||
|
|
9d5db86bf3 | ||
|
|
39efed5fc1 | ||
|
|
21c2bf22dd | ||
|
|
6c94b3a423 | ||
|
|
683248db0b | ||
|
|
9032400528 | ||
|
|
1a925b1acf | ||
|
|
dadc8c2c60 | ||
|
|
01c1b44bfc | ||
|
|
a394a99d8f | ||
|
|
4213707b7f | ||
|
|
d53b8272ee | ||
|
|
fdd688ac68 | ||
|
|
5bb961bdc6 | ||
|
|
9e7cc06f97 | ||
|
|
1d184d9000 | ||
|
|
a5e2ebf647 | ||
|
|
29eb573de5 | ||
|
|
ec6c3da5ec | ||
|
|
e2d4f8961e | ||
|
|
a48c680201 | ||
|
|
c869f2c962 | ||
|
|
f77fc9a06d | ||
|
|
e12eae93b9 | ||
|
|
d92fb32574 | ||
|
|
541dba3d79 | ||
|
|
033ed17125 | ||
|
|
aaeb663d15 | ||
|
|
c337005985 | ||
|
|
192eeee348 | ||
|
|
27c97684b9 | ||
|
|
41d5fa70ed | ||
|
|
4206e9c175 | ||
|
|
8658bb05dd | ||
|
|
9b707016a9 | ||
|
|
4b02826883 | ||
|
|
b29774ea71 | ||
|
|
bf68e90a8e | ||
|
|
cc5cdcd831 | ||
|
|
07f23ff7d9 | ||
|
|
2985da6dc9 | ||
|
|
1523973749 | ||
|
|
ccafd78a14 | ||
|
|
4f71fe0d55 | ||
|
|
7bd6b6b4d1 | ||
|
|
2b976489a2 | ||
|
|
1440f20f95 | ||
|
|
941e7e27c0 | ||
|
|
5428c6ab2f | ||
|
|
851bb65d17 | ||
|
|
a3ce04b7e8 | ||
|
|
4d68ca6aa2 | ||
|
|
29c6767d3c | ||
|
|
d8f5f7975c | ||
|
|
4cd8476837 | ||
|
|
112257449f | ||
|
|
ff9cf4adf0 | ||
|
|
f2d387bc9c | ||
|
|
2ceb5150e2 | ||
|
|
00006ec721 | ||
|
|
00aa6948ab | ||
|
|
aad32ec965 | ||
|
|
bd24ed3af7 | ||
|
|
9fc455bcec | ||
|
|
0f8ba1e7e8 | ||
|
|
d3137af3d7 | ||
|
|
775dd037d6 | ||
|
|
49cbfe130c | ||
|
|
9cd61dd996 | ||
|
|
292c4aa060 | ||
|
|
56a265930d | ||
|
|
4171d110a4 | ||
|
|
2568241ef8 | ||
|
|
d0775565e9 | ||
|
|
3c6b2db919 | ||
|
|
ca4d4a096c | ||
|
|
ff27db6b83 | ||
|
|
c9ecb6c563 | ||
|
|
b9e5782264 | ||
|
|
d852f81cb0 | ||
|
|
6137aa5d8e | ||
|
|
131b67ee83 | ||
|
|
db6f00be08 | ||
|
|
2ecc80985a | ||
|
|
93dbfd5110 | ||
|
|
ae0c384c85 | ||
|
|
f60ff1fb26 | ||
|
|
08a81696a1 | ||
|
|
8375a8ae63 | ||
|
|
31d8cf5118 | ||
|
|
77a9956d91 |
19
.github/workflows/build.yaml
vendored
19
.github/workflows/build.yaml
vendored
@@ -2,11 +2,7 @@ name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [ closed ]
|
||||
|
||||
branches: [ master ]
|
||||
jobs:
|
||||
once:
|
||||
name: Create release
|
||||
@@ -37,10 +33,19 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build
|
||||
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s" -o build/${{ matrix.os }}/kubescape # && md5sum build/${{ matrix.os }}/kubescape > build/${{ matrix.os }}/kubescape.md5
|
||||
env:
|
||||
RELEASE: v1.0.${{ github.run_number }}
|
||||
ArmoBEServer: api.armo.cloud
|
||||
ArmoERServer: report.armo.cloud
|
||||
ArmoWebsite: portal.armo.cloud
|
||||
CGO_ENABLED: 0
|
||||
run: python3 --version && python3 build.py
|
||||
|
||||
- name: Upload Release binaries
|
||||
id: upload-release-asset
|
||||
|
||||
40
.github/workflows/build_dev.yaml
vendored
Normal file
40
.github/workflows/build_dev.yaml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: build-dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
pull_request:
|
||||
branches: [ dev ]
|
||||
types: [ closed ]
|
||||
jobs:
|
||||
build:
|
||||
name: Create cross-platform dev build
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
RELEASE: v1.0.${{ github.run_number }}
|
||||
ArmoBEServer: api.armo.cloud
|
||||
ArmoERServer: report.euprod1.cyberarmorsoft.com
|
||||
ArmoWebsite: portal.armo.cloud
|
||||
CGO_ENABLED: 0
|
||||
run: python3 --version && python3 build.py
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: kubescape-${{ matrix.os }}
|
||||
path: build/${{ matrix.os }}/kubescape
|
||||
38
.github/workflows/master_pr_checks.yaml
vendored
Normal file
38
.github/workflows/master_pr_checks.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: master-pr
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [ edited, opened, synchronize, reopened ]
|
||||
jobs:
|
||||
build:
|
||||
name: Create cross-platform build
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
RELEASE: v1.0.${{ github.run_number }}
|
||||
ArmoBEServer: api.armo.cloud
|
||||
ArmoERServer: report.armo.cloud
|
||||
ArmoWebsite: portal.armo.cloud
|
||||
CGO_ENABLED: 0
|
||||
run: python3 --version && python3 build.py
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: kubescape-${{ matrix.os }}
|
||||
path: build/${{ matrix.os }}/kubescape
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
*.vs*
|
||||
*go.sum*
|
||||
*kubescape*
|
||||
*debug*
|
||||
*debug*
|
||||
.idea
|
||||
99
CONTRIBUTING.md
Normal file
99
CONTRIBUTING.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Contributing
|
||||
|
||||
First, it is awesome that you are considering contributing to Kubescape! Contributing is important and fun and we welcome your efforts.
|
||||
|
||||
When contributing, we categorize contributions into two:
|
||||
* Small code changes or fixes, whose scope are limited to a single or two files
|
||||
* Complex features and improvements, whose are not limited
|
||||
|
||||
If you have a small change, feel free to fire up a Pull Request.
|
||||
|
||||
When planning a bigger change, please first discuss the change you wish to make via issue,
|
||||
email, or any other method with the owners of this repository before making a change. Most likely your changes or features are great, but sometimes we might already going to this direction (or the exact opposite ;-) ) and we don't want to waste your time.
|
||||
|
||||
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||
build.
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
variables, exposed ports, useful file locations and container parameters.
|
||||
3. We will merge the Pull Request in once you have the sign-off.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
### Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
### Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
We will distance those who are constantly adhere to unacceptable behavior.
|
||||
|
||||
### Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
### Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
### Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
### Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
142
README.md
142
README.md
@@ -3,21 +3,23 @@
|
||||
[](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
|
||||
[](https://goreportcard.com/report/github.com/armosec/kubescape)
|
||||
|
||||
Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
|
||||
Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
|
||||
|
||||
Use Kubescape to test clusters or scan single YAML files and integrate it to your processes.
|
||||
Use Kubescape to test clusters or scan single YAML files and integrate it to your processes.
|
||||
|
||||
<img src="docs/demo.gif">
|
||||
|
||||
# TL;DR
|
||||
## Install & Run
|
||||
|
||||
### Install:
|
||||
## Install:
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/armosec/kubescape/master/install.sh | /bin/bash
|
||||
```
|
||||
|
||||
### Run:
|
||||
[Install on windows](#install-on-windows)
|
||||
|
||||
[Install on macOS](#install-on-macos)
|
||||
|
||||
## Run:
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
@@ -26,46 +28,92 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp
|
||||
|
||||
<img src="docs/summary.png">
|
||||
|
||||
### Click [👍](https://github.com/armosec/kubescape/stargazers) if you want us to continue to develop and improve Kubescape 😀
|
||||
|
||||
### Flags
|
||||
# Being part of the team
|
||||
|
||||
We invite you to our team! We are excited about this project and want to return the love we get.
|
||||
|
||||
Want to contribute? Want to discuss something? Have an issue?
|
||||
|
||||
* Open a issue, we are trying to respond within 48 hours
|
||||
* [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server!
|
||||
|
||||
[<img src="docs/discord-banner.png" width="100" alt="logo" align="center">](https://armosec.github.io/kubescape/)
|
||||
|
||||
# Options and examples
|
||||
|
||||
## Install on Windows
|
||||
|
||||
**Requires powershell v5.0+**
|
||||
|
||||
``` powershell
|
||||
iwr -useb https://raw.githubusercontent.com/armosec/kubescape/master/install.ps1 | iex
|
||||
```
|
||||
|
||||
Note: if you get an error you might need to change the execution policy (i.e. enable Powershell) with
|
||||
|
||||
``` powershell
|
||||
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
|
||||
```
|
||||
|
||||
## Install on macOS
|
||||
|
||||
1. ```
|
||||
brew tap armosec/kubescape
|
||||
```
|
||||
2. ```
|
||||
brew install kubescape
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| flag | default | description | options |
|
||||
| --- | --- | --- | --- |
|
||||
| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces |
|
||||
| `-s`/`--silent` | Display progress messages | Silent progress messages |
|
||||
| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold| `0` -> `100` |
|
||||
| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` |
|
||||
| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` |
|
||||
| `-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-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) |
|
||||
| `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json). If not set will download exceptions 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 | |
|
||||
|
||||
## Usage & Examples
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
|
||||
* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to [Armo portal](https://portal.armo.cloud/)
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --submit
|
||||
```
|
||||
|
||||
* Scan local `yaml`/`json` files before deploying
|
||||
|
||||
* Scan a running Kubernetes cluster with [`mitre`](https://www.microsoft.com/security/blog/2020/04/02/attack-matrix-kubernetes/) framework and submit results to [Armo portal](https://portal.armo.cloud/)
|
||||
```
|
||||
kubescape scan framework mitre --exclude-namespaces kube-system,kube-public --submit
|
||||
```
|
||||
|
||||
* Scan local `yaml`/`json` files before deploying. [Take a look at the demonstration](https://youtu.be/Ox6DaR7_4ZI)
|
||||
```
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
|
||||
* Scan `yaml`/`json` files from url
|
||||
* Scan `yaml`/`json` files from url
|
||||
```
|
||||
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
|
||||
```
|
||||
|
||||
* Output in `json` format
|
||||
* Output in `json` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
|
||||
```
|
||||
|
||||
* Output in `junit xml` format
|
||||
* Output in `junit xml` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
|
||||
```
|
||||
@@ -77,7 +125,7 @@ kubescape scan framework nsa --exceptions examples/exceptions.json
|
||||
|
||||
### Helm Support
|
||||
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
|
||||
```
|
||||
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
@@ -86,36 +134,55 @@ for example:
|
||||
```
|
||||
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
### Offline Support <img src="docs/new-feature.svg">
|
||||
### Offline Support
|
||||
|
||||
It is possible to run Kubescape offline!
|
||||
|
||||
First download the framework and then scan with `--use-from` flag
|
||||
|
||||
* 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 store save to `~/.kubescape/<framework name>.json`
|
||||
```
|
||||
kubescape download framework nsa --output nsa.json
|
||||
```
|
||||
|
||||
* Scan using the downloaded framework
|
||||
2. Scan using the downloaded framework
|
||||
```
|
||||
kubescape scan framework nsa --use-from nsa.json
|
||||
```
|
||||
|
||||
Kubescape is an open source project, we welcome your feedback and ideas for improvement. We’re also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.
|
||||
|
||||
# How to build
|
||||
# How to build
|
||||
|
||||
Note: development (and the release process) is done with Go `1.16`
|
||||
## Build using python (3.7^) script
|
||||
|
||||
Kubescpae can be built using:
|
||||
|
||||
``` sh
|
||||
python build.py
|
||||
```
|
||||
|
||||
Note: In order to built using the above script, one must set the environment
|
||||
variables in this script:
|
||||
|
||||
+ RELEASE
|
||||
+ ArmoBEServer
|
||||
+ ArmoERServer
|
||||
+ ArmoWebsite
|
||||
|
||||
|
||||
## Build using go
|
||||
|
||||
Note: development (and the release process) is done with Go `1.17`
|
||||
|
||||
1. Clone Project
|
||||
```
|
||||
git clone git@github.com:armosec/kubescape.git kubescape && cd "$_"
|
||||
git clone https://github.com/armosec/kubescape.git kubescape && cd "$_"
|
||||
```
|
||||
|
||||
2. Build
|
||||
```
|
||||
go mod tidy && go build -o kubescape .
|
||||
go build -o kubescape .
|
||||
```
|
||||
|
||||
3. Run
|
||||
@@ -125,19 +192,31 @@ go mod tidy && go build -o kubescape .
|
||||
|
||||
4. Enjoy :zany_face:
|
||||
|
||||
## How to build in Docker
|
||||
|
||||
1. Clone Project
|
||||
```
|
||||
git clone https://github.com/armosec/kubescape.git kubescape && cd "$_"
|
||||
```
|
||||
|
||||
2. Build
|
||||
```
|
||||
docker build -t kubescape -f build/Dockerfile .
|
||||
```
|
||||
|
||||
# Under the hood
|
||||
|
||||
## Tests
|
||||
Kubescape is running the following tests according to what is defined by [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
|
||||
* Non-root containers
|
||||
* Immutable container filesystem
|
||||
* Privileged containers
|
||||
* Immutable container filesystem
|
||||
* Privileged containers
|
||||
* hostPID, hostIPC privileges
|
||||
* hostNetwork access
|
||||
* allowedHostPaths field
|
||||
* Protecting pod service account tokens
|
||||
* Resource policies
|
||||
* Control plane hardening
|
||||
* Control plane hardening
|
||||
* Exposed dashboard
|
||||
* Allow privilege escalation
|
||||
* Applications credentials in configuration files
|
||||
@@ -149,16 +228,15 @@ Kubescape is running the following tests according to what is defined by [Kubern
|
||||
* Ingress and Egress blocked
|
||||
* Container hostPort
|
||||
* Network policies
|
||||
* Symlink Exchange Can Allow Host Filesystem Access (CVE-2021-25741)
|
||||
|
||||
|
||||
|
||||
## Technology
|
||||
Kubescape based on OPA engine: https://github.com/open-policy-agent/opa and ARMO's posture controls.
|
||||
Kubescape based on OPA engine: https://github.com/open-policy-agent/opa and ARMO's posture controls.
|
||||
|
||||
The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/).
|
||||
The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/).
|
||||
|
||||
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. We’re also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.
|
||||
|
||||
|
||||
|
||||
82
build.py
Normal file
82
build.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
BASE_GETTER_CONST = "github.com/armosec/kubescape/cautils/getter"
|
||||
BE_SERVER_CONST = BASE_GETTER_CONST + ".ArmoBEURL"
|
||||
ER_SERVER_CONST = BASE_GETTER_CONST + ".ArmoERURL"
|
||||
WEBSITE_CONST = BASE_GETTER_CONST + ".ArmoFEURL"
|
||||
|
||||
def checkStatus(status, msg):
|
||||
if status != 0:
|
||||
sys.stderr.write(msg)
|
||||
exit(status)
|
||||
|
||||
|
||||
def getBuildDir():
|
||||
currentPlatform = platform.system()
|
||||
buildDir = "build/"
|
||||
|
||||
if currentPlatform == "Windows": buildDir += "windows-latest"
|
||||
elif currentPlatform == "Linux": buildDir += "ubuntu-latest"
|
||||
elif currentPlatform == "Darwin": buildDir += "macos-latest"
|
||||
else: raise OSError("Platform %s is not supported!" % (currentPlatform))
|
||||
|
||||
return buildDir
|
||||
|
||||
def getPackageName():
|
||||
packageName = "kubescape"
|
||||
# if platform.system() == "Windows": packageName += ".exe"
|
||||
|
||||
return packageName
|
||||
|
||||
|
||||
def main():
|
||||
print("Building Kubescape")
|
||||
|
||||
# print environment variables
|
||||
print(os.environ)
|
||||
|
||||
# Set some variables
|
||||
packageName = getPackageName()
|
||||
buildUrl = "github.com/armosec/kubescape/cmd.BuildNumber"
|
||||
releaseVersion = os.getenv("RELEASE")
|
||||
ArmoBEServer = os.getenv("ArmoBEServer")
|
||||
ArmoERServer = os.getenv("ArmoERServer")
|
||||
ArmoWebsite = os.getenv("ArmoWebsite")
|
||||
|
||||
# Create build directory
|
||||
buildDir = getBuildDir()
|
||||
|
||||
if not os.path.isdir(buildDir):
|
||||
os.makedirs(buildDir)
|
||||
|
||||
# Build kubescape
|
||||
ldflags = "-w -s -X %s=%s -X %s=%s -X %s=%s -X %s=%s" \
|
||||
% (buildUrl, releaseVersion, BE_SERVER_CONST, ArmoBEServer,
|
||||
ER_SERVER_CONST, ArmoERServer, WEBSITE_CONST, ArmoWebsite)
|
||||
status = subprocess.call(["go", "build", "-o", "%s/%s" % (buildDir, packageName), "-ldflags" ,ldflags])
|
||||
checkStatus(status, "Failed to build kubescape")
|
||||
|
||||
test_cli_prints(buildDir,packageName)
|
||||
|
||||
|
||||
sha1 = hashlib.sha1()
|
||||
with open(buildDir + "/" + packageName, "rb") as kube:
|
||||
sha1.update(kube.read())
|
||||
with open(buildDir + "/" + packageName + ".sha1", "w") as kube_sha:
|
||||
kube_sha.write(sha1.hexdigest())
|
||||
|
||||
print("Build Done")
|
||||
|
||||
def test_cli_prints(buildDir,packageName):
|
||||
bin_cli = os.path.abspath(os.path.join(buildDir,packageName))
|
||||
|
||||
print(f"testing CLI prints on {bin_cli}")
|
||||
status = str(subprocess.check_output([bin_cli, "-h"]))
|
||||
assert "download" in status, "download is missing: " + status
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
15
build/Dockerfile
Normal file
15
build/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.17-alpine as builder
|
||||
ENV GOPROXY=https://goproxy.io,direct
|
||||
ENV GO111MODULE=on
|
||||
|
||||
WORKDIR /work
|
||||
ADD . .
|
||||
RUN GOOS=linux CGO_ENABLED=0 go build -ldflags="-s -w " -installsuffix cgo -o kubescape .
|
||||
|
||||
FROM alpine
|
||||
COPY --from=builder /work/kubescape /usr/bin/kubescape
|
||||
|
||||
# # Download the frameworks. Use the "--use-default" flag when running kubescape
|
||||
# RUN kubescape download framework nsa && kubescape download framework mitre
|
||||
|
||||
CMD ["kubescape"]
|
||||
@@ -3,7 +3,7 @@ package apis
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ func BEHttpRequest(loginobj *LoginObject, beURL,
|
||||
return nil, fmt.Errorf("Error #%v Due to: %v", resp.StatusCode, resp.Status)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func MakeBackendConnector(client *http.Client, baseURL string, loginDetails *CustomerLoginDetails) (*BackendConnector, error) {
|
||||
if err := ValidateBEConnectorMakerInput(client, baseURL, loginDetails); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn := &BackendConnector{BaseURL: baseURL, Credentials: loginDetails, HTTPClient: client}
|
||||
err := conn.Login()
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func ValidateBEConnectorMakerInput(client *http.Client, baseURL string, loginDetails *CustomerLoginDetails) error {
|
||||
if client == nil {
|
||||
fmt.Errorf("You must provide an initialized httpclient")
|
||||
}
|
||||
if len(baseURL) == 0 {
|
||||
return fmt.Errorf("you must provide a valid backend url")
|
||||
}
|
||||
|
||||
if loginDetails == nil || (len(loginDetails.Email) == 0 && len(loginDetails.Password) == 0) {
|
||||
return fmt.Errorf("you must provide valid login details")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *BackendConnector) Login() error {
|
||||
if !r.IsExpired() {
|
||||
return nil
|
||||
}
|
||||
|
||||
loginInfoBytes, err := json.Marshal(r.Credentials)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal credentials properly")
|
||||
}
|
||||
|
||||
beURL := fmt.Sprintf("%v/%v", r.BaseURL, "login")
|
||||
|
||||
req, err := http.NewRequest("POST", beURL, bytes.NewReader(loginInfoBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Referer", strings.Replace(beURL, "dashbe", "cpanel", 1))
|
||||
resp, err := r.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read login response")
|
||||
}
|
||||
|
||||
loginS := &BELoginResponse{}
|
||||
json.Unmarshal(body, &loginS)
|
||||
|
||||
loginS.Cookies = resp.Cookies()
|
||||
r.BELoginResponse = loginS
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BackendConnector) IsExpired() bool {
|
||||
return r.BELoginResponse == nil || r.BELoginResponse.ToLoginObject().IsExpired()
|
||||
}
|
||||
|
||||
func (r *BackendConnector) GetBaseURL() string {
|
||||
return r.BaseURL
|
||||
}
|
||||
func (r *BackendConnector) GetLoginObj() *LoginObject {
|
||||
return r.BELoginResponse.ToLoginObject()
|
||||
}
|
||||
func (r *BackendConnector) GetClient() *http.Client {
|
||||
return r.HTTPClient
|
||||
}
|
||||
|
||||
func (r *BackendConnector) HTTPSend(httpverb string,
|
||||
endpoint string,
|
||||
payload []byte,
|
||||
f HTTPReqFunc,
|
||||
qryData interface{}) ([]byte, error) {
|
||||
|
||||
beURL := fmt.Sprintf("%v/%v", r.GetBaseURL(), endpoint)
|
||||
req, err := http.NewRequest(httpverb, beURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.IsExpired() {
|
||||
r.Login()
|
||||
}
|
||||
|
||||
loginobj := r.GetLoginObj()
|
||||
req.Header.Set("Authorization", loginobj.Authorization)
|
||||
f(req, qryData)
|
||||
q := req.URL.Query()
|
||||
q.Set("customerGUID", loginobj.GUID)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
for _, cookie := range loginobj.Cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
resp, err := r.GetClient().Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
fmt.Printf("req:\n%v\nresp:%v\n", req, resp)
|
||||
return nil, fmt.Errorf("Error #%v Due to: %v", resp.StatusCode, resp.Status)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -2,11 +2,10 @@ package apis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
@@ -231,7 +230,7 @@ func BELogin(loginDetails *CustomerLoginDetails, login string, cfg string) (*BEL
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package apis
|
||||
|
||||
// func TestLogin2BE(t *testing.T) {
|
||||
|
||||
// loginDetails := CustomerLoginDetails{Email: "lalafi@cyberarmor.io", Password: "***", CustomerName: "CyberArmorTests"}
|
||||
// res, err := BELogin(loginDetails, "login")
|
||||
// if err != nil {
|
||||
// t.Errorf("failed to get raw audit is different ")
|
||||
// }
|
||||
// k := res.ToLoginObject()
|
||||
|
||||
// fmt.Printf("%v\n", k)
|
||||
|
||||
// }
|
||||
|
||||
// func TestGetMicroserviceOverview(t *testing.T) {
|
||||
// // client := &http.Client{}
|
||||
// loginDetails := CustomerLoginDetails{Email: "lalafi@cyberarmor.io", Password: "***", CustomerName: "CyberArmorTests"}
|
||||
// loginobj, err := BELogin(loginDetails, "login")
|
||||
// if err != nil {
|
||||
// t.Errorf("failed to get raw audit is different ")
|
||||
// }
|
||||
// k := loginobj.ToLoginObject()
|
||||
// beURL := GetBEInfo("")
|
||||
|
||||
// res, err := BEHttpRequest(k, beURL,
|
||||
// "GET",
|
||||
// "v1/microservicesOverview",
|
||||
// nil,
|
||||
// BasicBEQuery,
|
||||
// k)
|
||||
|
||||
// if err != nil {
|
||||
// t.Errorf("failed to get raw audit is different ")
|
||||
// }
|
||||
|
||||
// s := string(res)
|
||||
|
||||
// fmt.Printf("%v\n", s)
|
||||
|
||||
// }
|
||||
@@ -1,5 +1,7 @@
|
||||
package armotypes
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
CostumerGuidQuery = "costumerGUID"
|
||||
ClusterNameQuery = "cluster"
|
||||
@@ -42,6 +44,10 @@ const (
|
||||
DesignatorSid DesignatorType = "Sid" // secret id
|
||||
)
|
||||
|
||||
func (dt DesignatorType) ToLower() DesignatorType {
|
||||
return DesignatorType(strings.ToLower(string(dt)))
|
||||
}
|
||||
|
||||
// attributes
|
||||
const (
|
||||
AttributeCluster = "cluster"
|
||||
|
||||
@@ -33,10 +33,10 @@ func (designator *PortalDesignator) GetLabels() map[string]string {
|
||||
|
||||
// DigestPortalDesignator - get cluster namespace and labels from designator
|
||||
func (designator *PortalDesignator) DigestPortalDesignator() (string, string, string, string, map[string]string) {
|
||||
switch designator.DesignatorType {
|
||||
case DesignatorAttributes, DesignatorAttribute:
|
||||
switch designator.DesignatorType.ToLower() {
|
||||
case DesignatorAttributes.ToLower(), DesignatorAttribute.ToLower():
|
||||
return designator.DigestAttributesDesignator()
|
||||
case DesignatorWlid, DesignatorWildWlid:
|
||||
case DesignatorWlid.ToLower(), DesignatorWildWlid.ToLower():
|
||||
return cautils.GetClusterFromWlid(designator.WLID), cautils.GetNamespaceFromWlid(designator.WLID), cautils.GetKindFromWlid(designator.WLID), cautils.GetNameFromWlid(designator.WLID), map[string]string{}
|
||||
// case DesignatorSid: // TODO
|
||||
default:
|
||||
|
||||
@@ -3,7 +3,6 @@ package cautils
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -154,7 +153,7 @@ func LoadConfig(configPath string, loadToEnv bool) (*ClusterConfig, error) {
|
||||
configPath = "/etc/config/clusterData.json"
|
||||
}
|
||||
|
||||
dat, err := ioutil.ReadFile(configPath)
|
||||
dat, err := os.ReadFile(configPath)
|
||||
if err != nil || len(dat) == 0 {
|
||||
return nil, fmt.Errorf("Config empty or not found. path: %s", configPath)
|
||||
}
|
||||
@@ -191,7 +190,7 @@ func (clusterConfig *ClusterConfig) LoadConfigToEnv() {
|
||||
func SetEnv(key, value string) {
|
||||
if e := os.Getenv(key); e == "" {
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
glog.Warning("%s: %s", key, err.Error())
|
||||
glog.Warningf("%s: %s", key, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
439
cautils/customerloader.go
Normal file
439
cautils/customerloader.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
configMapName = "kubescape"
|
||||
configFileName = "config"
|
||||
)
|
||||
|
||||
func ConfigFileFullPath() string { return getter.GetDefaultPath(configFileName + ".json") }
|
||||
|
||||
// ======================================================================================
|
||||
// =============================== Config structure =====================================
|
||||
// ======================================================================================
|
||||
|
||||
type ConfigObj struct {
|
||||
CustomerGUID string `json:"customerGUID"`
|
||||
Token string `json:"invitationParam"`
|
||||
CustomerAdminEMail string `json:"adminMail"`
|
||||
}
|
||||
|
||||
func (co *ConfigObj) Json() []byte {
|
||||
if b, err := json.Marshal(co); err == nil {
|
||||
return b
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// ======================================================================================
|
||||
// =============================== interface ============================================
|
||||
// ======================================================================================
|
||||
type IClusterConfig interface {
|
||||
// setters
|
||||
SetCustomerGUID(customerGUID string) error
|
||||
|
||||
// getters
|
||||
GetCustomerGUID() string
|
||||
GetConfigObj() *ConfigObj
|
||||
GetK8sAPI() *k8sinterface.KubernetesApi
|
||||
GetBackendAPI() getter.IBackend
|
||||
GetDefaultNS() string
|
||||
GenerateURL()
|
||||
}
|
||||
|
||||
// ClusterConfigSetup - Setup the desired cluster behavior regarding submittion to the Armo BE
|
||||
func ClusterConfigSetup(scanInfo *ScanInfo, k8s *k8sinterface.KubernetesApi, beAPI getter.IBackend) IClusterConfig {
|
||||
/*
|
||||
|
||||
If "First run (local config not found)" -
|
||||
Default - Do not send report (local)
|
||||
Local - Do not send report
|
||||
Submit - Create tenant & Submit report
|
||||
|
||||
If "Submitted but not signed up" -
|
||||
Default - Delete local config & Do not send report (local)
|
||||
Local - Delete local config & Do not send report
|
||||
Submit - Submit report
|
||||
|
||||
If "Signed up user" -
|
||||
Default - Submit report (submit)
|
||||
Local - Do not send report
|
||||
Submit - Submit report
|
||||
|
||||
*/
|
||||
clusterConfig := NewClusterConfig(k8s, beAPI)
|
||||
clusterConfig.LoadConfig()
|
||||
|
||||
if !IsSubmitted(clusterConfig) {
|
||||
if scanInfo.Submit {
|
||||
return clusterConfig // submit - Create tenant & Submit report
|
||||
}
|
||||
return NewEmptyConfig() // local/default - Do not send report
|
||||
}
|
||||
if !IsRegistered(clusterConfig) {
|
||||
if scanInfo.Submit {
|
||||
return clusterConfig // submit/default - Submit report
|
||||
}
|
||||
DeleteConfig(k8s)
|
||||
return NewEmptyConfig() // local - Delete local config & Do not send report
|
||||
}
|
||||
if scanInfo.Local {
|
||||
return NewEmptyConfig() // local - Do not send report
|
||||
}
|
||||
return clusterConfig // submit/default - Submit report
|
||||
}
|
||||
|
||||
// ======================================================================================
|
||||
// ============================= Mock Config ============================================
|
||||
// ======================================================================================
|
||||
type EmptyConfig struct {
|
||||
}
|
||||
|
||||
func NewEmptyConfig() *EmptyConfig { return &EmptyConfig{} }
|
||||
func (c *EmptyConfig) GetConfigObj() *ConfigObj { return &ConfigObj{} }
|
||||
func (c *EmptyConfig) SetCustomerGUID(customerGUID string) error { return nil }
|
||||
func (c *EmptyConfig) GetCustomerGUID() string { return "" }
|
||||
func (c *EmptyConfig) GetK8sAPI() *k8sinterface.KubernetesApi { return nil } // TODO: return mock obj
|
||||
func (c *EmptyConfig) GetDefaultNS() string { return k8sinterface.GetDefaultNamespace() }
|
||||
func (c *EmptyConfig) GetBackendAPI() getter.IBackend { return nil } // TODO: return mock obj
|
||||
func (c *EmptyConfig) GenerateURL() {
|
||||
message := fmt.Sprintf("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: https://%s", getter.GetArmoAPIConnector().GetFrontendURL())
|
||||
InfoTextDisplay(os.Stdout, message+"\n")
|
||||
}
|
||||
|
||||
// ======================================================================================
|
||||
// ========================== Cluster Config ============================================
|
||||
// ======================================================================================
|
||||
|
||||
type ClusterConfig struct {
|
||||
k8s *k8sinterface.KubernetesApi
|
||||
defaultNS string
|
||||
backendAPI getter.IBackend
|
||||
configObj *ConfigObj
|
||||
}
|
||||
|
||||
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBackend) *ClusterConfig {
|
||||
return &ClusterConfig{
|
||||
k8s: k8s,
|
||||
backendAPI: backendAPI,
|
||||
configObj: &ConfigObj{},
|
||||
defaultNS: k8sinterface.GetDefaultNamespace(),
|
||||
}
|
||||
}
|
||||
func (c *ClusterConfig) GetConfigObj() *ConfigObj { return c.configObj }
|
||||
func (c *ClusterConfig) GetK8sAPI() *k8sinterface.KubernetesApi { return c.k8s }
|
||||
func (c *ClusterConfig) GetDefaultNS() string { return c.defaultNS }
|
||||
func (c *ClusterConfig) GetBackendAPI() getter.IBackend { return c.backendAPI }
|
||||
|
||||
func (c *ClusterConfig) GenerateURL() {
|
||||
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = getter.GetArmoAPIConnector().GetFrontendURL()
|
||||
if c.configObj == nil {
|
||||
return
|
||||
}
|
||||
message := fmt.Sprintf("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: %s", u.String())
|
||||
if c.configObj.CustomerAdminEMail != "" {
|
||||
InfoTextDisplay(os.Stdout, message+"\n")
|
||||
return
|
||||
}
|
||||
u.Path = "account/sign-up"
|
||||
q := u.Query()
|
||||
q.Add("invitationToken", c.configObj.Token)
|
||||
q.Add("customerGUID", c.configObj.CustomerGUID)
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
InfoTextDisplay(os.Stdout, message+"\n")
|
||||
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetCustomerGUID() string {
|
||||
if c.configObj != nil {
|
||||
return c.configObj.CustomerGUID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) SetCustomerGUID(customerGUID string) error {
|
||||
|
||||
if customerGUID != "" && c.GetCustomerGUID() != customerGUID {
|
||||
c.configObj.CustomerGUID = customerGUID // override config customerGUID
|
||||
}
|
||||
|
||||
customerGUID = c.GetCustomerGUID()
|
||||
|
||||
// get from armoBE
|
||||
tenantResponse, err := c.backendAPI.GetCustomerGUID(customerGUID)
|
||||
if err == nil && tenantResponse != nil {
|
||||
if tenantResponse.AdminMail != "" { // this customer already belongs to some user
|
||||
c.configObj.CustomerAdminEMail = tenantResponse.AdminMail
|
||||
} else {
|
||||
c.configObj.Token = tenantResponse.Token
|
||||
c.configObj.CustomerGUID = tenantResponse.TenantID
|
||||
}
|
||||
} else {
|
||||
if err != nil && !strings.Contains(err.Error(), "already exists") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update/create config
|
||||
if c.existsConfigMap() {
|
||||
c.updateConfigMap()
|
||||
} else {
|
||||
c.createConfigMap()
|
||||
}
|
||||
if existsConfigFile() {
|
||||
c.updateConfigFile()
|
||||
} else {
|
||||
c.createConfigFile()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) LoadConfig() {
|
||||
// get from configMap
|
||||
if c.existsConfigMap() {
|
||||
c.configObj, _ = c.loadConfigFromConfigMap()
|
||||
} else if existsConfigFile() { // get from file
|
||||
c.configObj, _ = loadConfigFromFile()
|
||||
} else {
|
||||
c.configObj = &ConfigObj{}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) ToMapString() map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
bc, _ := json.Marshal(c.configObj)
|
||||
json.Unmarshal(bc, &m)
|
||||
return m
|
||||
}
|
||||
func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) {
|
||||
if c.k8s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bData, err := json.Marshal(configMap.Data); err == nil {
|
||||
return readConfig(bData)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) existsConfigMap() bool {
|
||||
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), 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{})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if val, ok := configMap.Data[key]; ok {
|
||||
return val, nil
|
||||
} else {
|
||||
return "", fmt.Errorf("value does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func GetValueFromConfigJson(key string) (string, error) {
|
||||
data, err := os.ReadFile(ConfigFileFullPath())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var obj map[string]interface{}
|
||||
if err := json.Unmarshal(data, &obj); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if val, ok := obj[key]; ok {
|
||||
return fmt.Sprint(val), nil
|
||||
} else {
|
||||
return "", fmt.Errorf("value does not exist")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SetKeyValueInConfigJson(key string, value string) error {
|
||||
data, err := os.ReadFile(ConfigFileFullPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var obj map[string]interface{}
|
||||
err = json.Unmarshal(data, &obj)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj[key] = value
|
||||
newData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(ConfigFileFullPath(), newData, 0664)
|
||||
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) SetKeyValueInConfigmap(key string, value string) error {
|
||||
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
configMap = &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(configMap.Data) == 0 {
|
||||
configMap.Data = make(map[string]string)
|
||||
}
|
||||
|
||||
configMap.Data[key] = value
|
||||
|
||||
if err != nil {
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
|
||||
} else {
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func existsConfigFile() bool {
|
||||
_, err := os.ReadFile(ConfigFileFullPath())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) createConfigMap() error {
|
||||
if c.k8s == nil {
|
||||
return nil
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
},
|
||||
}
|
||||
c.updateConfigData(configMap)
|
||||
|
||||
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
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{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.updateConfigData(configMap)
|
||||
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) updateConfigFile() error {
|
||||
if err := os.WriteFile(ConfigFileFullPath(), c.configObj.Json(), 0664); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) createConfigFile() error {
|
||||
if err := os.WriteFile(ConfigFileFullPath(), c.configObj.Json(), 0664); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) {
|
||||
if len(configMap.Data) == 0 {
|
||||
configMap.Data = make(map[string]string)
|
||||
}
|
||||
m := c.ToMapString()
|
||||
for k, v := range m {
|
||||
if s, ok := v.(string); ok {
|
||||
configMap.Data[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
func loadConfigFromFile() (*ConfigObj, error) {
|
||||
dat, err := os.ReadFile(ConfigFileFullPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return readConfig(dat)
|
||||
}
|
||||
func readConfig(dat []byte) (*ConfigObj, error) {
|
||||
|
||||
if len(dat) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
configObj := &ConfigObj{}
|
||||
err := json.Unmarshal(dat, configObj)
|
||||
|
||||
return configObj, err
|
||||
}
|
||||
|
||||
// Check if the customer is submitted
|
||||
func IsSubmitted(clusterConfig *ClusterConfig) bool {
|
||||
return clusterConfig.existsConfigMap() || existsConfigFile()
|
||||
}
|
||||
|
||||
// Check if the customer is registered
|
||||
func IsRegistered(clusterConfig *ClusterConfig) bool {
|
||||
|
||||
// get from armoBE
|
||||
tenantResponse, err := clusterConfig.backendAPI.GetCustomerGUID(clusterConfig.GetCustomerGUID())
|
||||
if err == nil && tenantResponse != nil {
|
||||
if tenantResponse.AdminMail != "" { // this customer already belongs to some user
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func DeleteConfig(k8s *k8sinterface.KubernetesApi) error {
|
||||
if err := DeleteConfigMap(k8s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := DeleteConfigFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func DeleteConfigMap(k8s *k8sinterface.KubernetesApi) error {
|
||||
return k8s.KubernetesClient.CoreV1().ConfigMaps(k8sinterface.GetDefaultNamespace()).Delete(context.Background(), configMapName, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
func DeleteConfigFile() error {
|
||||
return os.Remove(ConfigFileFullPath())
|
||||
}
|
||||
@@ -24,8 +24,8 @@ var FailureDisplay = color.New(color.Bold, color.FgHiRed).FprintfFunc()
|
||||
var WarningDisplay = color.New(color.Bold, color.FgCyan).FprintfFunc()
|
||||
var FailureTextDisplay = color.New(color.Faint, color.FgHiRed).FprintfFunc()
|
||||
var InfoDisplay = color.New(color.Bold, color.FgHiYellow).FprintfFunc()
|
||||
var InfoTextDisplay = color.New(color.Faint, color.FgHiYellow).FprintfFunc()
|
||||
var SimpleDisplay = color.New(color.Bold, color.FgHiWhite).FprintfFunc()
|
||||
var InfoTextDisplay = color.New(color.Bold, color.FgHiYellow).FprintfFunc()
|
||||
var SimpleDisplay = color.New().FprintfFunc()
|
||||
var SuccessDisplay = color.New(color.Bold, color.FgHiGreen).FprintfFunc()
|
||||
var DescriptionDisplay = color.New(color.Faint, color.FgWhite).FprintfFunc()
|
||||
|
||||
|
||||
@@ -3,28 +3,94 @@ package getter
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// =======================================================================================================================
|
||||
// =============================================== ArmoAPI ===============================================================
|
||||
// =======================================================================================================================
|
||||
|
||||
var (
|
||||
// ATTENTION!!!
|
||||
// Changes in this URLs variable names, or in the usage is affecting the build process! BE CAREFULL
|
||||
armoERURL = "report.armo.cloud"
|
||||
armoBEURL = "api.armo.cloud"
|
||||
armoFEURL = "portal.armo.cloud"
|
||||
|
||||
armoDevERURL = "report.eudev3.cyberarmorsoft.com"
|
||||
armoDevBEURL = "eggdashbe.eudev3.cyberarmorsoft.com"
|
||||
armoDevFEURL = "armoui.eudev3.cyberarmorsoft.com"
|
||||
)
|
||||
|
||||
// Armo API for downloading policies
|
||||
type ArmoAPI struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
apiURL string
|
||||
erURL string
|
||||
feURL string
|
||||
}
|
||||
|
||||
func NewArmoAPI() *ArmoAPI {
|
||||
var globalArmoAPIConnecctor *ArmoAPI
|
||||
|
||||
func SetARMOAPIConnector(armoAPI *ArmoAPI) {
|
||||
globalArmoAPIConnecctor = armoAPI
|
||||
}
|
||||
|
||||
func GetArmoAPIConnector() *ArmoAPI {
|
||||
if globalArmoAPIConnecctor == nil {
|
||||
glog.Error("returning nil API connector")
|
||||
}
|
||||
return globalArmoAPIConnecctor
|
||||
}
|
||||
|
||||
func NewARMOAPIDev() *ArmoAPI {
|
||||
apiObj := newArmoAPI()
|
||||
|
||||
apiObj.apiURL = armoDevBEURL
|
||||
apiObj.erURL = armoDevERURL
|
||||
apiObj.feURL = armoDevFEURL
|
||||
|
||||
return apiObj
|
||||
}
|
||||
|
||||
func NewARMOAPIProd() *ArmoAPI {
|
||||
apiObj := newArmoAPI()
|
||||
|
||||
apiObj.apiURL = armoBEURL
|
||||
apiObj.erURL = armoERURL
|
||||
apiObj.feURL = armoFEURL
|
||||
|
||||
return apiObj
|
||||
}
|
||||
|
||||
func NewARMOAPICustomized(armoERURL, armoBEURL, armoFEURL string) *ArmoAPI {
|
||||
apiObj := newArmoAPI()
|
||||
|
||||
apiObj.erURL = armoERURL
|
||||
apiObj.apiURL = armoBEURL
|
||||
apiObj.feURL = armoFEURL
|
||||
|
||||
return apiObj
|
||||
}
|
||||
|
||||
func newArmoAPI() *ArmoAPI {
|
||||
return &ArmoAPI{
|
||||
httpClient: &http.Client{},
|
||||
baseURL: "https://dashbe.euprod1.cyberarmorsoft.com",
|
||||
httpClient: &http.Client{Timeout: time.Duration(61) * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) GetFrontendURL() string {
|
||||
return armoAPI.feURL
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) GetReportReceiverURL() string {
|
||||
return armoAPI.erURL
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error) {
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name))
|
||||
if err != nil {
|
||||
@@ -35,20 +101,11 @@ func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error)
|
||||
if err = JSONDecoder(respStr).Decode(framework); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name))
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name+".json"))
|
||||
|
||||
return framework, err
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
|
||||
requestURI := "v1/armoFrameworks"
|
||||
requestURI += fmt.Sprintf("?customerGUID=%s", "11111111-1111-1111-1111-111111111111")
|
||||
requestURI += fmt.Sprintf("&frameworkName=%s", strings.ToUpper(frameworkName))
|
||||
requestURI += "&getRules=true"
|
||||
|
||||
return urlEncoder(fmt.Sprintf("%s/%s", armoAPI.baseURL, requestURI))
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
exceptions := []armotypes.PostureExceptionPolicy{}
|
||||
if customerGUID == "" {
|
||||
@@ -66,11 +123,26 @@ func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armot
|
||||
return exceptions, nil
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
|
||||
requestURI := "api/v1/armoPostureExceptions"
|
||||
requestURI += fmt.Sprintf("?customerGUID=%s", customerGUID)
|
||||
if clusterName != "" {
|
||||
requestURI += fmt.Sprintf("&clusterName=%s", clusterName)
|
||||
func (armoAPI *ArmoAPI) GetCustomerGUID(customerGUID string) (*TenantResponse, error) {
|
||||
url := armoAPI.getCustomerURL()
|
||||
if customerGUID != "" {
|
||||
url = fmt.Sprintf("%s?customerGUID=%s", url, customerGUID)
|
||||
}
|
||||
return urlEncoder(fmt.Sprintf("%s/%s", armoAPI.baseURL, requestURI))
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant := &TenantResponse{}
|
||||
if err = JSONDecoder(respStr).Decode(tenant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
type TenantResponse struct {
|
||||
TenantID string `json:"tenantId"`
|
||||
Token string `json:"token"`
|
||||
Expires string `json:"expires"`
|
||||
AdminMail string `json:"adminMail,omitempty"`
|
||||
}
|
||||
|
||||
44
cautils/getter/armoapiutils.go
Normal file
44
cautils/getter/armoapiutils.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = armoAPI.apiURL
|
||||
u.Path = "v1/armoFrameworks"
|
||||
q := u.Query()
|
||||
q.Add("customerGUID", "11111111-1111-1111-1111-111111111111")
|
||||
q.Add("frameworkName", strings.ToUpper(frameworkName))
|
||||
q.Add("getRules", "true")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, 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)
|
||||
// if clusterName != "" { // TODO - fix customer name support in Armo BE
|
||||
// q.Add("clusterName", clusterName)
|
||||
// }
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getCustomerURL() string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = armoAPI.apiURL
|
||||
u.Path = "api/v1/createTenant"
|
||||
return u.String()
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package getter
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
@@ -23,16 +23,14 @@ type DownloadReleasedPolicy struct {
|
||||
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
|
||||
return &DownloadReleasedPolicy{
|
||||
hostURL: "",
|
||||
httpClient: &http.Client{},
|
||||
httpClient: &http.Client{Timeout: 61 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (drp *DownloadReleasedPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
return []armotypes.PostureExceptionPolicy{}, nil
|
||||
}
|
||||
|
||||
func (drp *DownloadReleasedPolicy) GetFramework(name string) (*opapolicy.Framework, error) {
|
||||
drp.setURL(name)
|
||||
if err := drp.setURL(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respStr, err := HttpGetter(drp.httpClient, drp.hostURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -43,7 +41,7 @@ func (drp *DownloadReleasedPolicy) GetFramework(name string) (*opapolicy.Framewo
|
||||
return framework, err
|
||||
}
|
||||
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name))
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name+".json"))
|
||||
return framework, err
|
||||
}
|
||||
|
||||
@@ -59,7 +57,7 @@ func (drp *DownloadReleasedPolicy) setURL(frameworkName string) error {
|
||||
return fmt.Errorf("failed to download file, status code: %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body from '%s', reason: %s", latestReleases, err.Error())
|
||||
}
|
||||
@@ -76,12 +74,13 @@ func (drp *DownloadReleasedPolicy) setURL(frameworkName string) error {
|
||||
if name == frameworkName {
|
||||
if url, ok := asset["browser_download_url"].(string); ok {
|
||||
drp.hostURL = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return fmt.Errorf("failed to download '%s' - not found", frameworkName)
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ import (
|
||||
|
||||
type IPolicyGetter interface {
|
||||
GetFramework(name string) (*opapolicy.Framework, error)
|
||||
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
// GetScores(scope, customerName, namespace string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
}
|
||||
|
||||
type IExceptionsGetter interface {
|
||||
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
}
|
||||
type IBackend interface {
|
||||
GetCustomerGUID(customerGUID string) (*TenantResponse, error)
|
||||
}
|
||||
|
||||
@@ -5,30 +5,42 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
func GetDefaultPath(frameworkName string) string {
|
||||
defaultfilePath := filepath.Join(DefaultLocalStore, frameworkName+".json")
|
||||
func GetDefaultPath(name string) string {
|
||||
defaultfilePath := filepath.Join(DefaultLocalStore, name)
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
defaultfilePath = filepath.Join(homeDir, defaultfilePath)
|
||||
}
|
||||
return defaultfilePath
|
||||
}
|
||||
|
||||
func SaveFrameworkInFile(framework *opapolicy.Framework, path string) error {
|
||||
func SaveFrameworkInFile(framework *opapolicy.Framework, pathStr string) error {
|
||||
encodedData, err := json.Marshal(framework)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(path, []byte(fmt.Sprintf("%v", string(encodedData))), 0644)
|
||||
err = os.WriteFile(pathStr, []byte(fmt.Sprintf("%v", string(encodedData))), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
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
|
||||
}
|
||||
@@ -86,29 +98,3 @@ func httpRespToString(resp *http.Response) (string, error) {
|
||||
|
||||
return respStr, err
|
||||
}
|
||||
|
||||
// URLEncoder encode url
|
||||
func urlEncoder(oldURL string) string {
|
||||
fullURL := strings.Split(oldURL, "?")
|
||||
baseURL, err := url.Parse(fullURL[0])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Prepare Query Parameters
|
||||
if len(fullURL) > 1 {
|
||||
params := url.Values{}
|
||||
queryParams := strings.Split(fullURL[1], "&")
|
||||
for _, i := range queryParams {
|
||||
queryParam := strings.Split(i, "=")
|
||||
val := ""
|
||||
if len(queryParam) > 1 {
|
||||
val = queryParam[1]
|
||||
}
|
||||
params.Add(queryParam[0], val)
|
||||
}
|
||||
baseURL.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
return baseURL.String()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package getter
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
@@ -29,7 +29,7 @@ func NewLoadPolicy(filePath string) *LoadPolicy {
|
||||
func (lp *LoadPolicy) GetFramework(frameworkName string) (*opapolicy.Framework, error) {
|
||||
|
||||
framework := &opapolicy.Framework{}
|
||||
f, err := ioutil.ReadFile(lp.filePath)
|
||||
f, err := os.ReadFile(lp.filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (lp *LoadPolicy) GetFramework(frameworkName string) (*opapolicy.Framework,
|
||||
func (lp *LoadPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
|
||||
exception := []armotypes.PostureExceptionPolicy{}
|
||||
f, err := ioutil.ReadFile(lp.filePath)
|
||||
f, err := os.ReadFile(lp.filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -105,7 +105,7 @@ func getAzureAADAccessToken() (string, error) {
|
||||
}
|
||||
|
||||
// Pull out response body
|
||||
responseBytes, err := ioutil.ReadAll(resp.Body)
|
||||
responseBytes, err := io.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading response body : %v", err)
|
||||
@@ -173,7 +173,7 @@ func excahngeAzureAADAccessTokenForACRRefreshToken(registry, tenantID, azureAADA
|
||||
}
|
||||
|
||||
// Pull out response body
|
||||
responseBytes, err := ioutil.ReadAll(resp.Body)
|
||||
responseBytes, err := io.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading response body : %v", err)
|
||||
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
// DO NOT REMOVE - load cloud providers auth
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
)
|
||||
|
||||
var ConnectedToCluster = true
|
||||
|
||||
// K8SConfig pointer to k8s config
|
||||
var K8SConfig *restclient.Config
|
||||
|
||||
@@ -27,8 +30,15 @@ type KubernetesApi struct {
|
||||
|
||||
// NewKubernetesApi -
|
||||
func NewKubernetesApi() *KubernetesApi {
|
||||
var kubernetesClient *kubernetes.Clientset
|
||||
var err error
|
||||
|
||||
kubernetesClient, err := kubernetes.NewForConfig(GetK8sConfig())
|
||||
if !IsConnectedToCluster() {
|
||||
fmt.Println(fmt.Errorf("failed to load kubernetes config: no configuration has been provided, try setting KUBECONFIG environment variable"))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
kubernetesClient, err = kubernetes.NewForConfig(GetK8sConfig())
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to load config file, reason: %s", err.Error())
|
||||
os.Exit(1)
|
||||
@@ -53,23 +63,59 @@ var RunningIncluster bool
|
||||
func LoadK8sConfig() error {
|
||||
kubeconfig, err := config.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubernetes config: %s\n", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
|
||||
return fmt.Errorf("failed to load kubernetes config: %s", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
|
||||
}
|
||||
if _, err := restclient.InClusterConfig(); err == nil {
|
||||
RunningIncluster = true
|
||||
}
|
||||
|
||||
K8SConfig = kubeconfig
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetK8sConfig get config. load if not loaded yet
|
||||
func GetK8sConfig() *restclient.Config {
|
||||
if K8SConfig == nil {
|
||||
if err := LoadK8sConfig(); err != nil {
|
||||
// print error
|
||||
fmt.Printf("%s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if !IsConnectedToCluster() {
|
||||
return nil
|
||||
}
|
||||
return K8SConfig
|
||||
}
|
||||
|
||||
func IsConnectedToCluster() bool {
|
||||
if K8SConfig == nil {
|
||||
if err := LoadK8sConfig(); err != nil {
|
||||
ConnectedToCluster = false
|
||||
}
|
||||
}
|
||||
return ConnectedToCluster
|
||||
}
|
||||
func GetClusterName() string {
|
||||
if !ConnectedToCluster {
|
||||
return ""
|
||||
}
|
||||
|
||||
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{})
|
||||
config, err := kubeConfig.RawConfig()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// TODO - Handle if empty
|
||||
return config.CurrentContext
|
||||
}
|
||||
|
||||
func GetDefaultNamespace() string {
|
||||
defaultNamespace := "default"
|
||||
clientCfg, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
|
||||
if err != nil {
|
||||
return defaultNamespace
|
||||
}
|
||||
apiContext, ok := clientCfg.Contexts[clientCfg.CurrentContext]
|
||||
if !ok || apiContext == nil {
|
||||
return defaultNamespace
|
||||
}
|
||||
namespace := apiContext.Namespace
|
||||
if apiContext.Namespace == "" {
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
return namespace
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func NewKubernetesApiMock() *KubernetesApi {
|
||||
// } else {
|
||||
// bla, _ := json.Marshal(clientResource)
|
||||
// // t.Errorf("BearerToken: %v", *K8SConfig)
|
||||
// // ioutil.WriteFile("bla.json", bla, 777)
|
||||
// // os.WriteFile("bla.json", bla, 777)
|
||||
// t.Errorf("clientResource: %s", string(bla))
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -41,11 +41,13 @@ type FrameworkReport struct {
|
||||
}
|
||||
type ControlReport struct {
|
||||
armotypes.PortalBase `json:",inline"`
|
||||
Control_ID string `json:"id,omitempty"` // to be Deprecated
|
||||
ControlID string `json:"controlID"`
|
||||
Name string `json:"name"`
|
||||
RuleReports []RuleReport `json:"ruleReports"`
|
||||
Remediation string `json:"remediation"`
|
||||
Description string `json:"description"`
|
||||
Score float32 `json:"score,omitempty"`
|
||||
Score float32 `json:"score"`
|
||||
BaseScore float32 `json:"baseScore,omitempty"`
|
||||
ARMOImprovement float32 `json:"ARMOImprovement,omitempty"`
|
||||
}
|
||||
@@ -100,6 +102,8 @@ type PolicyRule struct {
|
||||
// Control represents a collection of rules which are combined together to single purpose
|
||||
type Control struct {
|
||||
armotypes.PortalBase `json:",inline"`
|
||||
Control_ID string `json:"id,omitempty"` // to be Deprecated
|
||||
ControlID string `json:"controlID"`
|
||||
CreationTime string `json:"creationTime"`
|
||||
Description string `json:"description"`
|
||||
Remediation string `json:"remediation"`
|
||||
|
||||
@@ -33,7 +33,8 @@ func MockFrameworkReportA() *FrameworkReport {
|
||||
Name: AMockFrameworkName,
|
||||
ControlReports: []ControlReport{
|
||||
{
|
||||
Name: AMockControlName,
|
||||
ControlID: "C-0010",
|
||||
Name: AMockControlName,
|
||||
RuleReports: []RuleReport{
|
||||
{
|
||||
Name: AMockRuleName,
|
||||
|
||||
@@ -83,11 +83,11 @@ func (controlReport *ControlReport) ListControlsInputKinds() []string {
|
||||
|
||||
func (controlReport *ControlReport) Passed() bool {
|
||||
for i := range controlReport.RuleReports {
|
||||
if len(controlReport.RuleReports[i].RuleResponses) == 0 {
|
||||
return true
|
||||
if len(controlReport.RuleReports[i].RuleResponses) != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) Warning() bool {
|
||||
@@ -120,9 +120,9 @@ func (ruleReport *RuleReport) GetNumberOfResources() int {
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfFailedResources() int {
|
||||
sum := 0
|
||||
for i := range ruleReport.RuleResponses {
|
||||
for i := len(ruleReport.RuleResponses) - 1; i >= 0; i-- {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "failed" {
|
||||
sum += 1
|
||||
sum += len(ruleReport.RuleResponses[i].AlertObject.K8SApiObjects)
|
||||
}
|
||||
}
|
||||
return sum
|
||||
@@ -132,8 +132,67 @@ func (ruleReport *RuleReport) GetNumberOfWarningResources() int {
|
||||
sum := 0
|
||||
for i := range ruleReport.RuleResponses {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "warning" {
|
||||
sum += 1
|
||||
sum += len(ruleReport.RuleResponses[i].AlertObject.K8SApiObjects)
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (postureReport *PostureReport) RemoveData() {
|
||||
for i := range postureReport.FrameworkReports {
|
||||
postureReport.FrameworkReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
func (frameworkReport *FrameworkReport) RemoveData() {
|
||||
for i := range frameworkReport.ControlReports {
|
||||
frameworkReport.ControlReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
func (controlReport *ControlReport) RemoveData() {
|
||||
for i := range controlReport.RuleReports {
|
||||
controlReport.RuleReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) RemoveData() {
|
||||
for i := range ruleReport.RuleResponses {
|
||||
ruleReport.RuleResponses[i].RemoveData()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RuleResponse) RemoveData() {
|
||||
r.AlertObject.ExternalObjects = nil
|
||||
|
||||
keepFields := []string{"kind", "apiVersion", "metadata"}
|
||||
keepMetadataFields := []string{"name", "namespace", "labels"}
|
||||
|
||||
for i := range r.AlertObject.K8SApiObjects {
|
||||
deleteFromMap(r.AlertObject.K8SApiObjects[i], keepFields)
|
||||
for k := range r.AlertObject.K8SApiObjects[i] {
|
||||
if k == "metadata" {
|
||||
if b, ok := r.AlertObject.K8SApiObjects[i][k].(map[string]interface{}); ok {
|
||||
deleteFromMap(b, keepMetadataFields)
|
||||
r.AlertObject.K8SApiObjects[i][k] = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFromMap(m map[string]interface{}, keepFields []string) {
|
||||
for k := range m {
|
||||
if StringInSlice(keepFields, k) {
|
||||
continue
|
||||
}
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
func StringInSlice(strSlice []string, str string) bool {
|
||||
for i := range strSlice {
|
||||
if strSlice[i] == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package resources
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -94,7 +93,7 @@ func LoadRegoFiles(dir string) map[string]string {
|
||||
// Compile the module. The keys are used as identifiers in error messages.
|
||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil && strings.HasSuffix(path, ".rego") && !info.IsDir() {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
glog.Errorf("LoadRegoFiles, Failed to load: %s: %v", path, err)
|
||||
} else {
|
||||
|
||||
@@ -1,17 +1 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadRegoDependenciesFromDir(t *testing.T) {
|
||||
dir, _ := os.Getwd()
|
||||
t.Errorf("%s", filepath.Join(dir, "rego/dependencies"))
|
||||
return
|
||||
// modules := LoadRegoDependenciesFromDir("")
|
||||
// if len(modules) == 0 {
|
||||
// t.Errorf("modules len == 0")
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -10,19 +10,23 @@ import (
|
||||
type ScanInfo struct {
|
||||
Getters
|
||||
PolicyIdentifier opapolicy.PolicyIdentifier
|
||||
UseExceptions string
|
||||
UseFrom string
|
||||
UseDefault bool
|
||||
Format string
|
||||
Output string
|
||||
ExcludedNamespaces string
|
||||
InputPatterns []string
|
||||
Silent bool
|
||||
FailThreshold uint16
|
||||
UseExceptions string // Load exceptions configuration
|
||||
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
|
||||
Format string // Format results (table, json, junit ...)
|
||||
Output string // Store results in an output file, Output file name
|
||||
ExcludedNamespaces string // DEPRECATED?
|
||||
InputPatterns []string // Yaml files input patterns
|
||||
Silent bool // Silent mode - Do not print progress logs
|
||||
FailThreshold uint16 // Failure score threshold
|
||||
DoNotSendResults bool // DEPRECATED
|
||||
Submit bool // Submit results to Armo BE
|
||||
Local bool // Do not submit results
|
||||
Account string // account ID
|
||||
}
|
||||
|
||||
type Getters struct {
|
||||
ExceptionsGetter getter.IPolicyGetter
|
||||
ExceptionsGetter getter.IExceptionsGetter
|
||||
PolicyGetter getter.IPolicyGetter
|
||||
}
|
||||
|
||||
@@ -39,7 +43,7 @@ func (scanInfo *ScanInfo) setUseExceptions() {
|
||||
// load exceptions from file
|
||||
scanInfo.ExceptionsGetter = getter.NewLoadPolicy(scanInfo.UseExceptions)
|
||||
} else {
|
||||
scanInfo.ExceptionsGetter = getter.NewArmoAPI()
|
||||
scanInfo.ExceptionsGetter = getter.GetArmoAPIConnector()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,7 +52,7 @@ func (scanInfo *ScanInfo) setUseFrom() {
|
||||
return
|
||||
}
|
||||
if scanInfo.UseDefault {
|
||||
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name)
|
||||
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name + ".json")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -66,12 +70,12 @@ func (scanInfo *ScanInfo) setOutputFile() {
|
||||
return
|
||||
}
|
||||
if scanInfo.Format == "json" {
|
||||
if filepath.Ext(scanInfo.Output) != "json" {
|
||||
if filepath.Ext(scanInfo.Output) != ".json" {
|
||||
scanInfo.Output += ".json"
|
||||
}
|
||||
}
|
||||
if scanInfo.Format == "junit" {
|
||||
if filepath.Ext(scanInfo.Output) != "xml" {
|
||||
if filepath.Ext(scanInfo.Output) != ".xml" {
|
||||
scanInfo.Output += ".xml"
|
||||
}
|
||||
}
|
||||
@@ -80,3 +84,8 @@ func (scanInfo *ScanInfo) setOutputFile() {
|
||||
func (scanInfo *ScanInfo) ScanRunningCluster() bool {
|
||||
return len(scanInfo.InputPatterns) == 0
|
||||
}
|
||||
|
||||
// func (scanInfo *ScanInfo) ConnectedToCluster(k8s k8sinterface.) bool {
|
||||
// _, err := k8s.KubernetesClient.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
|
||||
// return err == nil
|
||||
// }
|
||||
|
||||
@@ -13,7 +13,13 @@ func TestConvertLabelsToString(t *testing.T) {
|
||||
spilltedA := strings.Split(rsrt, ";")
|
||||
spilltedB := strings.Split(str, ";")
|
||||
for i := range spilltedA {
|
||||
if spilltedA[i] != spilltedB[i] {
|
||||
exists := false
|
||||
for j := range spilltedB {
|
||||
if spilltedB[j] == spilltedA[i] {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("%s != %s", spilltedA[i], spilltedB[i])
|
||||
}
|
||||
}
|
||||
|
||||
18
cmd/cluster.go
Normal file
18
cmd/cluster.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// clusterCmd represents the cluster command
|
||||
var clusterCmd = &cobra.Command{
|
||||
Use: "cluster",
|
||||
Short: "Set configuration for cluster",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(clusterCmd)
|
||||
}
|
||||
50
cmd/cluster_get.go
Normal file
50
cmd/cluster_get.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <key>",
|
||||
Short: "Get configuration in cluster",
|
||||
Long: ``,
|
||||
ValidArgs: supportedFrameworks,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
|
||||
k8s := k8sinterface.NewKubernetesApi()
|
||||
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector())
|
||||
val, err := clusterConfig.GetValueByKeyFromConfigMap(key)
|
||||
if err != nil {
|
||||
if err.Error() == "value does not exist." {
|
||||
fmt.Printf("Could net get value from configmap, reason: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Println(key + "=" + val)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
clusterCmd.AddCommand(getCmd)
|
||||
}
|
||||
44
cmd/cluster_set.go
Normal file
44
cmd/cluster_set.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var setCmd = &cobra.Command{
|
||||
Use: "set <key>=<value>",
|
||||
Short: "Set 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: <key>=<value>")
|
||||
}
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 2 {
|
||||
return fmt.Errorf("requires one argument: <key>=<value>")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
data := keyValue[1]
|
||||
|
||||
k8s := k8sinterface.NewKubernetesApi()
|
||||
clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector())
|
||||
if err := clusterConfig.SetKeyValueInConfigmap(key, data); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Value added successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
clusterCmd.AddCommand(setCmd)
|
||||
}
|
||||
18
cmd/config.go
Normal file
18
cmd/config.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Set configuration",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
var downloadInfo cautils.DownloadInfo
|
||||
|
||||
var downloadCmd = &cobra.Command{
|
||||
Use: "download framework <framework-name>",
|
||||
Use: fmt.Sprintf("download framework <framework-name> [flags]\nSupported frameworks: %s", validFrameworks),
|
||||
Short: "Download framework controls",
|
||||
Long: ``,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -24,7 +24,7 @@ var downloadCmd = &cobra.Command{
|
||||
downloadInfo.FrameworkName = args[1]
|
||||
g := getter.NewDownloadReleasedPolicy()
|
||||
if downloadInfo.Path == "" {
|
||||
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName)
|
||||
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName + ".json")
|
||||
}
|
||||
frameworks, err := g.GetFramework(downloadInfo.FrameworkName)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/opaprocessor"
|
||||
@@ -22,7 +21,8 @@ import (
|
||||
)
|
||||
|
||||
var scanInfo cautils.ScanInfo
|
||||
var supportedFrameworks = []string{"nsa"}
|
||||
var supportedFrameworks = []string{"nsa", "mitre"}
|
||||
var validFrameworks = strings.Join(supportedFrameworks, ", ")
|
||||
|
||||
type CLIHandler struct {
|
||||
policyHandler *policyhandler.PolicyHandler
|
||||
@@ -30,7 +30,8 @@ type CLIHandler struct {
|
||||
}
|
||||
|
||||
var frameworkCmd = &cobra.Command{
|
||||
Use: "framework <framework name> [`<glob patter>`/`-`] [flags]",
|
||||
|
||||
Use: fmt.Sprintf("framework <framework name> [`<glob pattern>`/`-`] [flags]\nSupported frameworks: %s", validFrameworks),
|
||||
Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(supportedFrameworks, ", ")),
|
||||
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
|
||||
ValidArgs: supportedFrameworks,
|
||||
@@ -38,7 +39,7 @@ var frameworkCmd = &cobra.Command{
|
||||
if len(args) < 1 && !(cmd.Flags().Lookup("use-from").Changed) {
|
||||
return fmt.Errorf("requires at least one argument")
|
||||
} else if len(args) > 0 {
|
||||
if !isValidFramework(args[0]) {
|
||||
if !isValidFramework(strings.ToLower(args[0])) {
|
||||
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
|
||||
}
|
||||
}
|
||||
@@ -49,13 +50,13 @@ var frameworkCmd = &cobra.Command{
|
||||
scanInfo.PolicyIdentifier.Kind = opapolicy.KindFramework
|
||||
|
||||
if !(cmd.Flags().Lookup("use-from").Changed) {
|
||||
scanInfo.PolicyIdentifier.Name = args[0]
|
||||
scanInfo.PolicyIdentifier.Name = strings.ToLower(args[0])
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if len(args[1:]) == 0 || args[1] != "-" {
|
||||
scanInfo.InputPatterns = args[1:]
|
||||
} else { // store stout to file
|
||||
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
|
||||
tempFile, err := os.CreateTemp(".", "tmp-kubescape*.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -85,28 +86,33 @@ func isValidFramework(framework string) bool {
|
||||
func init() {
|
||||
scanCmd.AddCommand(frameworkCmd)
|
||||
scanInfo = cautils.ScanInfo{}
|
||||
frameworkCmd.Flags().StringVar(&scanInfo.UseFrom, "use-from", "", "Path to load framework from")
|
||||
frameworkCmd.Flags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load framework from default path")
|
||||
frameworkCmd.Flags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to file containing list of exceptions")
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from check")
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. supported formats: "pretty-printer"/"json"/"junit"`)
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. print output to file and not stdout")
|
||||
frameworkCmd.Flags().StringVar(&scanInfo.UseFrom, "use-from", "", "Load local framework object from specified path. If not used will download latest")
|
||||
frameworkCmd.Flags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load local framework object from default path. If not used will download latest")
|
||||
frameworkCmd.Flags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from Armo management portal")
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system, kube-public")
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer"/"json"/"junit"`)
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages")
|
||||
frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code -1")
|
||||
frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code 1")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Deprecated. Please use `--keep-local` instead")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.Submit, "submit", "", false, "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 submitted")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.Local, "keep-local", "", false, "If you do not want your Kubescape results reported to Armo backend. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results")
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file")
|
||||
|
||||
}
|
||||
|
||||
func CliSetup() error {
|
||||
flag.Parse()
|
||||
|
||||
if 100 < scanInfo.FailThreshold {
|
||||
fmt.Println("bad argument: out of range threshold")
|
||||
os.Exit(1)
|
||||
}
|
||||
flagValidation()
|
||||
|
||||
var k8s *k8sinterface.KubernetesApi
|
||||
if scanInfo.ScanRunningCluster() {
|
||||
var clusterConfig cautils.IClusterConfig
|
||||
if !scanInfo.ScanRunningCluster() {
|
||||
k8sinterface.ConnectedToCluster = false
|
||||
clusterConfig = cautils.NewEmptyConfig()
|
||||
} else {
|
||||
k8s = k8sinterface.NewKubernetesApi()
|
||||
// setup cluster config
|
||||
clusterConfig = cautils.ClusterConfigSetup(&scanInfo, k8s, getter.GetArmoAPIConnector())
|
||||
}
|
||||
|
||||
processNotification := make(chan *cautils.OPASessionObj)
|
||||
@@ -115,12 +121,22 @@ func CliSetup() error {
|
||||
// policy handler setup
|
||||
policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
|
||||
|
||||
// cli handler setup
|
||||
cli := NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
panic(err)
|
||||
if err := clusterConfig.SetCustomerGUID(scanInfo.Account); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
cautils.CustomerGUID = clusterConfig.GetCustomerGUID()
|
||||
cautils.ClusterName = k8sinterface.GetClusterName()
|
||||
|
||||
// cli handler setup
|
||||
go func() {
|
||||
cli := NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// processor setup - rego run
|
||||
go func() {
|
||||
opaprocessorObj := opaprocessor.NewOPAProcessorHandler(&processNotification, &reportResults)
|
||||
@@ -130,6 +146,9 @@ func CliSetup() error {
|
||||
resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output))
|
||||
score := resultsHandling.HandleResults()
|
||||
|
||||
// print report url
|
||||
clusterConfig.GenerateURL()
|
||||
|
||||
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
|
||||
if score < adjustedFailThreshold {
|
||||
return fmt.Errorf("Scan score is bellow threshold")
|
||||
@@ -156,14 +175,27 @@ func (clihandler *CLIHandler) Scan() error {
|
||||
}
|
||||
switch policyNotification.NotificationType {
|
||||
case opapolicy.TypeExecPostureScan:
|
||||
go func() {
|
||||
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
//
|
||||
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("notification type '%s' Unknown", policyNotification.NotificationType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func flagValidation() {
|
||||
if scanInfo.DoNotSendResults {
|
||||
fmt.Println("Deprecated. Please use `--keep-local` instead")
|
||||
}
|
||||
|
||||
if scanInfo.Submit && scanInfo.Local {
|
||||
fmt.Println("You can use `keep-local` or `submit`, but not both")
|
||||
os.Exit(1)
|
||||
}
|
||||
if 100 < scanInfo.FailThreshold {
|
||||
fmt.Println("bad argument: out of range threshold")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
17
cmd/local.go
Normal file
17
cmd/local.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var localCmd = &cobra.Command{
|
||||
Use: "local",
|
||||
Short: "Set configuration locally (for config.json)",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(localCmd)
|
||||
}
|
||||
45
cmd/local_get.go
Normal file
45
cmd/local_get.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var localGetCmd = &cobra.Command{
|
||||
Use: "get <key>",
|
||||
Short: "Get configuration locally",
|
||||
Long: ``,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
|
||||
val, err := cautils.GetValueFromConfigJson(key)
|
||||
if err != nil {
|
||||
if err.Error() == "value does not exist." {
|
||||
fmt.Printf("Could net get value from: %s, reason: %s\n", cautils.ConfigFileFullPath(), err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Println(key + "=" + val)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
localCmd.AddCommand(localGetCmd)
|
||||
}
|
||||
40
cmd/local_set.go
Normal file
40
cmd/local_set.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var localSetCmd = &cobra.Command{
|
||||
Use: "set <key>=<value>",
|
||||
Short: "Set configuration locally",
|
||||
Long: ``,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 1 {
|
||||
return fmt.Errorf("requires one argument: <key>=<value>")
|
||||
}
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 2 {
|
||||
return fmt.Errorf("requires one argument: <key>=<value>")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
data := keyValue[1]
|
||||
|
||||
if err := cautils.SetKeyValueInConfigJson(key, data); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Value added successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
localCmd.AddCommand(localSetCmd)
|
||||
}
|
||||
45
cmd/root.go
45
cmd/root.go
@@ -1,15 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/golang/glog"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var armoBEURLs = ""
|
||||
|
||||
const envFlagUsage = "Send report results to specific URL. Format:<ReportReceiver>,<Backend>,<Frontend>.\n\t\tExample:report.armo.cloud,api.armo.cloud,portal.armo.cloud"
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kubescape",
|
||||
Short: "Kubescape is a tool for testing Kubernetes security posture",
|
||||
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA specifications.`,
|
||||
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA \ MITRE ATT&CK® specifications.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
flag.Parse()
|
||||
InitArmoBEConnector()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
@@ -17,9 +31,38 @@ func Execute() {
|
||||
}
|
||||
|
||||
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 {
|
||||
glog.Errorf("Too many URLs")
|
||||
os.Exit(1)
|
||||
}
|
||||
switch len(urlSlices) {
|
||||
case 1:
|
||||
switch urlSlices[0] {
|
||||
case "dev":
|
||||
getter.SetARMOAPIConnector(getter.NewARMOAPIDev())
|
||||
case "":
|
||||
getter.SetARMOAPIConnector(getter.NewARMOAPIProd())
|
||||
default:
|
||||
glog.Errorf("--environment flag usage: %s", envFlagUsage)
|
||||
os.Exit(1)
|
||||
}
|
||||
case 2:
|
||||
glog.Errorf("--environment flag usage: %s", envFlagUsage)
|
||||
os.Exit(1)
|
||||
case 3:
|
||||
getter.SetARMOAPIConnector(getter.NewARMOAPICustomized(urlSlices[0], urlSlices[1], urlSlices[2]))
|
||||
}
|
||||
}
|
||||
|
||||
49
cmd/version.go
Normal file
49
cmd/version.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var BuildNumber string
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Get current version",
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("Your current version is: " + BuildNumber)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func GetLatestVersion() (string, error) {
|
||||
latestVersion := "https://api.github.com/repos/armosec/kubescape/releases/latest"
|
||||
resp, err := http.Get(latestVersion)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestVersion, err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || 301 < resp.StatusCode {
|
||||
return "", fmt.Errorf("failed to download file, status code: %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response body from '%s', reason: %s", latestVersion, err.Error())
|
||||
}
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestVersion, err.Error())
|
||||
}
|
||||
return fmt.Sprintf("%v", data["tag_name"]), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
BIN
docs/discord-banner.png
Normal file
BIN
docs/discord-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/favicon.ico
Normal file
BIN
docs/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
24
docs/index.html
Normal file
24
docs/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>
|
||||
Kubscape Website
|
||||
</title>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<style>
|
||||
img {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<img src="kubescape.png" alt="Kubescap logo" style="width:20%">
|
||||
<iframe src="https://discordapp.com/widget?id=893048809884643379&theme=dark" width="350" height="500"
|
||||
allowtransparency="true" frameborder="0"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"></iframe>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
89
docs/run-options.md
Normal file
89
docs/run-options.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<img src="kubescape.png" width="300" alt="logo" align="center">
|
||||
|
||||
# More detailed look on command line arguments and options
|
||||
|
||||
## Simple run:
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| flag | default | description | options |
|
||||
| --- | --- | --- | --- |
|
||||
| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces |
|
||||
| `-s`/`--silent` | Display progress messages | Silent progress messages |
|
||||
| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold| `0` -> `100` |
|
||||
| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` |
|
||||
| `-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-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 |
|
||||
| `--results-locally` | `false` | Kubescape sends scan results to Armo management portal to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features | `true`/`false`|
|
||||
|
||||
## Usage & Examples
|
||||
|
||||
### Examples
|
||||
|
||||
* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
* Scan local `yaml`/`json` files before deploying
|
||||
```
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
|
||||
* Scan `yaml`/`json` files from url
|
||||
```
|
||||
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
|
||||
```
|
||||
|
||||
* Output in `json` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
|
||||
```
|
||||
|
||||
* Output in `junit xml` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
|
||||
```
|
||||
|
||||
* Scan with exceptions, objects with exceptions will be presented as `warning` and not `fail` <img src="docs/new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa --exceptions examples/exceptions.json
|
||||
```
|
||||
|
||||
### Helm Support
|
||||
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
|
||||
```
|
||||
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
for example:
|
||||
```
|
||||
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
### Offline Support <img src="docs/new-feature.svg">
|
||||
|
||||
It is possible to run Kubescape offline!
|
||||
|
||||
First download the framework and then scan with `--use-from` flag
|
||||
|
||||
* Download and save in file, if file name not specified, will store save to `~/.kubescape/<framework name>.json`
|
||||
```
|
||||
kubescape download framework nsa --output nsa.json
|
||||
```
|
||||
|
||||
* Scan using the downloaded framework
|
||||
```
|
||||
kubescape scan framework nsa --use-from nsa.json
|
||||
```
|
||||
|
||||
Kubescape is an open source project, we welcome your feedback and ideas for improvement. We’re also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.
|
||||
|
||||
|
||||
BIN
docs/summary.png
BIN
docs/summary.png
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
56
go.mod
56
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/armosec/kubescape
|
||||
|
||||
go 1.16
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.40.30
|
||||
@@ -28,3 +28,57 @@ require (
|
||||
k8s.io/client-go v0.22.1
|
||||
sigs.k8s.io/controller-runtime v0.9.6
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.81.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.18 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
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/bytecodealliance/wasmtime-go v0.28.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-logr/logr v0.4.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.2.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
k8s.io/klog/v2 v2.9.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
|
||||
k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
26
install.ps1
Normal file
26
install.ps1
Normal file
@@ -0,0 +1,26 @@
|
||||
Write-Host "Installing Kubescape..." -ForegroundColor Cyan
|
||||
|
||||
$BASE_DIR=$env:USERPROFILE + "\.kubescape"
|
||||
$packageName = "/kubescape-windows-latest"
|
||||
|
||||
# Get latest release url
|
||||
$config = Invoke-WebRequest "https://api.github.com/repos/armosec/kubescape/releases/latest" | ConvertFrom-Json
|
||||
$url = $config.html_url.Replace("/tag/","/download/")
|
||||
$fullUrl = $url + $packageName
|
||||
|
||||
# Create a new directory if needed
|
||||
New-Item -Path $BASE_DIR -ItemType "directory" -ErrorAction SilentlyContinue
|
||||
|
||||
# Download the binary
|
||||
Invoke-WebRequest -Uri $fullUrl -OutFile $BASE_DIR\kubescape.exe
|
||||
|
||||
# Update user PATH if needed
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
if (-not $currentPath.Contains($BASE_DIR)) {
|
||||
$confirmation = Read-Host "Add kubescape to user path? (y/n)"
|
||||
if ($confirmation -eq 'y') {
|
||||
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$BASE_DIR;", "User")
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Finished Installation" -ForegroundColor Green
|
||||
56
install.sh
56
install.sh
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Installing Kubescape..."
|
||||
echo -e "\033[0;36mInstalling Kubescape..."
|
||||
echo
|
||||
|
||||
BASE_DIR=~/.kubescape
|
||||
@@ -9,36 +9,50 @@ KUBESCAPE_EXEC=kubescape
|
||||
|
||||
osName=$(uname -s)
|
||||
if [[ $osName == *"MINGW"* ]]; then
|
||||
osName=windows-latest
|
||||
osName=windows
|
||||
elif [[ $osName == *"Darwin"* ]]; then
|
||||
osName=macos-latest
|
||||
osName=macos
|
||||
else
|
||||
osName=ubuntu-latest
|
||||
osName=ubuntu
|
||||
fi
|
||||
|
||||
GITHUB_OWNER=armosec
|
||||
|
||||
DOWNLOAD_URL=$(curl --silent "https://api.github.com/repos/$GITHUB_OWNER/kubescape/releases/latest" | grep -o "browser_download_url.*${osName}.*")
|
||||
DOWNLOAD_URL=${DOWNLOAD_URL//\"}
|
||||
DOWNLOAD_URL=${DOWNLOAD_URL/browser_download_url: /}
|
||||
|
||||
mkdir -p $BASE_DIR
|
||||
|
||||
OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC
|
||||
|
||||
DOWNLOAD_URL="https://github.com/armosec/kubescape/releases/latest/download/kubescape-${osName}-latest"
|
||||
curl --progress-bar -L $DOWNLOAD_URL -o $OUTPUT
|
||||
echo -e "\033[32m[V] Downloaded Kubescape"
|
||||
|
||||
# Ping download counter
|
||||
curl --silent https://us-central1-elated-pottery-310110.cloudfunctions.net/kubescape-download-counter -o /dev/null
|
||||
|
||||
chmod +x $OUTPUT || sudo chmod +x $OUTPUT
|
||||
rm -f /usr/local/bin/$KUBESCAPE_EXEC || sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
|
||||
cp $OUTPUT /usr/local/bin || sudo cp $OUTPUT /usr/local/bin
|
||||
# Checking if SUDO needed/exists
|
||||
SUDO=
|
||||
if [ "$(id -u)" -ne 0 ] && [ -n "$(which sudo)" ]; then
|
||||
SUDO=sudo
|
||||
fi
|
||||
|
||||
|
||||
# Find install dir
|
||||
install_dir=/usr/local/bin #default
|
||||
for pdir in ${PATH//:/ }; do
|
||||
edir="${pdir/#\~/$HOME}"
|
||||
if [[ $edir == $HOME/* ]]; then
|
||||
install_dir=$edir
|
||||
mkdir -p $install_dir 2>/dev/null || true
|
||||
SUDO=
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
chmod +x $OUTPUT 2>/dev/null
|
||||
$SUDO rm -f /usr/local/bin/$KUBESCAPE_EXEC 2>/dev/null || true # clearning up old install
|
||||
$SUDO cp $OUTPUT $install_dir/$KUBESCAPE_EXEC
|
||||
rm -rf $OUTPUT
|
||||
|
||||
echo -e "[V] Finished Installation"
|
||||
echo
|
||||
echo -e "\033[32mFinished Installation."
|
||||
|
||||
echo -e "\033[0m"
|
||||
$KUBESCAPE_EXEC version
|
||||
echo
|
||||
|
||||
echo -e "\033[35m Usage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
|
||||
echo
|
||||
echo -e "\033[35mUsage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
|
||||
|
||||
echo -e "\033[0m"
|
||||
|
||||
18
main.go
18
main.go
@@ -1,7 +1,23 @@
|
||||
package main
|
||||
|
||||
import "github.com/armosec/kubescape/cmd"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/armosec/kubescape/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
CheckLatestVersion()
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
func CheckLatestVersion() {
|
||||
latest, err := cmd.GetLatestVersion()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
} else if latest != cmd.BuildNumber {
|
||||
fmt.Println("Warning: You are not updated to the latest release: " + latest)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy/resources"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
const ScoreConfigPath = "/resources/config"
|
||||
@@ -42,7 +42,7 @@ func NewOPAProcessor(sessionObj *cautils.OPASessionObj) *OPAProcessor {
|
||||
|
||||
func NewOPAProcessorHandler(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessorHandler {
|
||||
|
||||
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.K8SConfig)
|
||||
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig())
|
||||
store, err := regoDependenciesData.TOStorage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -70,7 +70,7 @@ func (opaHandler *OPAProcessorHandler) ProcessRulesListenner() {
|
||||
opap.updateResults()
|
||||
|
||||
// update score
|
||||
opap.updateScore()
|
||||
// opap.updateScore()
|
||||
|
||||
// report
|
||||
*opaHandler.reportResults <- opaSessionObj
|
||||
@@ -92,6 +92,7 @@ func (opap *OPAProcessor) Process() error {
|
||||
}
|
||||
|
||||
opap.PostureReport.FrameworkReports = frameworkReports
|
||||
opap.PostureReport.ReportID = uuid.NewV4().String()
|
||||
opap.PostureReport.ReportGenerationTime = time.Now().UTC()
|
||||
// glog.Infof(fmt.Sprintf("Done 'Process'. reportID: %s", opap.PostureReport.ReportID))
|
||||
cautils.StopSpinner()
|
||||
@@ -104,13 +105,16 @@ func (opap *OPAProcessor) processFramework(framework *opapolicy.Framework) (*opa
|
||||
|
||||
frameworkReport := opapolicy.FrameworkReport{}
|
||||
frameworkReport.Name = framework.Name
|
||||
|
||||
controlReports := []opapolicy.ControlReport{}
|
||||
for i := range framework.Controls {
|
||||
controlReport, err := opap.processControl(&framework.Controls[i])
|
||||
if err != nil {
|
||||
errs = fmt.Errorf("%v\n%s", errs, err.Error())
|
||||
}
|
||||
controlReports = append(controlReports, *controlReport)
|
||||
if controlReport != nil {
|
||||
controlReports = append(controlReports, *controlReport)
|
||||
}
|
||||
}
|
||||
frameworkReport.ControlReports = controlReports
|
||||
return &frameworkReport, errs
|
||||
@@ -121,6 +125,8 @@ func (opap *OPAProcessor) processControl(control *opapolicy.Control) (*opapolicy
|
||||
|
||||
controlReport := opapolicy.ControlReport{}
|
||||
controlReport.PortalBase = control.PortalBase
|
||||
controlReport.ControlID = control.ControlID
|
||||
controlReport.Control_ID = control.Control_ID // TODO: delete when 'id' is deprecated
|
||||
|
||||
controlReport.Name = control.Name
|
||||
controlReport.Description = control.Description
|
||||
@@ -136,6 +142,9 @@ func (opap *OPAProcessor) processControl(control *opapolicy.Control) (*opapolicy
|
||||
ruleReports = append(ruleReports, *ruleReport)
|
||||
}
|
||||
}
|
||||
if len(ruleReports) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
controlReport.RuleReports = ruleReports
|
||||
return &controlReport, errs
|
||||
}
|
||||
@@ -219,6 +228,10 @@ func (opap *OPAProcessor) regoEval(inputObj []map[string]interface{}, compiledRe
|
||||
|
||||
func (opap *OPAProcessor) updateScore() {
|
||||
|
||||
if !k8sinterface.ConnectedToCluster {
|
||||
return
|
||||
}
|
||||
|
||||
// calculate score
|
||||
s := score.NewScore(k8sinterface.NewKubernetesApi(), ScoreConfigPath)
|
||||
s.Calculate(opap.PostureReport.FrameworkReports)
|
||||
@@ -226,6 +239,8 @@ func (opap *OPAProcessor) updateScore() {
|
||||
|
||||
func (opap *OPAProcessor) updateResults() {
|
||||
for f, frameworkReport := range opap.PostureReport.FrameworkReports {
|
||||
sumFailed := 0
|
||||
sumTotal := 0
|
||||
for c, controlReport := range opap.PostureReport.FrameworkReports[f].ControlReports {
|
||||
for r, ruleReport := range opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports {
|
||||
// editing the responses -> removing duplications, clearing secret data, etc.
|
||||
@@ -235,6 +250,10 @@ func (opap *OPAProcessor) updateResults() {
|
||||
ruleExceptions := exceptions.ListRuleExceptions(opap.Exceptions, frameworkReport.Name, controlReport.Name, ruleReport.Name)
|
||||
exceptions.AddExceptionsToRuleResponses(opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports[r].RuleResponses, ruleExceptions)
|
||||
}
|
||||
sumFailed += controlReport.GetNumberOfFailedResources()
|
||||
sumTotal += controlReport.GetNumberOfResources()
|
||||
opap.PostureReport.FrameworkReports[f].ControlReports[c].Score = float32(percentage(controlReport.GetNumberOfResources(), controlReport.GetNumberOfFailedResources()))
|
||||
}
|
||||
opap.PostureReport.FrameworkReports[f].Score = float32(percentage(sumTotal, sumTotal-sumFailed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ func editRuleResponses(ruleResponses []opapolicy.RuleResponse) []opapolicy.RuleR
|
||||
// resource found -> remove from slice
|
||||
ruleResponses = removeFromSlice(ruleResponses, i)
|
||||
lenRuleResponses -= 1
|
||||
i -= 1
|
||||
break
|
||||
} else {
|
||||
cleanRuleResponses(w)
|
||||
@@ -139,3 +140,13 @@ func listMatchKinds(match []opapolicy.RuleMatchObjects) []string {
|
||||
}
|
||||
return matchKinds
|
||||
}
|
||||
|
||||
func percentage(big, small int) int {
|
||||
if big == 0 {
|
||||
if small == 0 {
|
||||
return 100
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return int(float64(float64(big-small)/float64(big)) * 100)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -131,7 +130,7 @@ func loadFiles(filePaths []string) ([]k8sinterface.IWorkload, []error) {
|
||||
}
|
||||
|
||||
func loadFile(filePath string) ([]byte, error) {
|
||||
return ioutil.ReadFile(filePath)
|
||||
return os.ReadFile(filePath)
|
||||
}
|
||||
func readFile(fileContent []byte, fileFromat FileFormat) ([]k8sinterface.IWorkload, []error) {
|
||||
|
||||
|
||||
@@ -6,29 +6,20 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
)
|
||||
|
||||
func combine(base, rel string) string {
|
||||
finalPath := []string{}
|
||||
sBase := strings.Split(base, "/")
|
||||
sRel := strings.Split(rel, "/")
|
||||
for i := range sBase {
|
||||
if cautils.StringInSlice(sRel, sBase[i]) != cautils.ValueNotFound {
|
||||
finalPath = append(finalPath, sRel...)
|
||||
break
|
||||
}
|
||||
finalPath = append(finalPath, sBase[i])
|
||||
}
|
||||
return fmt.Sprintf("/%s", filepath.Join(finalPath...))
|
||||
}
|
||||
func onlineBoutiquePath() string {
|
||||
o, _ := os.Getwd()
|
||||
return combine(o, "github.com/armosec/kubescape/examples/online-boutique/*")
|
||||
return filepath.Join(filepath.Dir(o), "examples/online-boutique/*")
|
||||
}
|
||||
|
||||
func TestListFiles(t *testing.T) {
|
||||
files, errs := listFiles([]string{onlineBoutiquePath()})
|
||||
workDir, err := os.Getwd()
|
||||
fmt.Printf("\n------------------\n%s,%v\n--------------\n", workDir, err)
|
||||
filesPath := onlineBoutiquePath()
|
||||
fmt.Printf("\n------------------\n%s\n--------------\n", filesPath)
|
||||
|
||||
files, errs := listFiles([]string{filesPath})
|
||||
if len(errs) > 0 {
|
||||
t.Error(errs)
|
||||
}
|
||||
@@ -44,15 +35,15 @@ func TestLoadFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadFile(t *testing.T) {
|
||||
files, _ := listFiles([]string{strings.Replace(onlineBoutiquePath(), "*", "bi-monitor.yaml", 1)})
|
||||
files, _ := listFiles([]string{strings.Replace(onlineBoutiquePath(), "*", "adservice.yaml", 1)})
|
||||
_, err := loadFile(files[0])
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}
|
||||
func TestLoadResources(t *testing.T) {
|
||||
|
||||
// k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
// policyHandler := &PolicyHandler{}
|
||||
// k8sResources, err := policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
// files, _ := listFiles([]string{onlineBoutiquePath()})
|
||||
// bb, err := loadFile(files[0])
|
||||
// if len(err) > 0 {
|
||||
|
||||
@@ -79,7 +79,7 @@ func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNo
|
||||
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
|
||||
var k8sResources *cautils.K8SResources
|
||||
var err error
|
||||
if scanInfo.ScanRunningCluster() {
|
||||
if k8sinterface.ConnectedToCluster {
|
||||
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, ¬ification.Designators, scanInfo.ExcludedNamespaces)
|
||||
} else {
|
||||
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
|
||||
@@ -2,7 +2,9 @@ package policyhandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
@@ -17,14 +19,16 @@ func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapoli
|
||||
switch rule.Kind {
|
||||
case opapolicy.KindFramework:
|
||||
receivedFramework, recExceptionPolicies, err := policyHandler.getFrameworkPolicies(rule.Name)
|
||||
if err != nil {
|
||||
errs = fmt.Errorf("%v\nKind: %v, Name: %s, error: %s", errs, rule.Kind, rule.Name, err.Error())
|
||||
}
|
||||
if receivedFramework != nil {
|
||||
frameworks = append(frameworks, *receivedFramework)
|
||||
if recExceptionPolicies != nil {
|
||||
exceptionPolicies = append(exceptionPolicies, recExceptionPolicies...)
|
||||
}
|
||||
} else if err != nil {
|
||||
if strings.Contains(err.Error(), "unsupported protocol scheme") {
|
||||
err = fmt.Errorf("failed to download from GitHub release, try running with `--use-default` flag")
|
||||
}
|
||||
return nil, nil, fmt.Errorf("kind: %v, name: %s, error: %s", rule.Kind, rule.Name, err.Error())
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -41,7 +45,7 @@ func (policyHandler *PolicyHandler) getFrameworkPolicies(policyName string) (*op
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions("", "")
|
||||
receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions(cautils.CustomerGUID, cautils.ClusterName)
|
||||
if err != nil {
|
||||
return receivedFramework, nil, err
|
||||
}
|
||||
|
||||
@@ -65,11 +65,8 @@ func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVer
|
||||
|
||||
// set labels
|
||||
listOptions := metav1.ListOptions{}
|
||||
if excludedNamespaces != "" && k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) {
|
||||
excludedNamespacesSlice := strings.Split(excludedNamespaces, ",")
|
||||
for _, excludedNamespace := range excludedNamespacesSlice {
|
||||
listOptions.FieldSelector += "metadata.namespace!=" + excludedNamespace + ","
|
||||
}
|
||||
if excludedNamespaces != "" {
|
||||
setFieldSelector(&listOptions, resource, excludedNamespaces)
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
set := k8slabels.Set(labels)
|
||||
@@ -93,3 +90,18 @@ func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVer
|
||||
return result.Items, nil
|
||||
|
||||
}
|
||||
|
||||
func setFieldSelector(listOptions *metav1.ListOptions, resource *schema.GroupVersionResource, excludedNamespaces string) {
|
||||
fieldSelector := "metadata."
|
||||
if resource.Resource == "namespaces" {
|
||||
fieldSelector += "name"
|
||||
} else if k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) {
|
||||
fieldSelector += "namespace"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
excludedNamespacesSlice := strings.Split(excludedNamespaces, ",")
|
||||
for _, excludedNamespace := range excludedNamespacesSlice {
|
||||
listOptions.FieldSelector += fmt.Sprintf("%s!=%s,", fieldSelector, excludedNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,7 @@ func calculatePostureScore(postureReport *opapolicy.PostureReport) float32 {
|
||||
totalFailed := 0
|
||||
for _, frameworkReport := range postureReport.FrameworkReports {
|
||||
for _, controlReport := range frameworkReport.ControlReports {
|
||||
for _, ruleReport := range controlReport.RuleReports {
|
||||
for _, ruleResponses := range ruleReport.RuleResponses {
|
||||
totalFailed += len(ruleResponses.AlertObject.K8SApiObjects)
|
||||
totalFailed += len(ruleResponses.AlertObject.ExternalObjects)
|
||||
}
|
||||
}
|
||||
totalFailed += controlReport.GetNumberOfFailedResources()
|
||||
totalResources += controlReport.GetNumberOfResources()
|
||||
}
|
||||
}
|
||||
@@ -133,8 +128,8 @@ func (printer *Printer) PrintResults() {
|
||||
|
||||
func (printer *Printer) printSummary(controlName string, controlSummary *ControlSummary) {
|
||||
cautils.SimpleDisplay(printer.writer, "Summary - ")
|
||||
cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed)
|
||||
cautils.WarningDisplay(printer.writer, "Warning:%v ", controlSummary.TotalWarnign)
|
||||
cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarnign)
|
||||
cautils.WarningDisplay(printer.writer, "Excluded:%v ", controlSummary.TotalWarnign)
|
||||
cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed)
|
||||
cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources)
|
||||
if controlSummary.TotalFailed > 0 {
|
||||
@@ -151,7 +146,7 @@ func (printer *Printer) printTitle(controlName string, controlSummary *ControlSu
|
||||
} else if controlSummary.TotalFailed != 0 {
|
||||
cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace)
|
||||
} else if controlSummary.TotalWarnign != 0 {
|
||||
cautils.WarningDisplay(printer.writer, "warning %v\n", emoji.NeutralFace)
|
||||
cautils.WarningDisplay(printer.writer, "excluded %v\n", emoji.NeutralFace)
|
||||
} else {
|
||||
cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
|
||||
}
|
||||
@@ -178,6 +173,10 @@ func (printer *Printer) printResult(controlName string, controlSummary *ControlS
|
||||
|
||||
}
|
||||
|
||||
func (printer *Printer) PrintUrl(url string) {
|
||||
cautils.InfoTextDisplay(printer.writer, url)
|
||||
}
|
||||
|
||||
func generateRow(control string, cs ControlSummary) []string {
|
||||
row := []string{control}
|
||||
row = append(row, cs.ToSlice()...)
|
||||
@@ -190,7 +189,7 @@ func generateRow(control string, cs ControlSummary) []string {
|
||||
}
|
||||
|
||||
func generateHeader() []string {
|
||||
return []string{"Control Name", "Failed Resources", "Warning Resources", "All Resources", "% success"}
|
||||
return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% success"}
|
||||
}
|
||||
|
||||
func percentage(big, small int) int {
|
||||
@@ -247,7 +246,7 @@ func (printer *Printer) getSortedControlsNames() []string {
|
||||
}
|
||||
|
||||
func getWriter(outputFile string) *os.File {
|
||||
|
||||
os.Remove(outputFile)
|
||||
if outputFile != "" {
|
||||
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,6 +28,8 @@ func (report *ReportEventReceiver) ActionSendReportListenner(opaSessionObj *caut
|
||||
if cautils.CustomerGUID == "" {
|
||||
return
|
||||
}
|
||||
//Add score
|
||||
opaSessionObj.PostureReport.RemoveData()
|
||||
if err := report.Send(opaSessionObj.PostureReport); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
@@ -36,12 +37,12 @@ func initEventReceiverURL() *url.URL {
|
||||
urlObj := url.URL{}
|
||||
|
||||
urlObj.Scheme = "https"
|
||||
urlObj.Host = "report.euprod1.cyberarmorsoft.com"
|
||||
urlObj.Host = getter.GetArmoAPIConnector().GetReportReceiverURL()
|
||||
urlObj.Path = "/k8s/postureReport"
|
||||
|
||||
q := urlObj.Query()
|
||||
q.Add("customerGUID", uuid.FromStringOrNil(cautils.CustomerGUID).String())
|
||||
q.Add("clusterName", cautils.ClusterName)
|
||||
|
||||
urlObj.RawQuery = q.Encode()
|
||||
|
||||
return &urlObj
|
||||
@@ -49,9 +50,7 @@ func initEventReceiverURL() *url.URL {
|
||||
|
||||
func hostToString(host *url.URL, reportID string) string {
|
||||
q := host.Query()
|
||||
if reportID != "" {
|
||||
q.Add("reportID", reportID) // TODO - do we add the reportID?
|
||||
}
|
||||
q.Add("reportID", reportID) // TODO - do we add the reportID?
|
||||
host.RawQuery = q.Encode()
|
||||
return host.String()
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ func (resultsHandler *ResultsHandler) HandleResults() float32 {
|
||||
|
||||
opaSessionObj := <-*resultsHandler.opaSessionObj
|
||||
|
||||
resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
|
||||
|
||||
score := resultsHandler.printerObj.ActionPrint(opaSessionObj)
|
||||
|
||||
resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package exceptions
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
@@ -68,6 +71,10 @@ func alertObjectToWorkloads(obj *opapolicy.AlertObject) []k8sinterface.IWorkload
|
||||
continue
|
||||
}
|
||||
resource = append(resource, r)
|
||||
ns := r.GetNamespace()
|
||||
if ns != "" {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return resource
|
||||
@@ -91,9 +98,9 @@ func hasException(designator *armotypes.PortalDesignator, workload k8sinterface.
|
||||
return false // if designators are empty
|
||||
}
|
||||
|
||||
// if cluster != "" && cluster != ClusterName { // TODO - where do we receive cluster name from?
|
||||
// return false // cluster name does not match
|
||||
// }
|
||||
if cluster != "" && cautils.ClusterName != "" && !regexCompare(cluster, cautils.ClusterName) { // TODO - where do we receive cluster name from?
|
||||
return false // cluster name does not match
|
||||
}
|
||||
|
||||
if namespace != "" && !compareNamespace(workload, namespace) {
|
||||
return false // namespaces do not match
|
||||
@@ -115,17 +122,17 @@ func hasException(designator *armotypes.PortalDesignator, workload k8sinterface.
|
||||
|
||||
func compareNamespace(workload k8sinterface.IWorkload, namespace string) bool {
|
||||
if workload.GetKind() == "Namespace" {
|
||||
return namespace == workload.GetName()
|
||||
return regexCompare(namespace, workload.GetName())
|
||||
}
|
||||
return namespace == workload.GetNamespace()
|
||||
return regexCompare(namespace, workload.GetNamespace())
|
||||
}
|
||||
|
||||
func compareKind(workload k8sinterface.IWorkload, kind string) bool {
|
||||
return kind == workload.GetKind()
|
||||
return regexCompare(kind, workload.GetKind())
|
||||
}
|
||||
|
||||
func compareName(workload k8sinterface.IWorkload, name string) bool {
|
||||
return name == workload.GetName()
|
||||
return regexCompare(workload.GetName(), name)
|
||||
}
|
||||
|
||||
func compareLabels(workload k8sinterface.IWorkload, attributes map[string]string) bool {
|
||||
@@ -134,3 +141,8 @@ func compareLabels(workload k8sinterface.IWorkload, attributes map[string]string
|
||||
|
||||
return designators.Matches(workloadLabels)
|
||||
}
|
||||
|
||||
func regexCompare(reg, name string) bool {
|
||||
r, _ := regexp.MatchString(reg, name)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package score
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
@@ -157,7 +157,7 @@ func getPostureFrameworksScores(weightPath string) map[string]map[string]Control
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
frameworksScoreMap := make(map[string]map[string]ControlScoreWeights)
|
||||
dat, err := ioutil.ReadFile(weightPath + "frameworkdict.json")
|
||||
dat, err := os.ReadFile(weightPath + "frameworkdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func getPostureResourceScores(weightPath string) map[string]float32 {
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
resourceScoreMap := make(map[string]float32)
|
||||
dat, err := ioutil.ReadFile(weightPath + "resourcesdict.json")
|
||||
dat, err := os.ReadFile(weightPath + "resourcesdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func loadResourcesMock() []map[string]interface{} {
|
||||
resources := make([]map[string]interface{}, 0)
|
||||
|
||||
dat, err := ioutil.ReadFile("resourcemocks.json")
|
||||
dat, err := os.ReadFile("resourcemocks.json")
|
||||
|
||||
if err != nil {
|
||||
return resources
|
||||
@@ -51,7 +51,7 @@ func getResouceByType(desiredType string) map[string]interface{} {
|
||||
func loadFrameworkMock() *opapolicy.FrameworkReport {
|
||||
report := &opapolicy.FrameworkReport{}
|
||||
|
||||
dat, err := ioutil.ReadFile("frameworkmock.json")
|
||||
dat, err := os.ReadFile("frameworkmock.json")
|
||||
|
||||
if err != nil {
|
||||
return report
|
||||
|
||||
14
website/index.html
Normal file
14
website/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
Kubscape Website
|
||||
</title>
|
||||
<h1>Kubscape Website</h1>
|
||||
</head>
|
||||
<body>
|
||||
<h2>
|
||||
Join us!!!
|
||||
<iframe src="https://discordapp.com/widget?id=893048809884643379&theme=dark" width="350" height="500" allowtransparency="true" frameborder="0" sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"></iframe>
|
||||
</h2>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user