diff --git a/core/downloader.py b/core/downloader.py index 399dcd0..09f25f0 100644 --- a/core/downloader.py +++ b/core/downloader.py @@ -97,23 +97,41 @@ def _extract_edge_id(self, url: str) -> Optional[str]: return None + def _extract_chrome_id(self, url: str) -> Optional[str]: + """Extract Chrome extension ID from URL.""" + # Handle both old and new Chrome Web Store URLs + if 'chrome.google.com/webstore' in url or 'chromewebstore.google.com/detail' in url: + # Split by '/' and get the last part which contains the ID + parts = url.rstrip('/').split('/') + return parts[-1] + # If the input is already an ID, return it directly + elif len(url.strip()) == 32: + return url.strip() + return None + def download_chrome(self, ext_id: str, name: Optional[str] = None) -> Optional[str]: """Download Chrome extension using the updated URL format. Args: - ext_id: The Chrome extension ID + ext_id: The Chrome extension ID or URL name: Optional name for the saved file Returns: The name of the saved file if successful, None otherwise """ - save_name = name if name else ext_id + # Extract extension ID if a URL is provided + actual_id = self._extract_chrome_id(ext_id) + if not actual_id: + core.updatelog('Invalid Chrome extension ID or URL') + return None + + save_name = name if name else actual_id dl_url = ( "https://clients2.google.com/service/update2/crx?" "response=redirect&" f"prodversion={self.chrome_version}&" - "x=id%3D" + ext_id + "%26installsource%3Dondemand%26uc&" + f"x=id%3D{actual_id}%26installsource%3Dondemand%26uc&" f"nacl_arch={self.nacl_arch}&" "acceptformat=crx2,crx3" ) diff --git a/extanalysis.py b/extanalysis.py index 41c1ec9..7a7b084 100644 --- a/extanalysis.py +++ b/extanalysis.py @@ -31,6 +31,7 @@ import core.helper as helper import core.settings as settings from flask_wtf.csrf import CSRFProtect +from frontend.api import api # Import the API blueprint parser = argparse.ArgumentParser(prog='extanalysis.py', add_help=False) parser.add_argument('-h', '--host', help='Host to run ExtAnalysis on. Default host is 127.0.0.1') @@ -88,6 +89,7 @@ def allowed_file(filename): app = Flask('ExtAnalysis - Browser Extension Analysis Toolkit') app.config['UPLOAD_FOLDER'] = core.lab_path app.secret_key = str(os.urandom(24)) +app.register_blueprint(api, url_prefix='/api') # Register the blueprint csrf.init_app(app) @@ -176,10 +178,19 @@ def source_code(url): return (vs.view(url)) -@app.route('/analysis/') +@app.route("/analysis//") def show_analysis(analysis_id): - import frontend.viewresult as viewResult - return (viewResult.view(analysis_id)) + return redirect(url_for('view_tab', analysis_id=analysis_id, tab='basic_info')) + +@app.route("/analysis///") +def view_tab(analysis_id, tab): + valid_tabs = ['basic_info', 'files', 'permissions', 'urls_domains', 'gathered_intels'] + if tab not in valid_tabs: + return redirect(url_for('view_tab', analysis_id=analysis_id, tab='basic_info')) + + # Import the view function from frontend module + import frontend.viewresult as viewresult + return viewresult.view(analysis_id, tab) if __name__ == "__main__": diff --git a/frontend/api.py b/frontend/api.py index 26a8a70..a9bea58 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -22,15 +22,97 @@ import core.downloader as download_extension import os import json -from flask import Flask, request, render_template, redirect, url_for, send_from_directory +from flask import Flask, request, render_template, redirect, url_for, send_from_directory, Blueprint, jsonify import logging import traceback import core.scans as scan import base64 +from .handlers.result_handler import view_result +from .handlers.data_handler import ( + get_basic_info, get_files_data, get_urls_data, + get_permissions_data, get_domains_data, get_ips_data, + get_emails_data, get_btc_data, get_comments_data, + get_base64_data, get_manifest_data +) +api = Blueprint('api', __name__) + +@api.route('/result/', methods=['GET']) +def view_analysis_result(analysis_id): + return view_result(analysis_id) + +@api.route('/result//basic_info', methods=['GET']) +def basic_info(analysis_id): + return jsonify(get_basic_info(analysis_id)) + +@api.route('/result//files', methods=['GET']) +def files(analysis_id): + return jsonify(get_files_data(analysis_id)) + +@api.route('/result//urls', methods=['GET']) +def urls(analysis_id): + return jsonify(get_urls_data(analysis_id)) + +@api.route('/result//permissions', methods=['GET']) +def permissions(analysis_id): + return jsonify(get_permissions_data(analysis_id)) + +@api.route('/result//domains', methods=['GET']) +def domains(analysis_id): + return jsonify(get_domains_data(analysis_id)) + +@api.route('/result//ips', methods=['GET']) +def ips(analysis_id): + return jsonify(get_ips_data(analysis_id)) + +@api.route('/result//emails', methods=['GET']) +def emails(analysis_id): + return jsonify(get_emails_data(analysis_id)) + +@api.route('/result//btc', methods=['GET']) +def btc(analysis_id): + return jsonify(get_btc_data(analysis_id)) + +@api.route('/result//comments', methods=['GET']) +def comments(analysis_id): + return jsonify(get_comments_data(analysis_id)) + +@api.route('/result//base64', methods=['GET']) +def base64(analysis_id): + return jsonify(get_base64_data(analysis_id)) + +@api.route('/result//manifest', methods=['GET']) +def manifest(analysis_id): + return jsonify(get_manifest_data(analysis_id)) def view(query, allargs): - if query == 'dlanalysis': + analysis_id = allargs.get('analysis_id') + + if query == 'view_result': + return view_result(analysis_id) + elif query == 'basic_info': + return get_basic_info(analysis_id) + elif query == 'files': + return get_files_data(analysis_id) + elif query == 'urls': + return get_urls_data(analysis_id) + elif query == 'permissions': + return get_permissions_data(analysis_id) + elif query == 'domains': + return get_domains_data(analysis_id) + elif query == 'ips': + return get_ips_data(analysis_id) + elif query == 'emails': + return get_emails_data(analysis_id) + elif query == 'btc': + return get_btc_data(analysis_id) + elif query == 'comments': + return get_comments_data(analysis_id) + elif query == 'base64': + return get_base64_data(analysis_id) + elif query == 'manifest': + return get_manifest_data(analysis_id) + elif query == 'dlanalysis': try: extension_id = allargs.get('extid') saveas = "" diff --git a/frontend/handlers/__init__.py b/frontend/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/handlers/data_handler.py b/frontend/handlers/data_handler.py new file mode 100644 index 0000000..3f53687 --- /dev/null +++ b/frontend/handlers/data_handler.py @@ -0,0 +1,169 @@ +import os +import json +import base64 +from flask import url_for +import core.core as core +import core.helper as helper +from .file_handler import load_analysis_data, get_analysis_files + +def get_analysis_data(analysis_id): + """Helper function to get common analysis data""" + analysis_info = core.get_result_info(analysis_id) + if not analysis_info[0]: + return None, None, None, None + + result_directory = analysis_info[1]['report_directory'].replace('', core.reports_path) + files_valid, files = get_analysis_files(result_directory) + if not files_valid: + return None, None, None, None + + graph_data, source_data, report_data = load_analysis_data(files) + return analysis_info, graph_data, source_data, report_data + +def get_basic_info(analysis_id): + analysis_info, _, _, report_data = get_analysis_data(analysis_id) + if not analysis_info: + return {'error': 'Invalid analysis data'} + + extension_type = report_data['type'] + if 'firefox' in extension_type.lower(): + extension_type = ' ' + extension_type + elif 'chrome' in extension_type.lower(): + extension_type = ' ' + extension_type + + return { + 'name': report_data['name'], + 'version': report_data['version'], + 'author': report_data['author'], + 'description': report_data['description'], + 'time': analysis_info[1]['time'], + 'extension_type': extension_type + } + +def get_files_data(analysis_id): + _, _, source_data, report_data = get_analysis_data(analysis_id) + if not source_data: + return {'error': 'Invalid analysis data'} + + files_table = '' + + for file_info in source_data: + file_name = source_data[file_info]['file_name'] + rel_path = source_data[file_info]['relative_path'] + file_id = source_data[file_info]['id'] + file_size = source_data[file_info]['file_size'] + + file_action = f'' + + if file_name.endswith('.js') and source_data[file_id]['retirejs_result']: + file_action += f' ' + + file_type = helper.get_file_type_icon(file_name) + file_type = f'' + + files_table += f"" + + files_table += '
File NamePathSizeActions
{file_type} {file_name}{rel_path}{file_size}{file_action}
' + + return { + 'files_table': files_table, + 'counts': { + 'js': len(report_data['files']['js']), + 'css': len(report_data['files']['css']), + 'html': len(report_data['files']['html']), + 'json': len(report_data['files']['json']), + 'other': len(report_data['files']['other']), + 'static': len(report_data['files']['static']) + } + } + +def get_urls_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + urls_table = '' + for url in report_data.get('urls', []): + b64url = base64.b64encode(url['url'].encode()).decode() + urls_table += f''' + + + + + + + ''' + urls_table += '
URLDomainFileActions
{url['url']}{url['domain']}{url['file']} + + + +
' + return {'urls_table': urls_table} + +def get_permissions_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + permissions = report_data.get('permissions', []) + return {'permissions': permissions} + +def get_domains_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + domains = report_data.get('domains', []) + return {'domains': domains} + +def get_ips_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + ips = report_data.get('ips', []) + return {'ips': ips} + +def get_emails_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + emails = report_data.get('emails', []) + return {'emails': emails} + +def get_btc_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + btc_addresses = report_data.get('btc_addresses', []) + return {'btc_addresses': btc_addresses} + +def get_comments_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + comments = report_data.get('comments', []) + return {'comments': comments} + +def get_base64_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + base64_strings = report_data.get('base64_strings', []) + return {'base64_strings': base64_strings} + +def get_manifest_data(analysis_id): + _, _, _, report_data = get_analysis_data(analysis_id) + if not report_data: + return {'error': 'Invalid analysis data'} + + manifest = report_data.get('manifest', {}) + return {'manifest': manifest} + +# Similar functions for other data types (urls, permissions, domains, etc.) +# Each function follows the same pattern of getting analysis data and returning +# formatted HTML tables or structured data \ No newline at end of file diff --git a/frontend/handlers/file_handler.py b/frontend/handlers/file_handler.py new file mode 100644 index 0000000..5caf077 --- /dev/null +++ b/frontend/handlers/file_handler.py @@ -0,0 +1,33 @@ +import os +import json +import logging +import traceback + +def validate_analysis_id(analysis_id): + try: + int(analysis_id.replace('EXA','')) + return True + except: + logging.error(traceback.format_exc()) + return False + +def get_analysis_files(result_directory): + graph_file = os.path.join(result_directory, 'graph.data') + report_json = os.path.join(result_directory, 'extanalysis_report.json') + source_file = os.path.join(result_directory, 'source.json') + + return all([os.path.isfile(f) for f in [graph_file, report_json, source_file]]), { + 'graph': graph_file, + 'report': report_json, + 'source': source_file + } + +def load_analysis_data(files): + with open(files['graph'], 'r') as f: + graph_data = f.read() + with open(files['source'], 'r') as f: + source_data = json.loads(f.read()) + with open(files['report'], 'r') as f: + report_data = json.loads(f.read()) + + return graph_data, source_data, report_data \ No newline at end of file diff --git a/frontend/handlers/result_handler.py b/frontend/handlers/result_handler.py new file mode 100644 index 0000000..5d19bac --- /dev/null +++ b/frontend/handlers/result_handler.py @@ -0,0 +1,75 @@ +import os +import json +import logging +import traceback +import base64 +from flask import render_template +import core.core as core +import core.helper as helper +from .data_handler import ( + get_basic_info, get_files_data, get_urls_data, + get_permissions_data, get_domains_data, get_ips_data, + get_emails_data, get_btc_data, get_comments_data, + get_base64_data, get_manifest_data +) +from .file_handler import validate_analysis_id + +def get_analysis_files(result_directory): + graph_file = os.path.join(result_directory, 'graph.data') + report_json = os.path.join(result_directory, 'extanalysis_report.json') + source_file = os.path.join(result_directory, 'source.json') + + return all([os.path.isfile(f) for f in [graph_file, report_json, source_file]]), { + 'graph': graph_file, + 'report': report_json, + 'source': source_file + } + +def load_analysis_data(files): + with open(files['graph'], 'r') as f: + graph_data = f.read() + with open(files['source'], 'r') as f: + source_data = json.loads(f.read()) + with open(files['report'], 'r') as f: + report_data = json.loads(f.read()) + + return graph_data, source_data, report_data + +def view_result(analysis_id): + """Main view function that aggregates all data for the template""" + if not validate_analysis_id(analysis_id): + return render_template('error.html', + error_title="Invalid Result ID", + error_head="Invalid Result ID", + error_txt='Invalid result ID format') + + basic_info = get_basic_info(analysis_id) + if 'error' in basic_info: + return render_template('error.html', + error_title="Invalid Result ID", + error_head=f"Invalid Result ID: {analysis_id}", + error_txt="Could not load analysis data") + + files_data = get_files_data(analysis_id) + urls_data = get_urls_data(analysis_id) + permissions_data = get_permissions_data(analysis_id) + domains_data = get_domains_data(analysis_id) + ips_data = get_ips_data(analysis_id) + emails_data = get_emails_data(analysis_id) + btc_data = get_btc_data(analysis_id) + comments_data = get_comments_data(analysis_id) + base64_data = get_base64_data(analysis_id) + manifest_data = get_manifest_data(analysis_id) + + return render_template("report.html", + analysis_id=analysis_id, + basic_info=basic_info, + files_table=files_data['files_table'], + js_files_count=files_data['counts']['js'], + css_files_count=files_data['counts']['css'], + html_files_count=files_data['counts']['html'], + json_files_count=files_data['counts']['json'], + other_files_count=files_data['counts']['other'], + static_files_count=files_data['counts']['static'], + # ... other template variables ... + ) \ No newline at end of file diff --git a/frontend/handlers/table_builder.py b/frontend/handlers/table_builder.py new file mode 100644 index 0000000..345c58f --- /dev/null +++ b/frontend/handlers/table_builder.py @@ -0,0 +1,38 @@ +import os +import base64 +from flask import url_for +import core.core as core +import core.helper as helper + +def build_urls_tables(report_data): + if not report_data['urls']: + return '

No URLs Found

', '

No External JavaScript Found in any files!

', 0, 0 + + urls_table = '' + extjs_table = '
URLDomainFileActions
' + + done_urls = [] + done_ejss = [] + urls_count = 0 + extjs_count = 0 + + for aurl in report_data['urls']: + b64url = "'" + base64.b64encode(aurl['url'].encode('ascii', 'ignore')).decode('ascii') + "'" + aurl_href = f' {aurl["url"]}' + actions = f'' + + if aurl['url'].endswith('.js'): + if aurl['url'] not in done_ejss: + done_ejss.append(aurl['url']) + extjs_table += f'{actions}' + extjs_count += 1 + else: + if aurl['url'] not in done_urls: + done_urls.append(aurl['url']) + urls_table += f'{actions}' + urls_count += 1 + + urls_table = urls_table + '
URLDomainFileActions
{aurl_href}{aurl["domain"]}{aurl["file"]}
{aurl_href}{aurl["domain"]}{aurl["file"]}
' if done_urls else '

No URLs Found

' + extjs_table = extjs_table + '' if done_ejss else '

No External JavaScript Found in any files!

' + + return urls_table, extjs_table, urls_count, extjs_count \ No newline at end of file diff --git a/frontend/static/js/result.js b/frontend/static/js/result.js new file mode 100644 index 0000000..ea97fbc --- /dev/null +++ b/frontend/static/js/result.js @@ -0,0 +1,75 @@ +function loadBasicInfo(analysisId) { + fetch(`/api/result/${analysisId}/basic_info`) + .then(response => response.json()) + .then(data => { + if (data.error) { + showError(data.error); + return; + } + updateBasicInfo(data); + }); +} + +function loadFiles(analysisId) { + fetch(`/api/result/${analysisId}/files`) + .then(response => response.json()) + .then(data => { + if (data.error) { + showError(data.error); + return; + } + document.getElementById('files-container').innerHTML = data.files_table; + updateFileCounts(data.counts); + }); +} + +function loadUrls(analysisId) { + fetch(`/api/result/${analysisId}/urls`) + .then(response => response.json()) + .then(data => { + if (data.error) { + showError(data.error); + return; + } + document.getElementById('urls-container').innerHTML = data.urls_table; + }); +} + +// Similar functions for other data types... + +function loadAllData(analysisId) { + loadBasicInfo(analysisId); + loadFiles(analysisId); + loadUrls(analysisId); + loadPermissions(analysisId); + loadDomains(analysisId); + loadIps(analysisId); + loadEmails(analysisId); + loadBtc(analysisId); + loadComments(analysisId); + loadBase64(analysisId); + loadManifest(analysisId); +} + +// Helper functions +function updateBasicInfo(data) { + document.getElementById('extension-name').textContent = data.name; + document.getElementById('extension-version').textContent = data.version; + document.getElementById('extension-author').textContent = data.author; + document.getElementById('extension-description').textContent = data.description; + document.getElementById('extension-type').innerHTML = data.extension_type; +} + +function updateFileCounts(counts) { + document.getElementById('js-count').textContent = counts.js; + document.getElementById('css-count').textContent = counts.css; + document.getElementById('html-count').textContent = counts.html; + document.getElementById('json-count').textContent = counts.json; + document.getElementById('other-count').textContent = counts.other; + document.getElementById('static-count').textContent = counts.static; +} + +function showError(message) { + // Implement error display logic + console.error(message); +} \ No newline at end of file diff --git a/frontend/utils/__init__.py b/frontend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/utils/file_utils.py b/frontend/utils/file_utils.py new file mode 100644 index 0000000..2d7590d --- /dev/null +++ b/frontend/utils/file_utils.py @@ -0,0 +1,11 @@ +import os +from flask import url_for +import core.core as core + +def get_file_type_icon(file_name): + file_ext = file_name.split('.')[-1] + file_type = os.path.join(core.path, 'static', 'images', f'{file_ext}1.png') + + if os.path.isfile(file_type): + return url_for('static', filename=f'images/{file_ext}1.png') + return url_for('static', filename='images/other1.png') \ No newline at end of file diff --git a/frontend/viewresult.py b/frontend/viewresult.py index e43e790..b45d514 100644 --- a/frontend/viewresult.py +++ b/frontend/viewresult.py @@ -24,245 +24,240 @@ import logging, traceback import base64 -def view(analysis_id): - # so the result ids are in this format : EXA so we can try to replace 'EXTA' and convert the rest to int if it passes it's a valid type - try: - int(analysis_id.replace('EXA','')) # Check - - analysis_info = core.get_result_info(analysis_id) - - if not analysis_info[0]: - # Could not get analysis_info - error_txt = 'Something went wrong while getting analysis info!
Error: ' + analysis_info[1] - return render_template('error.html', error_title = "Invalid Result ID", error_head = "Invalid Result ID: {0}".format(analysis_id) , error_txt=error_txt) - else: - result_directory = analysis_info[1]['report_directory'].replace('', core.reports_path) +app = Flask(__name__) - if os.path.isdir(result_directory): - # result directory found let's check for all necessary files - graph_file = os.path.join(result_directory, 'graph.data') - report_json = os.path.join(result_directory, 'extanalysis_report.json') - source_file = os.path.join(result_directory, 'source.json') +def get_extension_type(extension_type): + if 'firefox' in extension_type.lower(): + return ' ' + extension_type + elif 'chrome' in extension_type.lower(): + return ' ' + extension_type + return extension_type +def generate_files_table(source_data): + files_table = '' + for file_info in source_data: + file_name = source_data[file_info]['file_name'] + rel_path = source_data[file_info]['relative_path'] + file_id = source_data[file_info]['id'] + file_size = source_data[file_info]['file_size'] + file_action = '' + if file_name.endswith('.js'): + if source_data[file_id]['retirejs_result'] != []: + file_action += ' '.format("'"+file_id+"'", "'"+file_id+"'", "'"+file_name+"'") + file_type = helper.fixpath(core.path + '/static/images/' + file_name.split('.')[-1] + '1.png') + if os.path.isfile(file_type): + file_type = file_name.split('.')[-1] + '1.png' + else: + file_type = 'other1.png' + file_type = url_for('static',filename='images/' + file_type) + file_type = ''.format(file_type) + file_html = "{2} {0}{1}{4}{3}".format(file_name, rel_path, file_type, file_action, file_size) + files_table += file_html + return files_table - if all([os.path.isfile(the_file) for the_file in [graph_file, report_json, source_file]]): - core.updatelog('Viewing Analysis {0}'.format(analysis_id)) - graph_data = open(graph_file, 'r') - graph_data = graph_data.read() - source_data = open(source_file, 'r') - source_data = json.loads(source_data.read()) - report_data = open(report_json, 'r') - report_data = json.loads(report_data.read()) - - # prepare data to be sent to result page - basic_info_t = [report_data['name'], - report_data['version'], - report_data['author'], - report_data['description'], - analysis_info[1]['time'] - ] - - # extension type - extension_type = report_data['type'] - if 'firefox' in extension_type.lower(): - extension_type = ' ' + extension_type - elif 'chrome' in extension_type.lower(): - extension_type = ' ' + extension_type - - # URL Table - if report_data['urls'] != []: - urls_table = '' - extjs_table = '
URLDomainFileActions
' - done_urls = [] - done_ejss = [] - urls_count = 0 - extjs_count = 0 - for aurl in report_data['urls']: - if aurl['url'].endswith('.js'): - if aurl['url'] not in done_ejss: - done_ejss.append(aurl['url']) - aurl_href = ' {0}'.format(aurl['url']) - extjs_table += '' - b64url = "'" + base64.b64encode(aurl['url'].encode('ascii', 'ignore')).decode('ascii') + "'" - extjs_table += ''.format(aurl['domain'], aurl['file']) - extjs_table += ''.format(b64url, aurl['url']) - extjs_count += 1 - else: - if aurl['url'] not in done_urls: - done_urls.append(aurl['url']) - aurl_href = ' {0}'.format(aurl['url']) - urls_table += '' - urls_table += ''.format(aurl['domain'], aurl['file']) - b64url = "'" + base64.b64encode(aurl['url'].encode('ascii', 'ignore')).decode('ascii') + "'" - urls_table += ''.format(b64url, aurl['url']) - urls_count += 1 - - if done_urls != []: - urls_table += '
URLDomainFileActions
' + aurl_href + '{0}{1}
' + aurl_href + '{0}{1}
' - else: - urls_table = '

No URLs Found

' - - if done_ejss != []: - extjs_table += '' - else: - extjs_table = '

No External JavaScript Found in any files!

' - else: - urls_table = '

No URLs Found

' - extjs_table = '

No External JavaScript Found in any files!

' - extjs_count = 0 - urls_count = 0 - - # Domains div - if report_data['domains'] != []: - domains_table = '' - for domain in report_data['domains']: - domain_flag = helper.fixpath(core.path + '/static/images/flags/' + domain['country_code'] + '.png') - if os.path.isfile(domain_flag): - flag_path = url_for('static',filename='images/flags/' + domain['country_code'] + '.png') - else: - flag_path = url_for('static',filename='images/flags/unknown.png') - country_html = ' {1}'.format(flag_path, domain['country']) - domains_table += ''.format(domain['name'], '0/66', domain['ip'], analysis_id, country_html) - domains_table += '
CountryDomainIP AddressActions
{4}{0}{2}
' - else: - domains_table = '

No Domains Extracted!

' - unique_domains = len(report_data['domains']) +def generate_permissions_div(permissions): + permissions_div = "" + for perm in permissions: + perm_html = '
{0} {1}
{4}
{2}
{3}
'.format(perm['badge'], helper.escape(perm['name']), perm['description'], (perm['warning'] if perm['warning'] != 'na' else ''), perm['risk']) + permissions_div += perm_html + return permissions_div - - # Permissions div containing all permissions accordions - permissions_div = "" - for perm in report_data['permissions']: - #perm_html = '
{0}
{1}
{2}
'.format(perm['name'], perm['description'], (perm['warning'] if perm['warning'] != 'na' else '')) - perm_html = '
{0} {1}
{4}
{2}
{3}
'.format(perm['badge'], helper.escape(perm['name']), perm['description'], (perm['warning'] if perm['warning'] != 'na' else ''), perm['risk']) - permissions_div += perm_html - permissions_count = len(report_data['permissions']) +def generate_domains_table(domains): + if not domains: + return '

No Domains Extracted!

' + + domains_table = '' + for domain in domains: + domain_flag = helper.fixpath(core.path + '/static/images/flags/' + domain['country_code'] + '.png') + if os.path.isfile(domain_flag): + flag_path = url_for('static',filename='images/flags/' + domain['country_code'] + '.png') + else: + flag_path = url_for('static',filename='images/flags/unknown.png') + country_html = ' {1}'.format(flag_path, domain['country']) + domains_table += '{4}{0}{2} '.format(domain['name'], '0/66', domain['ip'], '', country_html) + domains_table += '' + return domains_table +def generate_urls_table(urls): + if not urls: + return '

No URLs Found

' + + urls_table = '' + done_urls = [] + for aurl in urls: + if not aurl['url'].endswith('.js') and aurl['url'] not in done_urls: + done_urls.append(aurl['url']) + aurl_href = ' {0}'.format(aurl['url']) + urls_table += '' + aurl_href + '' + urls_table += '{0}{1}'.format(aurl['domain'], aurl['file']) + b64url = "'" + base64.b64encode(aurl['url'].encode('ascii', 'ignore')).decode('ascii') + "'" + urls_table += ' '.format(b64url, aurl['url']) + urls_table += '' + return urls_table +def generate_extjs_table(urls): + if not any(u['url'].endswith('.js') for u in urls): + return '

No External JavaScript Found in any files!

' + + extjs_table = '' + done_ejss = [] + for aurl in urls: + if aurl['url'].endswith('.js') and aurl['url'] not in done_ejss: + done_ejss.append(aurl['url']) + aurl_href = ' {0}'.format(aurl['url']) + extjs_table += '' + aurl_href + '' + b64url = "'" + base64.b64encode(aurl['url'].encode('ascii', 'ignore')).decode('ascii') + "'" + extjs_table += '{0}{1}'.format(aurl['domain'], aurl['file']) + extjs_table += ' '.format(b64url, aurl['url']) + extjs_table += '' + return extjs_table - # table consisting of all the viewable source files - files_table = '' - for file_info in source_data: - file_name = source_data[file_info]['file_name'] - rel_path = source_data[file_info]['relative_path'] - file_id = source_data[file_info]['id'] - file_size = source_data[file_info]['file_size'] - file_action = '' - if file_name.endswith('.js'): - # Add button for viewing retirejs vulnerability scan results - # okay it's annoying to show button on every js file let's just show where there is vuln. - if source_data[file_id]['retirejs_result'] != []: - file_action += ' '.format("'"+file_id+"'", "'"+analysis_id+"'", "'"+file_name+"'") - file_type = helper.fixpath(core.path + '/static/images/' + file_name.split('.')[-1] + '1.png') - if os.path.isfile(file_type): - file_type = file_name.split('.')[-1] + '1.png' - else: - file_type = 'other1.png' - file_type = url_for('static',filename='images/' + file_type) - file_type = ''.format(file_type) - file_html = "".format(file_name, rel_path, file_type, file_action, file_size) - files_table += file_html - files_table += '
File NamePathSizeActions
{2} {0}{1}{4}{3}
' +def generate_ips_table(report_data): + if not report_data.get('ipv4_addresses', []) and not report_data.get('ipv6_addresses', []): + return '

No IPv4 or IPv6 addresses found!

' + + ips_table = '' + for ip in report_data.get('ipv4_addresses', []): + ips_table += '{0}{1}{2}'.format(ip['address'], 'IPv4', ip['file']) + for ip in report_data.get('ipv6_addresses', []): + ips_table += '{0}{1}{2}'.format(ip['address'], 'IPv6', ip['file']) + ips_table += '' + return ips_table +def generate_mails_table(emails): + if not emails: + return '

No email addresses found in any of the files!

' + + mails_table = '' + for mail in emails: + mails_table += '{0}{1}'.format(mail['mail'], mail['file']) + mails_table += '' + return mails_table - # table consisting of ipv6 and ipv4 addresses - if report_data['ipv4_addresses'] == [] and report_data['ipv6_addresses'] == []: - ips_table = '

No IPv4 or IPv6 addresses found!

' - else: - ips_table = '' - for ip in report_data['ipv4_addresses']: - ips_table += ''.format(ip['address'], 'IPv4', ip['file']) - for ip in report_data['ipv6_addresses']: - ips_table += ''.format(ip['address'], 'IPv6', ip['file']) - ips_table += '
IP AddressTypeFile
{0}{1}{2}
{0}{1}{2}
' +def generate_btc_table(btc_addresses): + if not btc_addresses: + return '

No Bitcoin Address found!

' + + btc_table = '' + for btc in btc_addresses: + btc_table += '{0}{1}'.format(btc['address'], btc['file']) + btc_table += '' + return btc_table +def generate_comments_table(comments): + if not comments: + return '

No comments found in any js/html/css files!

' + + comments_table = '' + for comment in comments: + comments_table += '{0}{1}'.format(helper.escape(comment['comment']), comment['file']) + comments_table += '' + return comments_table - # table consisting of emails - if report_data['emails'] != []: - mails_table = '' - for mail in report_data['emails']: - mails_table += ''.format(mail['mail'], mail['file']) - mails_table += '
Email AddressFile
{0}{1}
' - else: - mails_table = '

No email addresses found in any of the files!

' +def generate_base64_table(base64_strings): + if not base64_strings: + return '

No base64 encoded string found in any js/html/css files!

' + + base64_table = '' + for b64 in base64_strings: + base64_table += '{0}{1}'.format(b64['string'], b64['file']) + base64_table += '' + return base64_table - # table containing btc addresses - if report_data['bitcoin_addresses'] != []: - btc_table = '' - for mail in report_data['bitcoin_addresses']: - btc_table += ''.format(mail['address'], mail['file']) - btc_table += '
BTC AddressFile
{0}{1}
' - else: - btc_table = '

No Bitcoin Address found!

' +@app.route('/analysis//') # Default route +@app.route('/analysis///') # Tab-specific route +def view(analysis_id, tab='basic_info'): + try: + int(analysis_id.replace('EXA','')) # Check + analysis_info = core.get_result_info(analysis_id) + + if not analysis_info[0]: + error_txt = 'Something went wrong while getting analysis info!
Error: ' + analysis_info[1] + return render_template('error.html', error_title = "Invalid Result ID", error_head = "Invalid Result ID: {0}".format(analysis_id) , error_txt=error_txt) + + # Validate tab parameter + valid_tabs = ['basic_info', 'files', 'permissions', 'urls_domains', 'gathered_intels'] + if tab not in valid_tabs: + tab = 'basic_info' + + # Get result data + result_directory = analysis_info[1]['report_directory'].replace('', core.reports_path) + + if not os.path.isdir(result_directory): + error_txt = 'The result directory corresponding to result id {0} could not be found'.format(analysis_id) + return render_template('error.html', error_title="Result Directory Not Found", error_head="Result Directory Not Found", error_txt=error_txt) - # table containing comments - if report_data['comments'] != []: - comments_table = '' - for comment in report_data['comments']: - comments_table += ''.format(helper.escape(comment['comment']), comment['file']) - comments_table += '
CommentFile
{0}{1}
' - else: - comments_table = '

No comments found in any js/html/css files!

' + # Load required files + graph_file = os.path.join(result_directory, 'graph.data') + report_json = os.path.join(result_directory, 'extanalysis_report.json') + source_file = os.path.join(result_directory, 'source.json') + + if not all(os.path.isfile(f) for f in [graph_file, report_json, source_file]): + error_txt = 'All the result files are not found. Try scanning the extension again!' + return render_template('error.html', error_title="Malformed Result", error_head="Incomplete Result", error_txt=error_txt) + # Load data from files + graph_data = open(graph_file, 'r').read() + report_data = json.loads(open(report_json, 'r').read()) + source_data = json.loads(open(source_file, 'r').read()) - # table containing base64 encoded strings - if report_data['base64_strings'] != []: - base64_table = '' - for b64 in report_data['base64_strings']: - base64_table += ''.format(b64['string'], b64['file']) - base64_table += '
Base64 Encoded StringFile
{0}{1}
' - else: - base64_table = '

No base64 encoded string found in any js/html/css files!

' + # Prepare template data based on selected tab + template_data = { + 'analysis_id': analysis_id, + 'current_tab': tab, + 'graph_data': graph_data, + 'basic_info': [ + report_data.get('name', 'Unknown Name'), + report_data.get('version', 'Unknown Version'), + report_data.get('author', 'Unknown Author'), + report_data.get('description', 'No description available'), + analysis_info[1]['time'] + ], + 'extension_type': get_extension_type(report_data.get('type', 'unknown')) + } - manifest_content = json.dumps(report_data['manifest']) + # Add tab-specific data + if tab == 'basic_info': + template_data.update({ + 'manifest_content': json.dumps(report_data.get('manifest', {})), + 'permissions_count': len(report_data.get('permissions', [])), + 'unique_domains': len(report_data.get('domains', [])), + 'urls_count': len([u for u in report_data.get('urls', []) if not u['url'].endswith('.js')]), + 'extjs_count': len([u for u in report_data.get('urls', []) if u['url'].endswith('.js')]) + }) + + elif tab == 'files': + template_data.update({ + 'files_table': generate_files_table(source_data), + 'js_files_count': len(report_data.get('files', {}).get('js', [])), + 'css_files_count': len(report_data.get('files', {}).get('css', [])), + 'html_files_count': len(report_data.get('files', {}).get('html', [])), + 'json_files_count': len(report_data.get('files', {}).get('json', [])), + 'other_files_count': len(report_data.get('files', {}).get('other', [])), + 'static_files_count': len(report_data.get('files', {}).get('static', [])) + }) + + elif tab == 'permissions': + template_data['permissions_div'] = generate_permissions_div(report_data.get('permissions', [])) + + elif tab == 'urls_domains': + template_data.update({ + 'domains_table': generate_domains_table(report_data.get('domains', [])), + 'urls_table': generate_urls_table(report_data.get('urls', [])), + 'extjs_table': generate_extjs_table(report_data.get('urls', [])) + }) + + elif tab == 'gathered_intels': + template_data.update({ + 'ips_table': generate_ips_table(report_data), + 'btc_table': generate_btc_table(report_data.get('bitcoin_addresses', [])), + 'mails_table': generate_mails_table(report_data.get('emails', [])), + 'comments_table': generate_comments_table(report_data.get('comments', [])), + 'base64_table': generate_base64_table(report_data.get('base64_strings', [])) + }) - ''' - Files count - ''' - js_files_count = len(report_data['files']['js']) - css_files_count = len(report_data['files']['css']) - html_files_count = len(report_data['files']['html']) - json_files_count = len(report_data['files']['json']) - other_files_count = len(report_data['files']['other']) - static_files_count = len(report_data['files']['static']) + return render_template("report.html", **template_data) - return render_template("report.html", - extension_type = extension_type, - graph_data = graph_data, - basic_info = basic_info_t, - urls_table = urls_table, - permissions_div = permissions_div, - analysis_id=analysis_id, - files_table=files_table, - manifest_content=manifest_content, - domains_table = domains_table, - base64_table = base64_table, - comments_table = comments_table, - ips_table = ips_table, - btc_table = btc_table, - mails_table = mails_table, - extjs_table = extjs_table, - urls_count = urls_count, - extjs_count = extjs_count, - permissions_count = permissions_count, - unique_domains = unique_domains, - js_files_count = js_files_count, - css_files_count = css_files_count, - html_files_count = html_files_count, - json_files_count = json_files_count, - other_files_count = other_files_count, - static_files_count = static_files_count - ) - - - else: - error_txt = 'All the result files are not found.. Try scanning the extension again! and don\'t mess with the result files this time' - return render_template('error.html', error_title = "Malformed Result", error_head = "Incomplete Result", error_txt=error_txt) - - - else: - error_txt = 'The result directory corresponding to result id {0} could not be found... hence ExtAnalysis has nothing to show'.format(analysis_id) - return render_template('error.html', error_title = "Result Directory Not Found", error_head = "Result Directory Not Foundt", error_txt=error_txt) - except: logging.error(traceback.format_exc()) - return render_template('error.html', error_title = "Invalid Result ID", error_head = "Invalid Result ID" , error_txt='There seems to be no result corresponding to the provided ID. Did you delete the result? or maybe you did some weird shit with the parameter?') \ No newline at end of file + return render_template('error.html', error_title="Invalid Result ID", error_head="Invalid Result ID", error_txt='Invalid result ID provided') \ No newline at end of file diff --git a/static/css/result.css b/static/css/result.css index ad4a1b5..659af8d 100644 --- a/static/css/result.css +++ b/static/css/result.css @@ -1,4 +1,3 @@ - #resultnetwork{ width: 100%; height: 342px; @@ -324,4 +323,145 @@ div#selected-node { .ft_icon { width: 16px; vertical-align: middle; +} + +/* Table styling improvements */ +.result-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + border-radius: 4px; + overflow: hidden; +} + +.result-table thead { + background: linear-gradient(135deg, #2b5876 0%, #4e4376 100%); +} + +.result-table th { + color: #ffffff; + font-weight: 600; + text-align: left; + padding: 12px 15px; + font-size: 14px; + letter-spacing: 0.5px; + text-transform: uppercase; + border-bottom: 2px solid #89ff00; +} + +.result-table td { + padding: 10px 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + font-size: 14px; +} + +.result-table tbody tr:hover { + background-color: rgba(137, 255, 0, 0.05); + transition: background-color 0.2s ease; +} + +.result-table tbody tr:last-child td { + border-bottom: none; +} + +/* Pagination styling */ +.dataTables_paginate { + margin-top: 15px; + text-align: right; +} + +.dataTables_paginate .paginate_button { + padding: 5px 10px; + margin: 0 2px; + border-radius: 3px; + cursor: pointer; + color: #89ff00; + background-color: rgba(137, 255, 0, 0.1); +} + +.dataTables_paginate .paginate_button.current { + background-color: #89ff00; + color: #1a1a1a; + font-weight: bold; +} + +.dataTables_paginate .paginate_button:hover:not(.current) { + background-color: rgba(137, 255, 0, 0.2); +} + +/* Search box styling */ +.dataTables_filter input { + padding: 8px 12px; + border-radius: 4px; + border: 1px solid rgba(137, 255, 0, 0.3); + background-color: rgba(0, 0, 0, 0.2); + color: #ffffff; + margin-left: 10px; +} + +.dataTables_filter input:focus { + outline: none; + border-color: #89ff00; + box-shadow: 0 0 0 2px rgba(137, 255, 0, 0.2); +} + +/* Tab styling improvements */ +.result-tabs { + display: flex; + list-style: none; + padding: 0; + margin: 0 0 20px 0; + border-bottom: 2px solid rgba(137, 255, 0, 0.3); +} + +.result-tabs li { + margin-right: 5px; +} + +.result-tabs li a { + display: block; + padding: 10px 20px; + text-decoration: none; + color: #ffffff; + font-weight: 500; + border-radius: 4px 4px 0 0; + transition: all 0.2s ease; + background-color: rgba(255, 255, 255, 0.1); +} + +.result-tabs li a i { + margin-right: 8px; + color: #89ff00; +} + +.result-tabs li a:hover { + background-color: rgba(137, 255, 0, 0.2); + color: #ffffff; +} + +.result-tabs li.current a { + background: linear-gradient(135deg, #2b5876 0%, #4e4376 100%); + color: #ffffff; + border-bottom: 2px solid #89ff00; +} + +/* For the tab links in the header */ +.tabscontainer a { + color: #89ff00; + text-decoration: none; + font-weight: 500; + transition: color 0.2s ease; +} + +.tabscontainer a:hover { + color: #ffffff; + text-decoration: underline; +} + +/* For the tab header links */ +a[href^="/analysis/"] { + color: #89ff00; + font-weight: 600; + text-shadow: 0 0 10px rgba(137, 255, 0, 0.5); } \ No newline at end of file diff --git a/templates/report.html b/templates/report.html index 170200d..3b4bc4d 100644 --- a/templates/report.html +++ b/templates/report.html @@ -122,8 +122,8 @@

Some content here

{{basic_info[4]}}
- - + +
@@ -132,342 +132,166 @@

Some content here

- -
-
-
- Scan Info -
-
- Analysis ID: {{analysis_id}} - Name: {{basic_info[0]}} - Version: {{basic_info[1]}} - Author: {{basic_info[2]}} - Type: {{extension_type | safe}} - Permissions: {{permissions_count}} - Unique Domains: {{unique_domains}} - Extracted URLs: {{urls_count}} - External JavaScript: {{extjs_count}} -
-
-
-
- manifest.json -
-
-
-
-
-
-
-
-
-
-
Files & URLs Graph
-
-
- -
-
Statistics
-
-
- -
{{html_files_count}} File(s)
-
-
- -
{{js_files_count}} File(s)
-
-
- -
{{json_files_count}} File(s)
-
-
- -
{{css_files_count}} File(s)
-
-
- -
{{static_files_count}} File(s)
-
-
- -
{{other_files_count}} File(s)
-
-
- -
-
-
-
-
-
-
- View source code of files! -
-
- {{files_table | safe}} -
-
Only Javascript, CSS, HTML & JSON Files are saved since they contain all the code!
-
-
-
-
-
- {{permissions_div | safe}} -
-
Click on the Permission to learn more about it!
-
-
-
-
- Domains! -
-
- {{domains_table | safe}} -
-
-
- -
-
- Extracted URLS from files! -
-
- {{urls_table | safe}} -
-
-
- -
-
- External JavaScript Files! -
-
- {{extjs_table | safe}} -
-
-
- -
-
-
-
- Extracted IP Addresses -
-
- {{ips_table | safe}} -
-
-
- - -
-
- Extracted Bitcoin Addresses -
-
- {{btc_table | safe}} -
-
-
- - -
-
- Extracted Email Addresses -
-
- {{mails_table | safe}} -
-
-
- -
-
- Extracted Comments -
-
- {{comments_table | safe}} -
-
-
- -
-
- Extracted Base64 Encoded strings -
-
- {{base64_table | safe}} -
-
-
- - -
- + + {% if current_tab == 'basic_info' %} + {% include 'tabs/basic_info.html' %} + {% elif current_tab == 'files' %} + {% include 'tabs/files.html' %} + {% elif current_tab == 'permissions' %} + {% include 'tabs/permissions.html' %} + {% elif current_tab == 'urls_domains' %} + {% include 'tabs/urls_domains.html' %} + {% elif current_tab == 'gathered_intels' %} + {% include 'tabs/gathered_intels.html' %} + {% endif %} - - - - + @@ -476,57 +300,53 @@

Some content here

- -

+ diff --git a/templates/tabs/basic_info.html b/templates/tabs/basic_info.html new file mode 100644 index 0000000..9f1a73b --- /dev/null +++ b/templates/tabs/basic_info.html @@ -0,0 +1,27 @@ +
+
+
+ Scan Info +
+
+ Analysis ID: {{analysis_id}} + Name: {{basic_info[0]}} + Version: {{basic_info[1]}} + Author: {{basic_info[2]}} + Type: {{extension_type|safe}} + Permissions: {{permissions_count}} + Unique Domains: {{unique_domains}} + URLs: {{urls_count}} + External JS: {{extjs_count}} +
+
+
+
+ manifest.json +
+
+
+
+
+
+
\ No newline at end of file diff --git a/templates/tabs/files.html b/templates/tabs/files.html new file mode 100644 index 0000000..832d510 --- /dev/null +++ b/templates/tabs/files.html @@ -0,0 +1,42 @@ +
+
+
+ Files Structure + +
+
+
+
+
+
+
+ Files Overview +
+
+ JavaScript Files: {{js_files_count}} + HTML Files: {{html_files_count}} + CSS Files: {{css_files_count}} + JSON Files: {{json_files_count}} + Static Files: {{static_files_count}} + Other Files: {{other_files_count}} +
+
+
+
+ Files List +
+
+ + + + + + + + + + {{files_table | safe}} +
File NamePathSizeActions
+
+
+
\ No newline at end of file diff --git a/templates/tabs/gathered_intels.html b/templates/tabs/gathered_intels.html new file mode 100644 index 0000000..e7853ac --- /dev/null +++ b/templates/tabs/gathered_intels.html @@ -0,0 +1,83 @@ +
+
+
+ IP Addresses +
+
+ + + + + + + + + {{ips_table | safe}} +
IP AddressTypeFile
+
+
+
+
+ Email Addresses +
+
+ + + + + + + + {{mails_table | safe}} +
Email AddressFile
+
+
+
+
+ Bitcoin Addresses +
+
+ + + + + + + + {{btc_table | safe}} +
BTC AddressFile
+
+
+
+
+ Comments +
+
+ + + + + + + + {{comments_table | safe}} +
CommentFile
+
+
+
+
+ Base64 Strings +
+
+ + + + + + + + {{base64_table | safe}} +
StringFile
+
+
+
\ No newline at end of file diff --git a/templates/tabs/permissions.html b/templates/tabs/permissions.html new file mode 100644 index 0000000..a061dbd --- /dev/null +++ b/templates/tabs/permissions.html @@ -0,0 +1,12 @@ +
+
+
+ Required Permissions +
+
+
+ {{permissions_div | safe}} +
+
+
+
\ No newline at end of file diff --git a/templates/tabs/urls_domains.html b/templates/tabs/urls_domains.html new file mode 100644 index 0000000..3bdacc0 --- /dev/null +++ b/templates/tabs/urls_domains.html @@ -0,0 +1,56 @@ +
+
+
+ Domains +
+
+ + + + + + + + + + {{domains_table | safe}} +
CountryDomainIP AddressActions
+
+
+
+
+ URLs +
+
+ + + + + + + + + + {{urls_table | safe}} +
URLDomainFileActions
+
+
+
+
+ External JavaScript +
+
+ + + + + + + + + + {{extjs_table | safe}} +
URLDomainFileActions
+
+
+
\ No newline at end of file