#!/usr/bin/env python
# tls-scan - lib: REST API
# https://github.com/ArtiomL/tls-scan
# Artiom Lichtenstein
# v1.0.1, 09/12/2016

import hashlib
import json
import log
import re
import requests
import time

__author__ = 'Artiom Lichtenstein'
__license__ = 'MIT'
__version__ = '1.0.1'

# SSL Labs REST API
class clsSLA(object):
	def __init__(self):
		# API entry point
		self.strAPIE = 'https://api.ssllabs.com/api/v2/'
		# API call: info
		self.strInfo = 'info'
		# API call: analyze
		self.strAnalyze = 'analyze?publish=off&all=done&host='
		# Parameter to initiate a new assessment
		self.strAnStNew = '&startNew=on'
		# API HTTPS session
		self.objHS = requests.session()
		# Add Content-Type to HTTP headers and modify User-Agent
		self.objHS.headers.update({ 'Content-Type': 'application/json', 'User-Agent': 'tls-scan v%s' % __version__ })
		# Concurrency = number of simultaneous assessments
		self.intConc = 1
		# Cool-off period after each new assessment (in sec.)
		self.intCool = 1
		# Polling interval (in sec.)
		self.intPoll = 5
		# Show IP addresses in reports
		self.boolIPs = False
		# Return full assessment JSON (only grades by default)
		self.boolJSON = False

	def funInfo(self):
		# Check availability of SSL Labs servers
		try:
			objHResp = json.loads(self.objHS.get(self.strAPIE + self.strInfo).content)
			self.intCool = objHResp['newAssessmentCoolOff'] / 1000
			log.funLog(2, 'Cool-off period after each new assessment: %s sec.' % self.intCool)
			return True if objHResp['currentAssessments'] < objHResp['maxAssessments'] else False

		except Exception as e:
			log.funLog(2, repr(e), 'err')

	def funAnalyze(self, strHost):
		# Initiate a new assessment for a host
		while True:
			try:
				objHResp = self.objHS.get(self.strAPIE + self.strAnalyze + strHost + self.strAnStNew)
				if objHResp.status_code in [429, 503, 529]:
					# 429 - client request rate too high or too many new assessments too fast
					# 503 - the service is not available (e.g. down for maintenance)
					# 529 - the service is overloaded
					log.funLog(2, 'Request rate too high or service unavailable [%s]! Sleeping for %s sec.' % (str(objHResp.status_code), str(self.intCool)))
					time.sleep(self.intCool)
					# Update cool-off period
					self.funInfo()
				elif objHResp.status_code == 200:
					log.funLog(1, 'New assessment started for %s: %s' % (strHost, json.loads(objHResp.content)['status']))
					return True

				else:
					log.funLog(1, 'New assessment failed for %s [%s]' % (strHost, str(objHResp.status_code)))
					return False

			except Exception as e:
				log.funLog(2, repr(e), 'err')
				break

	def funGrades(self, diOper):
		# Parse endpoints to get the grades
		lstGrades = []
		for diEP in diOper['endpoints']:
			strStaMess = diEP['statusMessage']
			strGrade = 'X'
			if strStaMess == 'Ready':
				strGrade = diEP['grade']
			lstGrades.append('[%s] %s, %s, %s' % (strGrade, diOper['host'], diEP['ipAddress'], strStaMess))
		return lstGrades

	def funOpStatus(self, strHost, boolAsync = False):
		# Check operation status
		# boolAsync = True: non-blocking, gets current status and exits (returns results only on 'ERROR' or 'READY')
		# boolAsync = False: blocking, loops while 'IN_PROGRESS', exits (with results) only on 'ERROR' or 'READY'
		strStatus = 'DNS'
		strURL = self.strAPIE + self.strAnalyze + strHost
		while strStatus == 'DNS':
			# Initial status
			try:
				diOper = json.loads(self.objHS.get(strURL).content)
				strStatus = diOper['status']
				log.funLog(3, 'Transaction status: %s' % strStatus)
				if strStatus == 'ERROR':
					return ['[X] %s, %s' % (strHost, diOper['statusMessage'])]

			except Exception as e:
				log.funLog(2, repr(e), 'err')
		if not boolAsync:
			# Log container for per-endpoint messages
			lstMessages = [None] * len(diOper['endpoints'])
			log.funLog(2, 'Total number of endpoints: %s' % len(lstMessages))
			while strStatus == 'IN_PROGRESS':
				try:
					if log.intLogLevel >= 3:
						for i, diEP in enumerate(diOper['endpoints']):
							if diEP['statusMessage'] == 'In progress':
								strDetMess = diEP['statusDetailsMessage']
								if strDetMess != lstMessages[i]:
									lstMessages[i] = strDetMess
									log.funLog(3, '%s, IP: %s, %s' % (strHost, diEP['ipAddress'], lstMessages[i]))
					else:
						time.sleep(self.intPoll)
					diOper = json.loads(self.objHS.get(strURL).content)
					strStatus = diOper['status']
				except Exception as e:
					log.funLog(2, repr(e), 'err')
		if strStatus == 'READY':
			log.funLog(1, 'Assessment complete for: %s' % strHost)
			return diOper if self.boolJSON else self.funGrades(diOper)

	@staticmethod
	def funValid(strHost):
		# Validate hostname
		return bool(re.compile(
				r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|'
				r'([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|'
				r'([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.'
				r'([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$'
			).match(strHost))

@@ -2704,16 +2704,29 @@
 es = %5B%5D%0A
+%09%09strIP = ''%0A
 %09%09for di
@@ -2752,16 +2752,16 @@
 ints'%5D:%0A
-
 %09%09%09strSt
@@ -2866,16 +2866,116 @@
 grade'%5D%0A
+%09%09%09strIP = diEP%5B'ipAddress'%5D if self.boolIPs else hashlib.sha256(diEP%5B'ipAddress'%5D).hexdigest()%5B:8%5D%0A
 %09%09%09lstGr
@@ -3033,33 +3033,21 @@
 host'%5D, 
-diEP%5B'ipAddress'%5D
+strIP
 , strSta

Update reapi.py