Merge lp:~ensoft-opensource/ensoft-sextant/trunk into lp:ensoft-sextant

Proposed by James
Status: Merged
Approved by: Martin Morrison
Approved revision: 178
Merged at revision: 2
Proposed branch: lp:~ensoft-opensource/ensoft-sextant/trunk
Merge into: lp:ensoft-sextant
Diff against target: 3108 lines (+2792/-161)
27 files modified
LICENSE.txt (+27/-0)
MANIFEST.in (+3/-0)
Parser/parser.py (+0/-161)
bin/sextant (+8/-0)
bin/sextant.bat (+3/-0)
doc/Program_upload_docs.mkd (+9/-0)
doc/Web_server.mkd (+20/-0)
doc/wiki/Reference (+45/-0)
etc/sextant.conf (+7/-0)
resources/web/index.html (+45/-0)
resources/web/interface.html (+103/-0)
resources/web/queryjavascript.js (+201/-0)
resources/web/style_sheet.css (+36/-0)
setup.cfg (+2/-0)
setup.py (+25/-0)
src/sextant/__init__.py (+21/-0)
src/sextant/__main__.py (+220/-0)
src/sextant/db_api.py (+610/-0)
src/sextant/errors.py (+20/-0)
src/sextant/export.py (+152/-0)
src/sextant/objdump_parser.py (+272/-0)
src/sextant/pyinput.py (+180/-0)
src/sextant/query.py (+113/-0)
src/sextant/tests.py (+261/-0)
src/sextant/update_db.py (+78/-0)
src/sextant/web/__init__.py (+8/-0)
src/sextant/web/server.py (+323/-0)
To merge this branch: bzr merge lp:~ensoft-opensource/ensoft-sextant/trunk
Reviewer Review Type Date Requested Status
Phil Connell Approve
Ensoft Patch Lander Pending
Review via email: mp+230964@code.launchpad.net

Commit message

setup.py no uploads config and web file to the correct location for the pypi installation.

Description of the change

setup.py no uploads config and web file to the correct location for the pypi installation.

To post a comment you must log in.
Revision history for this message
Phil Connell (pconnell) wrote :

Merging in without a review, to get trunk in sync with latest code.

Full review to be done at some point.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'LICENSE.txt'
--- LICENSE.txt 1970-01-01 00:00:00 +0000
+++ LICENSE.txt 2014-08-15 12:04:08 +0000
@@ -0,0 +1,27 @@
1Copyright (c) 2014, Ensoft Ltd.
2All rights reserved.
3
4Redistribution and use in source and binary forms, with or without
5modification, are permitted provided that the following conditions are met:
6
71. Redistributions of source code must retain the above copyright notice, this
8 list of conditions and the following disclaimer.
92. Redistributions in binary form must reproduce the above copyright notice,
10 this list of conditions and the following disclaimer in the documentation
11 and/or other materials provided with the distribution.
12
13THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
24The views and conclusions contained in the software and documentation are those
25of the authors and should not be interpreted as representing official policies,
26either expressed or implied, of the FreeBSD Project.
27
028
=== added file 'MANIFEST.in'
--- MANIFEST.in 1970-01-01 00:00:00 +0000
+++ MANIFEST.in 2014-08-15 12:04:08 +0000
@@ -0,0 +1,3 @@
1include *.py
2recursive-include resources *.html *.js *.css *.gif
3
04
=== removed directory 'Parser'
=== removed file 'Parser/parser.py'
--- Parser/parser.py 2014-07-03 12:28:28 +0000
+++ Parser/parser.py 1970-01-01 00:00:00 +0000
@@ -1,161 +0,0 @@
1__author__ = 'patrickas'
2
3import re
4import argparse
5
6class parsed_object():
7 """This object has a name (which is the verbatim name like '<__libc_start_main@plt>'), a position (which
8 is the virtual memory location in hex, like '08048320', extracted from the dump), and a canonical_position
9 (which is the virtual memory location in hex but stripped of leading 0s, so it should be a unique id).
10 It also has a list what_do_i_call of parsed_objects it calls using the assembly keyword 'call'.
11 It has a list original_code of its assembler code, too, in case it's useful."""
12
13 @staticmethod
14 def get_canonical_position(position):
15 return position.lstrip('0')
16
17 def __eq__(self, other):
18 return self.canonical_position == other.canonical_position and self.name == other.name
19
20 def __init__(self, input_lines, assembler_section = ''):
21 """Creates a new parsed_object given the relevant definition-lines from objdump -S.
22 A sample first definition-line is '08048300 <__gmon_start__@plt>:\n' but this function
23 expects to see the entire definition eg
24
25080482f0 <puts@plt>:
26 80482f0: ff 25 00 a0 04 08 jmp *0x804a000
27 80482f6: 68 00 00 00 00 push $0x0
28 80482fb: e9 e0 ff ff ff jmp 80482e0 <_init+0x30>
29
30 We also might expect assembler_section, which is for instance '.init' in 'Disassembly of section .init:'
31 """
32 self.name = re.search(r'<.+>', input_lines[0]).group(0)
33 self.position = re.search(r'^[0-9a-f]+', input_lines[0]).group(0)
34 self.canonical_position = parsed_object.get_canonical_position(self.position)
35 self.assembler_section = assembler_section
36 self.original_code = input_lines[1:]
37
38 #todo: work out what this node calls, and store it in what_do_i_call
39 lines_where_i_call = [line for line in input_lines if re.search(r'\tcall [0-9a-f]+ <.+>\n', line)]
40
41 self.what_do_i_call = []
42 for line in lines_where_i_call:
43 called = (re.search(r'\tcall [0-9a-f]+ <.+>\n', line).group(0))[8:]
44 address, name = called.split(' ')
45 self.what_do_i_call.append((address, name.rstrip('\n')))
46
47
48
49 def __str__(self):
50 return 'Memory address ' + self.position + ' with name ' + self.name + ' in section ' + str(self.assembler_section)
51
52 def __repr__(self):
53 out_str = 'Disassembly of section ' + self.assembler_section + ':\n\n' + self.position + ' ' + self.name + ':\n'
54 return out_str + '\n'.join([' ' + line for line in self.original_code])
55
56class Parser:
57 # Class to manipulate the output of objdump
58
59 def __init__(self, input_file_location):
60 """Creates a new Parser, given an input file path. That path should be an output from objdump -S."""
61 file = open(input_file_location, 'r')
62 self.source_string_list = [line for line in file]
63 file.close()
64 self.parsed_objects = []
65
66 def create_objects(self):
67 """ Go through the source_string_list, getting object names (like <main>) along with the corresponding
68 definitions, and put them into parsed_objects """
69
70 parsed_objects = []
71 current_object = []
72 current_section = ''
73 for line in self.source_string_list[4:]: # we bodge, since the file starts with a little bit of guff
74 if re.match(r'[0-9a-f]+ <.+>:\n', line):
75 # we are a starting line
76 current_object = [line]
77 elif re.match(r'Disassembly of section', line):
78 current_section = re.search(r'section .+:\n', line).group(0).lstrip('section ').rstrip(':\n')
79 current_object = []
80 elif line == '\n':
81 #we now need to stop parsing the current block, and store it
82 if len(current_object) > 0:
83 parsed_objects.append(parsed_object(current_object, current_section))
84 else:
85 current_object.append(line)
86
87 # now we should be done. We assumed that blocks begin with r'[0-9a-f]+ <.+>:\n' and end with a newline.
88 #clear duplicates:
89
90 self.parsed_objects = []
91 for obj in parsed_objects:
92 if obj not in self.parsed_objects:
93 self.parsed_objects.append(obj)
94
95 # by this point, each object contains a self.what_do_i_call which is a list of tuples ('address', 'name').
96
97 def object_lookup(self, object_name = '', object_address = ''):
98 """Returns the object with name object_name or address object_address (at least one must be given).
99 If objects with the given name or address
100 are not found, returns None."""
101
102 if object_name == '' and object_address == '':
103 return None
104
105 trial_obj = self.parsed_objects
106
107 if object_name != '':
108 trial_obj = [obj for obj in trial_obj if obj.name == object_name]
109
110 if object_address != '':
111 trial_obj = [obj for obj in trial_obj if obj.canonical_position == parsed_object.get_canonical_position(object_address)]
112
113 if len(trial_obj) == 0:
114 return None
115
116 return trial_obj
117
118 def link_objects(self):
119 """Goes through the objects which have already been imported, making their self.what_do_i_call into a list of
120 objects, rather than a list of (address, name)"""
121 #todo: implement this
122
123 for obj in self.parsed_objects:
124 output_list = []
125 for called_thing in obj.what_do_i_call:
126 ref = called_thing[0]
127 if self.object_lookup(object_address=ref) is None:
128 #here, we have an object which is not found in our object_lookup
129 pass
130 else:
131 #called_thing looks like ('address', 'name'); we need to pass this list one by one into object_lookup
132 for looked_up in self.object_lookup(object_address=ref):
133 output_list.append(looked_up)
134 obj.what_do_i_call = output_list
135
136def main():
137
138
139 p = Parser(r"C:\Users\patrickas\Desktop\Objdump_parser\objdump_samples\objdump_libcopp.txt") #command line input of this, or accept a .o file (to run objdump against)
140 p.create_objects()
141
142 print('Functions found:')
143
144 for obj in p.parsed_objects:
145 print(str(obj))
146
147 p.link_objects()
148
149 func_to_investigate = '<copp_sampler_et_ht_lookup>' #do this for all functions
150
151 print('Investigation of {}:'.format(func_to_investigate))
152
153 #we can investigate a particular object:
154 main_func = [obj for obj in p.parsed_objects if obj.name == func_to_investigate][0]
155 print('{} calls '.format(func_to_investigate),end='')
156 print([str(f) for f in main_func.what_do_i_call])
157
158 print('Ended investigation')
159
160if __name__ == "__main__":
161 main()
162\ No newline at end of file0\ No newline at end of file
1631
=== added directory 'bin'
=== added file 'bin/sextant'
--- bin/sextant 1970-01-01 00:00:00 +0000
+++ bin/sextant 2014-08-15 12:04:08 +0000
@@ -0,0 +1,8 @@
1#!/bin/bash
2
3DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4
5export PYTHONPATH=${PYTHONPATH}:${DIR}/../src
6
7python -m sextant $@
8
09
=== added file 'bin/sextant.bat'
--- bin/sextant.bat 1970-01-01 00:00:00 +0000
+++ bin/sextant.bat 2014-08-15 12:04:08 +0000
@@ -0,0 +1,3 @@
1set DIR=%~dp0..\src\
2
3cmd /C "set PYTHONPATH=%DIR% && c:\python27\python -m sextant %*"
04
=== added directory 'doc'
=== added file 'doc/Instructions For yED use.docx'
1Binary files doc/Instructions For yED use.docx 1970-01-01 00:00:00 +0000 and doc/Instructions For yED use.docx 2014-08-15 12:04:08 +0000 differ5Binary files doc/Instructions For yED use.docx 1970-01-01 00:00:00 +0000 and doc/Instructions For yED use.docx 2014-08-15 12:04:08 +0000 differ
=== added file 'doc/Program_upload_docs.mkd'
--- doc/Program_upload_docs.mkd 1970-01-01 00:00:00 +0000
+++ doc/Program_upload_docs.mkd 2014-08-15 12:04:08 +0000
@@ -0,0 +1,9 @@
1# Function pointers
2
3We treat function pointers as functions in their own right, but we rename them to `_._function_pointer_n` where `n` is a numerical identifier (unique to each function pointer call). When ProgramConverter is used to produce graphical output for graphs with function pointers in, they are displayed as yellow nodes with name `(function pointer)`.
4
5Optionally, the flag `--ignore-function-pointers` can be provided to `program_upload.py` to cause the parser to pretend function pointer calls simply do not exist.
6
7# Query parameters
8We take the mandatory `--input-file`, a filepath for a program file whose call graph we wish to upload to the Neo4J server. Optionally we can specify that it is an ``--object-file` to have `objdump` run against it; otherwise, we assume the input was a text file consisting of `objdump`'s output.
9Optional (if specified in config file) `--remote-neo4j`, the location of the Neo4J server (eg. `http://localhost:7474`).
0\ No newline at end of file10\ No newline at end of file
111
=== added file 'doc/Web_server.mkd'
--- doc/Web_server.mkd 1970-01-01 00:00:00 +0000
+++ doc/Web_server.mkd 2014-08-15 12:04:08 +0000
@@ -0,0 +1,20 @@
1The server is running on Twisted. It has a static directory (`/trunk/sextant/web/public_html` in the repository) hosted at `/hello_world` for the moment. It also has `/database_properties` and `/output_graph.svg`. This page documents (so far) the dynamically generated pages.
2
3# database_properties
4This page is designed to be roughly equivalent to the Python script `/graph_properties.py`. It takes arguments `query` of 'functions' or 'properties' and optionally `program_name` (which is ignored unless 'query=functions' was specified). Its return content-type is `text/plain`. The page returns a list of function names as:
5
6 ["func1", "func2", ... , "funcn"]
7
8if `query=functions` was specified, where the `funci` are the function names in the database under program-name `program_name`. It returns `None` if there was no program with the given name, and `[]` if there were no functions in the existing program with the given name.
9
10If instead `query=programs` was specified, we return:
11
12 ["prog1", ... , "progn"]
13
14where the `progi` are the program names stored in the database. If there are no programs in the database, we return `[]`.
15
16# output_graph.svg
17This page is roughly equivalent to `/query_graph.py`. It takes arguments `program_name`, `query`. If the `program_name` was not found in the database, raises a 404 Not Found error. The `query` must be `whole_program`, `functions_calling` or `functions_called_by`; if it is not found, we use `whole_program` as a default.
18If `query` is `functions_calling` or `functions_called_by`, we require a `func1` parameter, as the name of the function of interest.
19
20The page returns a GraphViz-generated file of the query run against the specified function. Its content-type is `image/svg+xml`.
0\ No newline at end of file21\ No newline at end of file
122
=== added directory 'doc/wiki'
=== added file 'doc/wiki/Reference'
--- doc/wiki/Reference 1970-01-01 00:00:00 +0000
+++ doc/wiki/Reference 2014-08-15 12:04:08 +0000
@@ -0,0 +1,45 @@
1
2= Sextant Notes =
3== Configuration ==
4In the config file you should specify:
5 * The url under which Neo4j is running,
6 * The port on which the web interface will run,
7 * Your definition of what is a common function (number of functions calling that function).
8An example file is provided in the etc folder.
9If your instance of Neo4j is local, please start running the server before uploading a program or performing queries.
10== File Management ==
11No more than one program can have the same name in the database.
12=== Uploading a File ===
13set-file-name and not-object-file are optional.
14{{{
15sextant add_program --input-file <name of file being input> --set-file-name <name file to be stored under in database> --not-object-file <True or False (default is False; pass through Objdump)>
16}}}
17=== Deleting File ===
18It is good practice to delete files from the server since having many graphs in the database can lead to negative effects on performance.
19{{{
20sextant delete_program --program name <program name to be deleted as stored in database>
21}}}
22== Queries ==
23=== Command Line ===
24Command line queries produce a text output, either as a list or in GraphML which can be opened in yED.
25All queries take the form.
26{{{
27sextant query --program-name <program name> --query <query name> --funcs <One or two functions as the arguments of the query> --suppress-common <True or False (default is False)>
28}}}
29Here common sense prevails. "return-all-program-names" doesn't have a "--program-name" argument or "--funcs" or "--suppress-common". Similarly whole-graph doesn't take any "--funcs". "--suppress-common" is always optional and can only be used for a GraphML output.
30The options for queries are:
31 * functions-calling,
32 * functions-called-by,
33 * calls-between,
34 * whole-graph,
35 * shortest-path,
36 * return-all-program-names,
37 * return-all-function-names.
38For help with queries from the command line and the arguments type:
39{{{
40sextant query -h
41}}}
42=== Web ===
43To run the web server type
44{{{ sextant web }}}
45into the command line; then navigate a web browser to the port specified.
0\ No newline at end of file46\ No newline at end of file
147
=== added directory 'etc'
=== added file 'etc/sextant.conf'
--- etc/sextant.conf 1970-01-01 00:00:00 +0000
+++ etc/sextant.conf 2014-08-15 12:04:08 +0000
@@ -0,0 +1,7 @@
1[Neo4j]
2# URL of the Neo4J server
3remote_neo4j = http://localhost:7474
4# port on which to serve Sextant Web
5port = 2905
6# number of calls incoming before we consider a function to be 'common'
7common_function_calls = 10
08
=== added directory 'resources'
=== added directory 'resources/web'
=== added file 'resources/web/index.html'
--- resources/web/index.html 1970-01-01 00:00:00 +0000
+++ resources/web/index.html 2014-08-15 12:04:08 +0000
@@ -0,0 +1,45 @@
1<!--
2 Sextant
3 Copyright 2014, Ensoft Ltd.
4 Author: Patrick Stevens
5
6 Home screen for Sextant-->
7
8<!DOCTYPE html>
9<html>
10<head>
11 <meta charset="utf-8"/>
12 <link rel="stylesheet" type="text/css" href="style_sheet.css"/>
13 <title>Sextant</title>
14</head>
15<body>
16<table>
17<tr>
18 <td>
19 <h1>You have reached the portal to Sextant.</h1>
20 </td>
21 <td>
22 </td>
23</tr>
24<tr>
25 <td>
26 <!--Sextant image-->
27 <img src="sextant.gif" alt = "sextant">
28 </td>
29 <td>
30 <!--link to the Query page-->
31 <form action='/interface.html' method="get">
32 <input type="submit" value="Query Page" class="button2"
33 name="link_query" id="form1_submit" />
34 </form>
35 <!--link to Database Properties page-->
36 <form action='/database_properties' method="get">
37 <input type="submit" value="Database Properties" class="button2"
38 name="link_properties" id="form2_submit" />
39 </form>
40 </td>
41</tr>
42</table>
43
44</body>
45</html>
046
=== added file 'resources/web/interface.html'
--- resources/web/interface.html 1970-01-01 00:00:00 +0000
+++ resources/web/interface.html 2014-08-15 12:04:08 +0000
@@ -0,0 +1,103 @@
1<!--
2 Sextant
3 Copyright 2014, Ensoft Ltd.
4 Author: James Harkin
5
6 Sextant web interface for Queries -->
7
8<!DOCTYPE html>
9<html>
10<head>
11<meta charset="utf-8"/>
12 <link rel="stylesheet" type="text/css" href="style_sheet.css"/>
13 <title> Webinterface test </title>
14</head>
15
16<body onload="get_names_for_autocomplete('programs')">
17<!-- Create a table with a border to hold the input file query name
18 and arguments. table remains fixed while page scrolls-->
19<table style="background-color:#FFFFFF; width: 100%;
20 border:1px solid black; position: fixed;">
21<tr style="height:20px">
22 <td style="width:250">
23 <h2>Input file</h2>
24 </td>
25 <td style="width:350">
26 <h2>Query</h2>
27 </td>
28 <td style="width:250">
29 <h2>Arguments</h2>
30 </td>
31 <td>
32 <form>
33 <!--checkbox used to supress common nodes-->
34 <label>
35 <input type="checkbox" id="suppress_common" value="True" />
36 Suppress common functions?
37 </label>
38 </form>
39 </td>
40</tr>
41<tr style="height:100px">
42 <td>
43 <!-- Autocomplete textbox for program names; populated on upload-->
44 <input list="program_names" id="program_name"
45 onblur="get_names_for_autocomplete('funcs')" style="size:20">
46 <datalist id="program_names">
47 </datalist>
48 </td>
49 <td>
50 <!-- Drop-down box to hold choice of queries-->
51 <form name="drop_list_1">
52 <SELECT id="query_list" onchange="display_when();">
53 <option value="whole_program">Whole graph</option>
54 <option value="functions_calling">
55 All functions calling specific function</option>
56 <option value="functions_called_by">
57 All functions called by a specific function</option>
58 <option value="call_paths">
59 All function call paths between two functions</option>
60 <option value="shortest_path">
61 Shortest path between two functions</option>
62 <option value="function_names">
63 All function names</option>
64 </SELECT>
65 </form>
66 <p>
67 query selected : <input type="text" id="query"
68 value="Whole graph" size="20">
69 </p>
70 </td>
71 <td>
72 <!-- Autocomplete text box for argument
73 1 Is only visible for relevant queries-->
74 <div id="argument_1">No arguments required.</div>
75 <input list="function_names" id="function_1" style="size:20; visibility:hidden">
76 <!--list to populate arguments. Updates when program name is specified-->
77 <datalist id="function_names">
78 </datalist>
79 <!-- Autocomplete text box for argument
80 2 Is only visible for relevant queries-->
81 <div id="argument_2"></div>
82 <input list="function_names" id="function_2" style="size:20; visibility:hidden">
83
84 </td>
85 <td>
86
87 <!--execute button; submits query-->
88 <br><input type="submit" value="Execute&#x00A;Query" class="button"
89 onclick="execute_query()"><br>
90 </td>
91</tr>
92</table>
93<!-- Paragraph for text output e.g. list of funtion names-->
94<p id="function_names_output" class="pos_functions" style="size:20; visibility:hidden"></p>
95
96<!-- Output for image file-->
97<img id=output_image src="sextant.jpg" alt="Execute query to display graph."
98 class="pos_img"
99 style="align: bottom; font-style: italic; color: #C0C0C0; font-size: 15px;">
100
101<script src="queryjavascript.js"></script>
102</body>
103</html>
0104
=== added file 'resources/web/queryjavascript.js'
--- resources/web/queryjavascript.js 1970-01-01 00:00:00 +0000
+++ resources/web/queryjavascript.js 2014-08-15 12:04:08 +0000
@@ -0,0 +1,201 @@
1// Sextant
2// Copyright 2014, Ensoft Ltd.
3// Author: James Harkin, Patrick Stevens
4//
5//Runs query and "GET"s either program names uploaded on the
6//server or function names from a specific program.
7
8
9function get_names_for_autocomplete(info_needed){
10 //Function queries to database to create a list
11 //which is used to populate the auto-complete text boxes.
12 var xmlhttp = new XMLHttpRequest();
13 xmlhttp.onreadystatechange = function(){
14 if (xmlhttp.status = 200){
15 var values_list = xmlhttp.responseText;
16 console.log(values_list)
17 values_list = JSON.parse(values_list);
18 if (info_needed =='programs'){
19 //We need to populate the program names list
20 add_options("program_names", values_list);
21 }
22 if (info_needed =='funcs'){
23 //We need to populate the functions list for the arguments
24 add_options("function_names", values_list);
25 }
26 }
27 }
28 if (info_needed == 'programs'){
29 var string = "/database_properties?query=" + info_needed + "&program_name=";
30 }
31 else{
32 var string = "/database_properties?query=" + "functions" +
33 "&program_name=" + document.getElementById("program_name").value;
34 if (info_needed == 'programs'){
35 var string = "/database_properties?query=" +
36 info_needed + "&program_name=" + prog_name;
37 }
38 //"GET" information from the specified url (string)
39 xmlhttp.open("GET", string, true);
40 xmlhttp.send();
41 }
42 xmlhttp.open("GET", string, true);
43 xmlhttp.send();
44}
45
46
47function add_options(selectedlist, values_list){
48 //Adds all the options obtained from the list of program
49 //names or function names to an auto complete drop-down box
50 var options = ''
51 if (values_list.length == 1 || values_list.length ==0){
52 options += '<option value="'+values_list+'"/>';
53 }
54 else{
55 for (var i=0; i < values_list.length;++i){
56 options += '<option value="'+values_list[i]+'"/>';
57 }
58 }
59 document.getElementById(selectedlist).innerHTML = options;
60}
61
62
63function display_when(){
64 //For each query specifies when auto-complete text boxes should be made
65 //visible or invisible and makes them read only
66 var query_list = document.getElementById("query_list");
67 document.getElementById("query").value =
68 query_list.options[query_list.selectedIndex].text;
69 var no_functions = new Array();
70 var prog_name = document.getElementById("program_name").value;
71 if (query_list.options[query_list.selectedIndex].value == "functions_calling"){
72 document.getElementById("argument_1").innerHTML = "Function being called";
73 document.getElementById("argument_2").innerHTML = "";
74 document.getElementById("function_1").readOnly = false;
75 document.getElementById("function_2").readOnly = true;
76 document.getElementById("function_1").style.visibility = "visible";
77 document.getElementById("function_2").style.visibility = "hidden";
78 document.getElementById("function_2").value = null;
79 }
80 if (query_list.options[query_list.selectedIndex].value == "functions_called_by"){
81 document.getElementById("argument_1").innerHTML = "Function calling";
82 document.getElementById("argument_2").innerHTML = "";
83 document.getElementById("function_1").readOnly = false;
84 document.getElementById("function_2").readOnly = true;
85 document.getElementById("function_1").style.visibility = "visible";
86 document.getElementById("function_2").style.visibility = "hidden";
87 document.getElementById("function_2").value = null;
88 }
89 if (query_list.options[query_list.selectedIndex].value == "call_paths"){
90 document.getElementById("argument_1").innerHTML = "Function calling";
91 document.getElementById("argument_2").innerHTML = "Function being called";
92 document.getElementById("function_1").readOnly = false;
93 document.getElementById("function_2").readOnly = false;
94 document.getElementById("function_1").style.visibility = "visible";
95 document.getElementById("function_2").style.visibility = "visible";
96 }
97 if (query_list.options[query_list.selectedIndex].value == "shortest_path"){
98 document.getElementById("argument_1").innerHTML = "Function calling";
99 document.getElementById("argument_2").innerHTML = "Function being called";
100 document.getElementById("function_1").readOnly = false;
101 document.getElementById("function_2").readOnly = false;
102 document.getElementById("function_1").style.visibility = "visible";
103 document.getElementById("function_2").style.visibility = "visible";
104 }
105 if (query_list.options[query_list.selectedIndex].value == "whole_program") {
106 document.getElementById("argument_1").innerHTML = "No arguments required.";
107 document.getElementById("argument_2").innerHTML = "";
108 document.getElementById("function_1").readOnly = true;
109 document.getElementById("function_2").readOnly = true;
110 document.getElementById("function_1").style.visibility = "hidden";
111 document.getElementById("function_2").style.visibility = "hidden";
112 document.getElementById("function_1").value = null;
113 document.getElementById("function_2").value = null;
114 }
115 if (query_list.options[query_list.selectedIndex].value == "function_names"){
116 document.getElementById("argument_1").innerHTML = "No arguments required.";
117 document.getElementById("argument_2").innerHTML = "";
118 document.getElementById("function_1").readOnly = true;
119 document.getElementById("function_2").readOnly = true;
120 document.getElementById("function_1").style.visibility = "hidden";
121 document.getElementById("function_2").style.visibility = "hidden";
122 document.getElementById("function_1").value = null;
123 document.getElementById("function_2").value = null;
124 }
125}
126
127
128
129function execute_query(){
130 //Returns error in alert window if query not executed properly,
131 //otherwise performs the query and outputs it
132 document.getElementById("output_image").src = "";
133 document.getElementById("output_image").alt = "Please wait loading...";
134 var query_id = document.getElementById("query_list").value;
135 if (query_id == "function_names"){
136 //url for page containing all function names
137 var string = "/database_properties?program_name=" +
138 document.getElementById("program_name").value + "&query=functions";
139 }
140 else{
141 //If not function names we will want a graph as an output;
142 //url returns svg file of graph.
143 var string = "/output_graph.svg?program_name=" +
144 document.getElementById("program_name").value +
145 "&query=" + query_id + "&func1=";
146 string = string + document.getElementById("function_1").value +
147 "&func2=" + document.getElementById("function_2").value;
148 string = string + "&suppress_common=" +
149 document.getElementById('suppress_common').checked.toString();
150
151 }
152 var xmlhttp = new XMLHttpRequest();
153 xmlhttp.open("GET", string, true);
154 xmlhttp.send();
155 xmlhttp.onreadystatechange = function(){
156 if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
157 //readyState == 4 means query has finished executing.
158 //status == 200 means "GET"ing has been successful.
159 if (query_id == "function_names"){
160 //Text output displayed in paragraph.
161 document.getElementById("function_names_output").innerHTML =
162 xmlhttp.responseText;
163 document.getElementById("function_names_output").style.visibility =
164 "visible"
165 //Clear current image if one exists.
166 document.getElementById("output_image").alt = "";
167 document.getElementById("output_image").src = "";
168 }
169 else{
170 document.getElementById("function_names_output").style.visibility =
171 "hidden"
172 document.getElementById("output_image").src = string;
173 }
174 }
175 else if (xmlhttp.readyState == 4 && xmlhttp.status == 400){
176 //Error occurred during query; display response.
177 document.getElementById("output_image").alt = "";
178 window.alert(xmlhttp.responseText);
179 }
180 else if(xmlhttp.readyState == 4 && xmlhttp.status == 404){
181 //Error occurred during query; display response.
182 document.getElementById("output_image").alt = "";
183 window.alert(xmlhttp.responseText);
184 }
185 else if(xmlhttp.readyState ==4 && xmlhttp.status == 204){
186 //Query executed correctly but graph returned is empty
187 document.getElementById("output_image").alt = "";
188 window.alert("Graph returned was empty");
189 }
190 else if (xmlhttp.readyState == 4 && xmlhttp.status == 502) {
191 //Error occurs if Neo4j isn't running
192 document.getElementById("output_image").alt = "";
193 window.alert("Bad Gateway received - are you sure the database server is running?");
194 }
195 else if(xmlhttp.readyState ==4){
196 //query executed correctly
197 document.getElementById("output_image").alt = "";
198 window.alert("Error not recognised");
199 }
200 }
201}
0202
=== added file 'resources/web/sextant.gif'
1Binary files resources/web/sextant.gif 1970-01-01 00:00:00 +0000 and resources/web/sextant.gif 2014-08-15 12:04:08 +0000 differ203Binary files resources/web/sextant.gif 1970-01-01 00:00:00 +0000 and resources/web/sextant.gif 2014-08-15 12:04:08 +0000 differ
=== added file 'resources/web/style_sheet.css'
--- resources/web/style_sheet.css 1970-01-01 00:00:00 +0000
+++ resources/web/style_sheet.css 2014-08-15 12:04:08 +0000
@@ -0,0 +1,36 @@
1pre {
2 color: green;
3 background: white;
4 font-family: monospace;
5}
6
7body {
8 font-family: Helvetica, sans-serif;
9}
10.button{
11 display:block; width:100px; height:100px; border-radius:50px; font-size:15px;
12 color:#fff; line-height:100px; text-align:center; background:#FF0000
13}
14
15.pos_img{
16 position:absolute;
17 left:0px;
18 top:270px;
19 z-index:-1
20
21}
22.pos_functions{
23 position:absolute;
24 left:0px;
25 top:250px;
26
27}
28
29.button2{
30 display:inline-block; width:200px; height:100px; border-radius:50px;
31 font-size:15px; color:#fff; line-height:100px;
32 text-align:center; background:#000000
33}
34
35
36
037
=== added file 'setup.cfg'
--- setup.cfg 1970-01-01 00:00:00 +0000
+++ setup.cfg 2014-08-15 12:04:08 +0000
@@ -0,0 +1,2 @@
1[metadata]
2description-file = README.md
0\ No newline at end of file3\ No newline at end of file
14
=== added file 'setup.py'
--- setup.py 1970-01-01 00:00:00 +0000
+++ setup.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,25 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: James Harkin, using work from Patrick Stevens and James Harkin
5# -----------------------------------------
6#
7
8import glob
9import os
10from setuptools import setup
11
12setup(
13 name='Sextant',
14 version='1.0',
15 description= 'Navigating the C',
16 url='http://open.ensoft.co.uk/Sextant',
17 license='Simplified BSD License',
18 packages=['sextant', 'sextant.web', 'resources', 'etc'],
19 package_dir={'sextant': 'src/sextant', 'resources': 'resources', 'etc': 'etc'},
20 scripts=['bin/sextant'],
21 install_requires=['neo4jrestclient', 'twisted'],
22 package_data={'resources': ['web/*'], 'etc': ['*.conf']},
23)
24
25
026
=== added directory 'src'
=== added directory 'src/sextant'
=== added file 'src/sextant/__init__.py'
--- src/sextant/__init__.py 1970-01-01 00:00:00 +0000
+++ src/sextant/__init__.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,21 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: Patrick Stevens, James Harkin
5# -----------------------------------------
6"""Program call graph recording and query framework."""
7
8from . import errors
9from . import pyinput
10
11__all__ = (
12 "SextantConnection",
13) + (
14 errors.__all__ +
15 pyinput.__all__
16)
17
18from .db_api import SextantConnection
19from .errors import *
20from .pyinput import *
21
022
=== added file 'src/sextant/__main__.py'
--- src/sextant/__main__.py 1970-01-01 00:00:00 +0000
+++ src/sextant/__main__.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,220 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: James Harkin, using work from Patrick Stevens and James Harkin
5# -----------------------------------------
6#invokes Sextant and argparse
7
8from __future__ import absolute_import, print_function
9
10import argparse
11try:
12 import ConfigParser
13except ImportError:
14 import configparser as ConfigParser
15import logging
16import logging.config
17import os
18import sys
19
20from . import update_db
21from . import query
22from . import db_api
23
24
25# @@@ Logging level should be configurable (with some structure to setting up
26# logging).
27logging.config.dictConfig({
28 "version": 1,
29 "handlers": {
30 "console": {
31 "class": "logging.StreamHandler",
32 "level": logging.INFO,
33 "stream": "ext://sys.stderr",
34 },
35 },
36 "root": {
37 "level": logging.DEBUG,
38 "handlers": ["console"],
39 },
40})
41log = logging.getLogger()
42
43
44def get_config_file():
45 # get the config file option for neo4j server location and web port number
46 _ROOT = os.path.abspath(os.path.dirname(__file__))
47 def get_data(path, file_name):
48 return os.path.join(_ROOT, path, file_name)
49
50 home_config = os.path.expanduser(os.path.join("~", ".sextant.conf"))
51 env_config = os.environ.get("SEXTANT_CONFIG", "")
52 example_config = get_data('../etc', 'sextant.conf')
53
54 try:
55 conffile = next(p
56 for p in (home_config, env_config, example_config)
57 if os.path.exists(p))
58 except StopIteration:
59 #No config files accessable
60 if "SEXTANT_CONFIG" in os.environ:
61 #SEXTANT_CONFIG environment variable is set
62 log.error("SEXTANT_CONFIG file %r doesn't exist.", env_config)
63 log.error("Sextant requires a configuration file.")
64 sys.exit(1)
65
66 log.info("Sextant is using config file %s", conffile)
67 return conffile
68
69conffile = get_config_file()
70
71conf = ConfigParser.ConfigParser()
72conf.read(conffile)
73
74#remote_neo4j = 'http://localhost:7474'
75#web_port = 2905
76#common_def = 10 # definition of a 'common' node
77try:
78 options = conf.options('Neo4j')
79except ConfigParser.NoSectionError:
80 pass
81else:
82 try:
83 remote_neo4j = conf.get('Neo4j', 'remote_neo4j')
84 except ConfigParser.NoOptionError:
85 pass
86
87 try:
88 web_port = conf.get('Neo4j', 'port')
89 except ConfigParser.NoOptionError:
90 pass
91
92 try:
93 common_def = conf.get('Neo4j', 'common_function_calls')
94 except ConfigParser.NoOptionError:
95 common_def = 10
96
97argumentparser = argparse.ArgumentParser(description="Invoke part of the SEXTANT program")
98subparsers = argumentparser.add_subparsers(title="subcommands")
99
100#set what will be defined as a "common function"
101db_api.set_common_cutoff(common_def)
102
103parsers = dict()
104
105# add each subparser in turn to the parsers dictionary
106
107parsers['add'] = subparsers.add_parser('add_program', help="add a program to the database")
108parsers['add'].add_argument('--input-file', required=True, metavar="FILE_NAME",
109 help="name of file to be put into database",
110 type=str, nargs=1)
111parsers['add'].add_argument('--set-file-name', metavar="FILE_NAME",
112 help="string to store this program under", type=str,
113 nargs=1)
114parsers['add'].add_argument('--not-object-file',
115 help='default False, if the input file is an object to be disassembled',
116 action='store_true')
117
118parsers['delete'] = subparsers.add_parser('delete_program',
119 help="delete a program from the database")
120parsers['delete'].add_argument('--program-name', required=True, metavar="PROG_NAME",
121 help="name of program as stored in the database",
122 type=str, nargs=1)
123
124parsers['query'] = subparsers.add_parser('query',
125 help="make a query of the database")
126parsers['query'].add_argument('--program-name', metavar="PROG_NAME",
127 help="name of program as stored in the database",
128 type=str, nargs=1)
129parsers['query'].add_argument('--query', required=True, metavar="QUERY",
130 help="functions-calling, functions-called-by, "
131 "calls-between, whole-graph, shortest-path, "
132 "return-all-program-names or "
133 "return-all-function-names; if the latter, "
134 "supply argument --program-name",
135 type=str, nargs=1)
136parsers['query'].add_argument('--funcs', metavar='FUNCS',
137 help='functions to pass to the query',
138 type=str, nargs='+')
139parsers['query'].add_argument('--suppress-common', metavar='BOOL',
140 help='suppress commonly called functions (True or False)',
141 type=str, nargs=1)
142
143parsers['web'] = subparsers.add_parser('web', help="start the web server")
144parsers['web'].add_argument('--port', metavar='PORT', type=int,
145 help='port on which to serve Sextant Web',
146 default=web_port)
147
148for parser_key in parsers:
149 parsers[parser_key].add_argument('--remote-neo4j', metavar="URL", nargs=1,
150 help="URL of neo4j server", type=str,
151 default=remote_neo4j)
152
153def _start_web(args):
154 # Don't import at top level -- this makes twisted dependency semi-optional,
155 # allowing non-web functionality to work with Python 3.
156 from .web import server
157 log.info("Serving site on port %s", args.port)
158 server.serve_site(input_database_url=args.remote_neo4j, port=args.port)
159
160parsers['web'].set_defaults(func=_start_web)
161
162def add_file(namespace):
163
164 try:
165 alternative_name = namespace.set_file_name[0]
166 except TypeError:
167 alternative_name = None
168
169 not_object_file = namespace.not_object_file
170 # the default is "yes, this is an object file" if not-object-file was
171 # unsupplied
172
173 update_db.upload_program(namespace.input_file[0],
174 namespace.remote_neo4j,
175 alternative_name, not_object_file)
176
177
178def delete_file(namespace):
179 update_db.delete_program(namespace.program_name[0],
180 namespace.remote_neo4j)
181
182parsers['add'].set_defaults(func=add_file)
183parsers['delete'].set_defaults(func=delete_file)
184
185
186def make_query(namespace):
187
188 arg1 = None
189 arg2 = None
190 try:
191 arg1 = namespace.funcs[0]
192 arg2 = namespace.funcs[1]
193 except TypeError:
194 pass
195
196 try:
197 program_name = namespace.program_name[0]
198 except TypeError:
199 program_name = None
200
201 try:
202 suppress_common = namespace.suppress_common[0]
203 except TypeError:
204 suppress_common = False
205
206 query.query(remote_neo4j=namespace.remote_neo4j,
207 input_query=namespace.query[0],
208 program_name=program_name, argument_1=arg1, argument_2=arg2,
209 suppress_common=suppress_common)
210
211parsers['query'].set_defaults(func=make_query)
212
213# parse the arguments
214
215parsed = argumentparser.parse_args()
216
217# call the appropriate function
218
219parsed.func(parsed)
220
0221
=== added file 'src/sextant/db_api.py'
--- src/sextant/db_api.py 1970-01-01 00:00:00 +0000
+++ src/sextant/db_api.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,610 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: Patrick Stevens, using work from Patrick Stevens and James Harkin
5# -----------------------------------------
6# API to interact with a Neo4J server: upload, query and delete programs in a DB
7
8__all__ = ("Validator", "AddToDatabase", "FunctionQueryResult", "Function",
9 "SextantConnection")
10
11import re # for validation of function/program names
12import logging
13
14from neo4jrestclient.client import GraphDatabase
15import neo4jrestclient.client as client
16
17COMMON_CUTOFF = 10
18# a function is deemed 'common' if it has more than this
19# many connections
20
21
22class Validator():
23 """ Sanitises/checks strings, to prevent Cypher injection attacks"""
24
25 @staticmethod
26 def validate(input_):
27 """
28 Checks whether we can allow a string to be passed into a Cypher query.
29 :param input_: the string we wish to validate
30 :return: bool(the string is allowed)
31 """
32 regex = re.compile(r'^[A-Za-z0-9\-:\.\$_@\*\(\)%\+,]+$')
33 return bool(regex.match(input_))
34
35 @staticmethod
36 def sanitise(input_):
37 """
38 Strips harmful characters from the given string.
39 :param input_: string to sanitise
40 :return: the sanitised string
41 """
42 return re.sub(r'[^\.\-_a-zA-Z0-9]+', '', input_)
43
44
45class AddToDatabase():
46 """Updates the database, adding functions/calls to a given program"""
47
48 def __init__(self, program_name='', sextant_connection=None):
49 """
50 Object which can be used to add functions and calls to a new program
51 :param program_name: the name of the new program to be created
52 (must already be validated against Validator)
53 :param sextant_connection: the SextantConnection to use for connections
54 """
55 # program_name must be alphanumeric, to avoid injection attacks easily
56 if not Validator.validate(program_name):
57 return
58
59 self.program_name = program_name
60 self.parent_database_connection = sextant_connection
61 self._functions = {}
62 self._new_tx = None
63
64 if self.parent_database_connection:
65 # we'll locally use db for short
66 db = self.parent_database_connection._db
67
68 parent_function = db.nodes.create(name=program_name, type='program')
69 self._parent_id = parent_function.id
70
71 self._new_tx = db.transaction(using_globals=False, for_query=True)
72
73 self._connections = []
74
75 def add_function(self, function_name):
76 """
77 Adds a function to the program, ready to be sent to the remote database.
78 If the function name is already in use, this method effectively does
79 nothing and returns True.
80
81 :param function_name: a string which must be alphanumeric
82 :return: True if the request succeeded, False otherwise
83 """
84 if not Validator.validate(function_name):
85 return False
86 if self.class_contains_function(function_name):
87 return True
88
89 if function_name[-4:] == "@plt":
90 display_name = function_name[:-4]
91 function_group = "plt_stub"
92 elif function_name[:20] == "_._function_pointer_":
93 display_name = function_name
94 function_group = "function_pointer"
95 else:
96 display_name = function_name
97 function_group = "normal"
98
99 query = ('START n = node({}) '
100 'CREATE (n)-[:subject]->(m:func {{type: "{}", name: "{}"}})')
101 query = query.format(self._parent_id, function_group, display_name)
102
103 self._new_tx.append(query)
104
105 self._functions[function_name] = function_name
106
107 return True
108
109 def class_contains_function(self, function_to_find):
110 """
111 Checks whether we contain a function with a given name.
112 :param function_to_find: string name of the function we wish to look up
113 :return: bool(the function exists in this AddToDatabase)
114 """
115 return function_to_find in self._functions
116
117 def class_contains_call(self, function_calling, function_called):
118 """
119 Checks whether we contain a call between the two named functions.
120 :param function_calling: string name of the calling-function
121 :param function_called: string name of the called function
122 :return: bool(function_calling calls function_called in us)
123 """
124 return (function_calling, function_called) in self._connections
125
126 def add_function_call(self, fn_calling, fn_called):
127 """
128 Adds a function call to the program, ready to be sent to the database.
129 Effectively does nothing if there is already a function call between
130 these two functions.
131 Function names must be alphanumeric for easy security purposes;
132 returns False if they fail validation.
133 :param fn_calling: the name of the calling-function as a string.
134 It should already exist in the AddToDatabase; if it does not,
135 this method will create a stub for it.
136 :param fn_called: name of the function called by fn_calling.
137 If it does not exist, we create a stub representation for it.
138 :return: True if successful, False otherwise
139 """
140 if not all((Validator.validate(fn_calling),
141 Validator.validate(fn_called))):
142 return False
143
144 if not self.class_contains_function(fn_called):
145 self.add_function(fn_called)
146 if not self.class_contains_function(fn_calling):
147 self.add_function(fn_calling)
148
149 if not self.class_contains_call(fn_calling, fn_called):
150 query = ('START p = node({}) '
151 'MATCH (p)-[:subject]->(n) WHERE n.name = "{}" '
152 'MATCH (p)-[:subject]->(m) WHERE m.name = "{}" '
153 'CREATE (n)-[:calls]->(m)')
154 query = query.format(self._parent_id, fn_calling, fn_called)
155 self._new_tx.append(query)
156
157 self._connections.append((fn_calling, fn_called))
158
159 return True
160
161 def commit(self):
162 """
163 Call this when you are finished with the object.
164 Changes are not synced to the remote database until this is called.
165 """
166 self._new_tx.commit()
167
168
169class FunctionQueryResult:
170 """A graph of function calls arising as the result of a Neo4J query."""
171
172 def __init__(self, parent_db, program_name='', rest_output=None):
173 self.program_name = program_name
174 self._parent_db_connection = parent_db
175 self.functions = self._rest_node_output_to_graph(rest_output)
176 self._update_common_functions()
177
178 def __eq__(self, other):
179 # we make a dictionary so that we can perform easy comparison
180 selfdict = {func.name: func for func in self.functions}
181 otherdict = {func.name: func for func in other.functions}
182
183 return self.program_name == other.program_name and selfdict == otherdict
184
185 def _update_common_functions(self):
186 """
187 Loop over all functions: increment the called-by count of their callees.
188 """
189 for func in self.functions:
190 for called in func.functions_i_call:
191 called.number_calling_me += 1
192
193 def _rest_node_output_to_graph(self, rest_output):
194 """
195 Convert the output of a REST API query into our internal representation.
196 :param rest_output: output of the REST call as a Neo4j QuerySequence
197 :return: iterable of <Function>s ready to initialise self.functions.
198 """
199
200 if rest_output is None or not rest_output.elements:
201 return []
202
203 # how we store this is: a dict
204 # with keys 'functionname'
205 # and values [the function object we will use,
206 # and a set of (function names this function calls),
207 # and numeric ID of this node in the Neo4J database]
208
209 result = {}
210
211 # initial pass for names of functions
212
213 # if the following assertion failed, we've probably called db.query
214 # to get it to not return client.Node objects, which is wrong.
215 # we attempt to handle this a bit later; this should never arise, but
216 # we can cope with it happening in some cases, like the test suite
217
218 if type(rest_output.elements) is not list:
219 logging.warning('Not a list: {}'.format(type(rest_output.elements)))
220
221 for node_list in rest_output.elements:
222 assert(isinstance(node_list, list))
223 for node in node_list:
224 if isinstance(node, client.Node):
225 name = node.properties['name']
226 node_id = node.id
227 node_type = node.properties['type']
228 else: # this is the handling we mentioned earlier;
229 # we are a dictionary instead of a list, as for some
230 # reason we've returned Raw rather than Node data.
231 # We should never reach this code, but just in case.
232 name = node['data']['name']
233 # hacky workaround to get the id
234 node_id = node['self'].split('/')[-1]
235 node_type = node['data']['type']
236
237 result[name] = [Function(self.program_name,
238 function_name=name,
239 function_type=node_type),
240 set(),
241 node_id]
242
243 # end initialisation of names-dictionary
244
245 if self._parent_db_connection is not None:
246 # This is the normal case, of extracting results from a server.
247 # We leave the other case in because it is useful for unit testing.
248
249 # We collect the name-name pairs of caller-callee, batched for speed
250 new_tx = self._parent_db_connection.transaction(using_globals=False,
251 for_query=True)
252 for index in result:
253 q = ("START n=node({})"
254 "MATCH n-[calls:calls]->(m)"
255 "RETURN n.name, m.name").format(result[index][2])
256 new_tx.append(q)
257
258 logging.debug('exec')
259 results = new_tx.execute()
260
261 # results is a list of query results, each of those being a list of
262 # calls.
263
264 for call_list in results:
265 if call_list:
266 # call_list has element 0 being an arbitrary call this
267 # function makes; element 0 of that call is the name of the
268 # function itself. Think {{'orig', 'b'}, {'orig', 'c'}}.
269 orig = call_list[0][0]
270 # result['orig'] is [<Function>, ('callee1','callee2')]
271 result[orig][1] |= set(list(zip(*call_list.elements))[1])
272 # recall: set union is denoted by |
273
274 else:
275 # we don't have a parent database connection.
276 # This has probably arisen because we created this object from a
277 # test suite, or something like that.
278 for node in rest_output.elements:
279 node_name = node[0].properties['name']
280 result[node_name][1] |= {relationship.end.properties['name']
281 for relationship in node[0].relationships.outgoing()}
282
283 logging.debug('Relationships complete.')
284
285 # named_function takes a function name and returns the Function object
286 # with that name, or None if none exists.
287 named_function = lambda name: result[name][0] if name in result else None
288
289 for function, calls, node_id in result.values():
290 what_i_call = [named_function(name)
291 for name in calls
292 if named_function(name) is not None]
293 function.functions_i_call = what_i_call
294
295 return [list_element[0]
296 for list_element in result.values()
297 if list_element[0]]
298
299 def get_functions(self):
300 """
301 :return: a list of Function objects present in the query result
302 """
303 return self.functions
304
305 def get_function(self, name):
306 """
307 Given a function name, returns the Function object which has that name.
308 If no function with that name exists, returns None.
309 """
310 func_list = [func for func in self.functions if func.name == name]
311 return None if len(func_list) == 0 else func_list[0]
312
313
314def set_common_cutoff(common_def):
315 """
316 Sets the number of incoming connections at which we deem a function 'common'
317 Default is 10 (which is used if this method is never called).
318 :param common_def: number of incoming connections
319 """
320 global COMMON_CUTOFF
321 COMMON_CUTOFF = common_def
322
323
324class Function(object):
325 """Represents a function which might appear in a FunctionQueryResult."""
326
327 def __eq__(self, other):
328 funcs_i_call_list = {func.name for func in self.functions_i_call}
329 funcs_other_calls_list = {func.name for func in other.functions_i_call}
330
331 return (self.parent_program == other.parent_program
332 and self.name == other.name
333 and funcs_i_call_list == funcs_other_calls_list
334 and self.attributes == other.attributes)
335
336 @property
337 def number_calling_me(self):
338 return self._number_calling_me
339
340 @number_calling_me.setter
341 def number_calling_me(self, value):
342 self._number_calling_me = value
343 self.is_common = (self._number_calling_me > COMMON_CUTOFF)
344
345 def __init__(self, program_name='', function_name='', function_type=''):
346 self.parent_program = program_name
347 self.attributes = []
348 self.type = function_type
349 self.functions_i_call = []
350 self.name = function_name
351 self.is_common = False
352 self._number_calling_me = 0
353 # care: _number_calling_me is not automatically updated, except by
354 # any invocation of FunctionQueryResult._update_common_functions.
355
356
357class SextantConnection:
358 """
359 RESTful connection to a remote database.
360 It can be used to create/delete/query programs.
361 """
362
363 def __init__(self, url):
364 self.url = url
365 self._db = GraphDatabase(url)
366
367 def new_program(self, name_of_program):
368 """
369 Request that the remote database create a new program with the given name.
370 This procedure will create a new program remotely; you can manipulate
371 that program using the returned AddToDatabase object.
372 The name can appear in the database already, but this is not recommended
373 because then delete_program will not know which to delete. Check first
374 using self.check_program_exists.
375 The name specified must pass Validator.validate()ion; this is a measure
376 to prevent Cypher injection attacks.
377 :param name_of_program: string program name
378 :return: AddToDatabase instance if successful
379 """
380
381 if not Validator.validate(name_of_program):
382 raise ValueError(
383 "{} is not a valid program name".format(name_of_program))
384
385 return AddToDatabase(sextant_connection=self,
386 program_name=name_of_program)
387
388 def delete_program(self, name_of_program):
389 """
390 Request that the remote database delete a specified program.
391 :param name_of_program: a string which must be alphanumeric only
392 :return: bool(request succeeded)
393 """
394 if not Validator.validate(name_of_program):
395 return False
396
397 q = """MATCH (n) WHERE n.name= "{}" AND n.type="program"
398 OPTIONAL MATCH (n)-[r]-(b) OPTIONAL MATCH (b)-[rel]-()
399 DELETE b,rel DELETE n, r""".format(name_of_program)
400
401 self._db.query(q)
402
403 return True
404
405 def _execute_query(self, prog_name='', query=''):
406 """
407 Executes a Cypher query against the remote database.
408 Note that this returns a FunctionQueryResult, so is unsuitable for any
409 other expected outputs (such as lists of names). For those instances,
410 it is better to run self._parent_database_connection_object.query
411 explicitly.
412 Intended only to be used for non-updating queries
413 (such as "get functions" rather than "create").
414 :param prog_name: name of the program the result object will reflect
415 :param query: verbatim query we wish the server to execute
416 :return: a FunctionQueryResult corresponding to the server's output
417 """
418 rest_output = self._db.query(query, returns=client.Node)
419
420 return FunctionQueryResult(parent_db=self._db,
421 program_name=prog_name,
422 rest_output=rest_output)
423
424 def get_program_names(self):
425 """
426 Execute query to retrieve a list of all programs in the database.
427 Any name in this list can be used verbatim in any SextantConnection
428 method which requires a program-name input.
429 :return: a list of function-name strings.
430 """
431 q = """MATCH (n) WHERE n.type = "program" RETURN n.name"""
432 program_names = self._db.query(q, returns=str).elements
433
434 result = [el[0] for el in program_names]
435
436 return set(result)
437
438 def check_program_exists(self, program_name):
439 """
440 Execute query to check whether a program with the given name exists.
441 Returns False if the program_name fails validation against Validator.
442 :return: bool(the program exists in the database).
443 """
444
445 if not Validator.validate(program_name):
446 return False
447
448 q = ("MATCH (base) WHERE base.name = '{}' AND base.type = 'program' "
449 "RETURN count(base)").format(program_name)
450
451 result = self._db.query(q, returns=int)
452 return result.elements[0][0] > 0
453
454 def check_function_exists(self, program_name, function_name):
455 """
456 Execute query to check whether a function with the given name exists.
457 We only check for functions which are children of a program with the
458 given program_name.
459 :param program_name: string name of the program within which to check
460 :param function_name: string name of the function to check for existence
461 :return: bool(names validate correctly, and function exists in program)
462 """
463 if not self.check_program_exists(program_name):
464 return False
465
466 if not Validator.validate(program_name):
467 return False
468
469 q = ("MATCH (base) WHERE base.name = '{}' AND base.type = 'program'"
470 "MATCH (base)-[r:subject]->(m) WHERE m.name = '{}'"
471 "RETURN count(m)").format(program_name, function_name)
472
473 result = self._db.query(q, returns=int)
474 return result.elements[0][0] > 0
475
476 def get_function_names(self, program_name):
477 """
478 Execute query to retrieve a list of all functions in the program.
479 Any of the output names can be used verbatim in any SextantConnection
480 method which requires a function-name input.
481 :param program_name: name of the program whose functions to retrieve
482 :return: None if program_name doesn't exist in the remote database,
483 a set of function-name strings otherwise.
484 """
485
486 if not self.check_program_exists(program_name):
487 return None
488
489 q = ("MATCH (base) WHERE base.name = '{}' AND base.type = 'program' "
490 "MATCH (base)-[r:subject]->(m) "
491 "RETURN m.name").format(program_name)
492 return {func[0] for func in self._db.query(q)}
493
494 def get_all_functions_called(self, program_name, function_calling):
495 """
496 Execute query to find all functions called by a function (indirectly).
497 If the given function is not present in the program, returns None;
498 likewise if the program_name does not exist.
499 :param program_name: a string name of the program we wish to query under
500 :param function_calling: string name of a function whose children to find
501 :return: FunctionQueryResult, maximal subgraph rooted at function_calling
502 """
503
504 if not self.check_program_exists(program_name):
505 return None
506
507 if not self.check_function_exists(program_name, function_calling):
508 return None
509
510 q = """MATCH (base) WHERE base.name = '{}' ANd base.type = 'program'
511 MATCH (base)-[:subject]->(m) WHERE m.name='{}'
512 MATCH (m)-[:calls*]->(n)
513 RETURN distinct n, m""".format(program_name, function_calling)
514
515 return self._execute_query(program_name, q)
516
517 def get_all_functions_calling(self, program_name, function_called):
518 """
519 Execute query to find all functions which call a function (indirectly).
520 If the given function is not present in the program, returns None;
521 likewise if the program_name does not exist.
522 :param program_name: a string name of the program we wish to query
523 :param function_called: string name of a function whose parents to find
524 :return: FunctionQueryResult, maximal connected subgraph with leaf function_called
525 """
526
527 if not self.check_program_exists(program_name):
528 return None
529
530 if not self.check_function_exists(program_name, function_called):
531 return None
532
533 q = """MATCH (base) WHERE base.name = '{}' AND base.type = 'program'
534 MATCH (base)-[r:subject]->(m) WHERE m.name='{}'
535 MATCH (n)-[:calls*]->(m) WHERE n.name <> '{}'
536 RETURN distinct n , m"""
537 q = q.format(program_name, function_called, program_name)
538
539 return self._execute_query(program_name, q)
540
541 def get_call_paths(self, program_name, function_calling, function_called):
542 """
543 Execute query to find all possible routes between two specific nodes.
544 If the given functions are not present in the program, returns None;
545 ditto if the program_name does not exist.
546 :param program_name: string program name
547 :param function_calling: string
548 :param function_called: string
549 :return: FunctionQueryResult, the union of all subgraphs reachable by
550 adding a source at function_calling and a sink at function_called.
551 """
552
553 if not self.check_program_exists(program_name):
554 return None
555
556 if not self.check_function_exists(program_name, function_called):
557 return None
558
559 if not self.check_function_exists(program_name, function_calling):
560 return None
561
562 q = r"""MATCH (pr) WHERE pr.name = '{}' AND pr.type = 'program'
563 MATCH p=(start {{name: "{}" }})-[:calls*]->(end {{name:"{}"}})
564 WHERE (pr)-[:subject]->(start)
565 WITH DISTINCT nodes(p) AS result
566 UNWIND result AS answer
567 RETURN answer"""
568 q = q.format(program_name, function_calling, function_called)
569
570 return self._execute_query(program_name, q)
571
572 def get_whole_program(self, program_name):
573 """Execute query to find the entire program with a given name.
574 If the program is not present in the remote database, returns None.
575 :param: program_name: a string name of the program we wish to return.
576 :return: a FunctionQueryResult consisting of the program graph.
577 """
578
579 if not self.check_program_exists(program_name):
580 return None
581
582 query = """MATCH (base) WHERE base.name = '{}' AND base.type = 'program'
583 MATCH (base)-[subject:subject]->(m)
584 RETURN DISTINCT (m)""".format(program_name)
585
586 return self._execute_query(program_name, query)
587
588 def get_shortest_path_between_functions(self, program_name, func1, func2):
589 """
590 Execute query to get a single, shortest, path between two functions.
591 :param program_name: string name of the program we wish to search under
592 :param func1: the name of the originating function of our shortest path
593 :param func2: the name of the function at which to terminate the path
594 :return: FunctionQueryResult shortest path between func1 and func2.
595 """
596 if not self.check_program_exists(program_name):
597 return None
598
599 if not self.check_function_exists(program_name, func1):
600 return None
601
602 if not self.check_function_exists(program_name, func2):
603 return None
604
605 q = """MATCH (func1 {{ name:"{}" }}),(func2 {{ name:"{}" }}),
606 p = shortestPath((func1)-[:calls*]->(func2))
607 UNWIND nodes(p) AS ans
608 RETURN ans""".format(func1, func2)
609
610 return self._execute_query(program_name, q)
0611
=== added file 'src/sextant/errors.py'
--- src/sextant/errors.py 1970-01-01 00:00:00 +0000
+++ src/sextant/errors.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,20 @@
1# -----------------------------------------------------------------------------
2# errors.py -- Sextant error definitions
3#
4# August 2014, Phil Connell
5#
6# Copyright 2014, Ensoft Ltd.
7# -----------------------------------------------------------------------------
8
9from __future__ import absolute_import, print_function
10
11__all__ = (
12 "MissingDependencyError",
13)
14
15
16class MissingDependencyError(Exception):
17 """
18 Raised if trying an operation for which an optional dependency is missing.
19 """
20
021
=== added file 'src/sextant/export.py'
--- src/sextant/export.py 1970-01-01 00:00:00 +0000
+++ src/sextant/export.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,152 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: Patrick Stevens, James Harkin
5# -----------------------------------------
6# Convert a program from internal Python representation to various output formats
7
8__all__ = "ProgramConverter"
9
10
11class ProgramConverter:
12 # Given our internal program representation, converts it to output formats.
13 # Currently supported: GraphML
14
15 @staticmethod
16 def get_supported_outputs():
17 return ['graphml', 'yed_graphml', 'dot']
18
19 @staticmethod
20 def get_display_name(function, suppress_common_nodes=False):
21 """
22 Given a Function object, retrieve the label we attach to it.
23 For instance, function-pointers are labelled "(function pointer)",
24 while the `main` function is usually labelled "main".
25 """
26 if function.type == "function_pointer":
27 name = "(function pointer)"
28 else:
29 name = function.name
30
31 if suppress_common_nodes and function.is_common:
32 name += ' (common)'
33 return name
34
35 @staticmethod
36 def to_dot(program, suppress_common_nodes=False):
37 """
38 Convert the program to DOT output format.
39 """
40 output_str = 'digraph "{}" {{\n '.format(program.program_name)
41 output_str += 'overlap=false; \n'
42
43 font_name = "helvetica"
44
45 for func in program.get_functions():
46 if func.type == "plt_stub":
47 output_str += ' "{}" [fillcolor=pink, style=filled]\n'.format(func.name)
48 elif func.type == "function_pointer":
49 output_str += ' "{}" [fillcolor=yellow, style=filled]\n'.format(func.name)
50
51 # in all cases, even if we've specified that we want a filled-in
52 # node already, DOT lets us add more information about that node
53 # so we can insist on turning that same node into a box-shape
54 # and changing its font.
55 output_str += ' "{}" [label="{}", fontname="{}", shape=box]\n'.format(func.name,
56 ProgramConverter.get_display_name(func, suppress_common_nodes),
57 font_name)
58 if func.is_common:
59 output_str += ' "{}" [fillcolor=lightgreen, style=filled]\n'.format(func.name)
60
61 for func_called in func.functions_i_call:
62 if not (suppress_common_nodes and func_called.is_common):
63 output_str += ' "{}" -> "{}"\n'.format(func.name, func_called.name)
64
65 output_str += '}'
66 return output_str
67
68
69
70 @staticmethod
71 def to_yed_graphml(program, suppress_common_nodes=False):
72 commonly_called = []
73 output_str = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
74<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" \
75xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" \
76xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
77"""
78 output_str += """<key for="graphml" id="d0" yfiles.type="resources"/>
79 <key for="port" id="d1" yfiles.type="portgraphics"/>
80 <key for="port" id="d2" yfiles.type="portgeometry"/>
81 <key for="port" id="d3" yfiles.type="portuserdata"/>
82 <key attr.name="url" attr.type="string" for="node" id="d4"/>
83 <key attr.name="description" attr.type="string" for="node" id="d5"/>
84 <key for="node" id="d6" yfiles.type="nodegraphics"/>
85 <key attr.name="url" attr.type="string" for="edge" id="d7"/>
86 <key attr.name="description" attr.type="string" for="edge" id="d8"/>
87 <key for="edge" id="d9" yfiles.type="edgegraphics"/>\n"""
88
89 output_str += """<graph id="{}" edgedefault="directed">\n""".format(program.program_name)
90
91 for func in program.get_functions():
92 display_func = ProgramConverter.get_display_name(func)
93 if func.type == "plt_stub":
94 colour = "#ff00ff"
95 elif func.type == "function_pointer":
96 colour = "#99ffff"
97 elif func.is_common:
98 colour = "#00FF00"
99 else:
100 colour = "#ffcc00"
101 output_str += """<node id="{}">
102 <data key="d6">
103 <y:ShapeNode>
104 <y:Geometry height="{}" width="{}" x="60.0" y="0.0"/>
105 <y:Fill color="{}" transparent="false"/>
106 <y:BorderStyle color="#000000" type="line" width="1.0"/>
107 <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog"
108 fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false"
109 height="18.701171875" modelName="custom" textColor="#000000" visible="true"
110 width="36.6953125" x="-3.34765625" y="5.6494140625">{}<y:LabelModel>
111 <y:SmartNodeLabelModel distance="4.0"/>
112 </y:LabelModel>
113 <y:ModelParameter>
114 <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0"
115 nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0"
116 upY="-1.0"/>
117 </y:ModelParameter>
118 </y:NodeLabel>
119 <y:Shape type="rectangle"/>
120 </y:ShapeNode>
121 </data>
122 </node>\n""".format(func.name, 20, len(display_func)*8, colour, display_func)
123 for callee in func.functions_i_call:
124 if callee not in commonly_called:
125 if not(suppress_common_nodes and callee.is_common):
126 output_str += """<edge source="{}" target="{}"> <data key="d9">
127 <y:PolyLineEdge>
128 <y:LineStyle color="#000000" type="line" width="1.0"/>
129 <y:Arrows source="none" target="standard"/>
130 <y:BendStyle smoothed="false"/>
131 </y:PolyLineEdge>
132 </data>
133 </edge>\n""".format(func.name, callee.name)
134
135 output_str += '</graph>\n<data key="d0"> <y:Resources/> </data>\n</graphml>'
136 return output_str
137
138 @staticmethod
139 def to_graphml(program):
140 output_str = """<?xml version="1.0" encoding="UTF-8"?>
141<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" \
142xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
143"""
144 output_str += '<graph id="{}" edgedefault="directed">\n'.format(program.program_name)
145
146 for func in program.get_functions():
147 output_str += '<node id="{}"> <data key="name">{}</data> </node>\n'.format(func.name, ProgramConverter.get_display_name(func))
148 for callee in func.functions_i_call:
149 output_str += '<edge source="{}" target="{}"> <data key="calls">1</data> </edge>\n'.format(func.name, callee.name)
150
151 output_str += '</graph>\n</graphml>'
152 return output_str
0\ No newline at end of file153\ No newline at end of file
1154
=== added file 'src/sextant/objdump_parser.py'
--- src/sextant/objdump_parser.py 1970-01-01 00:00:00 +0000
+++ src/sextant/objdump_parser.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,272 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: Patrick Stevens
5# -----------------------------------------
6
7#!/usr/bin/python3
8
9import re
10import argparse
11import os.path
12import subprocess
13import logging
14
15
16class ParsedObject():
17 """
18 Represents a function as parsed from an objdump disassembly.
19 Has a name (which is the verbatim name like '__libc_start_main@plt'),
20 a position (which is the virtual memory location in hex, like '08048320'
21 extracted from the dump),
22 and a canonical_position (which is the virtual memory location in hex
23 but stripped of leading 0s, so it should be a
24 unique id).
25 It also has a list what_do_i_call of ParsedObjects it calls using the
26 assembly keyword 'call'.
27 It has a list original_code of its assembler code, too, in case it's useful.
28 """
29
30 @staticmethod
31 def get_canonical_position(position):
32 return position.lstrip('0')
33
34 def __eq__(self, other):
35 return self.name == other.name
36
37 def __init__(self, input_lines=None, assembler_section='', function_name='',
38 ignore_function_pointers=True, function_pointer_id=None):
39 """
40 Create a new ParsedObject given the definition-lines from objdump -S.
41 A sample first definition-line is '08048300 <__gmon_start__@plt>:\n'
42 but this method
43 expects to see the entire definition eg
44
45080482f0 <puts@plt>:
46 80482f0: ff 25 00 a0 04 08 jmp *0x804a000
47 80482f6: 68 00 00 00 00 push $0x0
48 80482fb: e9 e0 ff ff ff jmp 80482e0 <_init+0x30>
49
50 We also might expect assembler_section, which is for instance '.init'
51 in 'Disassembly of section .init:'
52 function_name is used if we want to give this function a custom name.
53 ignore_function_pointers=True will pretend that calls to (eg) *eax do
54 not exist; setting to False makes us create stubs for those calls.
55 function_pointer_id is only used internally; it refers to labelling
56 of function pointers if ignore_function_pointers is False. Each
57 stub is given a unique numeric ID: this parameter tells init where
58 to start counting these IDs from.
59
60 """
61 if input_lines is None:
62 # get around Python's inability to pass in empty lists by value
63 input_lines = []
64
65 self.name = function_name or re.search(r'<.+>', input_lines[0]).group(0).strip('<>')
66 self.what_do_i_call = []
67 self.position = ''
68
69 if input_lines:
70 self.position = re.search(r'^[0-9a-f]+', input_lines[0]).group(0)
71 self.canonical_position = ParsedObject.get_canonical_position(self.position)
72 self.assembler_section = assembler_section
73 self.original_code = input_lines[1:]
74
75 call_regex_compiled = (ignore_function_pointers and re.compile(r'\tcall. +[^\*]+\n')) or re.compile(r'\tcall. +.+\n')
76
77 lines_where_i_call = [line for line in input_lines if call_regex_compiled.search(line)]
78
79 if not ignore_function_pointers and not function_pointer_id:
80 function_pointer_id = [1]
81
82 for line in lines_where_i_call:
83 # we'll catch call and callq for the moment
84 called = (call_regex_compiled.search(line).group(0))[8:].lstrip(' ').rstrip('\n')
85 if called[0] == '*' and ignore_function_pointers == False:
86 # we have a function pointer, which we'll want to give a distinct name
87 address = '0'
88 name = '_._function_pointer_' + str(function_pointer_id[0])
89 function_pointer_id[0] += 1
90
91 self.what_do_i_call.append((address, name))
92
93 else: # we're not on a function pointer
94 called_split = called.split(' ')
95 if len(called_split) == 2:
96 address, name = called_split
97 name = name.strip('<>')
98 # we still want to remove address offsets like +0x09 from the end of name
99 match = re.match(r'^.+(?=\+0x[a-f0-9]+$)', name)
100 if match is not None:
101 name = match.group(0)
102 self.what_do_i_call.append((address, name.strip('<>')))
103 else: # the format of the "what do i call" is not recognised as a name/address pair
104 self.what_do_i_call.append(tuple(called_split))
105
106 def __str__(self):
107 if self.position:
108 return 'Memory address ' + self.position + ' with name ' + self.name + ' in section ' + str(
109 self.assembler_section)
110 else:
111 return 'Name ' + self.name
112
113 def __repr__(self):
114 out_str = 'Disassembly of section ' + self.assembler_section + ':\n\n' + self.position + ' ' + self.name + ':\n'
115 return out_str + '\n'.join([' ' + line for line in self.original_code])
116
117
118class Parser:
119 # Class to manipulate the output of objdump
120
121 def __init__(self, input_file_location='', file_contents=None, sections_to_view=None, ignore_function_pointers=False):
122 """Creates a new Parser, given an input file path. That path should be an output from objdump -D.
123 Alternatively, supply file_contents, as a list of each line of the objdump output. We expect newlines
124 to have been stripped from the end of each of these lines.
125 sections_to_view makes sure we only use the specified sections (use [] for 'all sections' and None for none).
126 """
127 if file_contents is None:
128 file_contents = []
129
130 if sections_to_view is None:
131 sections_to_view = []
132
133 if input_file_location:
134 file_to_read = open(input_file_location, 'r')
135 self.source_string_list = [line for line in file_to_read]
136 file_to_read.close()
137 elif file_contents:
138 self.source_string_list = [string + '\n' for string in file_contents]
139 self.parsed_objects = []
140 self.sections_to_view = sections_to_view
141 self.ignore_function_pointers = ignore_function_pointers
142 self.pointer_identifier = [1]
143
144 def create_objects(self):
145 """ Go through the source_string_list, getting object names (like 'main') along with the corresponding
146 definitions, and put them into parsed_objects """
147 if self.sections_to_view is None:
148 return
149
150 is_in_section = lambda name: self.sections_to_view == [] or name in self.sections_to_view
151
152 parsed_objects = []
153 current_object = []
154 current_section = ''
155 regex_compiled_addr_and_name = re.compile(r'[0-9a-f]+ <.+>:\n')
156 regex_compiled_section = re.compile(r'section .+:\n')
157
158 for line in self.source_string_list[4:]: # we bodge, since the file starts with a little bit of guff
159 if regex_compiled_addr_and_name.match(line):
160 # we are a starting line
161 current_object = [line]
162 elif re.match(r'Disassembly of section', line):
163 current_section = regex_compiled_section.search(line).group(0).lstrip('section ').rstrip(':\n')
164 current_object = []
165 elif line == '\n':
166 # we now need to stop parsing the current block, and store it
167 if len(current_object) > 0 and is_in_section(current_section):
168 parsed_objects.append(ParsedObject(input_lines=current_object, assembler_section=current_section,
169 ignore_function_pointers=self.ignore_function_pointers,
170 function_pointer_id=self.pointer_identifier))
171 else:
172 current_object.append(line)
173
174 # now we should be done. We assumed that blocks begin with r'[0-9a-f]+ <.+>:\n' and end with a newline.
175 # clear duplicates:
176
177 self.parsed_objects = []
178 for obj in parsed_objects:
179 if obj not in self.parsed_objects: # this is so that if we jump into the function at an offset,
180 # we still register it as being the old function, not some new function at a different address
181 # with the same name
182 self.parsed_objects.append(obj)
183
184 # by this point, each object contains a self.what_do_i_call which is a list of tuples
185 # ('address', 'name') if the address and name were recognised, or else (thing1, thing2, ...)
186 # where the instruction was call thing1 thing2 thing3... .
187
188 def object_lookup(self, object_name='', object_address=''):
189 """Returns the object with name object_name or address object_address (at least one must be given).
190 If objects with the given name or address
191 are not found, returns None."""
192
193 if object_name == '' and object_address == '':
194 return None
195
196 trial_obj = self.parsed_objects
197
198 if object_name != '':
199 trial_obj = [obj for obj in trial_obj if obj.name == object_name]
200
201 if object_address != '':
202 trial_obj = [obj for obj in trial_obj if
203 obj.canonical_position == ParsedObject.get_canonical_position(object_address)]
204
205 if len(trial_obj) == 0:
206 return None
207
208 return trial_obj
209
210def get_parsed_objects(filepath, sections_to_view, not_object_file, readable=False, ignore_function_pointers=False):
211 if sections_to_view is None:
212 sections_to_view = [] # because we use None for "no sections"; the intent of not providing any sections
213 # on the command line was to look at all sections, not none
214
215 # first, check whether the given file exists
216 if not os.path.isfile(filepath):
217 logging.error('Input file does not exist')
218 return False
219
220 #now the file should exist
221 if not not_object_file: #if it is something we need to run through objdump first
222 #we need first to run the object file through objdump
223
224 objdump_file_contents = subprocess.check_output(['objdump', '-D', filepath])
225 objdump_str = objdump_file_contents.decode('utf-8')
226
227 p = Parser(file_contents=objdump_str.split('\n'), sections_to_view=sections_to_view, ignore_function_pointers=ignore_function_pointers)
228 else:
229 try:
230 p = Parser(input_file_location=filepath, sections_to_view=sections_to_view, ignore_function_pointers=ignore_function_pointers)
231 except UnicodeDecodeError:
232 logging.error('File could not be parsed as a string. Did you mean to supply --object-file?')
233 return False
234
235 if readable: # if we're being called from the command line
236 print('File read; beginning parse.')
237 #file is now read, and we start parsing
238
239 p.create_objects()
240 return p.parsed_objects
241
242def main():
243 argumentparser = argparse.ArgumentParser(description="Parse the output of objdump.")
244 argumentparser.add_argument('--filepath', metavar="FILEPATH", help="path to input file", type=str, nargs=1)
245 argumentparser.add_argument('--not-object-file', help="import text objdump output instead of the compiled file", default=False,
246 action='store_true')
247 argumentparser.add_argument('--sections-to-view', metavar="SECTIONS",
248 help="sections of disassembly to view, like '.text'; leave blank for 'all'",
249 type=str, nargs='*')
250 argumentparser.add_argument('--ignore-function-pointers', help='whether to skip parsing calls to function pointers', action='store_true', default=False)
251
252 parsed = argumentparser.parse_args()
253
254 filepath = parsed.filepath[0]
255 sections_to_view = parsed.sections_to_view
256 not_object_file = parsed.not_object_file
257 readable = True
258 function_pointers = parsed.ignore_function_pointers
259
260 parsed_objs = get_parsed_objects(filepath, sections_to_view, not_object_file, readable, function_pointers)
261 if parsed_objs is False:
262 return 1
263
264 if readable:
265 for named_function in parsed_objs:
266 print(named_function.name)
267 print([f[-1] for f in named_function.what_do_i_call]) # use [-1] to get the last element, since:
268 #either we are in ('address', 'name'), when we want the last element, or else we are in (thing1, thing2, ...)
269 #so for the sake of argument we'll take the last thing
270
271if __name__ == "__main__":
272 main()
0273
=== added file 'src/sextant/pyinput.py'
--- src/sextant/pyinput.py 1970-01-01 00:00:00 +0000
+++ src/sextant/pyinput.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,180 @@
1# -----------------------------------------------------------------------------
2# pyinput.py -- Input information from Python programs.
3#
4# August 2014, Phil Connell
5#
6# Copyright 2014, Ensoft Ltd.
7# -----------------------------------------------------------------------------
8
9from __future__ import absolute_import, print_function
10
11__all__ = (
12 "trace",
13)
14
15
16import contextlib
17import sys
18
19from . import errors
20from . import db_api
21
22
23# Optional, should be checked at API entrypoints requiring entrails (and
24# yes, the handling is a bit fugly).
25try:
26 import entrails
27except ImportError:
28 _entrails_available = False
29 class entrails:
30 EntrailsOutput = object
31else:
32 _entrails_available = True
33
34
35class _SextantOutput(entrails.EntrailsOutput):
36 """Record calls traced by entrails in a sextant database."""
37
38 # Internal attributes:
39 #
40 # _conn:
41 # Sextant connection.
42 # _fns:
43 # Stack of function names (implemented as a list), reflecting the current
44 # call stack, based on enter, exception and exit events.
45 # _prog:
46 # Sextant program representation.
47 _conn = None
48 _fns = None
49 _prog = None
50
51 def __init__(self, conn, program_name):
52 """
53 Initialise this output.
54
55 conn:
56 Connection to the Sextant database.
57 program_name:
58 String used to refer to the traced program in sextant.
59
60 """
61 self._conn = conn
62 self._fns = []
63 self._prog = self._conn.new_program(program_name)
64 self._tracer = self._trace()
65 next(self._tracer)
66
67 def _add_frame(self, event):
68 """Add a function call to the internal stack."""
69 name = event.qualname()
70 self._fns.append(name)
71 self._prog.add_function(name)
72
73 try:
74 prev_name = self._fns[-2]
75 except IndexError:
76 pass
77 else:
78 self._prog.add_function_call(prev_name, name)
79
80 def _remove_frame(self, event):
81 """Remove a function call from the internal stack."""
82 assert event.qualname() == self._fns[-1], \
83 "Unexpected event for {}".format(event.qualname())
84 self._fns.pop()
85
86 def _handle_simple_event(self, what, event):
87 """Handle a single trace event, not needing recursive processing."""
88 handled = True
89
90 if what == "enter":
91 self._add_frame(event)
92 elif what == "exit":
93 self._remove_frame(event)
94 else:
95 handled = False
96
97 return handled
98
99 def _trace(self):
100 """Coroutine that processes trace events it's sent."""
101 while True:
102 what, event = yield
103
104 handled = self._handle_simple_event(what, event)
105 if not handled:
106 if what == "exception":
107 # An exception doesn't necessarily mean the current stack
108 # frame is exiting. Need to check whether the next event is
109 # an exception in a different stack frame, implying that
110 # the exception is propagating up the stack.
111 while True:
112 prev_event = event
113 prev_name = event.qualname()
114 what, event = yield
115 if event == "exception":
116 if event.qualname() != prev_name:
117 self._remove_frame(prev_event)
118 else:
119 handled = self._handle_simple_event(what, event)
120 assert handled
121 break
122
123 else:
124 raise NotImplementedError
125
126 def close(self):
127 self._prog.commit()
128
129 def enter(self, event):
130 self._tracer.send(("enter", event))
131
132 def exception(self, event):
133 self._tracer.send(("exception", event))
134
135 def exit(self, event):
136 self._tracer.send(("exit", event))
137
138
139# @@@ config parsing shouldn't be done in __main__ (we want to get the neo4j
140# url from there...)
141@contextlib.contextmanager
142def trace(conn, program_name=None, filters=None):
143 """
144 Context manager that records function calls in its context block.
145
146 e.g. given this code:
147
148 with sextant.trace("http://localhost:7474"):
149 foo()
150 bar()
151
152 The calls to foo() and bar() (and their callees, at any depth) will be
153 recorded in the sextant database.
154
155 conn:
156 Instance of SextantConnection that will be used to record calls.
157 program_name:
158 String used to refer to the traced program in sextant. Defaults to
159 sys.argv[0].
160 filters:
161 Optional iterable of entrails filters to apply.
162
163 """
164 if not _entrails_available:
165 raise errors.MissingDependencyError(
166 "Entrails is required to trace execution")
167
168 if program_name is None:
169 program_name = sys.argv[0]
170
171 tracer = entrails.Entrails(filters=filters)
172 tracer.add_output(_SextantOutput(conn, program_name))
173
174 tracer.start_trace()
175 try:
176 yield
177 finally:
178 # Flush traced data.
179 tracer.end_trace()
180
0181
=== added file 'src/sextant/query.py'
--- src/sextant/query.py 1970-01-01 00:00:00 +0000
+++ src/sextant/query.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,113 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: James Harkin, Patrick Stevens
5# -----------------------------------------
6#API for performing queries on the database
7
8#!/usr/bin/python3
9import argparse
10import requests, urllib # for different kinds of exception
11import logging
12
13from . import db_api
14from .export import ProgramConverter
15
16def query(remote_neo4j, input_query, program_name=None, argument_1=None, argument_2=None, suppress_common=False):
17
18 try:
19 db = db_api.SextantConnection(remote_neo4j)
20 except requests.exceptions.ConnectionError as err:
21 logging.exception("Could not connect to Neo4J server {}. Are you sure it is running?".format(remote_neo4j))
22 logging.exception(str(err))
23 return 2
24 #Not supported in python 2
25 #except (urllib.exceptions.MaxRetryError):
26 # logging.error("Connection was refused to {}. Are you sure the server is running?".format(remote_neo4j))
27 # return 2
28 except Exception as err:
29 logging.exception(str(err))
30 return 2
31
32 prog = None
33 names_list = None
34
35 if input_query == 'functions-calling':
36 if argument_1 == None:
37 print('Supply one function name to functions-calling.')
38 return 1
39 prog = db.get_all_functions_calling(program_name, argument_1)
40 elif input_query == 'functions-called-by':
41 if argument_1 == None:
42 print('Supply one function name to functions-called-by.')
43 return 1
44 prog = db.get_all_functions_called(program_name, argument_1)
45 elif input_query == 'calls-between':
46 if (argument_1 == None and argument_2 == None):
47 print('Supply two function names to calls-between.')
48 return 1
49 prog = db.get_call_paths(program_name, argument_1, argument_2)
50 elif input_query == 'whole-graph':
51 prog = db.get_whole_program(program_name)
52 elif input_query == 'shortest-path':
53 if argument_1 == None and argument_2 == None:
54 print('Supply two function names to shortest-path.')
55 return 1
56 prog = db.get_shortest_path_between_functions(program_name, argument_1, argument_2)
57 elif input_query == 'return-all-function-names':
58 if program_name != None:
59 func_names = db.get_function_names(program_name)
60 if func_names:
61 names_list = list(func_names)
62 else:
63 print('No functions were found in program %s on server %s.' % (program_name, remote_neo4j))
64 else:
65 list_of_programs = db.get_program_names()
66 if not list_of_programs:
67 print('Server %s database empty.' % (remote_neo4j))
68 return 0
69 func_list = []
70 for prog_name in list_of_programs:
71 func_list += db.get_function_names(prog_name)
72 if not func_list:
73 print('Server %s contains no functions.' % (remote_neo4j))
74 else:
75 names_list = func_list
76 elif input_query == 'return-all-program-names':
77 list_found = list(db.get_program_names())
78 if not list_found:
79 print('No programs were found on server {}.'.format(remote_neo4j))
80 else:
81 names_list = list_found
82 else:
83 print('Query unrecognised.')
84 return 2
85
86 if prog:
87 print(ProgramConverter.to_yed_graphml(prog, suppress_common))
88 elif names_list is not None:
89 print(names_list)
90 else:
91 print('Nothing was returned from the query.')
92
93
94def main():
95 argumentparser = argparse.ArgumentParser(description="Return GraphML representation or list from graph queries.")
96 argumentparser.add_argument('--remote-neo4j', required=True, metavar="URL", help="URL of neo4j server", type=str, nargs=1)
97 argumentparser.add_argument('--program-name', metavar="PROG_NAME", help="name of program as stored in the database",
98 type=str, nargs=1)
99 argumentparser.add_argument('--query', required=True, metavar="QUERY",
100 help="""functions-calling, functions-called-by, calls-between, whole-graph, shortest-path,
101 return-all-program-names or return-all-function-names; if return-all-function-names,
102 supply argument -program-name""", type=str, nargs=1)
103 argumentparser.add_argument('--funcs', metavar='FUNCS', help='functions to pass to the query', type=str, nargs='+')
104
105 parsed = argumentparser.parse_args()
106 names_list = None
107
108 query(remote_neo4j=parsed.remote_neo4j[0], input_query=parsed.query[0], arguments=parsed.funcs,
109 program_name=parsed.program_name[0])
110
111
112if __name__ == '__main__':
113 main()
0114
=== added file 'src/sextant/tests.py'
--- src/sextant/tests.py 1970-01-01 00:00:00 +0000
+++ src/sextant/tests.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,261 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: Patrick Stevens, James Harkin
5# -----------------------------------------
6#Testing module
7
8import unittest
9
10from db_api import Function
11from db_api import FunctionQueryResult
12from db_api import SextantConnection
13from db_api import Validator
14
15
16class TestFunctionQueryResults(unittest.TestCase):
17 def setUp(self):
18 # we need to set up the remote database by using the neo4j_input_api
19 self.remote_url = 'http://ensoft-sandbox:7474'
20
21 self.setter_connection = SextantConnection(self.remote_url)
22 self.program_1_name = 'testprogram'
23 self.upload_program = self.setter_connection.new_program(self.program_1_name)
24 self.upload_program.add_function('func1')
25 self.upload_program.add_function('func2')
26 self.upload_program.add_function('func3')
27 self.upload_program.add_function('func4')
28 self.upload_program.add_function('func5')
29 self.upload_program.add_function('func6')
30 self.upload_program.add_function('func7')
31 self.upload_program.add_function_call('func1', 'func2')
32 self.upload_program.add_function_call('func1', 'func4')
33 self.upload_program.add_function_call('func2', 'func1')
34 self.upload_program.add_function_call('func2', 'func4')
35 self.upload_program.add_function_call('func3', 'func5')
36 self.upload_program.add_function_call('func4', 'func4')
37 self.upload_program.add_function_call('func4', 'func5')
38 self.upload_program.add_function_call('func5', 'func1')
39 self.upload_program.add_function_call('func5', 'func2')
40 self.upload_program.add_function_call('func5', 'func3')
41 self.upload_program.add_function_call('func6', 'func7')
42
43 self.upload_program.commit()
44
45 self.one_node_program_name = 'testprogram1'
46 self.upload_one_node_program = self.setter_connection.new_program(self.one_node_program_name)
47 self.upload_one_node_program.add_function('lonefunc')
48
49 self.upload_one_node_program.commit()
50
51 self.empty_program_name = 'testprogramblank'
52 self.upload_empty_program = self.setter_connection.new_program(self.empty_program_name)
53
54 self.upload_empty_program.commit()
55
56 self.getter_connection = SextantConnection(self.remote_url)
57
58 def tearDown(self):
59 self.setter_connection.delete_program(self.upload_program.program_name)
60 self.setter_connection.delete_program(self.upload_one_node_program.program_name)
61 self.setter_connection.delete_program(self.upload_empty_program.program_name)
62 del(self.setter_connection)
63
64 def test_17_get_call_paths(self):
65 reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name)
66 reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
67 Function(self.program_1_name, 'func3'),
68 Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')]
69 reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4
70 reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4
71 reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5
72 reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5
73 reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3
74 self.assertEquals(reference1, self.getter_connection.get_call_paths(self.program_1_name, 'func1', 'func2'))
75 self.assertIsNone(self.getter_connection.get_call_paths('not a prog', 'func1', 'func2')) # shouldn't validation
76 self.assertIsNone(self.getter_connection.get_call_paths('notaprogram', 'func1', 'func2'))
77 self.assertIsNone(self.getter_connection.get_call_paths(self.program_1_name, 'notafunc', 'func2'))
78 self.assertIsNone(self.getter_connection.get_call_paths(self.program_1_name, 'func1', 'notafunc'))
79
80 def test_02_get_whole_program(self):
81 reference = FunctionQueryResult(parent_db=None, program_name=self.program_1_name)
82 reference.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
83 Function(self.program_1_name, 'func3'),
84 Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5'),
85 Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')]
86 reference.functions[0].functions_i_call = reference.functions[1:4:2] # func1 calls func2, func4
87 reference.functions[1].functions_i_call = reference.functions[0:4:3] # func2 calls func1, func4
88 reference.functions[2].functions_i_call = [reference.functions[4]] # func3 calls func5
89 reference.functions[3].functions_i_call = reference.functions[3:5] # func4 calls func4, func5
90 reference.functions[4].functions_i_call = reference.functions[0:3] # func5 calls func1, func2, func3
91 reference.functions[5].functions_i_call = [reference.functions[6]] # func6 calls func7
92
93
94 self.assertEqual(reference, self.getter_connection.get_whole_program(self.program_1_name))
95 self.assertIsNone(self.getter_connection.get_whole_program('nottherightprogramname'))
96
97 def test_03_get_whole_one_node_program(self):
98 reference = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
99 reference.functions = [Function(self.one_node_program_name, 'lonefunc')]
100
101 self.assertEqual(reference, self.getter_connection.get_whole_program(self.one_node_program_name))
102
103 def test_04_get_whole_empty_program(self):
104 reference = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name)
105 reference.functions = []
106
107 self.assertEqual(reference, self.getter_connection.get_whole_program(self.empty_program_name))
108
109 def test_05_get_function_names(self):
110 reference = {'func1', 'func2', 'func3', 'func4', 'func5', 'func6', 'func7'}
111 self.assertEqual(reference, self.getter_connection.get_function_names(self.program_1_name))
112
113 def test_06_get_function_names_one_node_program(self):
114 reference = {'lonefunc'}
115 self.assertEqual(reference, self.getter_connection.get_function_names(self.one_node_program_name))
116
117 def test_07_get_function_names_empty_program(self):
118 reference = set()
119 self.assertEqual(reference, self.getter_connection.get_function_names(self.empty_program_name))
120
121 def test_09_validation_is_used(self):
122 self.assertFalse(self.getter_connection.get_function_names('not alphanumeric'))
123 self.assertFalse(self.getter_connection.get_whole_program('not alphanumeric'))
124 self.assertFalse(self.getter_connection.check_program_exists('not alphanumeric'))
125 self.assertFalse(self.getter_connection.check_function_exists('not alphanumeric', 'alpha'))
126 self.assertFalse(self.getter_connection.check_function_exists('alpha', 'not alpha'))
127 self.assertFalse(self.getter_connection.get_all_functions_called('alphaprogram', 'not alpha function'))
128 self.assertFalse(self.getter_connection.get_all_functions_called('not alpha program', 'alphafunction'))
129 self.assertFalse(self.getter_connection.get_all_functions_calling('not alpha program', 'alphafunction'))
130 self.assertFalse(self.getter_connection.get_all_functions_calling('alphaprogram', 'not alpha function'))
131 self.assertFalse(self.getter_connection.get_call_paths('not alpha program','alphafunc1', 'alphafunc2'))
132 self.assertFalse(self.getter_connection.get_call_paths('alphaprogram','not alpha func 1', 'alphafunc2'))
133 self.assertFalse(self.getter_connection.get_call_paths('alphaprogram','alphafunc1', 'not alpha func 2'))
134
135 def test_08_get_program_names(self):
136 reference = {self.program_1_name, self.one_node_program_name, self.empty_program_name}
137 self.assertEqual(reference, self.getter_connection.get_program_names())
138
139
140 def test_11_get_all_functions_called(self):
141 reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 1,2,3,4,5 component
142 reference2 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 6,7 component
143 reference3 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 7 component
144 reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
145 Function(self.program_1_name, 'func3'),
146 Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')]
147 reference2.functions = [Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')]
148 reference3.functions = []
149
150 reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4
151 reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4
152 reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5
153 reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5
154 reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3
155
156 reference2.functions[0].functions_i_call = [reference2.functions[1]]
157
158 self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func1'))
159 self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func2'))
160 self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func3'))
161 self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func4'))
162 self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.program_1_name, 'func5'))
163
164 self.assertEquals(reference2, self.getter_connection.get_all_functions_called(self.program_1_name, 'func6'))
165
166 self.assertEquals(reference3, self.getter_connection.get_all_functions_called(self.program_1_name, 'func7'))
167
168 self.assertIsNone(self.getter_connection.get_all_functions_called(self.program_1_name, 'nottherightfunction'))
169 self.assertIsNone(self.getter_connection.get_all_functions_called('nottherightprogram', 'func2'))
170
171 def test_12_get_all_functions_called_1(self):
172 reference1 = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
173 reference1.functions = []
174
175 d=self.getter_connection.get_all_functions_called(self.one_node_program_name, 'lonefunc')
176 self.assertEquals(reference1, self.getter_connection.get_all_functions_called(self.one_node_program_name,
177 'lonefunc'))
178 self.assertIsNone(self.getter_connection.get_all_functions_called(self.one_node_program_name,
179 'not the right function'))
180 self.assertIsNone(self.getter_connection.get_all_functions_called('not the right program', 'lonefunc'))
181
182 def test_13_get_all_functions_called_blank(self):
183 reference1 = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name)
184 reference1.functions = []
185
186 self.assertIsNone(self.getter_connection.get_all_functions_called(self.empty_program_name,
187 'not the right function'))
188
189 def test_14_get_all_functions_calling(self):
190 reference1 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 1,2,3,4,5 component
191 reference2 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 6,7 component
192 reference3 = FunctionQueryResult(parent_db=None, program_name=self.program_1_name) # this will be the 7 component
193 reference1.functions = [Function(self.program_1_name, 'func1'), Function(self.program_1_name, 'func2'),
194 Function(self.program_1_name, 'func3'),
195 Function(self.program_1_name, 'func4'), Function(self.program_1_name, 'func5')]
196
197 reference1.functions[0].functions_i_call = reference1.functions[1:4:2] # func1 calls func2, func4
198 reference1.functions[1].functions_i_call = reference1.functions[0:4:3] # func2 calls func1, func4
199 reference1.functions[2].functions_i_call = [reference1.functions[4]] # func3 calls func5
200 reference1.functions[3].functions_i_call = reference1.functions[3:5] # func4 calls func4, func5
201 reference1.functions[4].functions_i_call = reference1.functions[0:3] # func5 calls func1, func2, func3
202
203 reference2.functions = [Function(self.program_1_name, 'func6'), Function(self.program_1_name, 'func7')]
204
205 reference2.functions[0].functions_i_call = [reference2.functions[1]]
206
207 reference3.functions = [Function(self.program_1_name, 'func6')]
208
209 reference3.functions = []
210
211 self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func1'))
212 self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func2'))
213 self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func3'))
214 self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func4'))
215 self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func5'))
216
217 self.assertEquals(reference2, self.getter_connection.get_all_functions_calling(self.program_1_name,'func7'))
218
219 self.assertEquals(reference3, self.getter_connection.get_all_functions_calling(self.program_1_name, 'func6'))
220
221 self.assertIsNone(self.getter_connection.get_all_functions_calling(self.program_1_name, 'nottherightfunction'))
222 self.assertIsNone(self.getter_connection.get_all_functions_calling('nottherightprogram', 'func2'))
223
224 def test_15_get_all_functions_calling_one_node_prog(self):
225 reference1 = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
226 reference1.functions = []
227 self.assertEquals(reference1, self.getter_connection.get_all_functions_calling(self.one_node_program_name,
228 'lonefunc'))
229 self.assertIsNone(self.getter_connection.get_all_functions_calling(self.one_node_program_name,
230 'not the right function'))
231 self.assertIsNone(self.getter_connection.get_all_functions_calling('not the right program', 'lonefunc'))
232
233 def test_16_get_all_functions_calling_blank_prog(self):
234 reference1 = FunctionQueryResult(parent_db=None, program_name=self.empty_program_name)
235 reference1.functions=[]
236
237 self.assertIsNone(self.getter_connection.get_all_functions_called(self.empty_program_name,
238 'not the right function'))
239
240
241
242 def test_18_get_call_paths_between_two_functions_one_node_prog(self):
243 reference = FunctionQueryResult(parent_db=None, program_name=self.one_node_program_name)
244 reference.functions = [] # that is, reference is the empty program with name self.one_node_program_name
245
246 self.assertEquals(self.getter_connection.get_call_paths(self.one_node_program_name, 'lonefunc', 'lonefunc'),
247 reference)
248 self.assertIsNone(self.getter_connection.get_call_paths(self.one_node_program_name, 'lonefunc', 'notafunc'))
249 self.assertIsNone(self.getter_connection.get_call_paths(self.one_node_program_name, 'notafunc', 'notafunc'))
250
251 def test_10_validator(self):
252 self.assertFalse(Validator.validate(''))
253 self.assertTrue(Validator.validate('thisworks'))
254 self.assertTrue(Validator.validate('th1sw0rks'))
255 self.assertTrue(Validator.validate('12345'))
256 self.assertFalse(Validator.validate('this does not work'))
257 self.assertTrue(Validator.validate('this_does_work'))
258 self.assertFalse(Validator.validate("'")) # string consisting of a single quote mark
259
260if __name__ == '__main__':
261 unittest.main()
0\ No newline at end of file262\ No newline at end of file
1263
=== added file 'src/sextant/update_db.py'
--- src/sextant/update_db.py 1970-01-01 00:00:00 +0000
+++ src/sextant/update_db.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,78 @@
1# -----------------------------------------
2# Sextant
3# Copyright 2014, Ensoft Ltd.
4# Author: Patrick Stevens, using work from Patrick Stevens and James Harkin
5# -----------------------------------------
6# Given a program file to upload, or a program name to delete from the server, does the right thing.
7
8__all__ = ("upload_program", "delete_program")
9
10from .db_api import SextantConnection, Validator
11from .objdump_parser import get_parsed_objects
12
13import logging
14import requests
15
16
17def upload_program(file_path, db_url, alternative_name=None, not_object_file=False):
18 """
19 Uploads a program to the remote database.
20 :param file_path: the path to the local file we wish to upload
21 :param db_url: the URL of the database (eg. http://localhost:7474)
22 :param alternative_name: a name to give the program to override the default
23 :param object_file: bool(the file is an objdump text output file, rather than a compiled binary)
24 :return: 1 if the program already exists in database, 2 if there was a connection error
25 """
26 try:
27 connection = SextantConnection(db_url)
28 except requests.exceptions.ConnectionError as err:
29 logging.exception("Could not connect to Neo4J server {}. Are you sure it is running?".format(db_url))
30 return 2
31 #except urllib.exceptions.MaxRetryError:
32 # logging.error("Connection was refused to {}. Are you sure the server is running?".format(db_url))
33 # return 2
34
35 program_names = connection.get_program_names()
36 if alternative_name is None:
37 if Validator.sanitise(file_path) in program_names:
38 logging.error("There is already a program under this name; please delete the previous one with the same name "
39 "and retry, or rename the input file.")
40 return 1
41 else:
42 if Validator.sanitise(alternative_name) in program_names:
43 logging.error("There is already a program under this name; please delete the previous one with the same name "
44 "and retry, or rename the input file.")
45 return 1
46
47 parsed_objects = get_parsed_objects(filepath=file_path, sections_to_view=['.text'],
48 not_object_file=not_object_file, ignore_function_pointers=False)
49
50 logging.warning('Objdump has parsed!')
51
52 if alternative_name is None:
53 program_representation = connection.new_program(Validator.sanitise(file_path))
54 else:
55 program_representation = connection.new_program(Validator.sanitise(alternative_name))
56
57
58
59 for obj in parsed_objects:
60 for called in obj.what_do_i_call:
61 if not program_representation.add_function_call(obj.name, called[-1]): # called is a tuple (address, name)
62 logging.error('Validation error: {} calling {}'.format(obj.name, called[-1]))
63
64 logging.warning('Sending {} named objects to server {}...'.format(len(parsed_objects), db_url))
65 program_representation.commit()
66 logging.info('Sending complete! Exiting.')
67
68
69def delete_program(program_name, db_url):
70 """
71 Deletes a program with the specified name from the database.
72 :param program_name: the name of the program to delete
73 :param db_url: the URL of the database (eg. http://localhost:7474)
74 :return: bool(success)
75 """
76 connection = SextantConnection(db_url)
77 connection.delete_program(program_name)
78 print('Deleted {} successfully.'.format(program_name))
079
=== added directory 'src/sextant/web'
=== added file 'src/sextant/web/__init__.py'
--- src/sextant/web/__init__.py 1970-01-01 00:00:00 +0000
+++ src/sextant/web/__init__.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,8 @@
1#----------------------------------------------------------------------------
2# __init__.py -- Web package root
3#
4# July 2014, Phil Connell
5#
6# (c) Ensoft Ltd, 2014
7#----------------------------------------------------------------------------
8
09
=== added file 'src/sextant/web/server.py'
--- src/sextant/web/server.py 1970-01-01 00:00:00 +0000
+++ src/sextant/web/server.py 2014-08-15 12:04:08 +0000
@@ -0,0 +1,323 @@
1#!/usr/bin/python2
2# -----------------------------------------
3# Sextant
4# Copyright 2014, Ensoft Ltd.
5# Author: Patrick Stevens, James Harkin
6# -----------------------------------------
7# Note: this must be run in Python 2.
8
9from twisted.web.server import Site, NOT_DONE_YET
10from twisted.web.resource import Resource
11from twisted.web.static import File
12from twisted.internet import reactor
13from twisted.internet.threads import deferToThread
14from twisted.internet import defer
15
16import logging
17
18import os
19import sys # hack to get the Sextant module imported
20sys.path.append(os.path.realpath('../..'))
21
22import json
23import requests
24
25import sextant.db_api as db_api
26import sextant.export as export
27import tempfile
28import subprocess
29
30from cgi import escape # deprecated in Python 3 in favour of html.escape, but we're stuck on Python 2
31
32database_url = None # the URL to access the database instance
33
34class Echoer(Resource):
35 # designed to take one name argument
36
37 def render_GET(self, request):
38 if "name" not in request.args:
39 return '<html><body>Greetings, unnamed stranger.</body></html>'
40
41 arg = escape(request.args["name"][0])
42 return '<html><body>Hello %s!</body></html>' % arg
43
44
45class SVGRenderer(Resource):
46
47 def error_creating_neo4j_connection(self, failure):
48 self.write("Error creating Neo4J connection: %s\n") % failure.getErrorMessage()
49
50 @staticmethod
51 def create_neo4j_connection():
52 return db_api.SextantConnection(database_url)
53
54 @staticmethod
55 def check_program_exists(connection, name):
56 return connection.check_program_exists(name)
57
58 @staticmethod
59 def get_whole_program(connection, name):
60 return connection.get_whole_program(name)
61
62 @staticmethod
63 def get_functions_calling(connection, progname, funcname):
64 return connection.get_all_functions_calling(progname, funcname)
65
66 @staticmethod
67 def get_plot(program, suppress_common_functions=False):
68 graph_dot = export.ProgramConverter.to_dot(program, suppress_common_functions)
69
70 file_written_to = tempfile.NamedTemporaryFile(delete=False)
71 file_out = tempfile.NamedTemporaryFile(delete=False)
72 file_written_to.write(graph_dot)
73 file_written_to.close()
74 subprocess.call(['dot', '-Tsvg', '-Kdot', '-o', file_out.name, file_written_to.name])
75
76 output = file_out.read().encode()
77 file_out.close()
78 return output
79
80 @defer.inlineCallbacks
81 def _render_plot(self, request):
82 if "program_name" not in request.args:
83 request.setResponseCode(400)
84 request.write("Supply 'program_name' parameter.")
85 request.finish()
86 defer.returnValue(None)
87
88 logging.info('enter')
89 name = request.args["program_name"][0]
90
91 try:
92 suppress_common = request.args["suppress_common"][0]
93 except KeyError:
94 suppress_common = False
95
96 if suppress_common == 'null' or suppress_common == 'true':
97 suppress_common = True
98 else:
99 suppress_common = False
100
101 try:
102 neo4jconnection = yield deferToThread(self.create_neo4j_connection)
103 except requests.exceptions.ConnectionError:
104 request.setResponseCode(502) # Bad Gateway
105 request.write("Could not reach Neo4j server at {}".format(database_url))
106 request.finish()
107 defer.returnValue(None)
108 neo4jconnection = None # to silence the "referenced before assignment" warnings later
109
110 logging.info('created')
111 exists = yield deferToThread(self.check_program_exists, neo4jconnection, name)
112 if not exists:
113 request.setResponseCode(404)
114 logging.info('returning nonexistent')
115 request.write("Name %s not found." % (escape(name)))
116 request.finish()
117 defer.returnValue(None)
118
119 logging.info('done created')
120 allowed_queries = ("whole_program", "functions_calling", "functions_called_by", "call_paths", "shortest_path")
121
122 if "query" not in request.args:
123 query = "whole_program"
124 else:
125 query = request.args["query"][0]
126
127 if query not in allowed_queries:
128 # raise 400 Bad Request error
129 request.setResponseCode(400)
130 request.write("Supply 'query' parameter, default is whole_program, allowed %s." % str(allowed_queries))
131 request.finish()
132 defer.returnValue(None)
133
134 if query == 'whole_program':
135 program = yield deferToThread(self.get_whole_program, neo4jconnection, name)
136 elif query == 'functions_calling':
137 if 'func1' not in request.args:
138 # raise 400 Bad Request error
139 request.setResponseCode(400)
140 request.write("Supply 'func1' parameter to functions_calling.")
141 request.finish()
142 defer.returnValue(None)
143 func1 = request.args['func1'][0]
144 program = yield deferToThread(self.get_functions_calling, neo4jconnection, name, func1)
145 elif query == 'functions_called_by':
146 if 'func1' not in request.args:
147 # raise 400 Bad Request error
148 request.setResponseCode(400)
149 request.write("Supply 'func1' parameter to functions_called_by.")
150 request.finish()
151 defer.returnValue(None)
152 func1 = request.args['func1'][0]
153 program = yield deferToThread(neo4jconnection.get_all_functions_called, name, func1)
154 elif query == 'call_paths':
155 if 'func1' not in request.args:
156 # raise 400 Bad Request error
157 request.setResponseCode(400)
158 request.write("Supply 'func1' parameter to call_paths.")
159 request.finish()
160 defer.returnValue(None)
161 if 'func2' not in request.args:
162 # raise 400 Bad Request error
163 request.setResponseCode(400)
164 request.write("Supply 'func2' parameter to call_paths.")
165 request.finish()
166 defer.returnValue(None)
167
168 func1 = request.args['func1'][0]
169 func2 = request.args['func2'][0]
170 program = yield deferToThread(neo4jconnection.get_call_paths, name, func1, func2)
171 elif query == 'shortest_path':
172 if 'func1' not in request.args:
173 # raise 400 Bad Request error
174 request.setResponseCode(400)
175 request.write("Supply 'func1' parameter to shortest_path.")
176 request.finish()
177 defer.returnValue(None)
178 if 'func2' not in request.args:
179 # raise 400 Bad Request error
180 request.setResponseCode(400)
181 request.write("Supply 'func2' parameter to shortest_path.")
182 request.finish()
183 defer.returnValue(None)
184
185 func1 = request.args['func1'][0]
186 func2 = request.args['func2'][0]
187 program = yield deferToThread(neo4jconnection.get_shortest_path_between_functions, name, func1, func2)
188
189 else: # unrecognised query, so we need to raise a Bad Request response
190 request.setResponseCode(400)
191 request.write("Query %s not recognised." % escape(query))
192 request.finish()
193 defer.returnValue(None)
194 program = None # silences the "referenced before assignment" warnings
195
196 if program is None:
197 request.setResponseCode(404)
198 request.write("At least one of the input functions was not found in program %s." % (escape(name)))
199 request.finish()
200 defer.returnValue(None)
201
202 logging.info('getting plot')
203 logging.info(program)
204 if not program.functions: # we got an empty program back: the program is in the Sextant but has no functions
205 request.setResponseCode(204)
206 request.finish()
207 defer.returnValue(None)
208
209 output = yield deferToThread(self.get_plot, program, suppress_common)
210 request.setHeader("content-type", "image/svg+xml")
211
212 logging.info('SVG: return')
213 request.write(output)
214 request.finish()
215
216 def render_GET(self, request):
217 self._render_plot(request)
218 return NOT_DONE_YET
219
220
221class GraphProperties(Resource):
222
223 @staticmethod
224 def _get_connection():
225 return db_api.SextantConnection(database_url)
226
227 @staticmethod
228 def _get_program_names(connection):
229 return connection.get_program_names()
230
231 @staticmethod
232 def _get_function_names(connection, program_name):
233 return connection.get_function_names(program_name)
234
235 @defer.inlineCallbacks
236 def _render_GET(self, request):
237 if "query" not in request.args:
238 request.setResponseCode(400)
239 request.setHeader("content-type", "text/plain")
240 request.write("Supply 'query' parameter of 'programs' or 'functions'.")
241 request.finish()
242 defer.returnValue(None)
243
244 query = request.args['query'][0]
245
246 logging.info('Properties: about to get_connection')
247
248 try:
249 neo4j_connection = yield deferToThread(self._get_connection)
250 except Exception:
251 request.setResponseCode(502) # Bad Gateway
252 request.write("Could not reach Neo4j server at {}.".format(database_url))
253 request.finish()
254 defer.returnValue(None)
255 neo4j_connection = None # just to silence the "referenced before assignment" warnings
256
257 logging.info('got connection')
258
259 if query == 'programs':
260 request.setHeader("content-type", "application/json")
261 prognames = yield deferToThread(self._get_program_names, neo4j_connection)
262 request.write(json.dumps(list(prognames)))
263 request.finish()
264 defer.returnValue(None)
265
266 elif query == 'functions':
267 if "program_name" not in request.args:
268 request.setResponseCode(400)
269 request.setHeader("content-type", "text/plain")
270 request.write("Supply 'program_name' parameter to ?query=functions.")
271 request.finish()
272 defer.returnValue(None)
273 program_name = request.args['program_name'][0]
274
275 funcnames = yield deferToThread(self._get_function_names, neo4j_connection, program_name)
276 if funcnames is None:
277 request.setResponseCode(404)
278 request.setHeader("content-type", "text/plain")
279 request.write("No program with name %s was found in the Sextant." % escape(program_name))
280 request.finish()
281 defer.returnValue(None)
282
283 request.setHeader("content-type", "application/json")
284 request.write(json.dumps(list(funcnames)))
285 request.finish()
286 defer.returnValue(None)
287
288 else:
289 request.setResponseCode(400)
290 request.setHeader("content-type", "text/plain")
291 request.write("'Query' parameter should be 'programs' or 'functions'.")
292 request.finish()
293 defer.returnValue(None)
294
295 def render_GET(self, request):
296 self._render_GET(request)
297 return NOT_DONE_YET
298
299
300def serve_site(input_database_url='http://localhost:7474', port=2905):
301
302 global database_url
303 database_url = input_database_url
304 # serve static directory at root
305 root = File(os.path.join(
306 os.path.dirname(os.path.abspath(__file__)),
307 "..", "..", "..", "resources", "web"))
308
309 # serve a dynamic Echoer webpage at /echoer.html
310 root.putChild("echoer.html", Echoer())
311
312 # serve a dynamic webpage at /Sextant_properties to return graph properties
313 root.putChild("database_properties", GraphProperties())
314
315 # serve a generated SVG at /output_graph.svg
316 root.putChild('output_graph.svg', SVGRenderer())
317
318 factory = Site(root)
319 reactor.listenTCP(port, factory)
320 reactor.run()
321
322if __name__ == '__main__':
323 serve_site()

Subscribers

People subscribed via source and target branches