def funScan(lstHosts, boolCache): # Initiate the scan strHLen = str(len(lstHosts)) for i, strHost in enumerate(lstHosts): if not boolCache and not objSLA.funAnalyze(strHost): # If cached reports aren't allowed (default) - but a new assessment has failed to start continue funResult(objSLA.funOpStatus(strHost)) log.funLog(2, '[%s/%s] Done.' % (str(i + 1), strHLen))
def funResult(amStatus): # Add grades to list or print assessment JSON global lstGrades, intRCount if isinstance(amStatus, list): for i in amStatus: log.funLog(1, i) lstGrades.extend(amStatus) intRCount += 1 elif isinstance(amStatus, dict): print(json.dumps(amStatus, indent=4))
def funReadCfg(strCFile): # Read external config file if not os.path.isfile(strCFile): log.funLog(1, 'Config file: %s is missing.' % strCFile, 'err') return {} try: # Open the config file with open(strCFile, 'r') as f: return json.load(f) except Exception as e: log.funLog(2, repr(e), 'err') return {}
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))) # Update cool-off period self.funInfo() time.sleep(self.intCool) 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 funInfo(self, boolConTune=False): # Check availability of SSL Labs servers, adjust concurrency and cool-off period if self.boolIM: self.strAnStNew += '&ignoreMismatch=on' 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.' % str(self.intCool)) intDelta = objHResp['maxAssessments'] - objHResp[ 'currentAssessments'] if boolConTune and self.intConc > intDelta: # Trim concurrency on init self.intConc = intDelta return True if intDelta > 0 else False except Exception as e: log.funLog(2, repr(e), 'err')
def funConScan(lstHosts, boolCache): # Concurrent scan strHLen = str(len(lstHosts)) # Split the hosts list into groups of the concurrency size (2D list) lstMatrix = [ lstHosts[i:i + objSLA.intConc] for i in range(0, len(lstHosts), objSLA.intConc) ] # Initiate the scan for g, lstGroup in enumerate(lstMatrix): # New assessment if not boolCache: for i, strHost in enumerate(lstGroup): log.funLog( 2, '[%s/%s] Starting...' % (str(g * objSLA.intConc + i + 1), strHLen)) objSLA.funAnalyze(strHost) time.sleep(objSLA.intCool) # Check status intReady = 0 while intReady < len(lstGroup): # Completed assessments counter intReady = 0 time.sleep(objSLA.intPoll) for i, strHost in enumerate(lstGroup): if strHost.endswith('#'): intReady += 1 continue # Non-blocking operation status amStatus = objSLA.funOpStatus(strHost, True) if amStatus: # Mark host as completed log.funLog( 2, '[%s/%s] Done.' % (str(g * objSLA.intConc + i + 1), strHLen)) lstGroup[i] += '#' intReady += 1 funResult(amStatus)
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' % str(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 # Show actual endpoint IP address or the first 8 chars of its SHA-256 hash strIP = diEP[ 'ipAddress'] if self.boolIPs else hashlib.sha256( diEP['ipAddress']).hexdigest()[:8] log.funLog( 3, '%s, IP: %s, %s' % (strHost, strIP, 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)
def funExit(): log.funLog(1, 'Exiting...')
def funBadExit(type, value, traceback): log.funLog(2, 'Unhandled Exception: %s, %s, %s' % (type, value, traceback))
def main(): global objSLA, strCFile, lstGrades, strMHead objArgs = funArgParser() # If run interactively, stdout is used for log messages (unless -j is set) if sys.stdout.isatty() and not objArgs.json: log.strLogMethod = 'stdout' # Set log level if objArgs.log: log.intLogLevel = objArgs.log # Ignore server certificate mismatch objSLA.boolIM = objArgs.im # Show real IP addresses argument objSLA.boolIPs = objArgs.ips # Full assessment JSON argument objSLA.boolJSON = objArgs.json # Config file location if objArgs.cfile: strCFile = objArgs.cfile # Concurrency if objArgs.conc: objSLA.intConc = objArgs.conc # Read config file diCfg = cfg.funReadCfg(strCFile) try: lstHosts = diCfg['hosts'] except Exception as e: log.funLog(1, 'Invalid config file: %s' % strCFile, 'err') log.funLog(2, repr(e), 'err') sys.exit(objExCodes.cfile) # List of hosts to scan (override) if objArgs.HOST: lstHosts = objArgs.HOST # Hosts list cleanup (remove invalid domains) lstHClean = [strHost for strHost in lstHosts if objSLA.funValid(strHost)] if len(lstHosts) > len(lstHClean): log.funLog( 1, 'Ignoring invalid hostname(s): %s' % ', '.join(list(set(lstHosts) - set(lstHClean))), 'err') lstHosts = lstHClean # Check SSL Labs availability if not objSLA.funInfo(True): log.funLog( 1, 'SSL Labs unavailable or maximum concurrent assessments exceeded.', 'err') sys.exit(objExCodes.nosrv) log.funLog( 1, 'Scanning %s host(s)... [Cache: %s, Concurrency: %s]' % (str(len(lstHosts)), bool(objArgs.cache), str(objSLA.intConc))) # Scan if objSLA.intConc > 1: # Concurrency funConScan(lstHosts, objArgs.cache) else: funScan(lstHosts, objArgs.cache) # Sort the grades in reverse and add line breaks strReport = '\r\n'.join(sorted(lstGrades, reverse=True)) # Send the report to a Slack channel if objArgs.slack and not objArgs.json: log.funLog(1, 'Slacking the report...') try: objSlack = slackclient.SlackClient(diCfg['token'].decode('base64')) diSResp = objSlack.api_call('chat.postMessage', username=strMFrom, channel=diCfg['channel'], text='```\n%s\n```' % strReport, icon_url=strSIcon) if not diSResp['ok']: raise Exception(diSResp['error']) except Exception as e: log.funLog(2, repr(e), 'err') # Mail the report if objArgs.mail and not objArgs.json: # Format MIME message objMIME = email.MIMEText( 'Total Hosts: [%s/%s], Concurrency: %s\r\n%s' % (str(intRCount), str(len(lstHosts)), str( objSLA.intConc), strReport)) log.funLog(1, 'Mailing the report...') try: objMIME['From'] = '%s <%s>' % (strMFrom, diCfg['from']) # Remove spaces and split recipients into a list delimited by , or ; lstTo = re.split(r',|;', diCfg['to'].replace(' ', '')) objMIME['To'] = ', '.join(lstTo) objMIME['Subject'] = strMSubj # Connect to SMTP server objMail = smtplib.SMTP(diCfg['server']) # Identification objMail.ehlo() # Encryption objMail.starttls() # Authentication objMail.login(diCfg['user'], diCfg['pass'].decode('base64')) # Send mail objMail.sendmail(diCfg['from'], lstTo, objMIME.as_string()) log.funLog(1, 'Success!') # Terminate the SMTP session and close the connection objMail.quit() except Exception as e: log.funLog(2, repr(e), 'err') else: print(strReport)