From e4bf980b97e2357e96d4f42eac8e8baa13c9ee44 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 26 Sep 2013 14:02:36 +0200 Subject: [PATCH] Fail* result browser for pruning experiments. Based on the database layout given by the pruner. Run ./run.py -c (Default config ~/.my.cnf) - Checks if objdump table exists - Added view for results per instruction - Added config file support for table details - Overview data loaded at server startup - Result type mapping configurable via config file Based on Flask and MySQLdb Change-Id: Ib49eac8f5c1e0ab23921aedb5bc53c34d0cde14d --- tools/analysis/resultbrowser/README | 19 ++ tools/analysis/resultbrowser/app/__init__.py | 4 + tools/analysis/resultbrowser/app/data.py | 143 ++++++++++++ tools/analysis/resultbrowser/app/details.py | 216 ++++++++++++++++++ tools/analysis/resultbrowser/app/model.py | 213 +++++++++++++++++ .../resultbrowser/app/static/css/barchart.css | 5 + .../resultbrowser/app/static/css/main.css | 214 +++++++++++++++++ .../resultbrowser/app/static/favicon.ico | Bin 0 -> 36885 bytes .../resultbrowser/app/templates/about.html | 11 + .../resultbrowser/app/templates/code.html | 50 ++++ .../resultbrowser/app/templates/index.html | 55 +++++ .../app/templates/instr_details.html | 65 ++++++ .../resultbrowser/app/templates/layout.html | 32 +++ tools/analysis/resultbrowser/app/views.py | 39 ++++ tools/analysis/resultbrowser/conf.yml | 69 ++++++ tools/analysis/resultbrowser/run.py | 5 + 16 files changed, 1140 insertions(+) create mode 100644 tools/analysis/resultbrowser/README create mode 100644 tools/analysis/resultbrowser/app/__init__.py create mode 100644 tools/analysis/resultbrowser/app/data.py create mode 100644 tools/analysis/resultbrowser/app/details.py create mode 100755 tools/analysis/resultbrowser/app/model.py create mode 100644 tools/analysis/resultbrowser/app/static/css/barchart.css create mode 100644 tools/analysis/resultbrowser/app/static/css/main.css create mode 100644 tools/analysis/resultbrowser/app/static/favicon.ico create mode 100644 tools/analysis/resultbrowser/app/templates/about.html create mode 100644 tools/analysis/resultbrowser/app/templates/code.html create mode 100644 tools/analysis/resultbrowser/app/templates/index.html create mode 100644 tools/analysis/resultbrowser/app/templates/instr_details.html create mode 100644 tools/analysis/resultbrowser/app/templates/layout.html create mode 100644 tools/analysis/resultbrowser/app/views.py create mode 100644 tools/analysis/resultbrowser/conf.yml create mode 100755 tools/analysis/resultbrowser/run.py diff --git a/tools/analysis/resultbrowser/README b/tools/analysis/resultbrowser/README new file mode 100644 index 00000000..46f1bac3 --- /dev/null +++ b/tools/analysis/resultbrowser/README @@ -0,0 +1,19 @@ +Fail* Result Browser + +Requirements: + * Python + * Flask (sudo pip install Flask) + * MySQLDB (sudo aptitude install python-mysqldb) + * YAML (sudo aptitude install python-yaml) + +Based on Flask web microframework (Werkzeug, Jinja 2) +and old school MySQL bindings. + +Connects to a Fail* result database given by a mysql config file. + +Usage: + ./run.py + Defaults to mysql config file ~/.my.cnf, or + ./run.py -c + +YAML based configuration for table and variant details. diff --git a/tools/analysis/resultbrowser/app/__init__.py b/tools/analysis/resultbrowser/app/__init__.py new file mode 100644 index 00000000..a6c9e339 --- /dev/null +++ b/tools/analysis/resultbrowser/app/__init__.py @@ -0,0 +1,4 @@ +from flask import Flask + +app = Flask(__name__) +from app import views diff --git a/tools/analysis/resultbrowser/app/data.py b/tools/analysis/resultbrowser/app/data.py new file mode 100644 index 00000000..da24db22 --- /dev/null +++ b/tools/analysis/resultbrowser/app/data.py @@ -0,0 +1,143 @@ +from pprint import pprint +import details +import model + +def scrub(table_name): + return ''.join( chr for chr in table_name if chr.isalnum() or chr == '_' ) + +class Resulttype: + def __init__(self, name, count): + self.name = name + self.count = count + + def getName(self): + return self.name + + def getCount(self): + return self.count + +class Variant: + def __init__(self, id, name, table, benchmark, detail): + self.id = id + self.dbname = name + self.parenttable = table # TableDetails + self.details = detail # VariantDetails + self.benchmark = benchmark # BenchmarkDetails + self.results = {} + self.totalresults = 0 + + + def getMapper(self): + mapper = self.benchmark.getMapper() + if not mapper: #try benchmark mapper + mapper = self.details.getMapper() + if not mapper: # of not there, try parent tables mapper + mapper = self.parenttable.getMapper() + if not mapper: # no mapper found at all, try default mapper + mapper = model.detaildealer.getDefaultMapper() + return mapper + + + def addResulttype(self, name, count): + mapper = self.getMapper() + label = mapper.getLabel(name) + oldcount = self.results.setdefault(label, 0) + self.results[label] = oldcount + count + self.totalresults += count + + def getResultLabels(self): + return self.results.keys() + + def getDBName(self): + return str(self.name) + + def getId(self): + return self.id + + def getResults(self): + return self.results + + def getTableDetails(self): + return self.parenttable + + def getBenchmarkDetails(self): + return self.benchmark + + def getDetails(self): + return self.details + + def getTotals(self): + return self.totalresults + + def __str__(self): + ret = "Variant: " + self.getDetails().getTitle() + " - " + self.getBenchmarkDetails().getTitle() +" (id: " + str( self.id )+ ")" + " " + ret += "Total Results: " + str( self.totalresults ) + "\n" + for v in self.results: + ret += "\t" + v.name + ": " + str( v.count ) + "\n" + return ret + + __repr__ = __str__ + +'''A ResultTable contains n Variants''' +class ResultTable: + + def __init__(self, name, cfg): + self.name = scrub(name) + self.details = cfg.getTable(name) + self.variants = {} + + def addVariant(self, var): + if var.getId() in self.variants: + return + self.variants[var.getId()] = var # Add if not existing yet + + def getVariant(self, id): + if id in self.variants: + return self.variants[id] + return None + + def getVariantById(self, varid): + for k,v in self.variants.items(): + if int(v.getId()) == int(varid): + return v + return None + + def getDetails(self): + return self.details + + def getVariants(self): + return self.variants + + def __str__(self): + ret = "Result: " + self.getDetails().getTitle() + "\n" + for k,v in self.variants.items(): + ret += "\t" + str(v) + "\n" + return ret + __repr__ = __str__ + +'''Overview has n ResultTables''' +class Overview: + def __init__(self): + self.tables = {} + + def add(self, table): + self.tables[table.getDetails().getDBName()] = table + + def getTables(self): + return self.tables + + def getTable(self, dbname): + return self.tables.get(dbname, None) + + def getVariantById(self, variant_id): + for key,table in self.tables.items(): + variant = table.getVariantById(variant_id) + if variant: + return variant + print "Variant not found." + return None + + def length(self): + return len(self.tables) + + diff --git a/tools/analysis/resultbrowser/app/details.py b/tools/analysis/resultbrowser/app/details.py new file mode 100644 index 00000000..e58cb25d --- /dev/null +++ b/tools/analysis/resultbrowser/app/details.py @@ -0,0 +1,216 @@ + +class BasicDetails(object): + + def __init__(self,name): + self.dbname = name + self.title = name + self.details = '' + self.mapper = None + + def getDBName(self): + return self.dbname + + def getDetails(self): + return self.details + + def setDetails(self,det): + self.details = det + + def getTitle(self): + return self.title + + def addMapper(self, mapper): + self.mapper = mapper + + def getMapper(self): + return self.mapper + + def extractDetails(self, dictionary): + self.details = dictionary.pop(('details'), '') + self.title = dictionary.pop(('title'), self.dbname) + custommapping = dictionary.pop(('mapping'), None) + if custommapping: + self.mapper = ResulttypeMapper() + self.mapper.add(custommapping) + else: + self.mapper = None + + + def __repr__(self): + return self.getTitle() + ": " + self.getDetails() + + + __str__ = __repr__ + + + +class BenchmarkDetails(BasicDetails): + + def __init__(self, dbname): + BasicDetails.__init__(self, dbname) + + def __repr__(self): + return "Benchmark: " + BasicDetails.__repr__(self) + + __str__ = __repr__ + + + +class VariantDetails(BasicDetails): + + def __init__(self, dbname): + BasicDetails.__init__(self, dbname) + self.benchmarks = {} + + def addBenchmark(self, bm): + self.benchmarks[bm.getDBName()] = bm + + def getBenchmark(self, dbbm): + return self.benchmarks.get(dbbm, BenchmarkDetails(dbbm)) + + def __repr__(self): + ret = "Variant: " + BasicDetails.__repr__(self) + for v in self.benchmarks.values(): + ret += "\n\t\t" + str(v) + return ret + + + __str__ = __repr__ + +class TableDetails(BasicDetails): + + def __init__(self, tbl): + BasicDetails.__init__(self, tbl) + self.variants = {} + + def addVariant(self, var): + self.variants[var.getDBName()] = var + + def getVariant(self, varname): + return self.variants.get(varname, VariantDetails(varname)) + + def __repr__(self): + ret = "Table: " + BasicDetails.__repr__(self) + "(" + self.getDBName() + ")" + for v in self.variants.values(): + ret += "\n\t" + str(v) + return ret + + __str__ = __repr__ + +from pprint import pprint + + +class ResulttypeMapper(object): + + def __init__(self): + self.mappings = {} + + def add(self, mapping): + for label, dbnamelist in mapping.items(): + self.mappings[label] = dbnamelist + + def getLabel(self, dbname): + for label, dbnamelist in self.mappings.items(): + if dbname in dbnamelist: + return label + return dbname + + def getLabelList(self): + return self.mappings.keys() + + def getDBNames(self, label): + return self.mappings.get(label, None) + + def __repr__(self): + ret = "Resulttype Mapper:" + for label,dbnames in self.mappings.items(): + ret += "\n\t" + label + for db in dbnames: + ret += "\n\t\t" + db + return ret + + __str__ = __repr__ + +import yaml +class DetailDealer: + + def __init__(self, configfile=None): + self.tables = {} + self.defaultMapper = ResulttypeMapper() + + if not configfile: + return + self.reload(configfile) + + if not self.tables: + print "DetailDealer: no details found for " + configfile + + def reload(self, configfile): + self.tables = {} + self.defaultMapper = ResulttypeMapper() + if not configfile: + return # no details. + f = open(configfile) + # use safe_load instead load + cfg = yaml.safe_load(f) + f.close() + # Read out default mapping, if existent + self.extractDefaults(cfg) + tables = cfg.pop('tables', None) + if tables: + for tablename,details in tables.items(): + tab = TableDetails(tablename) + # pop: return and remove when key present, else return 'notfound' + tab.extractDetails(details) + variants = details.pop('variants') + for variantname, vdetails in variants.items(): + var = VariantDetails(variantname) + var.extractDetails(vdetails) + benchmarks = vdetails.pop('benchmarks') + for benchmark, bdetails in benchmarks.items(): + bm = BenchmarkDetails(benchmark) + bm.extractDetails(bdetails) + var.addBenchmark(bm) + tab.addVariant(var) + self.tables[tab.getDBName()] = (tab) + + + def extractDefaults(self, cfg): + defs = cfg.pop('defaults', None) + if defs: + defmap = defs.pop('mapping', None) + if defmap: + self.defaultMapper.add(defmap) + + def getDefaultMapper(self): + return self.defaultMapper + + def getTable(self, tablename): + tab = self.tables.get(tablename, None) + if tab: + return tab + return TableDetails(tablename) + + def getVariant(self, tablename, variantname): + tab = self.getTable(tablename) + if tab: + return tab.getVariant(variantname) + return VariantDetails(variantname) # Default + + def getBenchmark(self, table, variant, bechmark): + tab = self.getTable(table) + if tab: + var = tab.getVariant(variant) + if var: + return var.getBenchmark(bechmark) + return BenchmarkDetails(benchmark) # Default + + def __repr__(self): + ret = str(self.defaultMapper) + '\n' + for tabledetails in self.tables.values(): + ret += str(tabledetails) + '\n' + return ret + +if __name__ == "__main__": + dd = DetailDealer('./test.yml') + pprint(dd) diff --git a/tools/analysis/resultbrowser/app/model.py b/tools/analysis/resultbrowser/app/model.py new file mode 100755 index 00000000..465c3f77 --- /dev/null +++ b/tools/analysis/resultbrowser/app/model.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python +import MySQLdb +import MySQLdb.cursors +import yaml + +import sys +import os.path + +from pprint import pprint +import data +import details + +"""Get command line options""" +from optparse import OptionParser +parser = OptionParser() +parser.add_option("-c", "--conf", type="string", help="MySQL config file", dest="config", default= os.path.join(os.path.expanduser("~"),".my.cnf")) +parser.add_option("-s", "--host", type="string", help="Webserver hostname", dest="host", default="localhost") +parser.add_option("-d", "--details", type="string", help="Detailed information (YAML configuration file)", dest="details", default=None) +parser.add_option("-p", "--port", type="string", help="Webserver port", dest="port", default="5000") +opts, args = parser.parse_args() + +"""Check if configuration files exist""" +def checkConfigFile(msg, fname): + if not os.path.isfile(fname): + sys.exit("Error: '" + fname + "' not found") + else: + print msg, "->", fname + +# Check sql config +sqlconfig = opts.config +checkConfigFile("MySQL config", sqlconfig) + +# Check details file +if opts.details: + checkConfigFile("Details", opts.details) + +# Instantiate global detail dealer, will be initialized in reloadOverview +detaildealer = details.DetailDealer() + + +"""Remove all characters from string except alphanuermics and _""" +def scrub(table_name): + return ''.join( chr for chr in table_name if chr.isalnum() or chr == '_' ) + +"""Global mysql handles""" +db = None +cur = None +def loadSession(dbconf): + global db + if db: + db.close() + db = MySQLdb.connect(read_default_file=dbconf, cursorclass=MySQLdb.cursors.DictCursor) + return db.cursor() + + +def closeSession(): + if cur: cur.close() + global db + db.close() + db = None + + +'''Populate variant results for overview data''' +def getVariants(cur, table): + restbl = table.getDetails().getDBName() + cur.execute("""SELECT resulttype, variant, variant_id, benchmark, count(*) as total from %s join fsppilot on %s.pilot_id=fsppilot.id join variant on fsppilot.variant_id=variant.id group by resulttype, variant.id ORDER BY variant.id""" % (restbl, restbl)) # % is used here, as a tablename must not be quoted + res = cur.fetchall() + rdic = {} + # Build dict with variant id as key + for r in res: + # if variant entry already exists: + variant = table.getVariant(int(r['variant_id'])) + if not variant: # if variant did not exist yet, create it: + variant_details = detaildealer.getVariant(restbl, r['variant']) + benchmark_details = detaildealer.getBenchmark(restbl, r['variant'], r['benchmark']) + table_details = detaildealer.getTable(restbl) + variant = data.Variant(int(r['variant_id']), r['variant'], table_details, benchmark_details, variant_details) + variant.addResulttype(r['resulttype'], r['total']) + table.addVariant(variant) + +'''Get overview data for index page''' +def reloadOverview(): + overview = data.Overview() + detaildealer.reload(opts.details) + cur = loadSession(sqlconfig) + cur.execute("show tables like 'result_%'") + result_tables = cur.fetchall() + results = {} + for rdic in result_tables: + # r is the tablename, -> result_FOOBAR + for key, tablename in rdic.items(): + table = data.ResultTable(tablename,detaildealer) + getVariants(cur, table) + overview.add(table) + # Check if objdump table exists + cur.execute("SHOW TABLES like 'objdump'") + objdump_exists = (len(cur.fetchall()) == 1) + closeSession() + return overview, objdump_exists + +"""Load overview data at server startup""" +print "Loading overview data from database. This may take a while ..." +overview_data, objdump_exists = reloadOverview() +print "done." +## Get overview data for views.index() +def getOverview(): + return overview_data + +def objdumpExists(): + return objdump_exists + + +"""Get Results for one variant id""" +def getVariantResult(table, variantid): + cur = loadSession(sqlconfig) + restbl = scrub(table) + + stmt = "SELECT resulttype, count(*) as total from %s join fsppilot on %s.pilot_id=fsppilot.id join variant on fsppilot.variant_id=variant.id" % (restbl, restbl) + where = " WHERE variant.id = %s group by resulttype ORDER BY resulttype" + stmt = stmt + where + cur.execute(stmt, variantid) + res = cur.fetchall() + closeSession() + return res + +'''Show objdump together with according injection result types.''' +def getCode(result_table, variant_id, resultlabel=None): + result_table = scrub(result_table) + filt = '' + if not variant_id or not result_table: + return None + variant = overview_data.getVariantById(variant_id) + mapper = variant.getMapper() + if resultlabel: + dbnames = mapper.getDBNames(resultlabel) + if dbnames: + filt = " and ( " + for dbn in dbnames[:-1]: + filt += "resulttype = '" + dbn + "' OR " + filt += "resulttype = '" + dbnames[-1] +"' ) " + else: + filt = " and resulttype = '" + resultlabel + "' " + + # I especially like this one: + select = "SELECT instr_address, opcode, disassemble, comment, COUNT(*) as totals, GROUP_CONCAT(DISTINCT resulttype SEPARATOR ', ') as results FROM %s " % result_table + join = "JOIN fsppilot ON pilot_id = fsppilot.id JOIN objdump ON objdump.instr_address = injection_instr_absolute " + where = "WHERE objdump.variant_id = %s AND fsppilot.variant_id = %s " + group = "GROUP BY injection_instr_absolute ORDER BY injection_instr_absolute " + + cur = loadSession(sqlconfig) + stmt = select + join + where + filt + group + cur.execute(stmt, (variant_id, variant_id)) + dump = cur.fetchall() + + closeSession() + resulttypes = variant.getResultLabels() + return dump, resulttypes + +def getCodeExcerpt(variant_id, instr_addr): + code = {} + limit = 4 + cur = loadSession(sqlconfig) + cur.execute( """(SELECT instr_address, opcode, disassemble, comment FROM objdump \ + WHERE instr_address < %s AND variant_id = %s \ + ORDER BY instr_address DESC LIMIT %s) \ + ORDER BY instr_address ASC""" , (instr_addr, variant_id, limit)) + below = cur.fetchall() + code['below'] = below + cur.execute("""SELECT instr_address, opcode, disassemble, comment FROM objdump \ + WHERE instr_address >= %s AND variant_id = %s \ + ORDER BY instr_address ASC LIMIT %s""", (instr_addr, variant_id, limit+1)) + upper = cur.fetchall() + code['upper'] = upper + closeSession() + return code + +def getResultsbyInstruction(result_table, variant_id, instr_addr, resultlabel=None): + restypefilter = None + if resultlabel: + variant = overview_data.getVariantById(variant_id) + mapper = variant.getMapper() + if resultlabel: + dbnames = mapper.getDBNames(resultlabel) + if dbnames: + restypefilter = " and ( " + for dbn in dbnames[:-1]: + restypefilter += "resulttype = '" + dbn + "' OR " + restypefilter += "resulttype = '" + dbnames[-1] +"' ) " + + + #select = "SELECT data_address, data_width, original_value, bitoffset, experiment_number, details, resulttype from %s " % scrub(result_table) + select = "SELECT * from %s " % scrub(result_table) + join = "JOIN fsppilot ON pilot_id = fsppilot.id " + where = "WHERE variant_id = %s and injection_instr_absolute = %s " + order = "ORDER BY data_address, bitoffset" + + cur = loadSession(sqlconfig) + if not restypefilter: + stmt = select + join + where + order + cur.execute(stmt, (variant_id, instr_addr)) + else: + stmt = select + join + where + restypefilter + order + cur.execute(stmt, (variant_id, instr_addr)) + + res = cur.fetchall() + closeSession() + return res + +def showDBstatus(): + res = "TODO" + return res + + diff --git a/tools/analysis/resultbrowser/app/static/css/barchart.css b/tools/analysis/resultbrowser/app/static/css/barchart.css new file mode 100644 index 00000000..b68912c3 --- /dev/null +++ b/tools/analysis/resultbrowser/app/static/css/barchart.css @@ -0,0 +1,5 @@ +dl.horizontal {font-size:12px; width:850px;} +dl.horizontal dt {float:left; width:300px; clear:both; margin:0 0 5px 0; padding:3px;} +dl.horizontal dd {float:left; width:500px; border:1px solid #aaaaaa; margin:0 0 5px 0; padding:2px; -moz-box-shadow: 1px 1px 3px #aaaaaa;} +dl.horizontal dd span {background:#91b4e6; display:block; color:black; text-indent:4px;} + diff --git a/tools/analysis/resultbrowser/app/static/css/main.css b/tools/analysis/resultbrowser/app/static/css/main.css new file mode 100644 index 00000000..a6ec20ed --- /dev/null +++ b/tools/analysis/resultbrowser/app/static/css/main.css @@ -0,0 +1,214 @@ + +body { + margin: 0; + padding: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #444; +} + +a:visited { + color : #444; +} + +a:link { + color : #444; + text-decoration: none; +} +/* + * Create dark grey header with a white logo + */ + +header { + background-color: #2B2B2B; + height: 30px; + width: 100%; + opacity: .9; + margin-bottom: 10px; +} + +header h1.logo { + margin: 0; + font-size: 1.5em; + color: #fff; + text-transform: uppercase; + float: left; +} + +header h1.logo:hover { + color: #fff; + text-decoration: none; +} + + +/* + * Center the body content + */ + +.container { + width: 95%; + margin: 0 auto; +} + +div.jumbo { + padding: 10px 0 30px 0; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +h2 { + font-size: 1.2em; + margin-top: 40px; + text-align: center; + letter-spacing: -1px; +} + +h3 { + font-size: 1.0em; + font-weight: 100; + margin-top: 30px; + text-align: center; + /*letter-spacing: -1px;*/ + color: #999; +} + + +#footer { + text-align: center; + color: #444; + font-size:10pt; +} + +/* + * Table styles + */ + +.codetable, .resulttable, .overviewtable{ + font-family: monospace; + margin: auto; +} + +.overviewtable { + border: 1px gray solid; + margin: auto; +} +/* +.overviewtable tr:nth-child(4n), .overviewtable tr:nth-child(4n-1) { + background: #fff; +} +.overviewtable tr:nth-child(4n-2), .overviewtable tr:nth-child(4n-3) { + background: #e8ffb3; +} +*/ + +.resulttable, .codetable, .overviewtable{ + margin: auto; + border: solid #ccc 1px; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 1px 1px #ccc; + -moz-box-shadow: 0 1px 1px #ccc; + box-shadow: 0 1px 1px #ccc; +} + +.codetable tr:hover, .resulttable tr:hover { + background: #fbf8e9; +/* -o-transition: all 0.1s ease-in-out; + -webkit-transition: all 0.1s ease-in-out; + -moz-transition: all 0.1s ease-in-out; + -ms-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; +*/ +} + +.resulttable td, .resulttable th, .codetable td, .codetable th { + border-left: none; + border-top: none; + padding: 2px; + text-align: left; + display: table-cell; +} + +.codetable td a { + text-decoration: none; + display: block; + padding: 0px; + height: 100%; +} + +.resulttable th, .codetable th, .overviewtable th { + background-color: #dce9f9; + background-image: -webkit-gradient(linear, left top, left bottom, from(#efe), to(#91b4e6)); + background-image: -webkit-linear-gradient(top, #efe, #91b4e6); + background-image: -moz-linear-gradient(top, #efe, #91b4e6); + background-image: -ms-linear-gradient(top, #efe, #91b4e6); + background-image: -o-linear-gradient(top, #efe, #91b4e6); + background-image: linear-gradient(top, #efe, #91b4e6); + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,.8) inset; + -moz-box-shadow:0 1px 0 rgba(255,255,255,.8) inset; + box-shadow: 0 1px 0 rgba(255,255,255,.8) inset; + border-top: none; + text-shadow: 0 1px 0 rgba(255,255,255,.5); +} + +.resulttable td:first-child, .resulttable th:first-child, .codetable td:first-child, .codetable th:first-child { + border-left: none; +} + +.resulttable th:first-child, .codetable th:first-child { + -moz-border-radius: 6px 0 0 0; + -webkit-border-radius: 6px 0 0 0; + border-radius: 6px 0 0 0; +} + +.resulttable th:last-child, .codetable th:last-child { + -moz-border-radius: 0 6px 0 0; + -webkit-border-radius: 0 6px 0 0; + border-radius: 0 6px 0 0; +} + +.resulttable th:only-child, .codetable th:only-child{ + -moz-border-radius: 6px 6px 0 0; + -webkit-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.resulttable tr:last-child td:first-child, .codetable tr:last-child td:first-child { + -moz-border-radius: 0 0 0 6px; + -webkit-border-radius: 0 0 0 6px; + border-radius: 0 0 0 6px; +} + +.resulttable tr:last-child td:last-child, .codetable tr:last-child td:last-child { + -moz-border-radius: 0 0 6px 0; + -webkit-border-radius: 0 0 6px 0; + border-radius: 0 0 6px 0; +} + +.resulttypemenu { + text-align: center; + font-size: 12px +} +/* + * Display navigation links inline + */ + +.menu { + float: right; + margin-top: 8px; +} + +.menu li { + display: inline; +} + +.menu li + li { + margin-left: 35px; +} + +.menu li a { + color: #999; + text-decoration: none; +} diff --git a/tools/analysis/resultbrowser/app/static/favicon.ico b/tools/analysis/resultbrowser/app/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cf9bfb1659a3f504b9bc26cf79dd052fb35ce4db GIT binary patch literal 36885 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 literal 0 HcmV?d00001 diff --git a/tools/analysis/resultbrowser/app/templates/about.html b/tools/analysis/resultbrowser/app/templates/about.html new file mode 100644 index 00000000..22d0562d --- /dev/null +++ b/tools/analysis/resultbrowser/app/templates/about.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} + +{% block content %} + +{% if status %} +

About

+{{ status }} +{% else %} +

Sorry, no status information.

+{% endif %} +{% endblock %} diff --git a/tools/analysis/resultbrowser/app/templates/code.html b/tools/analysis/resultbrowser/app/templates/code.html new file mode 100644 index 00000000..920b1da8 --- /dev/null +++ b/tools/analysis/resultbrowser/app/templates/code.html @@ -0,0 +1,50 @@ +{% extends "layout.html" %} + +{% block content %} + +

Variant Results

+ +{% if resulttypes %} + + + + + + + +
Result Table{{ variant_details.getTableDetails().getTitle() }}
Variant {{ variant_details.getDetails().getTitle() }}
Details {{ variant_details.getDetails().getDetails() }}
Benchmark {{ variant_details.getBenchmarkDetails().getTitle() }}
Details {{ variant_details.getBenchmarkDetails().getDetails() }}
Result Count {{ results|length }}
+
+

| All Results | + {% for restype in resulttypes %} + {{ restype}} | + {% endfor %} +

+
+ + + + + + + + + + {% for d in results %} + {% set link = url_for('instr_details', table=request.args.get('table'), variant_id=request.args.get('variant_id'), benchmark=request.args.get('benchmark'), variant=request.args.get('variant'), resulttype=request.args.get('resulttype'), instr_address=d['instr_address'] ) %} + + + + + + + + + {% endfor %} +
AddressOpcodeDisassemblyCommentResults# Results
{{ "0x%x"|format(d['instr_address']) }}{{ d['opcode'] }}{{ d['disassemble'] }}{{ d['comment'] }}{{ d['results'] }}{{ d['totals'] }}
+{% else %} +

Sorry, no dump found.

+{% endif %} + +{%endblock %} + + diff --git a/tools/analysis/resultbrowser/app/templates/index.html b/tools/analysis/resultbrowser/app/templates/index.html new file mode 100644 index 00000000..31abf68e --- /dev/null +++ b/tools/analysis/resultbrowser/app/templates/index.html @@ -0,0 +1,55 @@ +{% extends "layout.html" %} + +{% block content %} +{%if overview %} + {% for tablekey, resulttable in overview.getTables().items() %} +

Result table: {{ resulttable.getDetails().getTitle() }}

+ -> Reload data <- +
+
Details: +
+ {{ resulttable.getDetails().getDetails() }} +
+
+ {% if not objdump_there %} + No objdump found + {% endif %} + + {% for varid, variant in resulttable.getVariants().items() %} + + + {% endif %} + + {% endfor %} +
+ {% set variant_title=variant.getDetails().getTitle() ~ " - " ~ variant.getBenchmarkDetails().getTitle() ~ " id: " ~ variant.getId() %} + {% if objdump_there %} + {{ variant_title }} + {% else %} + {{ variant_title }} + {% endif %} + (Total: {{ variant.getTotals() }}) +
+ {% if variant.getDetails().getDetails() %} + Variant Details: {{ variant.getDetails().getDetails() }} + {% endif %} + {% if variant.getBenchmarkDetails().getDetails() %} +
Benchmark Details: {{ variant.getBenchmarkDetails().getDetails() }}
+
+ {% for reslabel,count in variant.getResults().items() %} +
+ {% if objdump_there %} + {{ reslabel }} + {% else %} + {{ reslabel }} + {% endif %} +
+
{{count}}
+ {% endfor %} +
+ {% endfor %} +{% else %} +

Sorry, no results found.

+{% endif %} + +{% endblock %} diff --git a/tools/analysis/resultbrowser/app/templates/instr_details.html b/tools/analysis/resultbrowser/app/templates/instr_details.html new file mode 100644 index 00000000..232c3792 --- /dev/null +++ b/tools/analysis/resultbrowser/app/templates/instr_details.html @@ -0,0 +1,65 @@ +{% extends "layout.html" %} + +{% block content %} +{% if code %} +

Result by Instruction

+ + + + + + + +
Result Table{{ variant_details.getTableDetails().getTitle() }}
Variant {{ variant_details.getDetails().getTitle() }}
Benchmark {{ variant_details.getBenchmarkDetails().getTitle() }}
Details {{ variant_details.getDetails().getDetails() }}
Instruction Address {{ "0x%x (Dec: %d)"|format(request.args.get('instr_address')|int, request.args.get('instr_address')|int) }}
Total Results{{ result|length }}
+
+

Code

+ + + + + + + + {% for d in code['below'] %} + + + + + + + {% endfor %} + + + + + + + {% for d in code['upper'][1:] %} + + + + + + + {% endfor %}
AddressOpcodeDisassemblyComment
{{ "0x%x"|format(d['instr_address']) }}{{ d['opcode'] }}{{ d['disassemble'] }}{{ d['comment'] }}
{{ "0x%x"|format(code['upper'][0]['instr_address']) }}{{ code['upper'][0]['opcode'] }}{{ code['upper'][0]['disassemble'] }}{{ code['upper'][0]['comment'] }}
{{ "0x%x"|format(d['instr_address']) }}{{ d['opcode'] }}{{ d['disassemble'] }}{{ d['comment'] }}
+
+

Results ({{ result|length }})

+ + + {% for key, value in result[0].items() -%} + + {% endfor -%} + + {% for r in result -%} + + {% for k,v in r.items() -%} + + {% endfor -%} + + {% endfor -%} + +
{{ key }}
{{ v }}
+{% else %} +

Sorry, no details found.

+{% endif %} +{% endblock %} diff --git a/tools/analysis/resultbrowser/app/templates/layout.html b/tools/analysis/resultbrowser/app/templates/layout.html new file mode 100644 index 00000000..e2a6402c --- /dev/null +++ b/tools/analysis/resultbrowser/app/templates/layout.html @@ -0,0 +1,32 @@ + + + + Fail* Results + + + + + +
+
+

Fail*

+ +
+
+ +
+ {% block content %} + {% endblock %} +
+ +
+ + + diff --git a/tools/analysis/resultbrowser/app/views.py b/tools/analysis/resultbrowser/app/views.py new file mode 100644 index 00000000..19ea6999 --- /dev/null +++ b/tools/analysis/resultbrowser/app/views.py @@ -0,0 +1,39 @@ +from flask import render_template,request +from app import app + +import model +import data + +@app.route('/') +@app.route('/index') +def index(): + reload_overview = request.args.get('reload', False) + if reload_overview: + print "Reloading overview..." + model.reloadOverview() + return render_template("index.html", overview=model.getOverview(), objdump_there = model.objdumpExists()) + +@app.route('/code') +def code(): + variant_id = request.args.get('variant_id', None) + resulttype = request.args.get('resulttype', None) + table = request.args.get('table', None) + res,restypes = model.getCode(table, variant_id, resulttype) + var_dets = model.getOverview().getVariantById(variant_id) + return render_template("code.html", results=res, resulttypes=restypes, variant_details=var_dets ) + +@app.route('/instr_details') +def instr_details(): + table = request.args.get('table', None) + variant_id = request.args.get('variant_id', None) + instr_addr = request.args.get('instr_address', None) + resulttype = request.args.get('resulttype', None) + codeexcerpt = model.getCodeExcerpt(variant_id, instr_addr) + var_dets = model.getOverview().getVariantById(variant_id) + results = model.getResultsbyInstruction(table, variant_id, instr_addr, resulttype) + return render_template("instr_details.html", code=codeexcerpt, result=results, variant_details=var_dets) + +@app.route('/about') +def about(): + stat = model.showDBstatus() + return render_template("about.html", status=stat) diff --git a/tools/analysis/resultbrowser/conf.yml b/tools/analysis/resultbrowser/conf.yml new file mode 100644 index 00000000..14105de3 --- /dev/null +++ b/tools/analysis/resultbrowser/conf.yml @@ -0,0 +1,69 @@ +# YAML-based: http://yaml.org/ +# Online parser for testing: http://yaml-online-parser.appspot.com/ +# Some notes: +# YAML is case-sensitive and structured by indention! +# +# The 'defaults' section describes an *optional* default result type mapping for all tables. +# The 'tables' section describes result tables in more detail. +# A table consists of variants, each variant of benchmarks. +# Each of these configuration items +# title: Table title +# details: Some textual description +# mapping: A distinct mapping, if not set, the parent item's mapping is used + +defaults: + mapping: + Everything OK: + - OK + - OK_DETECTED_ERROR + - OK_WRONG_CONTROL_FLOW + Outside Data Section: + - ERR_OUTSIDE_DATA + Hardware Trap: + - ERR_OUTSIDE_TEXT + - ERR_TRAP + Silent Data Corruption: + - ERR_WRONG_RESULT + +tables: + result_CoredVoterProtoMsg: + title: CoRed Voter Experiment Results + variants: + x86_cored_voter: + title: x86 CoRed Voter Experiment + details: Some interesting details about the experiment. + + benchmarks: + ean-random-4: + title: Random 4 bit injections + details: | + The details can also written this way. + The pipe insert the newlines. Cool, isn't it? + + ean-random-5: + title: Random 5 bit injections + details: Details about 5 bit random injection benchmark. + mapping: + Alright: + - OK + - OK_DETECTED_ERROR + - OK_WRONG_CONTROL_FLOW + Not Alright: + - ERR_OUTSIDE_DATA + - ERR_OUTSIDE_TEXT + - ERR_TRAP + - ERR_WRONG_RESULT + Timeout: + - ERR_TIMEOUT + # Another variant within result_CoredVoterProtoMsg + x86_cored_voter2: + title: variant title + details: variant details + benchmarks: + ean-random-2: + title: benchmarktitle + details: some benchmark details + + ean-random-3: + title: benchmark random 3 + details: some benchmark 3 details diff --git a/tools/analysis/resultbrowser/run.py b/tools/analysis/resultbrowser/run.py new file mode 100755 index 00000000..10848933 --- /dev/null +++ b/tools/analysis/resultbrowser/run.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from app import app +from app import model + +app.run(debug=False, port=int(model.opts.port), host=model.opts.host)