Fix --conf option validation
[integration.git] / test / security / check_for_ingress_and_nodeports.py
1 #!/usr/bin/env python3
2 #   COPYRIGHT NOTICE STARTS HERE
3 #
4 #   Copyright 2019 Samsung Electronics Co., Ltd.
5 #
6 #   Licensed under the Apache License, Version 2.0 (the "License");
7 #   you may not use this file except in compliance with the License.
8 #   You may obtain a copy of the License at
9 #
10 #       http://www.apache.org/licenses/LICENSE-2.0
11 #
12 #   Unless required by applicable law or agreed to in writing, software
13 #   distributed under the License is distributed on an "AS IS" BASIS,
14 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 #   See the License for the specific language governing permissions and
16 #   limitations under the License.
17 #
18 #   COPYRIGHT NOTICE ENDS HERE
19
20 # Check all node ports exposed outside of kubernetes cluster looking for plain http and https.
21 # Check all ingress controller services exposed outside of the kubernetes cluster
22 # looking for plain http and https.  This script looks for K8S NodePorts and ingress services declared
23 # in the K8S cluster configurations and check if service is alive or not.
24 # Automatic detect nodeport or ingress protocol HTTP or HTTPS it also detect if particular service uses HTTPS
25 # with self signed certificate (HTTPU).
26 # Verbose option retrives HTTP header and prints it for each service
27 #
28 # Dependencies:
29 #
30 #     pip3 install kubernetes
31 #     pip3 install colorama
32 #
33 # Environment:
34 #   This script should be run on the local machine which has network access to the onap K8S cluster.
35 #   It requires k8s cluster config file on local machine.
36 #
37 # Example usage:
38 #   Display exposed nodeport and ingress resources declared in the K8S cluster without scanning:
39 #       check_for_ingress_and_nodeports.py
40 #   Scan declared nodeports:
41 #       check_for_ingress_and_nodeports.py --scan-nodeport
42 #   Scan declared exposed ingress resources:
43 #       check_for_ingress_and_nodeports.py --scan-ingress
44
45 from kubernetes import client, config
46 import http.client
47 import ssl
48 import socket
49 from enum import Enum
50 import argparse
51 import sys
52 import colorama
53 from colorama import Fore
54 import urllib.parse
55 from os import path
56
57 """ List all nodeports """
58 def list_nodeports(v1):
59     ret = {}
60     svc = v1.list_namespaced_service(K8S_NAMESPACE)
61     for i in svc.items:
62         if i.spec.ports:
63             ports = [ j.node_port for j in i.spec.ports if j.node_port ]
64             if ports:
65                 ret[i.metadata.name] = ports
66     return ret
67
68 # Class enum for returning current http mode
69 class ScanMode(Enum):
70     HTTPS = 0   #Safe https
71     HTTPU = 1   #Unsafe https
72     HTTP = 2    #Pure http
73     def __str__(self):
74         return self.name
75
76 #Read the ingress controller http and https ports from the kubernetes cluster
77 def find_ingress_ports(v1):
78     svc = v1.list_namespaced_service(K8S_INGRESS_NS)
79     http_port = 0
80     https_port = 0
81     for item in svc.items:
82         if item.metadata.name == K8S_INGRESS_NS:
83             for pinfo in item.spec.ports:
84                 if pinfo and pinfo.name == 'http':
85                     http_port = pinfo.node_port
86                 elif pinfo and pinfo.name == 'https':
87                     https_port = pinfo.node_port
88
89             return http_port,https_port
90         else: return(80,443)
91
92 # List all ingress devices
93 def list_ingress(xv1b):
94     SSL_ANNOTATION = 'nginx.ingress.kubernetes.io/ssl-redirect'
95     inglist = xv1b.list_namespaced_ingress(K8S_NAMESPACE)
96     svc_list = {}
97     for ing in inglist.items:
98         svc_name = ing.metadata.labels['app']
99         arr = []
100         annotations = ing.metadata.annotations
101         for host in ing.spec.rules:
102             arr.append(host.host)
103         if (SSL_ANNOTATION in annotations) and annotations[SSL_ANNOTATION]=="true":
104             smode = ScanMode.HTTPS
105         else: smode = ScanMode.HTTP
106         svc_list[svc_name] = [ arr, smode ]
107     return svc_list
108
109 # Scan single port
110 def scan_single_port(host,port,scanmode):
111     ssl_unverified = ssl._create_unverified_context()
112     if scanmode==ScanMode.HTTP:
113         conn = http.client.HTTPConnection(host,port,timeout=10)
114     elif scanmode==ScanMode.HTTPS:
115         conn = http.client.HTTPSConnection(host,port,timeout=10)
116     elif scanmode==ScanMode.HTTPU:
117         conn = http.client.HTTPSConnection(host,port,timeout=10,context=ssl_unverified)
118     outstr = None
119     retstatus = False
120     try:
121         conn.request("GET","/")
122         outstr = conn.getresponse()
123     except http.client.BadStatusLine as exc:
124         outstr = "Non HTTP proto" +  str(exc)
125         retstatus = exc
126     except ConnectionRefusedError as exc:
127         outstr = "Connection refused" + str(exc)
128         retstatus = exc
129     except ConnectionResetError as exc:
130         outstr = "Connection reset" + str(exc)
131         retstatus = exc
132     except socket.timeout as exc:
133         outstr = "Connection timeout" + str(exc)
134         retstatus = exc
135     except ssl.SSLError as exc:
136         outstr = "SSL error" + str(exc)
137         retstatus = exc
138     except OSError as exc:
139         outstr = "OS error" + str(exc)
140         retstatus = exc
141     conn.close()
142     return retstatus,outstr
143
144 # Scan port
145 def scan_portn(port):
146     host =  urllib.parse.urlsplit(v1c.host).hostname
147     for mode in ScanMode:
148         retstatus, out = scan_single_port(host,port,mode)
149         if not retstatus:
150             result = port, mode, out.getcode(), out.read().decode('utf-8'),mode
151             break
152         else:
153             result = port, retstatus, out, None,mode
154     return result
155
156
157 def scan_port(host, http, https, mode):
158     if mode==ScanMode.HTTP:
159         retstatus, out = scan_single_port(host,http,ScanMode.HTTP)
160         if not retstatus:
161             return host, ScanMode.HTTP, out.getcode(), out.read().decode('utf-8'), mode
162         else:
163             return host, retstatus, out, None, mode
164     elif mode==ScanMode.HTTPS:
165         retstatus, out = scan_single_port(host,https,ScanMode.HTTPS)
166         if not retstatus:
167             return host, ScanMode.HTTPS, out.getcode(), out.read().decode('utf-8'), mode
168         else:
169             retstatus, out = scan_single_port(host,https,ScanMode.HTTPU)
170             if not retstatus:
171                 return host, ScanMode.HTTPU, out.getcode(), out.read().decode('utf-8'), mode
172             else:
173                 return host, retstatus, out, None, mode
174
175
176 # Visualise scan result
177 def console_visualisation(cname, name, retstatus, httpcode, out, mode, httpcodes = None):
178     if httpcodes is None: httpcodes=[]
179     print(Fore.YELLOW,end='')
180     print( cname,name, end='\t',sep='\t')
181     if isinstance(retstatus,ScanMode):
182         if httpcode in httpcodes: estr = Fore.RED + '[ERROR '
183         else:  estr = Fore.GREEN + '[OK '
184         print( estr, retstatus, str(httpcode)+ ']'+Fore.RESET,end='')
185         if VERBOSE: print( '\t',str(out) )
186         else: print()
187     else:
188         if not out: out = str(retstatus)
189         print( Fore.RED, '[ERROR ' +str(mode) +']', Fore.RESET,'\t', str(out))
190
191 # Visualize compare results
192 def console_compare_visualisation(cname,d1,d2):
193     print(Fore.YELLOW,end='')
194     print(cname, end='\t',sep='\t')
195     if d1!=d2:
196         print(Fore.RED + '[ERROR] '+ Fore.RESET)
197         if d1[0]!=d2[0]:
198             print('\tCode:',d1[0],'!=',d2[0])
199         if d1[1]!=d2[1]:
200             print('\t******** Response #1 ********\n',d1[1])
201             print('\t******** Response #2 ********\n',d2[1])
202     else:
203         print(Fore.GREEN + '[OK ',d1[0],']', Fore.RESET,sep='')
204         if VERBOSE and d1[1]:
205             print(d1[1])
206
207
208 # Port detector type
209 def check_onap_ports():
210     print("Scanning onap NodePorts")
211     check_list = list_nodeports(v1)
212     if not check_list:
213         print(Fore.RED + 'Unable to find any declared node port in the K8S cluster', Fore.RESET)
214     for k,v in check_list.items():
215         for port in v:
216             console_visualisation(k,*scan_portn(port) )
217
218 #Check ONAP ingress
219 def check_onap_ingress():
220     print("Scanning onap ingress services")
221     ihttp,ihttps = find_ingress_ports(v1)
222     check_list = list_ingress(v1b)
223     if not check_list:
224         print(Fore.RED+ 'Unable to find any declared ingress service in the K8S cluster', Fore.RESET)
225     for k,v in check_list.items():
226         for host in v[0]:
227             console_visualisation(k,*scan_port(host,ihttp,ihttps,v[1]),httpcodes=[404])
228
229 #Print onap all ingress ports and node ports
230 def onap_list_all():
231     ihttp,ihttps = find_ingress_ports(v1)
232     host =  urllib.parse.urlsplit(v1c.host).hostname
233     print( 'Cluster IP' + Fore.YELLOW, host, Fore.RESET )
234     print('Ingress ' + Fore.RED + 'HTTP'  + Fore.RESET + '  port:',Fore.YELLOW, ihttp, Fore.RESET)
235     print('Ingress ' + Fore.RED + 'HTTPS' + Fore.RESET + ' port:',Fore.YELLOW, ihttps, Fore.RESET)
236     print(Fore.YELLOW+"Onap NodePorts list:",Fore.RESET)
237     check_list = list_nodeports(v1)
238     for name,ports in check_list.items():
239         print(Fore.GREEN, name,Fore.RESET,":", *ports)
240     print(Fore.YELLOW+"Onap ingress controler services list:",Fore.RESET)
241     check_list = list_ingress(v1b)
242     for name,hosts in check_list.items():
243         print(Fore.GREEN, name + Fore.RESET,":", *hosts[0], Fore.RED+':', hosts[1],Fore.RESET)
244
245 #Scan and compare nodeports and ingress check for results
246 def compare_nodeports_and_ingress():
247     ihttp,ihttps = find_ingress_ports(v1)
248     print('Scanning nodeport services ...')
249     check_list = list_nodeports(v1)
250     if not check_list:
251         print(Fore.RED + 'Unable to find any declared node port in the K8S cluster', Fore.RESET)
252     valid_results = {}
253     for k,v in check_list.items():
254         for port in v:
255             nodeport_results = scan_portn(port)
256             if isinstance(nodeport_results[1],ScanMode) and nodeport_results[2] != 404:
257                 valid_results[k] = nodeport_results
258             if VERBOSE: console_visualisation(k,*nodeport_results)
259     check_list = list_ingress(v1b)
260     if not check_list:
261         print(Fore.RED+ 'Unable to find any declared ingress service in the K8S cluster', Fore.RESET)
262     print('Scanning ingress services ...')
263     ing_valid_results = {}
264     for k,v in check_list.items():
265         for host in v[0]:
266             ingress_results = scan_port(host,ihttp,ihttps,v[1])
267             if isinstance(ingress_results[1],ScanMode) and ingress_results[2]!=404:
268                 ing_valid_results[k] = ingress_results
269             if VERBOSE: console_visualisation(k,*ingress_results,httpcodes=[404])
270     ks1 = set(valid_results.keys())
271     ks2 = set(ing_valid_results.keys())
272     diff_keys = (ks1 - ks2) | (ks2 - ks1)
273     common_keys = ks1 & ks2
274     if VERBOSE and diff_keys:
275         print(Fore.BLUE + '[WARNING] Non matching nodes and ingress list:')
276         for key in diff_keys: print(key,sep='\t')
277         print(Fore.RESET + 'Please check is it correct.')
278     print('Matching ingress and nodeport host scan results:')
279     for scan_key in common_keys:
280         s1 = valid_results[scan_key][2:4]
281         s2 = ing_valid_results[scan_key][2:4]
282         num_failures = 0
283         if s1!=s2: ++num_failures
284         console_compare_visualisation(scan_key,s1,s2)
285     return num_failures
286
287 def kube_config_exists(conf):
288     try:
289         assert path.exists(conf)
290     except AssertionError:
291         raise argparse.ArgumentTypeError(f'Fatal! K8S config {conf} does not exist')
292     else:
293         return conf
294
295 if __name__ == "__main__":
296     colorama.init()
297     parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
298     command_group = parser.add_mutually_exclusive_group()
299     command_group.add_argument("--scan-nodeport",
300        default=False, action='store_true',
301        help='Scan onap for node services'
302     )
303     command_group.add_argument("--scan-ingress",
304        default=False, action='store_true',
305        help='Scan onap for ingress services'
306     )
307     command_group.add_argument("--scan-and-compare",
308        default=False, action='store_true',
309        help='Scan nodeports and ingress and compare results'
310     )
311     parser.add_argument( "--namespace",
312         default='onap', action='store',
313         help = 'kubernetes onap namespace'
314     )
315     parser.add_argument( "--ingress-namespace",
316         default='ingress-nginx', action='store',
317         help = 'kubernetes ingress namespace'
318     )
319     parser.add_argument( "--conf",
320         default='~/.kube/config', action='store',
321         help = 'kubernetes config file',
322         type = kube_config_exists
323     )
324     parser.add_argument("--verbose",
325        default=False, action='store_true',
326        help='Verbose output'
327     )
328     args = parser.parse_args()
329     K8S_NAMESPACE = args.namespace
330     K8S_INGRESS_NS  = args.ingress_namespace
331     VERBOSE = args.verbose
332     config.load_kube_config(config_file=args.conf)
333     v1 = client.CoreV1Api()
334     v1b = client.ExtensionsV1beta1Api()
335     v1c = client.Configuration()
336     if args.scan_nodeport: check_onap_ports()
337     elif args.scan_ingress: check_onap_ingress()
338     elif args.scan_and_compare: sys.exit(compare_nodeports_and_ingress())
339     else: onap_list_all()