#!/usr/bin/env python3 import errno import json import locale import os import shutil #import ssl import sqlite3 import sys import threading from collections import OrderedDict, defaultdict from flask import Flask, make_response, send_file, jsonify from flask.ext.cors import CORS from flask_restful import reqparse, abort, Api, Resource from time import gmtime, strftime from locale import getlocale from subprocess import Popen, PIPE # TODO # - TLS app = Flask(__name__, static_url_path=os.path.dirname(os.path.abspath(__file__))) CORS(app) api = Api(app) parser = reqparse.RequestParser() # for /tools parser.add_argument('search') # for /assessments/jobs and /tools/jobs parser.add_argument('id') parser.add_argument('assessment') parser.add_argument('tool') parser.add_argument('target') parser.add_argument('target_name') parser.add_argument('protocol') parser.add_argument('port_number') parser.add_argument('user') parser.add_argument('password') parser.add_argument('user_file') parser.add_argument('password_file') parser.add_argument('url') class RunThreads (threading.Thread): def __init__(self, tool, job, context): threading.Thread.__init__(self) self.tool_stdout = '' self.tool_sterr = '' self.tool = tool self.job = job self.context = context def execute_tool(self, job, context): # Always check any tools provided by # community members print("Running") print("Execute string " + self.tool['execute_string']) proc = Popen(self.tool['execute_string'], stdout=PIPE, stderr=PIPE, shell=True) decode_locale = lambda s: s.decode(getlocale()[1]) self.tool_stdout, self.tool_stderr = map(decode_locale, proc.communicate()) # Callback / pause from here return_code = proc.returncode # Zip resulting directory - This is crap because when doing assessment # this will be run at each tool execution. zip_file = os.path.dirname(os.path.abspath(__file__)) + \ "/" + job['target_name'] + '_' + str(job['id']) shutil.make_archive(zip_file, 'zip', job['output_dir']) # TODO This could be dodgy shutil.rmtree(job['output_dir']) # Update completed and return_code field in db con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') cur = con.cursor() if context == 'tool': cur.execute("UPDATE tool_jobs SET executed = 1, return_code = ?, zip_file = ? WHERE id = ?",(str(return_code),str(zip_file),str(job['id']))) con.commit() if context == 'assessment': # Pull and check for 0 cur.execute("SELECT return_code FROM assessment_jobs WHERE id = ? AND return_code == 0",(str(job['id']))) data = cur.fetchall() if len(data) == 0 or data[0][0] == 0: cur.execute("UPDATE assessment_jobs SET executed = 1, return_code = ?, zip_file = ? WHERE id = ?",(str(return_code),str(zip_file),str(job['id']))) con.commit() # Close connection if con: con.close() def run(self): self.execute_tool(self.job, self.context) class Pong(Resource): def get(self): return { 'message':'pong' } class Assessments(Resource): # Get assessments def get(self): args = parser.parse_args() con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() # If /assessments?search=xxx not specified then SELECT * if args['search'] != None: cur.execute("SELECT * FROM assessments WHERE name LIKE ? OR description LIKE ?",('%' + args['search'] + '%','%' + args['search'] + '%')) else: cur.execute("SELECT * FROM assessments") data = dict(result=[dict(r) for r in cur.fetchall()]) # Add tools to assessment for i, assessment in enumerate(data['result']): cur.execute("SELECT tool FROM assessment_tools WHERE assessment = ?",(str(i+1),)) tool_ids = dict(result=[dict(r) for r in cur.fetchall()]) assessment['tools'] = tool_ids['result'] # Close connection if con: con.close() return data # Submit a new tool def post(self): args = parser.parse_args() return args class AssessmentsId(Resource): # Get assessments def get(self, assessment_id): args = parser.parse_args() con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT * FROM assessments WHERE id = ?",(assessment_id),) data = dict(result=[dict(r) for r in cur.fetchall()]) # Add tools to assessment cur.execute("SELECT tool FROM assessment_tools WHERE assessment = ?",(str(assessment_id),)) tool_ids = dict(result=[dict(r) for r in cur.fetchall()]) data['result'][0]['tools'] = tool_ids['result'] # Close connection if con: con.close() return data class Tools(Resource): # Get tools def get(self): args = parser.parse_args() con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() # If /tools?search=xxx not specified then SELECT * if args['search'] != None: cur.execute("SELECT * FROM tools WHERE name LIKE ? OR description LIKE ?",('%' + args['search'] + '%','%' + args['search'] + '%')) else: cur.execute("SELECT * FROM tools") data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data # Submit a new tool def post(self): args = parser.parse_args() return args class ToolsId(Resource): # Get tools def get(self, tool_id): args = parser.parse_args() con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT * FROM tools WHERE id =?",(tool_id,)) data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data # Submit a new tool def post(self): args = parser.parse_args() return args class ToolsJobs(Resource): # List jobs def get (self): #args = parser.parse_args() con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() # If /jobs?search=xxx not specified then SELECT * #if args['search'] != None: # cur.execute("SELECT * FROM tool_jobs WHERE tool LIKE ? OR target LIKE ? OR target_name LIKE ? OR protocol LIKE ? OR port_number LIKE ? OR user like ? OR password LIKE ? OR user_file LIKE ? OR password_file LIKE ?",('%' + args['search'] + '%','%' + args['search'] + '%','%' + args['search'] + '%','%' + args['search'] + '%','%' + args['search'] + '%','%' + args['search'] + '%','%' + args['search'] + '%','%' + args['search'] + '%','%' + args['search'] + '%')) #else: cur.execute("SELECT * FROM tool_jobs") data = [dict(r) for r in cur.fetchall()] # Close connection if con: con.close() return data # Submit new job def post(self): args = parser.parse_args() con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') cur = con.cursor() # curl -i --data "tool=1&target=localhost&target_name=target_name&protocol=https&port_number=1337&user=a_user&password=a_password&user_file=/user/file&password_file=/password/file" http://127.0.0.1:5000/jobs cur.execute("INSERT INTO tool_jobs(tool,target,target_name,protocol,port_number,user,password,user_file,password_file,url) VALUES(?,?,?,?,?,?,?,?,?,?)",(args['tool'],args['target'],args['target_name'].replace('/','_'),args['protocol'],args['port_number'],args['user'],args['password'],args['user_file'],args['password_file'],args['url'])) data = cur.lastrowid con.commit() # Close connection if con: con.close() return { 'id':data }, 201 # List individual jobs class ToolsJobsId(Resource): def get(self, job_id): con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT * FROM tool_jobs WHERE id = ?",(job_id,)) data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data # Execute job class ToolsJobsIdExecute(Resource): def post(self): # Process placeholders tool = {} job = {} args = parser.parse_args() con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() # Get job id columns cur.execute("SELECT * FROM tool_jobs WHERE id = ?",(args['id'],)) job_data = dict(result=[dict(r) for r in cur.fetchall()]) # Index is now tied to database schema, yuck job = job_data['result'][0] tool['id'] = job['tool'] # Get tool execute string and tool name cur.execute("SELECT * FROM tools WHERE id = ?",(tool['id'],)) tool_data = dict(result=[dict(r) for r in cur.fetchall()]) tool = tool_data['result'][0] # TODO Allow set in config file job['tools_directory'] = "/root/tools" job['date'] = strftime("%Y%m%d_%H%M%S%z") job['output_dir'] = os.path.dirname(os.path.abspath(__file__)) + \ '/' + strftime("%Y%m%d") + \ "_torc_" + \ job_data['result'][0]['target_name'] + \ "_" + job_data['result'][0]['target'].replace('/','_') + \ "_" + str(job_data['result'][0]['id']) + \ "_" + tool['name'] try: os.makedirs(job['output_dir']) except OSError as e: pass #if e.errno == errno.EEXIST: # return {'message':'Directory exists'}, 500 # Get dependencies cur.execute("SELECT dependency from dependencies WHERE tool = ?",(tool['id'],)) dependency = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() ddict_options = defaultdict(lambda:'') for option in job: ddict_options[option] = job[option] tool['execute_string'] = tool['execute_string'].format(**ddict_options) thread = [] # Tool string generated, execute thread.append(RunThreads(tool,job,'tool')) # If main process dies, everything else *SHOULD* as well thread[-1].daemon = True # Start threads thread[-1].start() return {'message':'Tool executed'}, 201 class Dependencies(Resource): def get(self): con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT * FROM dependency_names") data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data class Options(Resource): def get(self): con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT * FROM options") data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data # Retrieve options for tool class OptionsId(Resource): def get(self, tool_id): con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT option, required FROM tool_options WHERE tool = ?",(tool_id,)) data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data class ToolsExports(Resource): def get(self): con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT id, zip_file FROM tool_jobs") data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data # Retrieve output for job id class ToolsExportsId(Resource): def get(self, job_id): con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT zip_file FROM tool_jobs WHERE id = ?",(job_id,)) data = dict(result=[dict(r) for r in cur.fetchall()]) zip_file = data['result'][0]['zip_file'] + '.zip' # Close connection if con: con.close() return send_file(zip_file, as_attachment=True) # Retrieve dependencies for tool class DependenciesId(Resource): def get(self, tool_id): con = sqlite3.connect(os.path.dirname(os.path.abspath(__file__)) + '/assets.db') con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT dependency FROM dependencies WHERE tool = ?",(tool_id,)) data = dict(result=[dict(r) for r in cur.fetchall()]) # Close connection if con: con.close() return data class CheckCertificate(): def __init__(self): if os.path.isfile(os.path.dirname(os.path.abspath(__file__)) + '/keys/cert.crt') != True: print("First run, generating certificate..") execute_string = "openssl req -nodes -x509 -newkey rsa:4096 -keyout " + \ os.path.dirname(os.path.abspath(__file__)) + \ "/keys/key.pem -out " + os.path.dirname(os.path.abspath(__file__)) + \ "/keys/cert.crt -days 356 -subj " + \ "\"/C=AP/ST=Land/L=torc/O=AP Team/subjectAltName=IP:127.0.0.1\"" proc = Popen(execute_string, stdout=PIPE, stderr=PIPE, shell=True) decode_locale = lambda s: s.decode(getlocale()[1]) self.tool_stdout, self.tool_stderr = map(decode_locale, proc.communicate()) # Callback / pause from here return_code = proc.returncode # Pong! # curl -i http://127.0.0.1:5000/ping api.add_resource(Pong, '/ping') # Fetch all tools # curl -i http://127.0.0.1:5000/tools api.add_resource(Tools, '/tools') # Fetch tool id # curl -i http://127.0.0.1:5000/tools/1 api.add_resource(ToolsId, '/tools/<tool_id>') # Fetch all assessments # curl -i http://127.0.0.1:5000/assessments api.add_resource(Assessments, '/assessments') # Fetch assessment id # curl -i http://127.0.0.1:5000/assessments/1 api.add_resource(AssessmentsId, '/assessments/<assessment_id>') # Fetch all tool jobs # curl -i http://127.0.0.1:5000/jobs api.add_resource(ToolsJobs, '/tools/jobs') # Fetch tool job id # curl -i http://127.0.0.1:5000/jobs/1 api.add_resource(ToolsJobsId, '/tools/jobs/<job_id>') # Execute tool job id # curl -i --data "id=1" http://127.0.0.1:5000/jobs/execute api.add_resource(ToolsJobsIdExecute, '/tools/jobs/execute') # Fetch all dependencies # curl -i http://127.0.0.1:5000/dependencies api.add_resource(Dependencies, '/dependencies') # Fetch all dependencies for tool id api.add_resource(DependenciesId, '/dependencies/<tool_id>') # TODO Review (/tools/options? /assessment/options?) # Fetch all options # curl -i http://127.0.0.1:5000/options api.add_resource(Options, '/options') # Fetch all options for tool id # curl -i http://127.0.0.1:5000/options/1 api.add_resource(OptionsId, '/options/<tool_id>') # Fetch all tool outputs # curl -i http://127.0.0.1:5000/exports api.add_resource(ToolsExports, '/tools/jobs/exports') # Fetch tool output from job id # curl -i http://127.0.0.1:5000/exports/1 api.add_resource(ToolsExportsId, '/tools/jobs/exports/<job_id>') def main(): # TODO Hack, fix #locale.setlocale(locale.LC_CTYPE, 'en_US.UTF-8') print(os.path.dirname(os.path.abspath(__file__))) # Check for certificate (or create if non-existent) #CheckCertificate() #context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) #context.load_cert_chain(os.path.dirname(os.path.abspath(__file__)) + "/keys/cert.crt", \ # os.path.dirname(os.path.abspath(__file__)) + "/keys/key.pem") if os.path.isfile('/.dockerenv'): print("Running in docker") #app.run(host='0.0.0.0',debug=True,threaded=True,ssl_context=context) app.run(host='0.0.0.0',debug=True,threaded=True) else: #app.run(debug=True,threaded=True,ssl_context=context) app.run(debug=True,threaded=True,port=5000) if __name__ == '__main__': main()