Skip to content
This repository was archived by the owner on Jan 8, 2024. It is now read-only.

Commit 35c0595

Browse files
author
Tristan Pourcelot
committed
Initial commit
1 parent a315828 commit 35c0595

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+9454
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
flask
2+
*.swp
3+
*.swo
4+
*.pyc
5+
*.db
6+
*.db-journal
7+
app/storage/*
8+
db_repository

LICENSE

+518
Large diffs are not rendered by default.

__init__.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from flask import Flask, render_template
2+
3+
from flask.ext.sqlalchemy import SQLAlchemy
4+
from flask_bootstrap import Bootstrap
5+
from flask_login import LoginManager
6+
from flask_marshmallow import Marshmallow
7+
from flask.ext.misaka import Misaka
8+
app = Flask(__name__)
9+
10+
app.config.from_object('config')
11+
12+
13+
# Init bootstrap extension
14+
Bootstrap(app)
15+
16+
17+
# Init SQL extension
18+
db = SQLAlchemy(app)
19+
ma = Marshmallow(app)
20+
Misaka(app)
21+
# Init login manager extension
22+
login_manager = LoginManager()
23+
login_manager.init_app(app)
24+
25+
from app.controllers.api import APIControl
26+
27+
api = APIControl(db)
28+
29+
from app.models import models
30+
from app.views import webui, apiview

app/controllers/__init__.py

Whitespace-only changes.

app/controllers/analysis.py

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""
2+
Analysis
3+
"""
4+
import os
5+
import re
6+
import importlib
7+
import inspect
8+
9+
from app import app, db
10+
from app.models.sample import Sample, AnalysisStatus
11+
from app.controllers.jobpool import JobPool
12+
13+
14+
class AnalysisFactory(object):
15+
"""
16+
Dynamically loads tasks from directory
17+
"""
18+
tasks_classes_container = None
19+
20+
def __init__(self):
21+
self.tasks_classes_container = []
22+
self.load_tasks()
23+
24+
def load_tasks(self):
25+
"""
26+
Dynamically loads the tasks in the tasks/ folder. The tasks must
27+
be loaded here in order to avoid too much memory usage.
28+
"""
29+
app.logger.info("Loading tasks")
30+
srcre = re.compile('.py$', re.IGNORECASE)
31+
tasks_files = filter(srcre.search,
32+
os.listdir(app.config['TASKS_PATH']))
33+
form_module = lambda fp: os.path.splitext(fp)[0]
34+
tasks_modules = map(form_module, tasks_files)
35+
for task_filename in tasks_modules:
36+
if not task_filename.startswith('__'):
37+
try:
38+
package_name = app.config['TASKS_PATH'].replace("/", ".")
39+
task_module = importlib.import_module(
40+
"." + task_filename, package=package_name)
41+
for task_name, task_class in inspect.getmembers(
42+
task_module):
43+
if task_name == task_filename and inspect.isclass(
44+
task_class):
45+
self.tasks_classes_container.append(
46+
(task_class, task_filename))
47+
app.logger.info("Loaded task %s" % (task_filename))
48+
except Exception as e:
49+
app.logger.error(
50+
"Could not load %s : %s" %
51+
(task_filename, e))
52+
continue
53+
return True
54+
55+
def create_analysis(self, sample):
56+
"""
57+
Creates a simple analysis from a sample.
58+
"""
59+
analysis = Analysis(sample)
60+
if analysis is None:
61+
app.logger.error("The factory couldn't generate an analysis...")
62+
return None
63+
self.assign_tasks(analysis, sample)
64+
return analysis
65+
66+
def assign_tasks(self, analysis, sample):
67+
"""
68+
Creates tasks, and, if they will run on the sample, add them to the
69+
analysis.
70+
"""
71+
for p_class, p_name in self.tasks_classes_container:
72+
try:
73+
p_instance = p_class(sample)
74+
if p_instance.will_run():
75+
analysis.add_task(p_instance, p_name)
76+
except Exception as e:
77+
app.logger.error("Could not load task %s : %s" % (p_name, e))
78+
app.logger.exception(e)
79+
pass
80+
return True
81+
82+
83+
class AnalysisController(object):
84+
"""
85+
Manages the creation, dispatch and management of analysis tasks
86+
"""
87+
jobpool = None
88+
factory = None
89+
90+
def __init__(self, max_instances=4):
91+
self.jobpool = JobPool(max_instances)
92+
self.factory = AnalysisFactory()
93+
94+
def create_analysis(self, sid, force=False):
95+
"""
96+
Creates an analysis for SID sample. If force, will create the analysis
97+
even if the analysis status is FINISHED or RUNNING.
98+
"""
99+
sample = Sample.query.get(sid)
100+
if sample is None:
101+
return None
102+
if sample.analysis_status == AnalysisStatus.RUNNING and not force:
103+
return None
104+
if sample.analysis_status == AnalysisStatus.FINISHED and not force:
105+
return None
106+
return self.factory.create_analysis(sample)
107+
108+
def dispatch_analysis(self, analysis):
109+
"""
110+
Send the analysis to the job queue.
111+
"""
112+
if analysis.tasks is None or len(analysis.tasks) == 0:
113+
return False
114+
self.jobpool.add_analysis(analysis)
115+
return True
116+
117+
def schedule_sample_analysis(self, sid, force=False):
118+
"""
119+
Create analysis, and dispatch it to execution pool.
120+
"""
121+
analysis = self.create_analysis(sid, force)
122+
if analysis is None:
123+
app.logger.error("No analysis generated for sample %d" % (sid))
124+
return False
125+
app.logger.info("Launching full analysis of sample %d" % (sid))
126+
self.dispatch_analysis(analysis)
127+
return True
128+
129+
def reschedule_all_analysis(self, force=False):
130+
"""
131+
Schedule all analyses in database. If "force" has been set to True,
132+
even FINISHED analyses are re-scheduled. RUNNING are also scheduled
133+
in order to recover from crashes.
134+
"""
135+
for sample in Sample.query.all():
136+
if force or sample.analysis_status == AnalysisStatus.TOSTART:
137+
self.schedule_sample_analysis(sample.id, force)
138+
elif sample.analysis_status == AnalysisStatus.RUNNING:
139+
self.schedule_sample_analysis(sample.id, force)
140+
141+
142+
class Analysis(object):
143+
"""
144+
Analysis object, contains tasks, and manages samples status.
145+
"""
146+
sid = None
147+
tasks = None
148+
149+
def __init__(self, sample=None):
150+
"""
151+
Only the sample ID is copyed, not the sample itself: on different
152+
processes/threads, several SQLAlchemy synchronization issues may
153+
appear.
154+
"""
155+
self.sid = sample.id
156+
self.tasks = []
157+
return
158+
159+
def set_started(self):
160+
"""
161+
Sets the analysis status to RUNNING (scheduled). Sets on dispatch.
162+
"""
163+
if self.sid:
164+
s = Sample.query.get(self.sid)
165+
if s:
166+
s.analysis_status = AnalysisStatus.RUNNING
167+
db.session.commit()
168+
return True
169+
170+
def set_finished(self):
171+
"""
172+
Sets the analysis status to FINISHED. Sets by the jobpool after tasks
173+
execution.
174+
"""
175+
if self.sid:
176+
sample = Sample.query.get(self.sid)
177+
if sample:
178+
sample.analysis_status = AnalysisStatus.FINISHED
179+
db.session.commit()
180+
return True
181+
182+
def add_task(self, task, tname):
183+
"""
184+
Adds a new task to the analysis. The task object is given, and the
185+
list is provided along with its execution level, in order to be
186+
priorized when the jobpool will execute them.
187+
"""
188+
try:
189+
execution_level = task.execution_level
190+
except Exception as e:
191+
app.logger.warning(
192+
"Could not read execution_level for task %s, default to 0" %
193+
(tname))
194+
execution_level = 0
195+
if execution_level < 0:
196+
execution_level = 0
197+
if execution_level > 32:
198+
execution_level = 32
199+
self.tasks.append((execution_level, task))
200+
app.logger.info("Task loaded: %s" % (tname))
201+
return True

app/controllers/api.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Main API controls for model management
3+
and data production.
4+
"""
5+
6+
import zipfile
7+
8+
9+
from app import app, db
10+
11+
from app.models.models import TLPLevel
12+
from app.models.sample import AnalysisStatus
13+
from app.controllers.analysis import AnalysisController
14+
from app.controllers.sample import SampleController
15+
from app.controllers.yara_rule import YaraController
16+
from app.controllers.family import FamilyController
17+
from app.controllers.user import UserController
18+
from app.controllers.idaactions import IDAActionsController
19+
from app.models.sample import FunctionInfo
20+
21+
22+
class APIControl(object):
23+
"""
24+
Object used as a global API.
25+
Data controllers are used for direct data manipulation.
26+
Methods are used for complex (cross-data) manipulation.
27+
TODO: create a brand new analysis scehduler working on database
28+
samples status, and remove the analysis creation from this class.
29+
"""
30+
31+
familycontrol = None
32+
samplecontrol = None
33+
usercontrol = None
34+
analysiscontrol = None
35+
dbhandle = None
36+
37+
familycontrol = FamilyController()
38+
yaracontrol = YaraController()
39+
samplecontrol = SampleController()
40+
usercontrol = UserController()
41+
analysiscontrol = AnalysisController(
42+
app.config['ANALYSIS_PROCESS_POOL_SIZE'])
43+
idacontrol = IDAActionsController()
44+
45+
def __init__(self, dbhandle=None):
46+
"""
47+
Initiate controllers.
48+
"""
49+
pass
50+
51+
def create_sample_and_run_analysis(
52+
self,
53+
file_data_stream,
54+
originate_filename,
55+
user=None,
56+
tlp_level=TLPLevel.TLPWHITE,
57+
family=None):
58+
"""
59+
Creates a new sample and a schedule an analysis. We also check the
60+
file header for ZIP pattern: if a ZIP pattern is found, any file
61+
inside the archive will be imported and scheduled for analysis.
62+
63+
TODO: move this to the SampleController, and start directly on new
64+
file submission.
65+
"""
66+
file_data = file_data_stream.read()
67+
if file_data.startswith("PK"):
68+
with zipfile.ZipFile(file_data, "r") as zcl:
69+
for name in zcl.namelist():
70+
mfile = zcl.open(name, "r")
71+
sample = self.samplecontrol.create_sample_from_file(
72+
mfile, name, user, tlp_level)
73+
if family is not None:
74+
self.familycontrol.add_sample(sample, family)
75+
if sample.analysis_status == AnalysisStatus.TOSTART:
76+
self.analysiscontrol.schedule_sample_analysis(
77+
sample.id)
78+
zcl.close()
79+
return None
80+
sample = self.samplecontrol.create_sample_from_file(
81+
file_data, originate_filename, user, tlp_level)
82+
if sample.analysis_status == AnalysisStatus.TOSTART:
83+
self.analysiscontrol.schedule_sample_analysis(sample.id)
84+
if family is not None:
85+
self.familycontrol.add_sample(sample, family)
86+
return sample
87+
88+
def add_actions_fromfunc_infos(self, funcinfos, sample_dst, sample_src):
89+
for fid_dst, fid_src in funcinfos:
90+
fsrc = FunctionInfo.query.get(fid_src)
91+
fdst = FunctionInfo.query.get(fid_dst)
92+
if fsrc is None or fdst is None:
93+
return False
94+
if fsrc not in sample_src.functions:
95+
return False
96+
if fdst not in sample_dst.functions:
97+
return False
98+
if fsrc.name.startswith("sub_"):
99+
continue
100+
act = self.idacontrol.add_name(int(fdst.address), fsrc.name)
101+
self.samplecontrol.add_idaaction(sample_dst.id, act)
102+
db.session.commit()
103+
return True

0 commit comments

Comments
 (0)