Module: KubeAutoAnalyzer

Defined in:
lib/kube_auto_analyzer.rb,
lib/kube_auto_analyzer/version.rb,
lib/kube_auto_analyzer/reporting.rb,
lib/kube_auto_analyzer/utility/network.rb,
lib/kube_auto_analyzer/vuln_checks/kubelet.rb,
lib/kube_auto_analyzer/api_checks/master_node.rb,
lib/kube_auto_analyzer/vuln_checks/api_server.rb,
lib/kube_auto_analyzer/api_checks/rbac_auditor.rb,
lib/kube_auto_analyzer/agent_checks/file_checks.rb,
lib/kube_auto_analyzer/api_checks/config_dumper.rb,
lib/kube_auto_analyzer/vuln_checks/amicontained.rb,
lib/kube_auto_analyzer/vuln_checks/service_token.rb,
lib/kube_auto_analyzer/agent_checks/process_checks.rb,
lib/kube_auto_analyzer/api_checks/authorization_checker.rb,
lib/kube_auto_analyzer/api_checks/authentication_checker.rb

Constant Summary collapse

VERSION =
"0.0.17"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Attribute Details

#executeObject

Returns the value of attribute execute.



2
3
4
# File 'lib/kube_auto_analyzer.rb', line 2

def execute
  @execute
end

Class Method Details

.audit_rbacObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/kube_auto_analyzer/api_checks/rbac_auditor.rb', line 2

def self.audit_rbac
  @log.debug("Entering the RBAC Auditor")
  target = @options.target_server
  @log.debug("Auditing RBAC on #{target}")
  @results[target][:rbac] = Hash.new
  cluster_roles = @rbac_client.get_cluster_roles
  @log.debug("got #{cluster_roles.length.to_s} cluster roles")
  cluster_role_bindings = @rbac_client.get_cluster_role_bindings
  @log.debug("got #{cluster_role_bindings.length.to_s} cluster role bindings")
  @results[target][:rbac][:cluster_roles] = Hash.new
  cluster_roles.each do |role|
    role_output = Hash.new
    role_output[:rules] = role.rules

    @log.debug("metadata in #{role.[:name]} , #{role.}")
    begin
      if role.[:labels]['kubernetes.io/bootstrapping'] == "rbac-defaults"
        role_output[:default] = true
      else
        role_output[:default] = false
      end
    rescue NoMethodError
      #If there's no method, it can't be a default...
      role_output[:default] = false
    end
    role_output[:subjects] = Array.new
    cluster_role_bindings.each do |binding|
      #So we're testing if the binding has any subjects and if so whether they apply to this role or not
      if binding.subjects 
        @log.debug("#{binding.roleRef[:name]} binding has #{binding.subjects.length.to_s} bindings")
      else
        @log.debug("#{binding.roleRef[:name]} has no subjects")
      end
      @log.debug(binding.roleRef[:kind] + ", " + role.[:name] + ", " + binding.roleRef[:name] + ", " + (binding.subjects ? binding.subjects.length.to_s : "0") )
      if binding.roleRef[:kind] == "ClusterRole"
        @log.debug("Matched the cluster role")
        if binding.roleRef[:name] == role.[:name]
          @log.debug("matched the role name")
          if binding.subjects
            binding.subjects.each do |subject|
              @log.debug("added a subject to the list")
              role_output[:subjects] << subject
            end
          end
        end
      end
    end
    @results[target][:rbac][:cluster_roles][role.[:name]] = role_output
  end
end

.check_amicontainedObject

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it’s looking for” perspective, its more with the vuln. checks as there’s not a CIS check for it.



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/kube_auto_analyzer/vuln_checks/amicontained.rb', line 5

def self.check_amicontained
  require 'json'
  @log.debug("Doing Am I contained check")
  target = @options.target_server
  @results[target]['vulns']['amicontained'] = Hash.new

  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node
  end
  
  nodes.each do |nod|
    node_hostname = nod..labels['kubernetes.io/hostname']
    node_ip = nod['status']['addresses'][0]['address']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod. = {}
    pod..name = container_name
    pod..namespace = "default"
    pod.spec = {}
    pod.spec.restartPolicy = "Never"
    pod.spec.containers = {}
    pod.spec.containers = [{name: "kubeautoanalyzerkubelettest", image: "raesene/kaa-agent:latest"}]
    pod.spec.containers[0].args = ["/amicontained.rb"]

    #Try the Toleration for Master
    pod.spec.tolerations = {}
    #pod.spec.tolerations = [{ key:"key", operator:"Equal", value:"value",effect:"NoSchedule"}]
    pod.spec.tolerations = [{ operator:"Exists" }]
    
    pod.spec.nodeselector = {}
    pod.spec.nodeselector['kubernetes.io/hostname'] = node_hostname
    begin
      @log.debug("About to start amicontained pod")
      @client.create_pod(pod)
      @log.debug("Executed the create pod")
      begin
        sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
      rescue
        retry
      end
      @log.debug ("started amicontained pod")
      results = JSON.parse(@client.get_pod_log(container_name,"default"))
      @results[target]['vulns']['amicontained'][node_ip] = results
    ensure
      @client.delete_pod(container_name,"default")
    end
  end
end

.check_authnObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/kube_auto_analyzer/api_checks/authentication_checker.rb', line 2

def self.check_authn
  @log.debug("Entering the Authentication Checker")
  target = @options.target_server
  @log.debug("Checking enabled Authentication Options on #{target}")
  @results[target][:authn] = Hash.new
  @results[target]['evidence'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod|
    if pod['metadata']['name'] =~ /kube-apiserver/
      @api_server = pod
    end
  end

  api_server_command_line = @api_server['spec']['containers'][0]['command']
  if api_server_command_line.index{|line| line =~ /--basic-auth-file/}
    @results[target][:authn][:basic] = true
  else
    @results[target][:authn][:basic] = false
  end

  if api_server_command_line.index{|line| line =~ /--token-auth-file/}
    @results[target][:authn][:token] = true
  else
    @results[target][:authn][:token] = false
  end

  if api_server_command_line.index{|line| line =~ /--client-ca-file/}
    @results[target][:authn][:certificate] = true
  else
    @results[target][:authn][:certificate] = false
  end

  if api_server_command_line.index{|line| line =~ /--oidc-issuer-url/}
    @results[target][:authn][:oidc] = true
  else
    @results[target][:authn][:oidc] = false
  end

  if api_server_command_line.index{|line| line =~ /--authentication-token-webhook-config-file/}
    @results[target][:authn][:webhook] = true
  else
    @results[target][:authn][:webhook] = false
  end

  if api_server_command_line.index{|line| line =~ /--requestheader-username-headers/}
    @results[target][:authn][:proxy] = true
  else
    @results[target][:authn][:proxy] = false
  end
  #Gather evidence for the API server
  @results[target]['evidence']['API Server'] = api_server_command_line
end

.check_authzObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/kube_auto_analyzer/api_checks/authorization_checker.rb', line 2

def self.check_authz
  @log.debug("Entering the authorization checker")
  target = @options.target_server
  @log.debug("Checking enabled authorization options on #{target}")
  @results[target][:authz] = Hash.new
  pods = @client.get_pods
  pods.each do |pod|
    if pod['metadata']['name'] =~ /kube-apiserver/
      @api_server = pod
    end
  end

  api_server_command_line = @api_server['spec']['containers'][0]['command']
  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*RBAC/}
    @results[target][:authz][:rbac] = true
    
  else
    @results[target][:authz][:rbac] = false      
  end

  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*ABAC/}
    @results[target][:authz][:abac] = true
  else
    @results[target][:authz][:abac] = false
  end

  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*Webhook/}
    @results[target][:authz][:webhook] = true
  else
    @results[target][:authz][:webhook] = false
  end
end

.check_filesObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/kube_auto_analyzer/agent_checks/file_checks.rb', line 3

def self.check_files
  require 'json'
  @log.debug ("entering File check")
  target = @options.target_server
  @results[target]['node_files'] = Hash.new


  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node
  end
  nodes.each do |nod|
    node_hostname = nod..labels['kubernetes.io/hostname']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod. = {}
    pod..name = container_name
    pod..namespace = "default"
    pod.spec = {}
    pod.spec.restartPolicy = "Never"
    pod.spec.containers = {}
    pod.spec.containers = [{name: "kubeautoanalyzerfiletest", image: "raesene/kaa-agent:latest"}]

    #Try the Toleration for Master
    pod.spec.tolerations = {}
    #Old version doesn't work with 1.8
    #pod.spec.tolerations = [{ key:"key", operator:"Equal", value:"value",effect:"NoSchedule"}]
    pod.spec.tolerations = [{ operator:"Exists" }]
    
    pod.spec.volumes = [{name: 'etck8s', hostPath: {path: '/etc'}}]
    pod.spec.containers[0].volumeMounts = [{mountPath: '/etc', name: 'etck8s'}]
    pod.spec.containers[0].args = ["/file-checker.rb","/etc/kubernetes"]
    pod.spec.nodeselector = {}
    begin
      pod.spec.nodeselector['kubernetes.io/hostname'] = node_hostname
      @client.create_pod(pod)
      begin
        sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
      rescue
        retry
      end
      files = JSON.parse(@client.get_pod_log(container_name,"default"))
    
      @results[target]['node_files'][node_hostname] = files
    ensure
      @client.delete_pod(container_name,"default")
    end

  end
  @log.debug("Finished Node File Check")
end

.check_kubelet_processObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/kube_auto_analyzer/agent_checks/process_checks.rb', line 3

def self.check_kubelet_process
  @log.debug("Entering Process Checks")
  target = @options.target_server
  @results[target]['kubelet_checks'] = Hash.new
  @results[target]['node_evidence'] = Hash.new


  nodes = Array.new
  @client.get_nodes.each do |node|
  #  unless node.spec.taints.to_s =~ /NoSchedule/
      nodes << node
  #  end
  end

  nodes.each do |nod|
    node_hostname = nod..labels['kubernetes.io/hostname']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod. = {}
    pod..name = container_name
    pod..namespace = "default"
    pod.spec = {}
    pod.spec.restartPolicy = "Never"
    pod.spec.containers = {}
    pod.spec.containers = [{name: "kaakubelettest", image: "raesene/kaa-agent:latest"}]
    
    #Try the Toleration for Master
    pod.spec.tolerations = {}
    #pod.spec.tolerations = [{ key:"key", operator:"Equal", value:"value",effect:"NoSchedule"}]
    pod.spec.tolerations = [{ operator:"Exists" }]
    
    pod.spec.containers[0].args = ["/process-checker.rb"]
    pod.spec.hostPID = true
    pod.spec.nodeselector = {}
    pod.spec.nodeselector['kubernetes.io/hostname'] = node_hostname
    begin
      @client.create_pod(pod)
      begin
        sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
      rescue
        retry
      end
      processes = JSON.parse(@client.get_pod_log(container_name,"default"))
      #If we didn't get more than one process, we're probably not reading the host ones
      #So either it's a bug or we don't have rights
      if processes.length < 2
        @log.debug("Process Check failed didn't get the node process list")
        @results[target]['kubelet_checks'][node_hostname]['Kubelet Not Found'] = "Error - couldn't see host process list"
        @client.delete_pod(container_name,"default")
        return
      end
      #puts processes
      kubelet_proc = ''
      processes.each do |proc|
        if proc =~ /kubelet/
          kubelet_proc = proc
        end
      end
      @results[target]['kubelet_checks'][node_hostname] = Hash.new
      unless kubelet_proc.length > 1
        @results[target]['kubelet_checks'][node_hostname]['Kubelet Not Found'] = "Error"
        @log.debug(processes)
        @client.delete_pod(container_name,"default")
        return
      end

      @results[target]['node_evidence'][node_hostname] = Hash.new
      @results[target]['node_evidence'][node_hostname]['kubelet'] = kubelet_proc


      
      #Checks
      unless kubelet_proc =~ /--allow-privileged=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.1 - Ensure that the --allow-privileged argument is set to false'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.1 - Ensure that the --allow-privileged argument is set to false'] = "Pass"
      end

      unless kubelet_proc =~ /--anonymous-auth=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.2 - Ensure that the --anonymous-auth argument is set to false'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.2 - Ensure that the --anonymous-auth argument is set to false'] = "Pass"
      end

      if kubelet_proc =~ /--authorization-mode\S*AlwaysAllow/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.3 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.3 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Pass"
      end

      unless kubelet_proc =~ /--client-ca-file/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.4 - Ensure that the --client-ca-file argument is set as appropriate'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.4 - Ensure that the --client-ca-file argument is set as appropriate'] = "Pass"
      end

      unless kubelet_proc =~ /--read-only-port=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.5 - Ensure that the --read-only-port argument is set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.5 - Ensure that the --read-only-port argument is set to 0'] = "Pass"
      end

      if kubelet_proc =~ /--streaming-connection-idle-timeout=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.6 - Ensure that the --streaming-connection-idle-timeout argument is not set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.6 - Ensure that the --streaming-connection-idle-timeout argument is not set to 0'] = "Pass"
      end

      unless kubelet_proc =~ /--protect-kernel-defaults=true/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.7 - Ensure that the --protect-kernel-defaults argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.7 - Ensure that the --protect-kernel-defaults argument is set to true'] = "Pass"
      end

      if kubelet_proc =~ /--make-iptables-util-chains=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.8 - Ensure that the --make-iptables-util-chains argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.8 - Ensure that the --make-iptables-util-chains argument is set to true'] = "Pass"
      end

      unless kubelet_proc =~ /--keep-terminated-pod-volumes=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.9 - Ensure that the --keep-terminated-pod-volumes argument is set to false'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.9 - Ensure that the --keep-terminated-pod-volumes argument is set to false'] = "Pass"
      end

      if kubelet_proc =~ /--hostname-override/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.10 - Ensure that the --hostname-override argument is not set'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.10 - Ensure that the --hostname-override argument is not set'] = "Pass"
      end      

      unless kubelet_proc =~ /--event-qps=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.11 - Ensure that the --event-qps argument is set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.11 - Ensure that the --event-qps argument is set to 0'] = "Pass"
      end

      unless (kubelet_proc =~ /--tls-cert-file/) && (kubelet_proc =~ /--tls-private-key-file/)
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.12 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.12 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Pass"
      end

      unless kubelet_proc =~ /--cadvisor-port=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.13 - Ensure that the --cadvisor-port argument is set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.13 - Ensure that the --cadvisor-port argument is set to 0'] = "Pass"
      end

      unless kubelet_proc =~ /--feature-gates=RotateKubeletClientCertificate=true/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.14 - Ensure that the RotateKubeletClientCertificate argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.14 - Ensure that the RotateKubeletClientCertificate argument is set to true'] = "Pass"
      end

      unless kubelet_proc =~ /--feature-gates=RotateKubeletServerCertificate=true/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.15 - Ensure that the RotateKubeletServerCertificate argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.15 - Ensure that the RotateKubeletServerCertificate argument is set to true'] = "Pass"
      end
    #Need an ensure block here to make sure that the pod is deleted after its run  
    ensure
      @client.delete_pod(container_name,"default")
    end

  end

end

.dump_configObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/kube_auto_analyzer/api_checks/config_dumper.rb', line 2

def self.dump_config
 @log.debug("Entering the config dumper module")
 target = @options.target_server
 @log.debug("dumping the config for #{target}")
 @results[target][:config] = Hash.new
 pods = @client.get_pods
 services = @client.get_services
 docker_images = Array.new
 #Specific requirement here in that it's useful to know what Docker images are in use on the cluster.
 pods.each do |pod|
   docker_images << pod.status[:containerStatuses][0][:image]
 end
 @log.debug("logged #{docker_images.length} docker images")
 @results[target][:config][:docker_images] = docker_images.uniq

 @results[target][:config][:pod_info] = Array.new

 #Lets record some information about each pod
 pods.each do |pod|
   currpod = Hash.new
   currpod[:name] = pod.[:name]
   currpod[:namespace] = pod.[:namespace]
   currpod[:service_account] = pod.spec[:serviceAccount]
   currpod[:host_ip] = pod[:status][:hostIP]
   currpod[:pod_ip] = pod[:status][:podIP]
   @results[target][:config][:pod_info] << currpod
 end

 @results[target][:config][:service_info] = Array.new

 services.each do |service|
   currserv = Hash.new
   currserv[:name] = service.[:name]
   currserv[:cluster_ip] = service.spec[:clusterIP]
   if service.spec[:externalIP]
     currserv[:external_ip] = service.spec[:externalIP]
   else
     currserv[:external_ip] = "None"
   end
   if service.spec[:ports]
     currserv[:ports] = Array.new
     service.spec[:ports].each do |port|
       currserv[:ports] << "#{port[:port]}/#{port[:protocol]}:#{port[:targetPort]}/#{port[:protocol]}"
     end
   else
     currserv[:ports] = "None"
   end
   @results[target][:config][:service_info] << currserv
 end


end

.execute(commmand_line_opts) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/kube_auto_analyzer.rb', line 19

def self.execute(commmand_line_opts)
  @options = commmand_line_opts
  require 'logger'
  begin
    require 'kubeclient'
  rescue LoadError
    puts "You need to install kubeclient for this, try 'gem install kubeclient'"
    exit
  end

  @base_dir = @options.report_directory
  if !File.exists?(@base_dir)
    Dir.mkdirs(@base_dir)
  end

  @log = Logger.new(@base_dir + '/kube-analyzer-log.txt')
  @log.level = Logger::DEBUG
  @log.debug("Log created at " + Time.now.to_s)
  @log.debug("Target API Server is " + @options.target_server)

  @report_file_name = @base_dir + '/' + @options.report_file
  if @options.json_report
    @json_report_file = File.new(@report_file_name + '.json','w+')
  end

  if @options.html_report
    @html_report_file = File.new(@report_file_name + '.html','w+')
  end
  @log.debug("New Report File created #{@report_file_name}")
      
  @results = Hash.new
  #TODO: Expose this as an option rather than hard-code to off
  unless @options.config_file
    ssl_options = { verify_ssl: OpenSSL::SSL::VERIFY_NONE}
    #TODO: Need to setup the other authentication options
    if @options.token.length > 1
      auth_options = { bearer_token: @options.token}
    elsif @options.token_file.length > 1
      auth_options = { bearer_token_file: @options.token_file}
    elsif @options.insecure 
      #Not sure this will actually work for no auth. needed, try and ooold cluster to check
      auth_options = {}
    end
    @results[@options.target_server] = Hash.new
    @client = Kubeclient::Client.new @options.target_server, 'v1', auth_options: auth_options, ssl_options: ssl_options
    @rbac_client = Kubeclient::Client.new @options.target_server + '/apis/rbac.authorization.k8s.io', 'v1', auth_options: auth_options, ssl_options: ssl_options
  else
    begin
      config = Kubeclient::Config.read(@options.config_file)
      if @options.context
        context = config.context(@options.context)
      else
        context = config.context
      end
    rescue Errno::ENOENT
      puts "Config File could not be read, check the path?"
      exit
    end
    if @options.nosslverify
      @client = Kubeclient::Client.new(
        context.api_endpoint,
        context.api_version,
        {
          ssl_options: {client_cert: context.ssl_options[:client_cert], client_key: context.ssl_options[:client_key],verify_ssl: OpenSSL::SSL::VERIFY_NONE},
          auth_options: context.auth_options
        }
      )
      @rbac_client = Kubeclient::Client.new(
        context.api_endpoint + '/apis/rbac.authorization.k8s.io',
        context.api_version,
        {
          ssl_options: {client_cert: context.ssl_options[:client_cert], client_key: context.ssl_options[:client_key],verify_ssl: OpenSSL::SSL::VERIFY_NONE},
          auth_options: context.auth_options
        }
      )
    else
      @client = Kubeclient::Client.new(
        context.api_endpoint,
        context.api_version,
        {
          ssl_options: context.ssl_options,
          auth_options: context.auth_options
        }
      )
      @rbac_client = Kubeclient::Client.new(
        context.api_endpoint + '/apis/rbac.authorization.k8s.io',
        context.api_version,
        {
          ssl_options: context.ssl_options,
          auth_options: context.auth_options
        }
      )
    end
    #We didn't specify the target on the command line so lets get it from the config file
    @options.target_server = context.api_endpoint
    @log.debug("target is " + @options.target_server)
    @results[context.api_endpoint] = Hash.new
  end
  #Test response
  begin
    @client.get_pods.to_s
  rescue => error
    puts error
    puts "Check of API connection failed."
    puts "try using kubectl with the same connection details"
    puts "to see what's going wrong."
    exit
  end
  if @options.cis_audit
    test_api_server
    test_scheduler
    test_controller_manager
    test_etcd
  end
  check_authn
  check_authz
  test_unauth_kubelet_external
  test_insecure_api_external
  if @options.agent_checks
    test_unauth_kubelet_internal
    test_insecure_api_internal
    test_service_token_internal
    if @options.cis_audit
      check_files
      check_kubelet_process
    end
    check_amicontained
  end
  if @options.dump_config
    dump_config
  end
  if @options.audit_rbac
    audit_rbac
  end
  if @options.html_report
    html_report
  end
  if @options.json_report
    json_report
  end
end

.html_reportObject



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/kube_auto_analyzer/reporting.rb', line 10

def self.html_report

  logo_path = File.join(__dir__, "data-logo.b64")
   = File.open(logo_path).read
  @log.debug("Starting HTML Report")
  @html_report_file << '
    <!DOCTYPE html>
    <head>
     <title> Kubernetes Auto Analyzer Report</title>
     <meta charset="utf-8"> 
     <style>
      body {
        font: normal 14px;
        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
        color: #C41230;
        background: #FFFFFF;
      }
      #kubernetes-analyzer {
        font-weight: bold;
        font-size: 48px;
        color: #C41230;
      }
      .master-node, .worker-node, .vuln-node {
        background: #F5F5F5;
        border: 1px solid black;
        padding-left: 6px;
      }
      #api-server-results {
        font-weight: italic;
        font-size: 36px;
        color: #C41230;
      }
      table, th, td {
        border-collapse: collapse;
        border: 1px solid black;
      }
      th {
       font: bold 11px;
       color: #C41230;
       background: #999999;
       letter-spacing: 2px;
       text-transform: uppercase;
       text-align: left;
       padding: 6px 6px 6px 12px;
      }
      td {
      background: #FFFFFF;
      padding: 6px 6px 6px 12px;
      color: #333333;
      }
      .container{
        display: flex;
      } 
      .fixed{
        width: 300px;
      }
      .flex-item{
        flex-grow: 1;
      }
  </style>
</head>
<body>

  '
  @html_report_file.puts '<img width="100" height="100" align="right"' + " src=#{} />"
  @html_report_file.puts "<h1>Kubernetes Auto Analyzer</h1>"
  @html_report_file.puts "<br><b>Server Reviewed : </b> #{@options.target_server}"
  if @options.cis_audit
    chartkick_path = File.join(__dir__, "js_files/chartkick.js")
    chartkick = File.open(chartkick_path).read
    highcharts_path = File.join(__dir__, "js_files/highcharts.js")
    highcharts = File.open(highcharts_path).read
    @html_report_file.puts "<script>#{chartkick}</script>"
    @html_report_file.puts "<script>#{highcharts}</script>"
    @html_report_file.puts '<br><br><div class="master-node"><h2>Master Node Results</h2><br>'
    #Charting setup counts for the passes and fails
    api_server_pass = 0
    api_server_fail = 0
    @results[@options.target_server]['api_server'].each do |test, result|
      if result == "Pass"
        api_server_pass  = api_server_pass + 1
      elsif result == "Fail"
        api_server_fail = api_server_fail + 1
      end
    end

    #Not a lot of point in scheduler when there's only one check...
    #scheduler_pass = 0
    #scheduler_fail = 0
    #@results[@options.target_server]['scheduler'].each do |test, result|
    #  if result == "Pass"
    #    scheduler_pass  = scheduler_pass + 1
    #  elsif result == "Fail"
    #     scheduler_fail = scheduler_fail + 1
    #  end
    #end

    controller_manager_pass = 0
    controller_manager_fail = 0
    @results[@options.target_server]['controller_manager'].each do |test, result|
      if result == "Pass"
        controller_manager_pass  = controller_manager_pass + 1
      elsif result == "Fail"
        controller_manager_fail = controller_manager_fail + 1
      end
    end

    etcd_pass = 0
    etcd_fail = 0
    @results[@options.target_server]['etcd'].each do |test, result|
      if result == "Pass"
        etcd_pass  = etcd_pass + 1
      elsif result == "Fail"
        etcd_fail = etcd_fail + 1
      end
    end

    #Start of Chart Divs
    @html_report_file.puts '<div class="container">'
    #API Server Chart
    @html_report_file.puts '<div class="fixed" id="chart-1" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    @html_report_file.puts '<script>new Chartkick.PieChart("chart-1", {"pass": ' + api_server_pass.to_s + ', "fail": ' + api_server_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"API Server Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #Scheduler Chart
    #@html_report_file.puts '<div class="flex-item" id="chart-2" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    #@html_report_file.puts '<script>new Chartkick.PieChart("chart-2", {"pass": ' + scheduler_pass.to_s + ', "fail": ' + scheduler_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"Scheduler Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #Controller Manager Chart
    @html_report_file.puts '<div class="fixed" id="chart-2" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    @html_report_file.puts '<script>new Chartkick.PieChart("chart-2", {"pass": ' + controller_manager_pass.to_s + ', "fail": ' + controller_manager_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"Controller Manager Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #etcd Chart
    @html_report_file.puts '<div class="fixed" id="chart-3" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    @html_report_file.puts '<script>new Chartkick.PieChart("chart-3", {"pass": ' + etcd_pass.to_s + ', "fail": ' + etcd_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"etcd Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #End of Chart Divs
    @html_report_file.puts '</div>'
    @html_report_file.puts "<h2>API Server</h2>"
    @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
    @results[@options.target_server]['api_server'].each do |test, result|      
      if result == "Fail"
        result = '<span style="color:red;">Fail</span>'
      elsif result == "Pass"
        result = '<span style="color:green;">Pass</span>'
      end
      @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br><h2>Scheduler</h2>"
    @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
    @results[@options.target_server]['scheduler'].each do |test, result|      
      if result == "Fail"
        result = '<span style="color:red;">Fail</span>'
      elsif result == "Pass"
        result = '<span style="color:green;">Pass</span>'
      end
      @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
    end
    @html_report_file.puts "</table>"

    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br><h2>Controller Manager</h2>"
    @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
    @results[@options.target_server]['controller_manager'].each do |test, result|      
      if result == "Fail"
        result = '<span style="color:red;">Fail</span>'
      elsif result == "Pass"
        result = '<span style="color:green;">Pass</span>'
      end
      @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
    end
    @html_report_file.puts "</table>"

    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br><h2>etcd</h2>"
    @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
    @results[@options.target_server]['etcd'].each do |test, result|      
      if result == "Fail"
        result = '<span style="color:red;">Fail</span>'
      elsif result == "Pass"
        result = '<span style="color:green;">Pass</span>'
      end
      @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
    end
    @html_report_file.puts "</table>"
    #Close the master Node Div
    @html_report_file.puts "</table></div>"
  end


  if @options.agent_checks
    @html_report_file.puts '<br><br><div class="worker-node"><h2>Worker Node Results</h2>'

    #Start of Chart Divs
    @html_report_file.puts '<div class="container">'
    @results[@options.target_server]['kubelet_checks'].each do |node, results|
      node_kubelet_pass = 0
      node_kubelet_fail = 0
      results.each do |test, result|
        if result == "Fail"
          node_kubelet_fail = node_kubelet_fail + 1
        elsif result == "Pass"
          node_kubelet_pass = node_kubelet_pass + 1
        end
      end


      #Create the Chart
      @html_report_file.puts '<div class="fixed" id="' + node + '" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
      @html_report_file.puts '<script>new Chartkick.PieChart("' + node + '", {"pass": ' + node_kubelet_pass.to_s + ', "fail": ' + node_kubelet_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"' + node + ' Kubelet Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'

    end



    #End of Chart Divs
    @html_report_file.puts '</div>'

    @results[@options.target_server]['kubelet_checks'].each do |node, results|
      @html_report_file.puts "<br><b>#{node} Kubelet Checks</b>"
      @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
      results.each do |test, result|
        if result == "Fail"
          result = '<span style="color:red;">Fail</span>'
        elsif result == "Pass"
          result = '<span style="color:green;">Pass</span>'
        end
        @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
      end
      @html_report_file.puts "</table>"
    end

    @html_report_file.puts "<br><br><h2>Evidence</h2><br>"
    @html_report_file.puts "<table><thead><tr><th>Host</th><th>Area</th><th>Output</th></tr></thead>"
    @results[@options.target_server]['node_evidence'].each do |node, evidence|
      evidence.each do |area, data|
        @html_report_file.puts "<tr><td>#{node}</td><td>#{area}</td><td>#{data}</td></tr>"   
      end
    end
    @html_report_file.puts "</table>"

  end
  #Close the Worker Node Div
  @html_report_file.puts '</div>'
  if @options.agent_checks
    @html_report_file.puts '<br><h2>Node File Permissions</h2>'
    @results[@options.target_server]['node_files'].each do |node, results|
      @html_report_file.puts "<br><b>#{node}</b><br>"
      @html_report_file.puts "<table><thead><tr><th>file</th><th>user</th><th>group</th><th>permissions</th></thead>"
      results.each do |file|
        @html_report_file.puts "<tr><td>#{file[0]}</td><td>#{file[1]}</td><td>#{file[2]}</td><td>#{file[3]}</td></tr>"
      end
      @html_report_file.puts "</table>"
    end
  end

  @html_report_file.puts '<br><h2>Vulnerability Checks</h2>'
  @html_report_file.puts '<br><h3>External Unauthenticated Access to the Kubelet</h3>'
  @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
  @results[@options.target_server]['vulns']['unauth_kubelet'].each do |node, result|
    unless (result =~ /Forbidden/ || result =~ /Not Open/ || result =~ /Unauthorized/)
      output = "Vulnerable"
    else
      output = result
    end
    @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
  end
  @html_report_file.puts "</table>"
  if @options.agent_checks
    @html_report_file.puts '<br><h3>Internal Unauthenticated Access to the Kubelet</h3>'
    @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
    @results[@options.target_server]['vulns']['internal_kubelet'].each do |node, result|
      unless (result =~ /Forbidden/ || result =~ /Not Open/)
        output = "Vulnerable"
      else
        output = result
      end
      @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
    end
    @html_report_file.puts "</table>"
  end

  @html_report_file.puts '<br><h3>External Insecure API Port Exposed</h3>'
  @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
  @results[@options.target_server]['vulns']['insecure_api_external'].each do |node, result|
    unless (result =~ /Forbidden/ || result =~ /Not Open/)
      output = "Vulnerable"
    else
      output = result
    end
    @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
  end
  @html_report_file.puts "</table>"
  if @options.agent_checks
    @html_report_file.puts '<br><h3>Internal Insecure API Port Exposed</h3>'
    @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
    @results[@options.target_server]['vulns']['insecure_api_internal'].each do |node, result|
      unless (result =~ /Forbidden/ || result =~ /Not Open/)
        output = "Vulnerable"
      else
        output = result
      end
      @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
    end
    @html_report_file.puts "</table>"
  end

  if @options.agent_checks
    @html_report_file.puts '<br><h3>Default Service Token In Use</h3>'
    @html_report_file.puts "<table><thead><tr><th>API endpoint</th><th>Result</th></thead>"
    @results[@options.target_server]['vulns']['service_token'].each do |node, result|
      unless (result =~ /Forbidden/ || result =~ /Not Open/)
        output = "Vulnerable"
      else
        output = result
      end
      @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
    end
    @html_report_file.puts "</table>"
  end

  if @options.agent_checks
    @html_report_file.puts '<br><h3>Container Configuration checks</h3>'
    @results[@options.target_server]['vulns']['amicontained'].each do |node, result|
      @html_report_file.puts "<br><b>#{node} Container Checks</b>"
      @html_report_file.puts "<table><thead><tr><th>Container item</th><th>Result</th></thead>"
      @html_report_file.puts "<tr><td>Runtime in Use</td><td>#{result['runtime']}</td></tr>"
      @html_report_file.puts "<tr><td>Host PID namespace used?</td><td>#{result['hostpid']}</td></tr>"
      @html_report_file.puts "<tr><td>AppArmor Profile</td><td>#{result['apparmor']}</td></tr>"
      @html_report_file.puts "<tr><td>User Namespaces in use?</td><td>#{result['uid_map']}</td></tr>"
      @html_report_file.puts "<tr><td>Inherited Capabilities</td><td>#{result['cap_inh']}</td></tr>"
      @html_report_file.puts "<tr><td>Effective Capabilities</td><td>#{result['cap_eff']}</td></tr>"
      @html_report_file.puts "<tr><td>Permitted Capabilities</td><td>#{result['cap_per']}</td></tr>"
      @html_report_file.puts "<tr><td>Bounded Capabilities</td><td>#{result['cap_bnd']}</td></tr>"
      @html_report_file.puts "</table>"
    end
  end




  @html_report_file.puts "<br><br><h2>Vulnerability Evidence</h2><br>"
  @html_report_file.puts "<table><thead><tr><th>Vulnerability</th><th>Host</th><th>Output</th></tr></thead>"
  @results[@options.target_server]['vulns']['unauth_kubelet'].each do |node, result|
    @html_report_file.puts "<tr><td>External Unauthenticated Kubelet Access</td><td>#{node}</td><td>#{result}</td></tr>"   
  end
  if @options.agent_checks
    @results[@options.target_server]['vulns']['internal_kubelet'].each do |node, result|
      @html_report_file.puts "<tr><td>Internal Unauthenticated Kubelet Access</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end
  @results[@options.target_server]['vulns']['insecure_api_external'].each do |node, result|
    @html_report_file.puts "<tr><td>External Insecure API Server Access</td><td>#{node}</td><td>#{result}</td></tr>"   
  end
  if @options.agent_checks
    @results[@options.target_server]['vulns']['insecure_api_internal'].each do |node, result|
      @html_report_file.puts "<tr><td>Internal Insecure API Server Access</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end
  if @options.agent_checks
    @results[@options.target_server]['vulns']['service_token'].each do |node, result|
      @html_report_file.puts "<tr><td>Default Service Token In Use</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end
  if @options.agent_checks
    @results[@options.target_server]['vulns']['amicontained'].each do |node, result|
      @html_report_file.puts "<tr><td>Am I Contained Output</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end

  @html_report_file.puts "</table>"

  #Show what cluster authentication modes are supported.
  @html_report_file.puts "<br><br><h1>Kubernetes Cluster Information</h1>"
  @html_report_file.puts "<br><br><h2>Kubernetes Authentication Options</h2>"
  @html_report_file.puts "<table><thead><tr><th>Authentication Option</th><th>Enabled?</th></tr></thead>"
  if @results[@options.target_server][:authn][:basic] == true
    @html_report_file.puts "<tr><td>Basic Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Basic Authentication</td><td>Disabled</td></tr>"
  end
  if @results[@options.target_server][:authn][:token] == true
    @html_report_file.puts "<tr><td>Token Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Token Authentication</td><td>Disabled</td></tr>"
  end
  if @results[@options.target_server][:authn][:certificate] == true
    @html_report_file.puts "<tr><td>Client Certificate Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Client Certificate Authentication</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authn][:oidc] == true
    @html_report_file.puts "<tr><td>OpenID Connect Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>OpenID Connect Authentication</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authn][:webhook] == true
    @html_report_file.puts "<tr><td>Webhook Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Webhook Authentication</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authn][:proxy] == true
    @html_report_file.puts "<tr><td>Proxy Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Proxy Authentication</td><td>Disabled</td></tr>"
  end

  @html_report_file.puts "</table>"

  #Show what cluster authorization modes are supported.
  @html_report_file.puts "<br><br>"
  @html_report_file.puts "<br><br><h2>Kubernetes Authorization Options</h2>"
  @html_report_file.puts "<table><thead><tr><th>Authorization Option</th><th>Enabled?</th></tr></thead>"

  if @results[@options.target_server][:authz][:rbac] == true
    @html_report_file.puts "<tr><td>Role Based Authorization</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Role Based Authorization</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authz][:abac] == true
    @html_report_file.puts "<tr><td>Attribute Based Authorization</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Attribute Based Authorization</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authz][:webhook] == true
    @html_report_file.puts "<tr><td>Webhook Authorization</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Webhook Authorization</td><td>Disabled</td></tr>"
  end

  @html_report_file.puts "</table>"    

  @html_report_file.puts "<br><br><h2>Evidence</h2><br>"
  @html_report_file.puts "<table><thead><tr><th>Area</th><th>Output</th></tr></thead>"
  @results[@options.target_server]['evidence'].each do |area, output|
    @html_report_file.puts "<tr><td>#{area}</td><td>#{output}</td></tr>"
  end
  @html_report_file.puts "</table>"  

  #Only show this section if we were asked to dump the config
  if @options.dump_config
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br><h2>Cluster Config Information</h2>"
    @html_report_file.puts "<table><thead><tr><th>Docker Images In Use</th></tr></thead>"
    @results[@options.target_server][:config][:docker_images].each do |image|
      @html_report_file.puts "<tr><td>#{image}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<table><thead><tr><th>Pod Name</th><th>Namespace</th><th>Service Account</th><th>Host IP</th><th>Pod IP</th></tr></thead>"
    @results[@options.target_server][:config][:pod_info].each do |pod|
      @html_report_file.puts "<tr><td>#{pod[:name]}</td><td>#{pod[:namespace]}</td><td>#{pod[:service_account]}</td><td>#{pod[:host_ip]}</td><td>#{pod[:pod_ip]}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<table><thead><tr><th>Service Name</th><th>Cluster IP</th><th>External IP</th><th>Port:Target Port</th></tr></thead>"
    @results[@options.target_server][:config][:service_info].each do |service|

      @html_report_file.puts "<tr><td>#{service[:name]}</td><td>#{service[:cluster_ip]}</td><td>#{service[:external_ip]}</td><td>#{service[:ports].join('<br>')}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
  end

  #Only show this section if we were asked to dump RBAC
  if @options.audit_rbac
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br><h2>Cluster Role Information</h2>"
    @html_report_file.puts "<table><thead><tr><th>Name</th><th>Default?</th><th>Subjects</th><th>Rules</th></tr></thead>"
    @results[@options.target_server][:rbac][:cluster_roles].each do |name, info|
      subjects = ''
      info[:subjects].each do |subject|
        subjects << "#{subject[:kind]}:#{subject[:namespace]}:#{subject[:name]}<br>"
      end
      rules = ''
      info[:rules].each do |rule|
        unless rule.verbs
          rule.verbs = Array.new
        end
        unless rule.apiGroups
          rule.apiGroups = Array.new
        end
        unless rule.resources
          rule.resources = Array.new
        end
        rules << "Verbs : #{rule.verbs.join(', ')}<br>API Groups : #{rule.apiGroups.join(', ')}<br>Resources : #{rule.resources.join(', ')}<br><hr>"
      end
      @html_report_file.puts "<tr><td>#{name}</td><td>#{info[:default]}</td><td>#{subjects}</td><td>#{rules}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
  end

  #Closing the report off
  @html_report_file.puts '</body></html>'
end

.is_port_open?(ip, port) ⇒ Boolean

Returns:

  • (Boolean)


4
5
6
7
8
9
10
11
12
13
14
15
# File 'lib/kube_auto_analyzer/utility/network.rb', line 4

def self.is_port_open?(ip, port)
  begin
    Socket.tcp(ip, port, connect_timeout: 2)
  rescue Errno::ECONNREFUSED
    return false
  rescue Errno::ETIMEDOUT
    return false
  rescue Errno::ENETUNREACH
    return false
  end
  true
end

.json_reportObject



3
4
5
6
7
8
# File 'lib/kube_auto_analyzer/reporting.rb', line 3

def self.json_report
  require 'json'
  @log.debug("Starting Report")
  @json_report_file.puts JSON.generate(@results) 

end

.test_api_serverObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 3

def self.test_api_server
  @log.debug("Entering the test API Server Method")
  target = @options.target_server
  @log.debug("target is #{target}")
  @results[target]['api_server'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /kube-apiserver/
      @api_server = pod
    end
  end

  unless @api_server
    @results[target]['api_server']['API Server Pod Not Found'] = "Error"
    return
  end
  
  api_server_command_line = @api_server['spec']['containers'][0]['command']

  #Check for Anonymous Auth
  unless api_server_command_line.index{|line| line =~ /--anonymous-auth=false/}
    @results[target]['api_server']['CIS 1.1.1 - Ensure that the --anonymous-auth argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.1 - Ensure that the --anonymous-auth argument is set to false'] = "Pass"
  end

  #Check for Basic Auth
  if api_server_command_line.index{|line| line =~ /--basic-auth-file/}
    @results[target]['api_server']['CIS 1.1.2 - Ensure that the --basic-auth-file argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.2 - Ensure that the --basic-auth-file argument is not set'] = "Pass"
  end

  #Check for Insecure Allow Any Token
  if api_server_command_line.index{|line| line =~ /--insecure-allow-any-token/}
    @results[target]['api_server']['CIS 1.1.3 - Ensure that the --insecure-allow-any-token argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.3 - Ensure that the --insecure-allow-any-token argument is not set'] = "Pass"
  end

  #Check to confirm that Kubelet HTTPS isn't set to false
  if api_server_command_line.index{|line| line =~ /--kubelet-https=false/}
    @results[target]['api_server']['CIS 1.1.4 - Ensure that the --kubelet-https argument is set to true'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.4 - Ensure that the --kubelet-https argument is set to true'] = "Pass"
  end

  #Check for Insecure Bind Address
  if api_server_command_line.index{|line| line =~ /--insecure-bind-address/}
    @results[target]['api_server']['CIS 1.1.5 - Ensure that the --insecure-bind-address argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.5 - Ensure that the --insecure-bind-address argument is not set'] = "Pass"
  end

  #Check for Insecure Bind port
  unless api_server_command_line.index{|line| line =~ /--insecure-port=0/}
    @results[target]['api_server']['CIS 1.1.6 - Ensure that the --insecure-port argument is set to 0'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.6 - Ensure that the --insecure-port argument is set to 0'] = "Pass"
  end

  #Check Secure Port isn't set to 0
  if api_server_command_line.index{|line| line =~ /--secure-port=0/}
    @results[target]['api_server']['CIS 1.1.7 - Ensure that the --secure-port argument is not set to 0'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.7 - Ensure that the --secure-port argument is not set to 0'] = "Pass"
  end

  #
  unless api_server_command_line.index{|line| line =~ /--profiling=false/}
    @results[target]['api_server']['CIS 1.1.8 - Ensure that the --profiling argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.8 - Ensure that the --profiling argument is set to false'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--repair-malformed-updates/}
    @results[target]['api_server']['CIS 1.1.9 - Ensure that the --repair-malformed-updates argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.9 - Ensure that the --repair-malformed-updates argument is set to false'] = "Pass"
  end

  if api_server_command_line.index{|line| line =~ /--admission-control\S*AlwaysAdmit/}
    @results[target]['api_server']['CIS 1.1.10 - Ensure that the admission control policy is not set to AlwaysAdmit'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.10 - Ensure that the admission control policy is not set to AlwaysAdmit'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*AlwaysPullImages/}
    @results[target]['api_server']['CIS 1.1.11 - Ensure that the admission control policy is set to AlwaysPullImages'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.11 - Ensure that the admission control policy is set to AlwaysPullImages'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*DenyEscalatingExec/}
    @results[target]['api_server']['CIS 1.1.12 - Ensure that the admission control policy is set to DenyEscalatingExec'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.12 - Ensure that the admission control policy is set to DenyEscalatingExec'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*SecurityContextDeny/}
    @results[target]['api_server']['CIS 1.1.13 - Ensure that the admission control policy is set to SecurityContextDeny'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.13 - Ensure that the admission control policy is set to SecurityContextDeny'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*NamespaceLifecycle/}
    @results[target]['api_server']['CIS 1.1.14 - Ensure that the admission control policy is set to NamespaceLifecycle'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.14 - Ensure that the admission control policy is set to NamespaceLifecycle'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--audit-log-path/}
    @results[target]['api_server']['CIS 1.1.15 - Ensure that the --audit-log-path argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.15 - Ensure that the --audit-log-path argument is set as appropriate'] = "Pass"
  end

  #TODO: This check needs to do something with the number of days but for now lets just check whether it's present.
  unless api_server_command_line.index{|line| line =~ /--audit-log-maxage/}
    @results[target]['api_server']['CIS 1.1.16 - Ensure that the --audit-log-maxage argument is set to 30 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.16 - Ensure that the --audit-log-maxage argument is set to 30 or as appropriate'] = "Pass"
  end

  #TODO: This check needs to do something with the number of backups but for now lets just check whether it's present.
  unless api_server_command_line.index{|line| line =~ /--audit-log-maxbackup/}
    @results[target]['api_server']['CIS 1.1.17 - Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.17 - Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate'] = "Pass"
  end

  #TODO: This check needs to do something with the size of backups but for now lets just check whether it's present.
  unless api_server_command_line.index{|line| line =~ /--audit-log-maxsize/}
    @results[target]['api_server']['CIS 1.1.18 - Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.18 - Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate'] = "Pass"
  end

  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*AlwaysAllow/}
    @results[target]['api_server']['CIS 1.1.19 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.19 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Pass"
  end

  if api_server_command_line.index{|line| line =~ /--token-auth-file/}
    @results[target]['api_server']['CIS 1.1.20 - Ensure that the --token-auth-file argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.20 - Ensure that the --token-auth-file argument is not set'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--kubelet-certificate-authority/}
    @results[target]['api_server']['CIS 1.1.21 - Ensure that the --kubelet-certificate-authority argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.21 - Ensure that the --kubelet-certificate-authority argument is set as appropriate'] = "Pass"
  end

  unless (api_server_command_line.index{|line| line =~ /--kubelet-client-certificate/} && api_server_command_line.index{|line| line =~ /--kubelet-client-key/})
    @results[target]['api_server']['CIS 1.1.22 - Ensure that the --kubelet-client-certificate and --kubelet-client-key arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.22 - Ensure that the --kubelet-client-certificate and --kubelet-client-key arguments are set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--service-account-lookup=true/}
    @results[target]['api_server']['CIS 1.1.23 - Ensure that the --service-account-lookup argument is set to true'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.23 - Ensure that the --service-account-lookup argument is set to true'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*PodSecurityPolicy/}
    @results[target]['api_server']['CIS 1.1.24 - Ensure that the admission control policy is set to PodSecurityPolicy'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.24 - Ensure that the admission control policy is set to PodSecurityPolicy'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--service-account-key-file/}
    @results[target]['api_server']['CIS 1.1.25 - Ensure that the --service-account-key-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.25 - Ensure that the --service-account-key-file argument is set as appropriate'] = "Pass"
  end

  unless (api_server_command_line.index{|line| line =~ /--etcd-certfile/} && api_server_command_line.index{|line| line =~ /--etcd-keyfile/})
    @results[target]['api_server']['CIS 1.1.26 - Ensure that the --etcd-certfile and --etcd-keyfile arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.26 - Ensure that the --etcd-certfile and --etcd-keyfile arguments are set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*ServiceAccount/}
    @results[target]['api_server']['CIS 1.1.27 - Ensure that the admission control policy is set to ServiceAccount'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.27 - Ensure that the admission control policy is set to ServiceAccount'] = "Pass"
  end

  unless (api_server_command_line.index{|line| line =~ /--tls-cert-file/} && api_server_command_line.index{|line| line =~ /--tls-private-key-file/})
    @results[target]['api_server']['CIS 1.1.28 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.28 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--client-ca-file/}
    @results[target]['api_server']['CIS 1.1.29 - Ensure that the --client-ca-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.29 - Ensure that the --client-ca-file argument is set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--etcd-cafile/}
    @results[target]['api_server']['CIS 1.1.30 - Ensure that the --etcd-cafile argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.30 - Ensure that the --etcd-cafile argument is set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--authorization-mode\S*Node/}
    @results[target]['api_server']['CIS 1.1.31 - Ensure that the --authorization-mode argument is set to Node'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.31 - Ensure that the --authorization-mode argument is set to Node'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*NodeRestriction/}
    @results[target]['api_server']['CIS 1.1.32 - Ensure that the admission control policy is set to NodeRestriction'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.32 - Ensure that the admission control policy is set to NodeRestriction'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--experimental-encryption-provider-config/}
    @results[target]['api_server']['CIS 1.1.33 - Ensure that the --experimental-encryption-provider-config argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.33 - Ensure that the --experimental-encryption-provider-config argument is set as appropriate'] = "Pass"
  end

  #1.1.34 can't be checked using this methodology so it's TBD

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*EventRateLimit/}
    @results[target]['api_server']['CIS 1.1.35 - Ensure that the admission control policy is set to EventRateLimit'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.35 - Ensure that the admission control policy is set to EventRateLimit'] = "Pass"
  end

  if api_server_command_line.index{|line| line =~ /--feature-gates=AdvancedAuditing=false/}
    @results[target]['api_server']['CIS 1.1.36 - Ensure that the AdvancedAuditing argument is not set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.36 - Ensure that the AdvancedAuditing argument is not set to false'] = "Pass"
  end

  #1.1.37 This one is dubious for a pass/fail test as the value should be evaluated against the relity of the cluster.

  
end

.test_controller_managerObject



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 277

def self.test_controller_manager
  target = @options.target_server
  @results[target]['controller_manager'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /kube-controller-manager/
      @controller_manager = pod
    end
  end

  unless @controller_manager
    @results[target]['controller_manager']['Controller Manager Pod Not Found'] = "Error"
    return
  end    


  controller_manager_command_line = @controller_manager['spec']['containers'][0]['command']

  unless controller_manager_command_line.index{|line| line =~ /--terminated-pod-gc-threshold/}
    @results[target]['controller_manager']['CIS 1.3.1 - Ensure that the --terminated-pod-gc-threshold argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.1 - Ensure that the --terminated-pod-gc-threshold argument is set as appropriate'] = "Pass"
  end 

  unless controller_manager_command_line.index{|line| line =~ /--profiling=false/}
    @results[target]['controller_manager']['CIS 1.3.2 - Ensure that the --profiling argument is set to false'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.2 - Ensure that the --profiling argument is set to false'] = "Pass"
  end  

  if controller_manager_command_line.index{|line| line =~ /--insecure-experimental-approve-all-kubelet-csrs-for-group/}
    @results[target]['controller_manager']['CIS 1.3.3 - Ensure that the --insecure-experimental-approve-all-kubelet-csrs-for-group argument is not set'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.3 - Ensure that the --insecure-experimental-approve-all-kubelet-csrs-for-group argument is not set'] = "Pass"
  end  

  unless controller_manager_command_line.index{|line| line =~ /--use-service-account-credentials=true/}
    @results[target]['controller_manager']['CIS 1.3.3 - Ensure that the --use-service-account-credentials argument is set to true'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.3 - Ensure that the --use-service-account-credentials argument is set to true'] = "Pass"
  end 

  unless controller_manager_command_line.index{|line| line =~ /--service-account-private-key-file/}
    @results[target]['controller_manager']['CIS 1.3.4 - Ensure that the --service-account-private-key-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.4 - Ensure that the --service-account-private-key-file argument is set as appropriate'] = "Pass"
  end 

  unless controller_manager_command_line.index{|line| line =~ /--root-ca-file/}
    @results[target]['controller_manager']['CIS 1.3.5 - Ensure that the --root-ca-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.5 - Ensure that the --root-ca-file argument is set as appropriate'] = "Pass"
  end 

  unless controller_manager_command_line.index{|line| line =~ /RotateKubeletServerCertificate=true/}
    @results[target]['controller_manager']['CIS 1.3.7 - Ensure that the RotateKubeletServerCertificate argument is set to true'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.7 - Ensure that the RotateKubeletServerCertificate argument is set to true'] = "Pass"
  end 

  @results[target]['evidence']['Controller Manager'] = controller_manager_command_line

end

.test_etcdObject



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 342

def self.test_etcd
  target = @options.target_server
  @results[target]['etcd'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /etcd/
      @etcd = pod
    end
  end
  
  unless @etcd
    @results[target]['etcd']['etcd Pod Not Found'] = "Error"
    return
  end

  etcd_command_line = @etcd['spec']['containers'][0]['command']

  unless (etcd_command_line.index{|line| line =~ /--cert-file/} && etcd_command_line.index{|line| line =~ /--key-file/})
    @results[target]['etcd']['CIS 1.5.1 - Ensure that the --cert-file and --key-file arguments are set as appropriate'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.1 - Ensure that the --cert-file and --key-file arguments are set as appropriate'] = "Pass"
  end 

  unless etcd_command_line.index{|line| line =~ /--client-cert-auth=true/}
    @results[target]['etcd']['CIS 1.5.2 - Ensure that the --client-cert-auth argument is set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.2 - Ensure that the --client-cert-auth argument is set to true'] = "Pass"
  end

  if etcd_command_line.index{|line| line =~ /--auto-tls argument=true/}
    @results[target]['etcd']['CIS 1.5.3 - Ensure that the --auto-tls argument is not set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.3 - Ensure that the --auto-tls argument is not set to true'] = "Pass"
  end

  unless (etcd_command_line.index{|line| line =~ /--peer-cert-file/} && etcd_command_line.index{|line| line =~ /--peer-key-file/})
    @results[target]['etcd']['CIS 1.5.4 - Ensure that the --peer-cert-file and --peer-key-file arguments are set as appropriate'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.4 - Ensure that the --peer-cert-file and --peer-key-file arguments are set as appropriate'] = "Pass"
  end 

  unless etcd_command_line.index{|line| line =~ /--peer-client-cert-auth=true/}
    @results[target]['etcd']['CIS 1.5.5 - Ensure that the --peer-client-cert-auth argument is set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.5 - Ensure that the --peer-client-cert-auth argument is set to true'] = "Pass"
  end

  if etcd_command_line.index{|line| line =~ /--peer-auto-tls argument=true/}
    @results[target]['etcd']['CIS 1.5.6 - Ensure that the --peer-auto-tls argument is not set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.6 - Ensure that the --peer-auto-tls argument is not set to true'] = "Pass"
  end

  #This isn't quite right as we should really check the dir. but as that's not easily done lets start with an existence check
  unless etcd_command_line.index{|line| line =~ /--wal-dir/}
    @results[target]['etcd']['CIS 1.5.7 - Ensure that the --wal-dir argument is set as appropriate'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.7 - Ensure that the --wal-dir argument is set as appropriate'] = "Pass"
  end

  unless etcd_command_line.index{|line| line =~ /--max-wals=0/}
    @results[target]['etcd']['CIS 1.5.8 - Ensure that the --max-wals argument is set to 0'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.8 - Ensure that the --max-wals argument is set to 0'] = "Pass"
  end



  @results[target]['evidence']['etcd'] = etcd_command_line
end

.test_insecure_api_externalObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/kube_auto_analyzer/vuln_checks/api_server.rb', line 3

def self.test_insecure_api_external
  @log.debug("Doing the external Insecure API check")
  target = @options.target_server
  unless @results[target]['vulns']
    @results[target]['vulns'] = Hash.new
  end
  @results[target]['vulns']['insecure_api_external'] = Hash.new
  #Check for whether the Insecure API port is visible outside the cluster
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  nodes.each do |nod|
    if is_port_open?(nod, 8080)
      begin
        pods_resp = RestClient::Request.execute(:url => "http://#{nod}:8080/api",:method => :get)
      rescue RestClient::Forbidden
        pods_resp = "Not Vulnerable - Request Forbidden"
      rescue RestClient::NotFound
        pods_resp = "Not Vulnerable - Request Not Found"
      end
      @results[target]['vulns']['insecure_api_external'][nod] = pods_resp
    else
      @results[target]['vulns']['insecure_api_external'][nod] = "Not Vulnerable - Port Not Open"
    end
  end
end

.test_insecure_api_internalObject

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it’s looking for” perspective, as a weakness in API Server, it makes more sense here.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/kube_auto_analyzer/vuln_checks/api_server.rb', line 33

def self.test_insecure_api_internal
  require 'json'

  @log.debug("Doing the internal Insecure API Server check")
  target = @options.target_server
  @results[target]['vulns']['insecure_api_internal'] = Hash.new
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  container_name = "kaainsecureapitest"
  pod = Kubeclient::Resource.new
  pod. = {}
  pod..name = container_name
  pod..namespace = "default"
  pod.spec = {}
  pod.spec.restartPolicy = "Never"
  pod.spec.containers = {}
  pod.spec.containers = [{name: "kubeautoanalyzerapitest", image: "raesene/kaa-agent:latest"}]
  pod.spec.containers[0].args = ["/api-server-checker.rb",nodes.join(',')]
  begin
    @log.debug("About to start API Server check pod")
    @client.create_pod(pod)
    @log.debug("Executed the create pod")
    sleep_count = 0
    begin
      sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
      sleep_count = sleep_count + 1
      @log.debug("Waited #{(5 * sleep_count).to_s} seconds for the API Server Check Pod")
    rescue
      retry
    end
    @log.debug ("started Kube API Check pod")
    results = JSON.parse(@client.get_pod_log(container_name,"default"))
    results.each do |node, results|
      @results[target]['vulns']['insecure_api_internal'][node] = results
    end
  ensure
    @client.delete_pod(container_name,"default")
  end
end

.test_schedulerObject



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 251

def self.test_scheduler
  target = @options.target_server
  @results[target]['scheduler'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /kube-scheduler/
      @scheduler = pod
    end
  end
  
  unless @scheduler
    @results[target]['scheduler']['Scheduler Pod Not Found'] = "Error"
    return
  end

  scheduler_command_line = @scheduler['spec']['containers'][0]['command']

  unless scheduler_command_line.index{|line| line =~ /--profiling=false/}
    @results[target]['scheduler']['CIS 1.2.1 - Ensure that the --profiling argument is set to false'] = "Fail"
  else
    @results[target]['scheduler']['CIS 1.2.1 - Ensure that the --profiling argument is set to false'] = "Pass"
  end  
  @results[target]['evidence']['Scheduler'] = scheduler_command_line
end

.test_service_token_internalObject

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it’s looking for” perspective, as a weakness in Kubelet, it makes more sense here.



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/kube_auto_analyzer/vuln_checks/service_token.rb', line 5

def self.test_service_token_internal
  require 'json'

  @log.debug("Doing the internal Service Token check")
  target = @options.target_server
  @results[target]['vulns']['service_token'] = Hash.new
  api_server_url = @client.api_endpoint.to_s
  container_name = "kaakubeletunauthtest"
  pod = Kubeclient::Resource.new
  pod. = {}
  pod..name = container_name
  pod..namespace = "default"
  pod.spec = {}
  pod.spec.restartPolicy = "Never"
  pod.spec.containers = {}
  pod.spec.containers = [{name: "kubeautoanalyzerservicetokentest", image: "raesene/kaa-agent:latest"}]
  pod.spec.containers[0].args = ["/service-token-checker.rb",api_server_url]
  begin
    @log.debug("About to start Service Token Check pod")
    @client.create_pod(pod)
    @log.debug("Executed the create pod")
    begin
      sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
    rescue
      retry
    end
    @log.debug ("started Service Token Check pod")
    results = JSON.parse(@client.get_pod_log(container_name,"default"))
    results.each do |node, results|
      @results[target]['vulns']['service_token'][api_server_url] = results
    end
  ensure
    @client.delete_pod(container_name,"default")
  end
end

.test_unauth_kubelet_externalObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/kube_auto_analyzer/vuln_checks/kubelet.rb', line 3

def self.test_unauth_kubelet_external
  @log.debug("Doing the external kubelet check")
  target = @options.target_server
  unless @results[target]['vulns']
    @results[target]['vulns'] = Hash.new
  end
  @results[target]['vulns']['unauth_kubelet'] = Hash.new
  #Check for whether the Kubelet port is visible outside the cluster
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  nodes.each do |nod|
    if is_port_open?(nod, 10250)
      begin
        pods_resp = RestClient::Request.execute(:url => "https://#{nod}:10250/runningpods",:method => :get, :verify_ssl => false)
      rescue RestClient::Forbidden
        pods_resp = "Not Vulnerable - Request Forbidden"
      rescue RestClient::Unauthorized
        pods_resp = "Not Vulnerable - Request Unauthorized"
      end
      @results[target]['vulns']['unauth_kubelet'][nod] = pods_resp
    else
      @results[target]['vulns']['unauth_kubelet'][nod] = "Not Vulnerable - Port Not Open"
    end
  end
end

.test_unauth_kubelet_internalObject

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it’s looking for” perspective, as a weakness in Kubelet, it makes more sense here.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/kube_auto_analyzer/vuln_checks/kubelet.rb', line 33

def self.test_unauth_kubelet_internal
  require 'json'

  @log.debug("Doing the internal kubelet check")
  target = @options.target_server
  @results[target]['vulns']['internal_kubelet'] = Hash.new
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  container_name = "kaakubeletunauthtest"
  pod = Kubeclient::Resource.new
  pod. = {}
  pod..name = container_name
  pod..namespace = "default"
  pod.spec = {}
  pod.spec.restartPolicy = "Never"
  pod.spec.containers = {}
  pod.spec.containers = [{name: "kubeautoanalyzerkubelettest", image: "raesene/kaa-agent:latest"}]
  pod.spec.containers[0].args = ["/kubelet-checker.rb",nodes.join(',')]
  begin
    @log.debug("About to start Kubelet check pod")
    @client.create_pod(pod)
    @log.debug("Executed the create pod")
    begin
      sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
    rescue
      retry
    end
    @log.debug ("started Kubelet Check pod")
    results = JSON.parse(@client.get_pod_log(container_name,"default"))
    results.each do |node, results|
      @results[target]['vulns']['internal_kubelet'][node] = results
    end
  ensure
    @client.delete_pod(container_name,"default")
  end
end