├── .gitignore ├── README.md ├── aleph ├── __init__.py ├── base.py ├── collectors │ └── __init__.py ├── components │ └── __init__.py ├── constants.py ├── datastore.py ├── plugins │ ├── email.py │ ├── peinfo.py │ ├── rararchive.py │ ├── strings.py │ ├── targziparchive.py │ ├── trid.py │ ├── urlextractor.py │ ├── urlparser.py │ ├── virustotal.py │ └── ziparchive.py ├── settings.py.orig ├── tasks.py ├── utils.py └── webui │ ├── __init__.py │ ├── database.py │ ├── email.py │ ├── forms.py │ ├── models.py │ ├── utils.py │ └── views │ ├── __init__.py │ ├── general.py │ ├── samples.py │ └── users.py ├── babel.cfg ├── bin ├── aleph-server.py ├── aleph-webui.sh └── db_create.py ├── messages.pot ├── requirements.txt └── webui ├── aleph-webui.py ├── resources └── GeoLiteCity.dat ├── static ├── css │ ├── AdminLTE.css │ ├── aleph.css │ ├── bootstrap.min.css │ ├── font-awesome.min.css │ └── ionicons.min.css ├── favicon.ico ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── ionicons.eot │ ├── ionicons.svg │ ├── ionicons.ttf │ └── ionicons.woff └── js │ ├── AdminLTE │ └── app.js │ ├── bootstrap.min.js │ ├── jquery-ui-1.10.3.min.js │ └── plugins │ └── bootbox.min.js ├── templates ├── 401.html ├── 404.html ├── _formhelpers.html ├── _paginationhelpers.html ├── general │ └── index.html ├── header.html ├── layout.html ├── samples │ ├── index.html │ ├── submissions.html │ ├── submit.html │ └── view.html ├── sidebar.html └── users │ ├── change_password.html │ ├── edit.html │ ├── index.html │ ├── login.html │ ├── mail_register.html │ ├── mail_register.txt │ ├── new.html │ ├── register.html │ └── settings.html └── translations ├── es └── LC_MESSAGES │ ├── messages.mo │ └── messages.po └── pt └── LC_MESSAGES ├── messages.mo └── messages.po /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.swn 4 | *.pyc 5 | *.log 6 | *.sqlite 7 | *.sample 8 | aleph/settings.py 9 | temp/ 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aleph - OpenSource /Malware Analysis Pipeline System 2 | 3 | [![Join the chat at https://gitter.im/trendmicro/aleph](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/trendmicro/aleph?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ## What? 6 | Aleph is designed to pipeline the analysis of malware samples. It has a series of collectors that will gather samples from many sources and shove them into the pipeline. The sample manager has a series of plugins that are ran against the sample and returns found data into JSON form. 7 | 8 | These JSON data can be further processed and queried in a objective manner instead of *grepping and regexing*. 9 | 10 | ## How? 11 | The main Aleph daemon is a loose-coupled python application and library. These are composed by the Aleph Service that spawns: 12 | 13 | 1. The Collectors. These are responsible for going to multiple sources (Filesystem folder, IMAP folder, FTP directory etc) and collect all the files there, store locally and add them to the processing queue. Each collector runs in its own process (fork). 14 | 2. Multiple (quantity is configurable) parallel SampleManager services (that will pull samples from the work queue and process them) and run the plugins that receives the sample path and return the JSON object of found artifacts. 15 | 3. The sample object is converted to JSON along with its data and is stored into an Elasticsearch backend. 16 | 17 | ## Installing Aleph 18 | ### Requirements 19 | In order to get a clean and nice install, you should download some requirements: 20 | Ubuntu/Debian 21 | 22 | apt-get install python-pyrex libffi-dev libfuzzy-dev python-dateutil libsqlite3-dev 23 | 24 | 25 | #### ElasticSearch 26 | First if you don't have an [Elasticsearch](http://www.elasticsearch.org/) instance ready, you must install one. 27 | 28 | For Debian/Ubuntu/Redhat/Fedora/CentOS (yum + apt basically) users, follow [this guide](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/setup-repositories.html). 29 | 30 | ** Remember: Elasticsearh uses JVM, so you also must install it =) ** 31 | 32 | #### Python modules 33 | We strongly suggest that you use python's virtual environment so you don't pollute the rest of your OS installation with python modules. To make a contained virtual environment, install _virtualenv_ with _pip_: 34 | 35 | pip install virtualenv 36 | 37 | Go to the desired Aleph installation folder and type the following to create and activate your virtual environment: 38 | 39 | virtualenv venv # 'venv' can be any name 40 | source venv/bin/activate 41 | 42 | There will be the environment name (venv) appended to your PS1 variable: 43 | 44 | (venv)(2014-08-19 17:36:%)(~/opt/aleph/) 45 | 46 | All python modules required are listed on the _requirements.txt_ file on the root repository folder. You can install all of them at once using _pip_: 47 | 48 | pip install -r requirements.txt 49 | 50 | Then clone the repository and copy the settings file: 51 | 52 | git clone https://github.com/merces/aleph.git --branch aleph-python --single-branch . 53 | cp aleph/settings.py.orig aleph/settings.py 54 | 55 | Edit settings.py and add a local source (a folder where Aleph will search for samples - **WARNING: ALEPH WILL MOVE THE SAMPLE THUS REMOVING FROM THE ORIGINAL FOLDER**) _The folder must exists as Aleph won't try to create them_ 56 | 57 | SAMPLE_SOURCES = [ 58 | ('local', {'path': '/opt/aleph/unprocessed_samples'}), 59 | ] 60 | 61 | Review your Elasticsearch installation URI 62 | 63 | ELASTICSEARCH_URI = '127.0.0.1:9200' 64 | 65 | ** Workaround step ** 66 | As I still finish some of the code, there are some folders that are not on the repository and must be created manually and set accordingly on the *settings.py* file: 67 | 68 | SAMPLE_TEMP_DIR = '/opt/aleph/temp' 69 | SAMPLE_STORAGE_DIR = '/opt/aleph/samples' 70 | 71 | Remember to verify folders permissioning. 72 | And Aleph is ready to run! 73 | 74 | #### Running 75 | Go to Aleph folder, activate the virtual environment and run the bin/aleph-server.py as following: 76 | 77 | cd /opt/aleph/ 78 | source venv/bin/activate 79 | ./bin/aleph-server.py 80 | 81 | And that's it. Check your logs under log/aleph.log to any troubleshooting. 82 | 83 | #### Install the Web interface(Webui) 84 | Edit the "SERVER_NAME" constant at your settings.py file. 85 | ex: SERVER_NAME = 'mydomain.com:90' 86 | 87 | then create the following entry: 88 | 89 | SECRET_KEY = 'Pu7s0m3cryp7l337here' #do not use this ;) 90 | SAMPLE_SUBMIT_FOLDER= '/some/path' #where samples will be submitted from webui 91 | 92 | Setup your database: 93 | 94 | python bin/db_create.py 95 | 96 | Run the webui script: 97 | 98 | bin/aleph-webui.sh 99 | 100 | To access your webinterface open your favorite browser at http://SERVER_NAME #That value you changed before. 101 | 102 | Login: admin 103 | Password: changeme12! 104 | 105 | 106 | **Note: For sake of Security's God, CHANGE YOUR PASSWORD! ;)** 107 | 108 | But if you do not like our webinterface you still can use other softwares to review and query data on elasticsearch. I strongly suggest this [Chrome REST client plugin](https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm?hl=en) or the great [Kibana](http://www.elasticsearch.org/guide/en/kibana/current/working-with-queries-and-filters.html) 109 | 110 | #### Currently implemented 111 | **Collectors** 112 | * FileCollector: grabs samples from a local directory 113 | * MailCollector: grabs samples from email attachments on a IMAP folder 114 | 115 | **Plugins** 116 | * PEInfo : extracts info from PE files such as entrypoint, number of sections and some PE characteristics(SEH/ASLR/DEP). 117 | * ZipArchivePlugin: extracts zip files and puts their contents back into analysis queue. 118 | * StringsPlugin: extracts strings from sample into three categories: All Strings, URI Strings and Filename Strings (not 100% but we do our best). 119 | * VirustotalPlugin: check a sample SHA256 hash against Virustotal database and get the report. If that hash doesnt exist, send the file to analise. 120 | * TrID: check the filetype of a sample. 121 | -------------------------------------------------------------------------------- /aleph/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, sys, logging, shutil 4 | from multiprocessing import Process, Queue 5 | from copy import copy 6 | 7 | from aleph import settings, collectors 8 | from aleph.components import SampleManager 9 | from aleph.datastore import es 10 | 11 | from aleph.settings import SAMPLE_STORAGE_DIR, SAMPLE_TEMP_DIR 12 | 13 | class AlephServer(object): 14 | 15 | # Properties 16 | logger = None 17 | 18 | running = False 19 | 20 | sample_managers = [] 21 | 22 | collectors = [] 23 | 24 | sample_queue = None 25 | 26 | def __init__(self): 27 | self.init_logger() 28 | self.sample_queue = Queue() 29 | self.init_db() 30 | self.create_directories() 31 | self.init_sample_managers() 32 | self.init_collectors() 33 | 34 | def __del__(self): 35 | self.stop_services() 36 | 37 | def init_db(self): 38 | try: 39 | es.setup() 40 | except Exception, e: 41 | raise elasticsearch.exceptions.ConnectionError("Unable to connect to elasticsearch database") 42 | 43 | 44 | def create_directories(self): 45 | 46 | if not os.path.exists(SAMPLE_STORAGE_DIR): 47 | try: 48 | os.mkdir(SAMPLE_STORAGE_DIR) 49 | self.logger.info("Directory %s created" % SAMPLE_STORAGE_DIR) 50 | except OSError, e: 51 | raise OSError("Unable to create sample storage dir at %s: %s" % (SAMPLE_STORAGE_DIR, str(e))) 52 | 53 | try: 54 | if os.path.exists(SAMPLE_TEMP_DIR): 55 | shutil.rmtree(SAMPLE_TEMP_DIR) 56 | os.mkdir(SAMPLE_TEMP_DIR) 57 | self.logger.info("Directory %s created" % SAMPLE_TEMP_DIR) 58 | except OSError, e: 59 | raise OSError("Unable to create sample temporary dir at %s: %s" % (SAMPLE_TEMP_DIR, str(e))) 60 | 61 | def init_logger(self): 62 | 63 | log_level = logging.DEBUG if settings.DEBUG else logging.INFO 64 | 65 | if not os.path.exists(settings.LOGGING['directory']): 66 | os.makedirs(settings.LOGGING['directory']) 67 | 68 | logging.basicConfig( 69 | filename=os.path.join(settings.LOGGING['directory'], settings.LOGGING['filename']), 70 | level=log_level, 71 | format=settings.LOGGING['format'], 72 | ) 73 | 74 | self.logger = logging.getLogger(self.__class__.__name__) 75 | self.logger.debug('Logger module initialized') 76 | 77 | def stop_services(self): 78 | for sample_manager in self.sample_managers: 79 | sample_manager.stop() 80 | for source, instance in self.collectors: 81 | if instance is not None and instance.is_alive(): 82 | instance.stop() 83 | 84 | def start_services(self): 85 | 86 | self.start_sample_managers() 87 | self.start_collectors() 88 | 89 | def init_sample_managers(self): 90 | 91 | for i in range(settings.SAMPLE_MANAGERS): 92 | self.sample_managers.append(self.sample_manager_instance()) 93 | 94 | def init_collectors(self): 95 | 96 | self.logger.info('Loading collectors from sources configuration') 97 | 98 | self.collectors = [] 99 | self.sources = copy(settings.SAMPLE_SOURCES) 100 | 101 | for source in self.sources: 102 | instance = self.collector_instance(source) 103 | self.collectors.append( ( source, instance ) ) 104 | 105 | def start_collectors(self): 106 | for source, instance in self.collectors: 107 | instance.start() 108 | 109 | def start_sample_managers(self): 110 | for manager in self.sample_managers: 111 | manager.start() 112 | 113 | def sample_manager_instance(self): 114 | return SampleManager(self.sample_queue) 115 | 116 | def collector_instance(self, source): 117 | source_type = source[0] 118 | source_params = source[1] 119 | 120 | if source_type not in collectors.COLLECTOR_MAP: 121 | raise NotImplementedError('%s collector is not implemented.' % source_type) 122 | 123 | instance = collectors.COLLECTOR_MAP[source_type](source_params, self.sample_queue) 124 | self.logger.info('Collector "%s" loaded' % (source[0])) 125 | return instance 126 | 127 | def run(self): 128 | self.logger.info('Starting AlephServer') 129 | self.start_services() 130 | print 'AlephServer started.' 131 | self.monitor() 132 | 133 | def monitor(self): 134 | 135 | self.running = True 136 | try: 137 | while self.running: 138 | # SampleManager 139 | for manager in self.sample_managers: 140 | if manager and manager.is_alive(): 141 | manager.join(1.0) 142 | else: 143 | self.sample_managers.remove(manager) 144 | self.sample_managers.append(self.sample_manager_instance()) 145 | 146 | # Collectors 147 | for source, instance in self.collectors: 148 | if instance is not None and instance.is_alive(): 149 | instance.join(1.0) 150 | else: 151 | instance = self.collector_instance(source) 152 | 153 | except (KeyboardInterrupt, SystemExit): 154 | self.logger.info('CTRL+C received. Killing all workers') 155 | print "CTRL+C received. Killing all workers" 156 | self.running = False 157 | self.stop_services() 158 | -------------------------------------------------------------------------------- /aleph/base.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process 2 | import uuid, magic, os, logging, binascii, hashlib, time, datetime, ssdeep 3 | from aleph.datastore import es 4 | from aleph.settings import SAMPLE_STORAGE_DIR, PLUGIN_SETTINGS, SAMPLE_MIN_FILESIZE, SAMPLE_MAX_FILESIZE 5 | from aleph.constants import SAMPLE_STATUS_NEW 6 | from aleph.utils import to_iso8601, humansize 7 | from shutil import move 8 | 9 | from time import sleep 10 | 11 | class PluginBase(object): 12 | 13 | logger = None 14 | 15 | mimetypes = [] # empty = all mimetypes 16 | mimetypes_except = [] # used when mimetypes is set to all (empty) 17 | 18 | name = None 19 | description = None 20 | 21 | options = {} 22 | default_options = {} 23 | required_options = [] 24 | 25 | depends = [] 26 | 27 | queue = None 28 | 29 | sample = None 30 | 31 | 32 | def __init__(self, queue): 33 | if not self.name: self.name = self.__class__.__name__ 34 | self.logger = logging.getLogger('Plugin:%s' % self.name) 35 | 36 | self.queue = queue 37 | self.sample = None 38 | 39 | options = PLUGIN_SETTINGS[self.name] if self.name in PLUGIN_SETTINGS else {} 40 | self.options = dict(self.default_options.items() + options.items()) 41 | 42 | if 'enabled' not in self.options: 43 | self.options['enabled'] = False 44 | 45 | if self.options['enabled']: 46 | self.validate_options() 47 | self.setup() 48 | 49 | # @@ OVERRIDE ME 50 | def setup(self): 51 | return True 52 | 53 | # @@ OVERRIDE ME 54 | def validate_options(self): 55 | self.check_required_options() 56 | 57 | def check_required_options(self): 58 | for option in self.required_options: 59 | if option not in self.options or self.options[option] is None: 60 | raise KeyError('Parameter "%s" not defined for %s plugin' % (option, self.__class__.__name__)) 61 | 62 | def can_run(self): 63 | 64 | if not self.options['enabled']: 65 | return False 66 | 67 | if not self.sample: 68 | return False 69 | 70 | if len(self.mimetypes) == 0: 71 | if self.sample.mimetype in self.mimetypes_except: 72 | return False 73 | elif self.sample.mimetype not in self.mimetypes: 74 | return False 75 | 76 | return True 77 | 78 | def process(self): 79 | raise NotImplementedError('Plugin process function not implemented') 80 | 81 | def release_sample(self): 82 | self.sample = None 83 | 84 | def set_sample(self, sample): 85 | self.sample = sample 86 | 87 | def create_sample(self, filepath, filename, mimetype=None): 88 | 89 | self.logger.debug('Creating sample from path %s' % filepath) 90 | 91 | sample = SampleBase(filepath, mimetype) 92 | 93 | # Save source 94 | sample.add_source(self.__class__.__name__, filename, self.sample.uuid) 95 | 96 | # Store XREF relations 97 | self.sample.add_xref('child', sample.uuid) 98 | sample.add_xref('parent', self.sample.uuid) 99 | 100 | sample.store_data() 101 | self.sample.store_data() 102 | self.queue.put(sample) 103 | 104 | return True 105 | 106 | class SampleBase(object): 107 | 108 | uuid = None 109 | mimetype_str = None 110 | mimetype = None 111 | path = None 112 | sources = [] 113 | timestamp = None 114 | process = False 115 | 116 | status = SAMPLE_STATUS_NEW; 117 | 118 | xrefs = { 119 | 'parent': [], 120 | 'child': [], 121 | } 122 | 123 | hashes = {} 124 | 125 | size = 0 126 | 127 | data = {} 128 | tags = [] 129 | 130 | def __init__(self, path, mimetype=None): 131 | 132 | self.path = path 133 | self.data = {} 134 | self.sources = [] 135 | self.tags = [] 136 | self.hashes = self.get_hashes() 137 | self.timestamp = to_iso8601() 138 | self.mimetype = mimetype 139 | 140 | self.status = SAMPLE_STATUS_NEW; 141 | self.xrefs = { 142 | 'parent': [], 143 | 'child': [], 144 | } 145 | 146 | 147 | # Size boundary check 148 | sample_size = os.stat(path).st_size 149 | 150 | if sample_size > SAMPLE_MAX_FILESIZE: 151 | os.unlink(path) 152 | raise ValueError('Sample %s (%s) is bigger than maximum file size allowed: %s' % (path, humansize(sample_size), humansize(SAMPLE_MAX_FILESIZE))) 153 | 154 | if sample_size < SAMPLE_MIN_FILESIZE and self.mimetype != "text/url": 155 | os.unlink(path) 156 | raise ValueError('Sample %s (%s) is smaller than minimum file size allowed: %s' % (path, humansize(sample_size), humansize(SAMPLE_MIN_FILESIZE))) 157 | 158 | if not self.check_exists(): 159 | self.store_sample() 160 | self.prepare_sample() 161 | self.store_data() 162 | 163 | def dispose(self): 164 | os.unlink(self.path) 165 | 166 | def set_status(self, status): 167 | self.status = status 168 | es.update(self.uuid, {'status': self.status}) 169 | 170 | def update_source(self): 171 | source_set = self.sources 172 | es.update(self.uuid, {'sources': source_set}) 173 | 174 | def check_exists(self): 175 | 176 | result = es.search({"hashes.sha256": self.hashes['sha256']}) 177 | exists = ('hits' in result and result['hits']['total'] != 0) 178 | if exists: 179 | data = result['hits']['hits'][0]['_source'] 180 | self.uuid = data['uuid'] 181 | self.sources = data['sources'] 182 | self.dispose() 183 | 184 | return exists 185 | 186 | def add_xref(self, relation, sample_uuid): 187 | 188 | xrefs = self.xrefs 189 | 190 | if relation not in [ 'parent', 'child' ]: 191 | raise KeyError('XRef Relation must be either \'parent\' or \'child\'') 192 | 193 | if sample_uuid not in xrefs[relation] and self.uuid != sample_uuid: 194 | xrefs[relation].append(sample_uuid) 195 | self.xrefs = xrefs 196 | 197 | def add_source(self, provider, filename, reference=None): 198 | sources = self.sources 199 | sources.append( {'timestamp': to_iso8601(), 'provider': provider, 'filename': filename, 'reference': reference} ) 200 | self.sources = sources 201 | 202 | def add_tag(self, tag_name): 203 | if tag_name not in self.tags: 204 | tags = self.tags 205 | tags.append(tag_name) 206 | self.tags = tags 207 | 208 | def add_data(self, plugin_name, data): 209 | 210 | self.data[plugin_name] = data 211 | 212 | def store_sample(self): 213 | 214 | sample_filename = "%s.sample" % self.hashes['sha256'] 215 | sample_path = os.path.join(SAMPLE_STORAGE_DIR, sample_filename) 216 | move(self.path, sample_path) 217 | self.path = sample_path 218 | 219 | def prepare_sample(self): 220 | 221 | # Get mimetype if not supplied 222 | if not self.mimetype: 223 | self.mimetype = magic.from_file(self.path, mime=True) 224 | self.mimetype_str = magic.from_file(self.path) 225 | 226 | # Get file size 227 | self.size = os.stat(self.path).st_size 228 | 229 | # Give it a nice uuid 230 | self.uuid = str(uuid.uuid1()) 231 | 232 | # Let it process 233 | self.process = True 234 | 235 | def store_data(self): 236 | es.save(self.toObject(), self.uuid) 237 | 238 | def get_hashes(self): 239 | 240 | hashes = {} 241 | # Calculate hashes 242 | with open(self.path) as handle: 243 | filedata = handle.read() 244 | hashes = { 245 | 'md5': hashlib.md5(filedata).hexdigest(), 246 | 'sha1': hashlib.sha1(filedata).hexdigest(), 247 | 'sha256': hashlib.sha256(filedata).hexdigest(), 248 | 'sha512': hashlib.sha512(filedata).hexdigest(), 249 | 'crc32': "%08X" % (binascii.crc32(filedata) & 0xFFFFFFFF), 250 | 'ssdeep': ssdeep.hash(filedata), 251 | } 252 | return hashes 253 | 254 | def toObject(self): 255 | return { 256 | 'uuid': self.uuid, 257 | 'status': self.status, 258 | 'path': self.path, 259 | 'mimetype': self.mimetype, 260 | 'mime': self.mimetype_str, 261 | 'hashes': self.hashes, 262 | 'data': self.data, 263 | 'tags': self.tags, 264 | 'timestamp' : self.timestamp, 265 | 'sources': self.sources, 266 | 'size': self.size, 267 | 'xrefs': self.xrefs 268 | } 269 | 270 | def __str__(self): 271 | 272 | return str(self.toObject) 273 | 274 | class CollectorBase(Process): 275 | 276 | logger = None 277 | 278 | handle = None 279 | queue = None 280 | options = {} 281 | default_options = {} 282 | required_options = [] 283 | 284 | sleep = 5.0 285 | 286 | def __init__(self, options, queue): 287 | 288 | super(CollectorBase, self).__init__() 289 | 290 | self.queue = queue 291 | self.options = dict(self.default_options.items() + options.items()) 292 | 293 | if 'sleep' in self.options: self.sleep = float(self.options['sleep']) 294 | 295 | self.logger = logging.getLogger(self.__class__.__name__) 296 | 297 | try: 298 | if not os.access(SAMPLE_STORAGE_DIR, os.W_OK): 299 | raise IOError('Cannot write to storage dir: %s' % SAMPLE_STORAGE_DIR) 300 | 301 | self.validate_options() 302 | self.setup() 303 | 304 | except Exception, e: 305 | self.logger.error('Error starting collector %s: %s' % (self.__class__.__name__, str(e))) 306 | 307 | def run(self): 308 | self.runnable = True 309 | self.logger.info('%s collector started' % self.__class__.__name__) 310 | while self.runnable: 311 | try: 312 | self.collect() 313 | sleep(self.sleep) 314 | except Exception, e: 315 | raise e 316 | 317 | def stop(self): 318 | self.runnable = False 319 | self.terminate() 320 | 321 | def __del__(self): 322 | self.teardown() 323 | self.stop() 324 | 325 | def check_required_options(self): 326 | for option in self.required_options: 327 | if option not in self.options or self.options[option] is None: 328 | raise KeyError('Parameter "%s" not defined for %s collector' % (option, self.__class__.__name__)) 329 | 330 | # @@ OVERRIDE ME 331 | def teardown(self): 332 | return True 333 | 334 | # @@ OVERRIDE ME 335 | def setup(self): 336 | return True 337 | 338 | # @@ OVERRIDE ME 339 | def validate_options(self): 340 | self.check_required_options() 341 | 342 | # @@ OVERRIDE ME 343 | def collect(self): 344 | raise NotImplementedError('Collector collection routine not implemented') 345 | 346 | def create_sample(self, filepath, sourcedata, mimetype=None): 347 | 348 | self.logger.debug('Creating sample from path %s (source: %s)' % (filepath, sourcedata[0])) 349 | 350 | sample = SampleBase(filepath, mimetype) 351 | sample.add_source(self.__class__.__name__, sourcedata[0], sourcedata[1] ) 352 | 353 | if sample.process: 354 | sample.store_data() 355 | self.queue.put(sample) 356 | -------------------------------------------------------------------------------- /aleph/collectors/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from aleph.base import CollectorBase 4 | from aleph.settings import SAMPLE_TEMP_DIR 5 | 6 | class FileCollector(CollectorBase): 7 | 8 | required_options = [ 'path' ] 9 | 10 | def validate_options(self): 11 | super(FileCollector, self).validate_options() 12 | 13 | if not os.access(self.options['path'], os.R_OK): 14 | try: 15 | os.mkdir(self.options['path']) 16 | self.logger.info("Directory %s created" % self.options['path']) 17 | except OSError, e: 18 | raise OSError("Unable to create sample storage dir at %s: %s" % (self.options['path'], str(e))) 19 | 20 | def collect(self): 21 | try: 22 | for dirname, dirnames, filenames in os.walk(self.options['path']): 23 | for filename in filenames: 24 | filepath = os.path.join(dirname, filename) 25 | if os.path.getsize(filepath) > 0: 26 | self.logger.info("Collecting file %s from %s" % (filepath, self.options['path'])) 27 | self.create_sample(os.path.join(self.options['path'], filepath), (filename, None)) 28 | except KeyboardInterrupt: 29 | pass 30 | 31 | import email, imaplib, tempfile, hashlib 32 | 33 | class MailCollector(CollectorBase): 34 | 35 | sleep = 60 # 1 minute 36 | 37 | imap_session = None 38 | 39 | default_options = { 40 | 'root_folder': 'Inbox', 41 | 'delete': True, 42 | 'ssl': True, 43 | 'port': 993, 44 | } 45 | 46 | required_options = [ 'host', 'username', 'password' ] 47 | 48 | def imap_login(self): 49 | # Log into IMAP 50 | if self.options['ssl']: 51 | self.imap_session = imaplib.IMAP4_SSL(self.options['host'], self.options['port']) 52 | else: 53 | self.imap_session = imaplib.IMAP4(self.options['host'], self.options['port']) 54 | 55 | rc, account = self.imap_session.login(self.options['username'], self.options['password']) 56 | 57 | if rc != 'OK': 58 | raise RuntimeError('Invalid credentials') 59 | 60 | # Set root folder for search 61 | self.imap_session.select(self.options['root_folder']) 62 | 63 | def process_message(self, message_parts): 64 | 65 | 66 | email_body = message_parts[0][1] 67 | mail = email.message_from_string(email_body) 68 | 69 | filename = "%s.eml" % hashlib.sha256(email_body).hexdigest() 70 | 71 | temp_file = tempfile.NamedTemporaryFile(dir=SAMPLE_TEMP_DIR, suffix='_%s' % filename, delete=False) 72 | temp_file.write(email_body) 73 | temp_file.close() 74 | 75 | self.create_sample(temp_file.name, (filename, mail['from'])) 76 | 77 | def collect(self): 78 | 79 | try: 80 | rc, data = self.imap_session.search(None, '(UNSEEN)') 81 | if rc != 'OK': 82 | raise RuntimeError('Error searching folder %s' % self.options['root_folder']) 83 | 84 | # Iterate over all messages 85 | for message_id in data[0].split(): 86 | rc, message_parts = self.imap_session.fetch(message_id, '(RFC822)') 87 | if rc != 'OK': 88 | raise RuntimeError('Error fetching message.') 89 | 90 | self.process_message(message_parts) 91 | if self.options['delete']: 92 | rc, flags_msg = self.imap_session.store(message_id, '+FLAGS', r'\Deleted') 93 | if rc != 'OK': 94 | raise RuntimeError('Error deleting message') 95 | 96 | if self.options['delete']: 97 | rc, ids = self.imap_session.expunge() 98 | if rc != 'OK': 99 | raise RuntimeError('Error running expunge') 100 | 101 | except Exception, e: 102 | raise 103 | 104 | 105 | def setup(self): 106 | try: 107 | self.imap_login() 108 | 109 | except Exception, e: 110 | return RuntimeError('Cannot connect to server: %s' % str(e)) 111 | 112 | def teardown(self): 113 | if self.imap_session: 114 | self.imap_session.close() 115 | self.imap_session.logout() 116 | 117 | 118 | COLLECTOR_MAP = { 119 | 'local': FileCollector, 120 | 'mail': MailCollector, 121 | } 122 | -------------------------------------------------------------------------------- /aleph/components/__init__.py: -------------------------------------------------------------------------------- 1 | import logging, os 2 | import pluginbase 3 | from multiprocessing import Process 4 | from aleph.base import SampleBase 5 | from aleph.constants import SAMPLE_STATUS_PROCESSING, SAMPLE_STATUS_PROCESSED 6 | from aleph.utils import get_path 7 | 8 | class SampleManager(Process): 9 | 10 | logger = None 11 | 12 | sample_queue = None 13 | plugins = [] 14 | plugin_source = None 15 | 16 | def __init__(self, sample_queue): 17 | super(SampleManager, self).__init__() 18 | 19 | self.logger = logging.getLogger(self.__class__.__name__) 20 | 21 | self.sample_queue = sample_queue 22 | 23 | self.plugins = [] 24 | self.plugin_source = None 25 | self.load_plugins() 26 | 27 | self.logger.info('SampleManager started') 28 | 29 | def load_plugins(self): 30 | self.logger.debug('Loading plugins from folder') 31 | 32 | plugin_base = pluginbase.PluginBase(package='aleph.plugins', searchpath=[get_path('plugins')]) 33 | self.plugin_source = plugin_base.make_plugin_source( 34 | searchpath=[get_path('plugins')]) 35 | 36 | for plugin_name in self.plugin_source.list_plugins(): 37 | plugin = self.plugin_source.load_plugin(plugin_name) 38 | self.plugins.append(plugin.setup(self.sample_queue)) 39 | self.logger.debug('Plugin "%s" loaded' % plugin_name) 40 | 41 | runs = 0 42 | max_runs = 30 # Max recursion runs before considering circular reference 43 | 44 | while (runs <= max_runs): 45 | rc = self.sort_plugins() 46 | if rc == 0: break 47 | runs += 1 48 | 49 | if runs == max_runs: self.logger.error('Possible circular reference in plugin chain') 50 | 51 | def get_plugin_index(self, name): 52 | for plugin in self.plugins: 53 | if plugin.name == name: return self.plugins.index(plugin) 54 | 55 | raise KeyError('Plugin %s not found in plugin list' % name) 56 | 57 | def sort_plugins(self): 58 | 59 | changed = 0 60 | for plugin in self.plugins: 61 | max_idx = 0 62 | if len(plugin.depends) != 0: 63 | for dep in plugin.depends: 64 | idx = self.get_plugin_index(dep) 65 | if idx > max_idx: max_idx = idx 66 | if max_idx != 0 and max_idx+1 > self.plugins.index(plugin): 67 | self.logger.debug( "Inserting plugin %s at position %d" % (plugin.name, max_idx+1)) 68 | self.plugins.remove(plugin) 69 | self.plugins.insert(max_idx+1, plugin) 70 | changed += 1 71 | 72 | return changed 73 | 74 | def process_sample(self): 75 | 76 | try: 77 | self.logger.info('Waiting for sample') 78 | sample = self.sample_queue.get() 79 | if sample.process: 80 | self.logger.info('Processing sample %s' % sample.uuid) 81 | sample.set_status(SAMPLE_STATUS_PROCESSING) 82 | self.apply_plugins(sample) 83 | sample.set_status(SAMPLE_STATUS_PROCESSED) 84 | sample.store_data() 85 | else: 86 | self.logger.info('Sample %s already processed. Updating source only.' % sample.uuid) 87 | sample.update_source() 88 | except (KeyboardInterrupt, SystemExit): 89 | pass 90 | except Exception, e: 91 | raise 92 | 93 | def apply_plugins(self, sample): 94 | for plugin in self.plugins: 95 | plugin.set_sample(sample) 96 | if plugin.can_run(): 97 | try: 98 | self.logger.debug('Applying plugin %s on sample %s' % (plugin.name, sample.uuid)) 99 | data = plugin.process() 100 | if data and len(data) > 0: 101 | sample.add_data(plugin.name, data) 102 | except Exception, e: 103 | self.logger.error("Plugin %s failed on sample %s: %s" % (plugin.name, sample.uuid, str(e))) 104 | pass 105 | plugin.release_sample() 106 | 107 | def run(self): 108 | self.runnable = True 109 | while self.runnable: 110 | self.process_sample() 111 | 112 | def stop(self): 113 | self.runnable = False 114 | if self.is_alive(): self.terminate() 115 | 116 | def __del__(self): 117 | self.stop() 118 | -------------------------------------------------------------------------------- /aleph/constants.py: -------------------------------------------------------------------------------- 1 | APP_TITLE = 'Aleph' 2 | CSRF_ENABLED = True 3 | BABEL_DEFAULT_TIMEZONE = 'UTC' 4 | BABEL_DEFAULT_LOCALE = 'en' 5 | LANGUAGES = { 6 | 'en': 'English', 7 | 'es': 'Spanish', 8 | 'pt-br': 'Brazilian Portuguese', 9 | } 10 | SAMPLE_STATUS_NEW = 0 11 | SAMPLE_STATUS_PROCESSING = 1 12 | SAMPLE_STATUS_PROCESSED = 2 13 | 14 | ACCOUNT_DISABLED=0 15 | ACCOUNT_ENABLED=1 16 | 17 | ACCOUNT_SUPERUSER=0 18 | ACCOUNT_PREMIUM=1 19 | ACCOUNT_REGULAR=2 20 | 21 | ITEMS_PER_PAGE = 15 22 | 23 | MIMETYPES_ARCHIVE = [ 24 | 'application/zip', 25 | 'application/gzip', 26 | 'application/x-gzip', 27 | 'application/x-rar', 28 | 'application/tar' 29 | ] -------------------------------------------------------------------------------- /aleph/datastore.py: -------------------------------------------------------------------------------- 1 | from elasticsearch import Elasticsearch, NotFoundError 2 | 3 | import logging 4 | 5 | from aleph.utils import dict_merge 6 | from aleph.settings import ELASTICSEARCH_URI, ELASTICSEARCH_INDEX, ELASTICSEARCH_TRACE, LOGGING 7 | 8 | class DataStore(object): 9 | 10 | es = None 11 | tracer = None 12 | 13 | def __init__(self): 14 | 15 | self.es = Elasticsearch(ELASTICSEARCH_URI) 16 | self.tracer = logging.getLogger('elasticsearch.trace') 17 | 18 | if ELASTICSEARCH_TRACE: 19 | self.tracer.setLevel(logging.DEBUG) 20 | self.tracer.addHandler(logging.FileHandler(LOGGING['filename'])) 21 | else: 22 | self.tracer.addHandler(logging.NullHandler()) 23 | 24 | def update(self, doc_id, partial_body): 25 | self.es.update(index=ELASTICSEARCH_INDEX, id=doc_id, doc_type='sample', body={'doc': partial_body }) 26 | 27 | def setup(self): 28 | self.es.indices.create(index=ELASTICSEARCH_INDEX, ignore=400) # Ignore already exists 29 | 30 | def count(self, q=None): 31 | 32 | if q: 33 | result = self.es.count(index=ELASTICSEARCH_INDEX, doc_type='sample', q=q) 34 | else: 35 | result = self.es.count(index=ELASTICSEARCH_INDEX, doc_type='sample') 36 | return result['count'] 37 | 38 | def all(self, size=10, start=0): 39 | try: 40 | result = self.es.search(index=ELASTICSEARCH_INDEX, doc_type='sample', body={ 41 | 'query': { 42 | 'match_all': {}, 43 | }, 44 | 'from': start, 45 | 'size': size, 46 | "sort": { 47 | "timestamp": { 48 | 'order': 'desc' 49 | }, 50 | } 51 | }) 52 | except NotFoundError: 53 | pass 54 | except Exception: 55 | raise 56 | 57 | return result 58 | 59 | 60 | def lucene_search(self, query, start=0, size=15): 61 | 62 | try: 63 | body = { 64 | "sort": { 65 | "timestamp": { 66 | 'order': 'desc' 67 | }, 68 | } 69 | } 70 | result = self.es.search(index=ELASTICSEARCH_INDEX, doc_type='sample', q=query, from_=start, size=size, body=body) 71 | except NotFoundError: 72 | pass 73 | except Exception: 74 | raise 75 | 76 | return result 77 | 78 | def search(self, query): 79 | 80 | result = [] 81 | 82 | try: 83 | result = self.es.search(index=ELASTICSEARCH_INDEX, doc_type='sample', body={'query': {'term': query } }) 84 | except NotFoundError: 85 | pass 86 | except Exception: 87 | raise 88 | 89 | return result 90 | 91 | def save(self, doc_data, doc_id): 92 | return self.merge_document('samples', 'sample', doc_data, doc_id) 93 | 94 | def get(self, doc_id): 95 | 96 | return self.es.get(index='samples', doc_type='sample', id=doc_id)['_source'] 97 | 98 | def merge_document(self, index, doc_type, doc_data, doc_id): 99 | 100 | try: 101 | self.es.indices.refresh(index) 102 | except Exception as e: 103 | raise IOError("Error updating ES index %s (%s)" % (index, e)) 104 | 105 | original_document = {} 106 | 107 | # Try to get current data if available 108 | try: 109 | original_document = self.es.get(index=index, doc_type=doc_type, id=doc_id) 110 | if 'hits' in original_document and original_document['hits']['total'] != 0: 111 | original_document = original_document['_source'] 112 | else: 113 | original_document = {} 114 | except NotFoundError as e: 115 | pass # not found, proceed 116 | except Exception as e: 117 | raise e 118 | 119 | if len(original_document) == 0 : 120 | return self.es.index(index, doc_type, doc_data, id=doc_id) 121 | 122 | # Merge and index 123 | merged_document = dict_merge(original_document, doc_data) 124 | 125 | return self.es.index(index=index, doc_type=doc_type, body=merged_document, id=doc_id) 126 | 127 | es = DataStore() 128 | -------------------------------------------------------------------------------- /aleph/plugins/email.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from aleph.base import PluginBase 4 | from aleph.settings import SAMPLE_TEMP_DIR 5 | import email, tempfile 6 | 7 | class EmailPlugin(PluginBase): 8 | 9 | name = 'email' 10 | default_options = { 'enabled': True } 11 | mimetypes = ['message/rfc822'] 12 | 13 | def process(self): 14 | 15 | with open(self.sample.path) as f: 16 | 17 | file_content = f.read() 18 | mail = email.message_from_string(file_content) 19 | 20 | # Get attachments 21 | for part in mail.walk(): 22 | if part.get_content_maintype() == 'multipart': 23 | continue 24 | if part.get('Content-Disposition') is None: 25 | continue 26 | filename = part.get_filename() 27 | 28 | if bool(filename): 29 | temp_file = tempfile.NamedTemporaryFile(dir=SAMPLE_TEMP_DIR, suffix='_%s' % filename, delete=False) 30 | temp_file.write(part.get_payload(decode=True)) 31 | self.create_sample(temp_file.name, filename) 32 | 33 | 34 | headers = [] 35 | for item in mail.items(): 36 | headers.append({'name': item[0], 'value': item[1]}) 37 | 38 | return { 39 | 'headers': headers, 40 | 'from': mail.get('From'), 41 | 'to': mail.get('To'), 42 | 'subject': mail.get('Subject'), 43 | } 44 | 45 | def setup(queue): 46 | plugin = EmailPlugin(queue) 47 | return plugin 48 | -------------------------------------------------------------------------------- /aleph/plugins/peinfo.py: -------------------------------------------------------------------------------- 1 | from aleph.base import PluginBase 2 | import pefile, sys, traceback, bitstring, string, hashlib, bz2 3 | import datetime, time 4 | 5 | class PEInfoPlugin(PluginBase): 6 | 7 | name = 'pe_info' 8 | default_options = { 'enabled': True } 9 | mimetypes = ['application/x-dosexec'] 10 | 11 | def process(self): 12 | 13 | try: 14 | pe = pefile.PE(self.sample.path, fast_load=True) 15 | pe.parse_data_directories( directories=[ 16 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'], 17 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT'], 18 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_TLS'], 19 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']]) 20 | 21 | data = {} 22 | 23 | # Get Architechture 24 | if pe.FILE_HEADER.Machine == 0x14C: # IMAGE_FILE_MACHINE_I386 25 | data['architechture'] = '32-bit' 26 | #data['pehash'] = self.pehash() # Temporarily disabled due problems 27 | self.sample.add_tag('i386') 28 | elif pe.FILE_HEADER.Machine == 0x8664: # IMAGE_FILE_MACHINE_AMD64 29 | data['architechture'] = '64-bit' 30 | self.sample.add_tag('amd64') 31 | else: 32 | data['architechture'] = 'N/A' 33 | 34 | # Executable Type 35 | self.sample.add_tag('dll' if pe.is_dll() else 'exe') 36 | if pe.is_driver(): 37 | self.sample.add_tag('driver') 38 | 39 | # Compilation time 40 | timestamp = pe.FILE_HEADER.TimeDateStamp 41 | 42 | if timestamp == 0: 43 | self.sample.add_tag('no-timestamp') 44 | else: 45 | data['compilation_timestamp'] = timestamp 46 | data['compilation_date'] = datetime.datetime.utcfromtimestamp(int(timestamp)).strftime('%Y-%m-%d %H:%M:%S') 47 | if (timestamp < 946692000): 48 | self.sample.add_tag('old-timestamp') 49 | elif (timestamp > time.time()): 50 | self.sample.add_tag('future-timestamp') 51 | 52 | #data['entry_point'] = pe.OPTIONAL_HEADER.AddressOfEntryPoint 53 | #data['image_base'] = pe.OPTIONAL_HEADER.ImageBase 54 | data['number_sections'] = pe.FILE_HEADER.NumberOfSections 55 | 56 | #check for ASLR, DEP/NX and SEH 57 | if pe.OPTIONAL_HEADER.DllCharacteristics > 0: 58 | if pe.OPTIONAL_HEADER.DllCharacteristics & 0x0040: 59 | data['aslr'] = True 60 | if pe.OPTIONAL_HEADER.DllCharacteristics & 0x0100: 61 | data['dep'] = True 62 | if (pe.OPTIONAL_HEADER.DllCharacteristics & 0x0400 63 | or (hasattr(pe, "DIRECTORY_ENTRY_LOAD_CONFIG") 64 | and pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct.SEHandlerCount > 0 65 | and pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct.SEHandlerTable != 0) 66 | or pe.FILE_HEADER.Machine == 0x8664): 67 | data['seh'] = True 68 | 69 | # Check imports 70 | if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'): 71 | imports = {} 72 | for lib in pe.DIRECTORY_ENTRY_IMPORT: 73 | imports[lib.dll] = [] 74 | for imp in lib.imports: 75 | if (imp.name != None) and (imp.name != ""): 76 | imports[lib.dll].append({'address': hex(imp.address), 'name': imp.name}) 77 | 78 | data['imports'] = imports 79 | 80 | # Check exports 81 | if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'): 82 | exports = [] 83 | for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols: 84 | exports.append({'address': hex(pe.OPTIONAL_HEADER.ImageBase + exp.address), 'name': exp.name, 'ordinal': exp.ordinal}) 85 | if exp.name == 'CPlApplet' and pe.is_dll(): 86 | self.sample.add_tag('cpl') 87 | 88 | data['exports'] = exports 89 | 90 | # Get sections 91 | if len(pe.sections) > 0: 92 | data['sections'] = [] 93 | for section in pe.sections: 94 | data['sections'].append({'name': section.Name.replace('\x00', ''), 'address': hex(section.VirtualAddress), 'virtual_size': hex(section.Misc_VirtualSize), 'raw_size': section.SizeOfRawData }) 95 | 96 | return data 97 | 98 | except Exception, e: 99 | print sys.exc_info()[0] 100 | print traceback.format_exc() 101 | self.logger.error('Cannot parse sample %s. Not PE?' % self.sample.uuid) 102 | raise 103 | 104 | #READ MORE AT 105 | #https://www.usenix.org/legacy/event/leet09/tech/full_papers/wicherski/wicherski_html/ 106 | def pehash(self): 107 | pe = pefile.PE(self.sample.path, fast_load=True) 108 | img_chars = bitstring.BitArray(hex(pe.FILE_HEADER.Characteristics)) 109 | img_chars = bitstring.BitArray(bytes = img_chars.tobytes()) 110 | img_chars_xor = img_chars[0:8] ^ img_chars[8:16] 111 | pehash_bin = bitstring.BitArray(img_chars_xor) 112 | sub_chars = bitstring.BitArray(hex(pe.FILE_HEADER.Machine)) 113 | sub_chars = bitstring.BitArray(bytes = sub_chars.tobytes()) 114 | sub_chars_xor = sub_chars[0:8] ^ sub_chars[8:16] 115 | pehash_bin.append(sub_chars_xor) 116 | stk_size = bitstring.BitArray(hex(pe.OPTIONAL_HEADER.SizeOfStackCommit)) 117 | stk_size_bits = string.zfill(stk_size.bin, 32) 118 | stk_size = bitstring.BitArray(bin=stk_size_bits) 119 | stk_size_xor = stk_size[8:16] ^ stk_size[16:24] ^ stk_size[24:32] 120 | stk_size_xor = bitstring.BitArray(bytes=stk_size_xor.tobytes()) 121 | pehash_bin.append(stk_size_xor) 122 | hp_size = bitstring.BitArray(hex(pe.OPTIONAL_HEADER.SizeOfHeapCommit)) 123 | hp_size_bits = string.zfill(hp_size.bin, 32) 124 | hp_size = bitstring.BitArray(bin=hp_size_bits) 125 | hp_size_xor = hp_size[8:16] ^ hp_size[16:24] ^ hp_size[24:32] 126 | hp_size_xor = bitstring.BitArray(bytes=hp_size_xor.tobytes()) 127 | pehash_bin.append(hp_size_xor) 128 | 129 | for section in pe.sections: 130 | sect_va = bitstring.BitArray(hex(section.VirtualAddress)) 131 | sect_va = bitstring.BitArray(bytes=sect_va.tobytes()) 132 | sect_va_bits = sect_va[8:32] 133 | pehash_bin.append(sect_va_bits) 134 | sect_rs = bitstring.BitArray(hex(section.SizeOfRawData)) 135 | sect_rs = bitstring.BitArray(bytes=sect_rs.tobytes()) 136 | sect_rs_bits = string.zfill(sect_rs.bin, 32) 137 | sect_rs = bitstring.BitArray(bin=sect_rs_bits) 138 | sect_rs = bitstring.BitArray(bytes=sect_rs.tobytes()) 139 | sect_rs_bits = sect_rs[8:32] 140 | pehash_bin.append(sect_rs_bits) 141 | sect_chars = bitstring.BitArray(hex(section.Characteristics)) 142 | sect_chars = bitstring.BitArray(bytes=sect_chars.tobytes()) 143 | sect_chars_xor = sect_chars[16:24] ^ sect_chars[24:32] 144 | pehash_bin.append(sect_chars_xor) 145 | address = section.VirtualAddress 146 | size = section.SizeOfRawData 147 | raw = pe.write()[address+size:] 148 | if size == 0: 149 | kolmog = bitstring.BitArray(float=1, length=32) 150 | pehash_bin.append(kolmog[0:8]) 151 | continue 152 | bz2_raw = bz2.compress(raw) 153 | bz2_size = len(bz2_raw) 154 | #k = round(bz2_size / size, 5) 155 | k = bz2_size / size 156 | kolmog = bitstring.BitArray(float=k, length=32) 157 | pehash_bin.append(kolmog[0:8]) 158 | m = hashlib.sha1() 159 | m.update(pehash_bin.tobytes()) 160 | return m.hexdigest() 161 | 162 | def setup(queue): 163 | plugin = PEInfoPlugin(queue) 164 | return plugin -------------------------------------------------------------------------------- /aleph/plugins/rararchive.py: -------------------------------------------------------------------------------- 1 | from rarfile import RarFile, RarExecError, BadRarFile 2 | from tempfile import mkdtemp 3 | from aleph.base import PluginBase 4 | import shutil, os, ntpath 5 | 6 | from aleph.settings import SAMPLE_TEMP_DIR, SAMPLE_MIN_FILESIZE 7 | 8 | class RarArchivePlugin(PluginBase): 9 | 10 | name = 'rararchive' 11 | default_options = { 'passwords': [ 'infected', 'evil', 'virus', 'malicious' ], 'enabled': True } 12 | mimetypes = ['application/x-rar'] 13 | 14 | def extract_file(self, path, dest, password=None): 15 | 16 | nl = [] 17 | 18 | with RarFile(str(path), 'r') as rarf: 19 | if password: 20 | rarf.setpassword(password) 21 | rarf.extractall(str(dest)) 22 | nl = rarf.namelist() 23 | 24 | return nl 25 | 26 | def process(self): 27 | 28 | temp_dir = mkdtemp(dir=SAMPLE_TEMP_DIR) 29 | 30 | self.options['passwords'].insert(0, None) # Append blank password 31 | current_password = None 32 | rar_contents = [] 33 | 34 | for password in set(self.options['passwords']): 35 | current_password = password 36 | try: 37 | self.logger.debug("Uncompressing file %s with password '%s'" % (self.sample.path, password)) 38 | rar_contents = self.extract_file(self.sample.path, temp_dir, password) 39 | 40 | for fname in rar_contents: 41 | fpath = os.path.join(temp_dir, fname).replace('\\', '/') 42 | if os.path.isfile(fpath) and os.stat(fpath).st_size >= SAMPLE_MIN_FILESIZE: 43 | head, tail = ntpath.split(fpath) 44 | self.create_sample(fpath, tail) 45 | shutil.rmtree(temp_dir) 46 | break # Stop bruting 47 | except (RarExecError, BadRarFile): 48 | continue # Invalid password 49 | 50 | ret = {} 51 | 52 | # Add general tags 53 | self.sample.add_tag('archive') 54 | self.sample.add_tag('rar') 55 | 56 | if len(rar_contents) == 0: 57 | self.logger.error('Unable to uncompress %s. Invalid password or corrupted file' % self.sample.path) 58 | self.sample.add_tag('corrupt') 59 | return ret 60 | 61 | if current_password: 62 | self.sample.add_tag('password-protected') 63 | ret['password'] = current_password 64 | 65 | ret['contents'] = rar_contents 66 | return ret 67 | 68 | def setup(queue): 69 | plugin = RarArchivePlugin(queue) 70 | return plugin 71 | -------------------------------------------------------------------------------- /aleph/plugins/strings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from aleph.base import PluginBase 4 | from aleph.constants import MIMETYPES_ARCHIVE 5 | import re 6 | 7 | class StringsPlugin(PluginBase): 8 | 9 | name = 'strings' 10 | default_options = { 'enabled': True } 11 | mimetypes_except = MIMETYPES_ARCHIVE + ['text/url'] 12 | 13 | all_regex = ur"[%s]{4,}" % r"A-Za-z0-9/\-:.,_$%'()[\]<> " 14 | url_regex = ur'(?i)\b((?:http[s]?:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))' 15 | filename_regex = r'\b([\w,%-.]+\.[A-Za-z]{3,4})\b' 16 | emailaddr_regex = r'((?:(?:[A-Za-z0-9]+_+)|(?:[A-Za-z0-9]+\-+)|(?:[A-Za-z0-9]+\.+)|(?:[A-Za-z0-9]+\++))*[A-Za-z0-9]+@(?:(?:\w+\-+)|(?:\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})' 17 | 18 | pattern_all = re.compile(all_regex) 19 | pattern_url = re.compile(url_regex, re.IGNORECASE) 20 | pattern_filename = re.compile(filename_regex) 21 | pattern_emailaddr = re.compile(emailaddr_regex) 22 | 23 | def process(self): 24 | 25 | with open(self.sample.path) as f: 26 | 27 | file_content = f.read().decode('latin1').encode('utf8') 28 | all_strings = [entry.strip() for entry in self.pattern_all.findall(file_content)] 29 | emailaddr_strings = [entry.strip() for entry in self.pattern_emailaddr.findall(file_content)] 30 | url_strings = [entry[0].strip() for entry in self.pattern_url.findall(file_content)] 31 | clean_content = file_content 32 | for token in url_strings+emailaddr_strings: 33 | clean_content = clean_content.replace(token, '') 34 | 35 | file_strings = [entry.strip() for entry in self.pattern_filename.findall(clean_content)] 36 | 37 | return { 38 | #'all': list(set(all_strings)), 39 | 'url': list(set(url_strings)), 40 | 'email': list(set(emailaddr_strings)), 41 | 'file': list(set(file_strings)), 42 | } 43 | 44 | def setup(queue): 45 | plugin = StringsPlugin(queue) 46 | return plugin 47 | -------------------------------------------------------------------------------- /aleph/plugins/targziparchive.py: -------------------------------------------------------------------------------- 1 | import tarfile 2 | from tempfile import mkdtemp 3 | from aleph.base import PluginBase 4 | import shutil, os, ntpath 5 | 6 | from aleph.settings import SAMPLE_TEMP_DIR 7 | 8 | class TarGzipArchivePlugin(PluginBase): 9 | 10 | name = 'archive_tar-gzip' 11 | default_options = { 'enabled': True } 12 | mimetypes = ['application/x-tar', 'application/gzip', 'application/x-gzip'] 13 | 14 | def extract_file(self, path, dest): 15 | 16 | nl = [] 17 | 18 | with tarfile.open(str(path), 'r') as tarf: 19 | tarf.extractall(str(dest)) 20 | nl = tarf.getnames() 21 | 22 | return nl 23 | 24 | def process(self): 25 | 26 | temp_dir = mkdtemp(dir=SAMPLE_TEMP_DIR) 27 | 28 | targzip_contents = [] 29 | 30 | self.logger.debug("Uncompressing gzip/tar file %s" % self.sample.path) 31 | targzip_contents = self.extract_file(self.sample.path, temp_dir) 32 | for fname in targzip_contents: 33 | fpath = os.path.join(temp_dir, fname) 34 | if os.path.isfile(fpath): 35 | head, tail = ntpath.split(fpath) 36 | self.create_sample(fpath, tail) 37 | shutil.rmtree(temp_dir) 38 | 39 | ret = {} 40 | 41 | if len(targzip_contents) == 0: 42 | self.logger.error('Unable to uncompress %s. Corrupted file?' % self.sample.path) 43 | return ret 44 | 45 | ret['contents'] = targzip_contents 46 | 47 | # Add general tags 48 | self.sample.add_tag('archive') 49 | self.sample.add_tag('tar-gzip') 50 | 51 | return ret 52 | 53 | def setup(queue): 54 | plugin = TarGzipArchivePlugin(queue) 55 | return plugin 56 | -------------------------------------------------------------------------------- /aleph/plugins/trid.py: -------------------------------------------------------------------------------- 1 | from aleph.base import PluginBase 2 | import subprocess, os 3 | import re 4 | 5 | class TrIDPlugin(PluginBase): 6 | 7 | name = 'trid' 8 | default_options = { 'enabled': False } 9 | required_options = [ 'trid_path', 'triddefs_path' ] 10 | 11 | def validate_options(self): 12 | super(TrIDPlugin, self).validate_options() 13 | 14 | if not os.access(self.options['trid_path'], os.X_OK): 15 | raise OSError('Cannot access TrID at %s' % self.trid_path) 16 | 17 | if not os.access(self.options['triddefs_path'], os.R_OK): 18 | raise OSError('Cannot access TrID definitions file at %s' % self.triddefs_path) 19 | 20 | def process(self): 21 | 22 | proc = subprocess.Popen([self.options['trid_path'], self.sample.path, '-d:%s' % self.options['triddefs_path']], stdout=subprocess.PIPE) 23 | output = proc.communicate()[0] 24 | 25 | if proc.returncode != 0: 26 | self.logger.error('Sample %s could not be parsed by TrID' % self.sample.uuid) 27 | return {} 28 | 29 | lines = output.split('\n') 30 | p = re.compile('^([0-9]{1,3}\.[0-9]%) \((\.[A-Z0-9]{3,4})\) (.*) \(([0-9].*)\)$') 31 | 32 | detections = [] 33 | 34 | for line in lines: 35 | line = line.strip() 36 | m = p.match(line) 37 | if m: 38 | detections.append({'description': m.group(3), 'extension': m.group(2), 'confidence': m.group(1)}) 39 | 40 | if len(detections) == 0: 41 | detections.append({'description': 'Unknown'}) 42 | 43 | return { 'detections': detections } 44 | 45 | def setup(queue): 46 | plugin = TrIDPlugin(queue) 47 | return plugin 48 | -------------------------------------------------------------------------------- /aleph/plugins/urlextractor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from aleph.base import PluginBase 4 | from aleph.settings import SAMPLE_TEMP_DIR 5 | from aleph.constants import MIMETYPES_ARCHIVE 6 | import tempfile, hashlib 7 | 8 | class UrlExtractorPlugin(PluginBase): 9 | 10 | name = 'urlextractor' 11 | default_options = { 'enabled': True } 12 | mimetypes_except = MIMETYPES_ARCHIVE + ['text/url'] 13 | depends = [ 'strings' ] 14 | 15 | def process(self): 16 | 17 | if not 'strings' in self.sample.data: 18 | return {} 19 | 20 | strs = self.sample.data['strings'] 21 | 22 | if 'url' in strs and len(strs['url']) > 0: 23 | for url in strs['url']: 24 | url_text = "[InternetShortcut]\nURL=%s" % url 25 | 26 | filename = "%s.url" % hashlib.sha256(url).hexdigest() 27 | 28 | temp_file = tempfile.NamedTemporaryFile(dir=SAMPLE_TEMP_DIR, suffix='_%s' % filename, delete=False) 29 | temp_file.write(url_text) 30 | temp_file.close() 31 | 32 | self.create_sample(temp_file.name, filename, mimetype="text/url") 33 | 34 | return { 35 | 'headers': mail.items(), 36 | 'from': mail.get('From'), 37 | 'to': mail.get('To'), 38 | 'subject': mail.get('Subject'), 39 | } 40 | 41 | def setup(queue): 42 | plugin = UrlExtractorPlugin(queue) 43 | return plugin 44 | -------------------------------------------------------------------------------- /aleph/plugins/urlparser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from aleph.base import PluginBase 4 | from aleph.settings import SAMPLE_TEMP_DIR 5 | import ConfigParser 6 | import urlparse, httplib 7 | from urllib import urlopen 8 | 9 | class UrlParserPlugin(PluginBase): 10 | 11 | name = 'urlparser' 12 | default_options = { 'enabled': True, 'probe_url': False, 'google_api_key': None } 13 | mimetypes = [ 'text/url' ] 14 | 15 | def google_safebrowsing(self, url): 16 | 17 | if not self.options['google_api_key']: 18 | return 'N/A' 19 | 20 | URL = "https://sb-ssl.google.com/safebrowsing/api/lookup?client=aleph-778&key={key}&appver=1.5.2&pver=3.1&url={url}" 21 | 22 | response = urlopen(URL.format(key=self.options['google_api_key'], url=url)).read().decode("utf8") 23 | 24 | if not response: 25 | return ['clean'] 26 | 27 | return response.split(',') 28 | 29 | def probe_url(self, url): 30 | 31 | urlparts = urlparse.urlparse(url, allow_fragments=True) 32 | if urlparts.scheme == 'https': 33 | conn = httplib.HTTPSConnection(urlparts.netloc) 34 | else: 35 | conn = httplib.HTTPConnection(urlparts.netloc) 36 | 37 | path = urlparts.path 38 | 39 | if urlparts.query: 40 | path += '?'+urlparts.query 41 | 42 | conn.request('HEAD', path) 43 | res = conn.getresponse() 44 | 45 | headers = dict(res.getheaders()) 46 | 47 | # If we are being redirected, spawn a new sample from new location 48 | if headers.has_key('location') and headers['location'] != url: 49 | url_text = "[InternetShortcut]\nURL=%s" % url 50 | 51 | filename = "%s.url" % hashlib.sha256(url).hexdigest() 52 | 53 | temp_file = tempfile.NamedTemporaryFile(dir=SAMPLE_TEMP_DIR, suffix='_%s' % filename, delete=False) 54 | temp_file.write(url_text).close() 55 | 56 | self.create_sample(temp_file.name, filename, mimetype="text/url") 57 | 58 | return { 59 | 'headers': headers, 60 | 'version': res.version, 61 | 'status': res.status 62 | } 63 | 64 | def process(self): 65 | 66 | config = ConfigParser.RawConfigParser() 67 | config.read(self.sample.path) 68 | 69 | url = config.get('InternetShortcut', 'URL') 70 | 71 | ret = { 72 | 'url': url, 73 | } 74 | 75 | # Google SafeBrowsing API (needs API key and SafeBrowsing API Activated on Project) 76 | # ref: https://developers.google.com/safe-browsing/lookup_guide#GettingStarted 77 | if self.options['google_api_key']: 78 | ret['google_safebrowsing'] = self.google_safebrowsing(url) 79 | 80 | # Probe URL for additional data 81 | if self.options['probe_url']: 82 | http_info = self.probe_url(url) 83 | ret['http_headers'] = http_info['headers'] 84 | ret['http_status'] = http_info['status'] 85 | ret['http_version'] = http_info['version'] 86 | 87 | return ret 88 | 89 | def setup(queue): 90 | plugin = UrlParserPlugin(queue) 91 | return plugin 92 | -------------------------------------------------------------------------------- /aleph/plugins/virustotal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from time import sleep 4 | from aleph.base import PluginBase 5 | from aleph.utils import in_string 6 | from aleph.constants import MIMETYPES_ARCHIVE 7 | from operator import itemgetter 8 | import virustotal 9 | 10 | class VirusTotalPlugin(PluginBase): 11 | 12 | name = 'virustotal' 13 | default_options = { 'api_limit': 7, 'retry_count': 3, 'retry_sleep': 10, 14 | 'report_sleep': 60, 'send_files' : True, 'enabled': False } 15 | required_options = [ 'api_key' ] 16 | mimetypes_except = MIMETYPES_ARCHIVE + ['text/url'] 17 | 18 | vt = None 19 | 20 | def setup(self): 21 | 22 | self.vt = virustotal.VirusTotal(self.options['api_key'],self.options['api_limit']) 23 | 24 | def process(self): 25 | 26 | count = int(self.options['retry_count']) 27 | 28 | for i in range(count): 29 | try: 30 | report = self.vt.get(self.sample.hashes['sha256']) 31 | 32 | if report is None: 33 | self.logger.info('Sample %s not found in VirusTotal' % self.sample.hashes['sha256']) 34 | if not self.options['send_files']: 35 | return { 'scan_id' : 'not found' } 36 | report = self.vt.scan(self.sample.path) 37 | sleep(self.options['report_sleep']) 38 | report.join() 39 | 40 | assert report.done() == True 41 | 42 | detections = [] 43 | for antivirus, malware in report: 44 | if malware is not None: 45 | 46 | self.parse_tags(malware) 47 | detections.append({'av': antivirus[0], 'version': antivirus[1], 'update': antivirus[2], 'result': malware}) 48 | 49 | if len(detections) > 0: 50 | self.sample.add_tag('malware') 51 | 52 | return { 53 | 'scan_id': report.id, 54 | 'positives': report.positives, 55 | 'total': report.total, 56 | 'detections': sorted(detections, key=itemgetter('av')), 57 | } 58 | 59 | except Exception, e: 60 | self.logger.warning('Error within VirusTotal API: %s (retrying in %s seconds [%d/%d])' % (str(e), self.options['retry_sleep'], i, count )) 61 | sleep(self.options['retry_sleep']) 62 | continue # silently ignore errors 63 | 64 | self.logger.error('Error within VirusTotal API: %s (failed - no more retries left)' % str(e)) 65 | return {} 66 | 67 | def parse_tags(self, malware_name): 68 | 69 | if in_string([ 'banker', 'banload' ], malware_name): 70 | self.sample.add_tag('banker') 71 | 72 | if in_string([ 'trojan' ], malware_name): 73 | self.sample.add_tag('trojan') 74 | 75 | 76 | def setup(queue): 77 | plugin = VirusTotalPlugin(queue) 78 | return plugin 79 | -------------------------------------------------------------------------------- /aleph/plugins/ziparchive.py: -------------------------------------------------------------------------------- 1 | from zipfile import ZipFile 2 | from tempfile import mkdtemp 3 | from aleph.base import PluginBase 4 | import shutil, os, ntpath 5 | 6 | from aleph.settings import SAMPLE_TEMP_DIR, SAMPLE_MIN_FILESIZE 7 | 8 | class ZipArchivePlugin(PluginBase): 9 | 10 | name = 'ziparchive' 11 | default_options = { 'passwords': [ 'infected', 'evil', 'virus', 'malicious' ], 'enabled': True } 12 | mimetypes = ['application/zip'] 13 | 14 | def extract_file(self, path, dest, password=None): 15 | nl = [] 16 | with ZipFile(str(path), 'r') as zipf: 17 | if password: 18 | zipf.setpassword(password) 19 | 20 | for member in zipf.infolist(): 21 | if member.file_size == 0: 22 | continue 23 | filename = unicode(member.filename, 'cp437').encode('utf8') 24 | source = zipf.open(member) 25 | if '/' in member.filename: 26 | try: 27 | os.makedirs(dest + '/' + member.filename[0:member.filename.rindex('/')]) 28 | except: 29 | pass 30 | target_file = os.path.join(dest, filename) 31 | target = file(target_file, 'wb') 32 | shutil.copyfileobj(source, target) 33 | nl.append(filename) 34 | return nl 35 | 36 | def process(self): 37 | 38 | temp_dir = mkdtemp(dir=SAMPLE_TEMP_DIR) 39 | 40 | self.options['passwords'].insert(0, '') # Append blank password 41 | current_password = None 42 | zip_contents = [] 43 | 44 | for password in set(self.options['passwords']): 45 | current_password = password 46 | try: 47 | self.logger.debug("Uncompressing file %s with password '%s'" % (self.sample.path, password)) 48 | zip_contents = self.extract_file(self.sample.path, temp_dir, password) 49 | 50 | for fname in zip_contents: 51 | fpath = os.path.join(temp_dir, fname) 52 | if os.path.isfile(fpath) and os.stat(fpath).st_size >= SAMPLE_MIN_FILESIZE: 53 | head, tail = ntpath.split(fpath) 54 | self.create_sample(fpath, tail) 55 | shutil.rmtree(temp_dir) 56 | break # Stop bruting 57 | except RuntimeError: 58 | continue # Invalid password 59 | except: 60 | break 61 | 62 | ret = {} 63 | 64 | # Add general tags 65 | self.sample.add_tag('archive') 66 | self.sample.add_tag('zip') 67 | 68 | if len(zip_contents) == 0: 69 | self.logger.error('Unable to uncompress %s. Invalid password or corrupted file' % self.sample.path) 70 | self.sample.add_tag('corrupt') 71 | return ret 72 | 73 | ret['contents'] = zip_contents 74 | 75 | if len(current_password) > 0: 76 | self.sample.add_tag('password-protected') 77 | ret['password'] = current_password 78 | 79 | return ret 80 | 81 | def setup(queue): 82 | plugin = ZipArchivePlugin(queue) 83 | return plugin 84 | -------------------------------------------------------------------------------- /aleph/settings.py.orig: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | 3 | SAMPLE_SOURCES = [ 4 | #('local', {'path': '/opt/aleph/unprocessed_samples'}), 5 | #('mail', {'host': 'imap.exampl.com', 'username': 'youruser@example.com', 'password': 'yourpassword', 'root_folder': 'Inbox' }) 6 | ] 7 | 8 | PLUGIN_SETTINGS = { 9 | #'virustotal': { 'enabled': True, 'api_key': '', 'api_limit': '7' } 10 | } 11 | 12 | ELASTICSEARCH_URI = '127.0.0.1:9200' 13 | ELASTICSEARCH_INDEX = 'samples' 14 | ELASTICSEARCH_TRACE = False 15 | 16 | SAMPLE_TEMP_DIR = '/opt/aleph/temp' 17 | SAMPLE_STORAGE_DIR = '/opt/aleph/samples' 18 | 19 | SAMPLE_MANAGERS=2 # Simultaneous sample analysis 20 | 21 | SAMPLE_MIN_FILESIZE=40 # bytes 22 | SAMPLE_MAX_FILESIZE=(1024*1024*30) # bytes 23 | 24 | # Mail Options 25 | MAIL_ENABLE = False 26 | MAIL_SERVER = 'smtp.googlemail.com' 27 | MAIL_PORT = 587 28 | MAIL_USE_TLS = True 29 | MAIL_USE_SSL = False 30 | MAIL_USERNAME = 'your-gmail-username' 31 | MAIL_PASSWORD = 'your-gmail-password' 32 | MAIL_SENDER = 'Your Name <%s>' % MAIL_USERNAME 33 | 34 | # WebUI Options 35 | import os 36 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), '../webui/db.sqlite' ) 37 | del os 38 | SERVER_NAME = 'localhost:5000' 39 | PREFERRED_URL_SCHEME = 'http' 40 | ALLOW_REGISTRATIONS = True 41 | SAMPLE_SUBMIT_FOLDER = None 42 | 43 | LOGGING = { 44 | 'directory': 'log/', 45 | 'filename': 'aleph.log', 46 | 'format': '%(asctime)s [%(name)s:%(funcName)s] %(levelname)s: %(message)s', 47 | } -------------------------------------------------------------------------------- /aleph/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from celery import Celery 3 | 4 | from aleph import SampleManager 5 | 6 | tasker = Celery('aleph', 7 | broker='amqp://', 8 | backend='amqp://', 9 | include=['aleph.tasks']) 10 | 11 | tasker.conf.update({ 12 | 'CELERYBEAT_SCHEDULE': { 13 | 'samplemanager-consume-sample': { 14 | 'task': 'tasks.sample_consume', 15 | 'schedule': timedelta(seconds=30), 16 | }, 17 | }, 18 | 'CELERY_TIMEZONE': 'UTC', 19 | 'CELERY_TASK_RESULT_EXPIRES': 3600, 20 | }) 21 | 22 | # Tasks 23 | 24 | @tasker.task 25 | def sample_consume(): 26 | 27 | sm = SampleManager() 28 | print sm 29 | 30 | 31 | if __name__ == '__main__': 32 | tasker.start() 33 | -------------------------------------------------------------------------------- /aleph/utils.py: -------------------------------------------------------------------------------- 1 | import os, sys, logging 2 | from copy import deepcopy 3 | from functools import partial 4 | 5 | CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) 6 | # Get path as a function - for PluginBase 7 | get_path = partial(os.path.join, CURRENT_DIR) 8 | 9 | def error(msg): 10 | # print help information and exit: 11 | logging.error(msg) 12 | sys.stderr.write(str(msg+"\n")) 13 | sys.exit(2) 14 | 15 | def dict_merge(target, *args): 16 | 17 | # Merge multiple dicts 18 | if len(args) > 1: 19 | for obj in args: 20 | dict_merge(target, obj) 21 | return target 22 | 23 | # Recursively merge dicts and set non-dict values 24 | obj = args[0] 25 | if not isinstance(obj, dict): 26 | return obj 27 | 28 | for k, v in obj.iteritems(): 29 | if k in target and isinstance(target[k], dict): 30 | dict_merge(target[k], v) 31 | else: 32 | target[k] = deepcopy(v) 33 | 34 | return target 35 | 36 | import pytz, datetime 37 | import dateutil.parser 38 | 39 | utc = pytz.utc 40 | 41 | def get_timezone_by_name(tz_name): 42 | 43 | try: 44 | timez = pytz.timezone(tz_name) 45 | return timez 46 | except Exception, e: 47 | return None 48 | 49 | def to_iso8601(when=None, tz=utc): 50 | if not when: 51 | when = datetime.datetime.now(tz) 52 | if not when.tzinfo: 53 | when = tz.localize(when) 54 | _when = when.strftime("%Y-%m-%dT%H:%M:%S.%f%z") 55 | return _when[:-8] + _when[-5:] # remove microseconds 56 | 57 | def from_iso8601(when=None, tz=utc): 58 | _when = dateutil.parser.parse(when) 59 | if not _when.tzinfo: 60 | _when = tz.localize(_when) 61 | return _when 62 | 63 | def in_string(tokens, string): 64 | return any(token in str(string).lower() for token in tokens) 65 | 66 | suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] 67 | def humansize(nbytes): 68 | if nbytes == 0: return '0 B' 69 | i = 0 70 | while nbytes >= 1024 and i < len(suffixes)-1: 71 | nbytes /= 1024. 72 | i += 1 73 | f = ('%.2f' % nbytes).rstrip('0').rstrip('.') 74 | return '%s %s' % (f, suffixes[i]) 75 | 76 | -------------------------------------------------------------------------------- /aleph/webui/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | from flask.ext.login import LoginManager, current_user 3 | from flask.ext.babel import Babel, format_datetime, lazy_gettext 4 | from flask.ext.mail import Mail 5 | 6 | import os 7 | 8 | app = Flask('aleph-webui') 9 | 10 | from aleph import settings, constants 11 | # Load configuration from file 12 | app.config.from_object(constants) 13 | app.config.from_object(settings) 14 | 15 | # Register i18n 16 | babel = Babel(app) 17 | 18 | # Register mail handler 19 | mail = Mail(app) 20 | 21 | # Login manager 22 | login_manager = LoginManager() 23 | login_manager.init_app(app) 24 | login_manager.login_message = lazy_gettext('Please log in to access this page.') 25 | 26 | # Import App Modules 27 | from aleph.webui.views import * 28 | from aleph.webui.database import db 29 | 30 | # Register blueprints 31 | app.register_blueprint(general.mod) 32 | app.register_blueprint(users.mod) 33 | app.register_blueprint(samples.mod) 34 | 35 | 36 | @app.teardown_request 37 | def remove_db(exception): 38 | db.session.remove() 39 | 40 | @app.errorhandler(401) 41 | def not_authorized(error): 42 | return render_template('401.html', hide_sidebar=True, hide_header=True, class_body='bg-black', class_html ='bg-black') 43 | 44 | @app.errorhandler(404) 45 | def not_found(error): 46 | return render_template('404.html', hide_sidebar=True, hide_header=True, class_body='bg-black', class_html ='bg-black') 47 | -------------------------------------------------------------------------------- /aleph/webui/database.py: -------------------------------------------------------------------------------- 1 | from aleph.webui import app 2 | from flask.ext.sqlalchemy import SQLAlchemy 3 | 4 | db = SQLAlchemy(app) 5 | -------------------------------------------------------------------------------- /aleph/webui/email.py: -------------------------------------------------------------------------------- 1 | from flask.ext.mail import Message 2 | from aleph.webui import mail 3 | from . import app 4 | import logging 5 | 6 | logger = logging.getLogger('Email') 7 | 8 | def send_email(subject, sender, recipients, text_body, html_body): 9 | if not app.config.get('MAIL_ENABLE'): 10 | if app.config.get('DEBUG'): 11 | logger.debug('Mail functions are disabled. Check MAIL_ENABLE config option.') 12 | return False 13 | 14 | try: 15 | msg = Message(subject, sender = sender, recipients = recipients) 16 | msg.body = text_body 17 | msg.html = html_body 18 | mail.send(msg) 19 | return True 20 | except Exception, e: 21 | logger.error('Error sending email: %s' % str(e)) 22 | return False 23 | -------------------------------------------------------------------------------- /aleph/webui/forms.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form 2 | from wtforms import TextField, BooleanField, SelectField, PasswordField, FileField 3 | from wtforms.validators import Required, Length, Email, EqualTo, Regexp, NumberRange, Optional 4 | from flask.ext.babel import gettext 5 | from flask import g 6 | from aleph.constants import LANGUAGES, ACCOUNT_REGULAR, ACCOUNT_PREMIUM, ACCOUNT_SUPERUSER 7 | from pytz import common_timezones 8 | 9 | class LoginForm(Form): 10 | username = TextField(gettext('Username'), validators = [Required()]) 11 | password = TextField(gettext('Password'), validators = [Required()]) 12 | remember_me = BooleanField('remember_me', default = False) 13 | 14 | class BasicUserForm(Form): 15 | first_name = TextField(gettext('First Name'), validators = [Length(min=2, max=30, message=gettext('First name must be between 2 and 30 characters long'))]) 16 | last_name = TextField(gettext('Last Name'), validators = [Length(min=2, max=255, message=gettext('Last name must be between 2 and 255 characters long'))]) 17 | 18 | email = TextField(gettext('Email'), validators = [Required(), Email()]) 19 | 20 | langs = [] 21 | for langcode, langname in LANGUAGES.iteritems(): 22 | langs.append((langcode, langname)) 23 | 24 | timezones = [] 25 | for timezone in common_timezones: 26 | timezones.append((timezone, timezone)) 27 | 28 | locale = SelectField(gettext('Language'), choices=langs, validators = [Optional()]) 29 | timezone = SelectField(gettext('Timezone'), choices=timezones, default='UTC', validators = [Optional()]) 30 | 31 | active = BooleanField(gettext('Account enabled'), default=True, validators = []) 32 | 33 | acc_types = [ 34 | (ACCOUNT_REGULAR, gettext('Regular account')), 35 | (ACCOUNT_PREMIUM, gettext('Premium account')), 36 | (ACCOUNT_SUPERUSER, gettext('Superuser account')), 37 | ] 38 | 39 | account_type = SelectField(gettext('Account Type'), coerce=int, default=ACCOUNT_REGULAR, choices=acc_types, validators = []) 40 | 41 | token = TextField(gettext('Token'), []) 42 | api_key = TextField(gettext('API Key'), []) 43 | 44 | 45 | class UserForm(BasicUserForm): 46 | login = TextField(gettext('Username'), validators = [Required(), Regexp(regex='^\w+$', message=gettext('Only alphanumeric characters valid'))]) 47 | 48 | class NewUserForm(UserForm): 49 | 50 | password = PasswordField(gettext('Password'), validators = [ 51 | Required(), 52 | EqualTo('confirm', message=gettext('Passwords must match')) 53 | ]) 54 | confirm = PasswordField(gettext('Confirm password'), validators = [Required()]) 55 | 56 | class ChangePasswordForm(Form): 57 | current_password = PasswordField(gettext('Current password'), validators = [Required()]) 58 | password = PasswordField(gettext('Password'), validators = [ 59 | Required(), 60 | EqualTo('confirm', message=gettext('Passwords must match')) 61 | ]) 62 | confirm = PasswordField(gettext('Confirm password'), validators = [Required()]) 63 | 64 | class SubmitSampleForm(Form): 65 | 66 | sample = FileField('Sample', validators=[Required()]) 67 | -------------------------------------------------------------------------------- /aleph/webui/models.py: -------------------------------------------------------------------------------- 1 | import hashlib, base64, hmac, random, urllib, datetime 2 | from aleph.webui import app 3 | from aleph.webui.database import db 4 | from aleph.constants import ACCOUNT_REGULAR, ACCOUNT_PREMIUM, ACCOUNT_SUPERUSER, ACCOUNT_ENABLED 5 | 6 | class Submission(db.Model): 7 | 8 | id = db.Column('id', db.Integer, primary_key=True) 9 | sample_uuid = db.Column(db.String(255)) 10 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 11 | filename = db.Column(db.String(), nullable=False) 12 | file_hash = db.Column(db.String(255), nullable=False) 13 | timestamp = db.Column(db.DateTime, nullable=False) 14 | 15 | def __init__(self): 16 | 17 | self.timestamp = datetime.datetime.utcnow() 18 | 19 | class User(db.Model): 20 | 21 | id = db.Column('id', db.Integer, primary_key=True) 22 | 23 | first_name = db.Column(db.String(30)) 24 | last_name = db.Column(db.String(255)) 25 | 26 | login = db.Column('login', db.String(16), nullable=False, unique = True) 27 | email = db.Column(db.String(255), nullable=False, unique = True) 28 | password = db.Column(db.String(200), nullable=False) 29 | 30 | locale = db.Column(db.String(4), default = app.config.get('BABEL_DEFAULT_LOCALE')) 31 | timezone = db.Column(db.String(32), default = app.config.get('BABEL_DEFAULT_TIMEZONE')) 32 | 33 | account_type = db.Column(db.Integer, default = ACCOUNT_REGULAR) 34 | active = db.Column(db.Integer, default = ACCOUNT_ENABLED) 35 | 36 | token = db.Column(db.String(255), unique = True) 37 | api_key = db.Column(db.String(255), unique = True) 38 | 39 | submissions = db.relationship('Submission', backref='user', lazy='dynamic') 40 | 41 | def generate_token(self, hashalg=hashlib.sha256, bits=32): 42 | 43 | salt = str(random.getrandbits(bits)) 44 | signature = hmac.new(app.config.get('SECRET_KEY'), msg=salt, digestmod=hashalg).digest() 45 | 46 | encodedSignature = base64.encodestring(signature).replace('\n', '').replace('=', '') 47 | 48 | return encodedSignature 49 | 50 | def __init__(self, login, email, password, active = ACCOUNT_ENABLED): 51 | self.login = login 52 | self.email = email 53 | self.password = password 54 | self.active = active 55 | 56 | if not self.active: 57 | self.token = self.generate_token() 58 | 59 | self.api_key = self.generate_token(hashalg=hashlib.sha512) 60 | 61 | def is_active(self): 62 | return (self.active == ACCOUNT_ENABLED) 63 | 64 | def is_authenticated(self): 65 | return True 66 | 67 | def is_anonymous(self): 68 | return False 69 | 70 | def get_id(self): 71 | try: 72 | return unicode(self.id) 73 | except AttributeError: 74 | raise NotImplementedError('No `id` attribute - override `get_id`') 75 | 76 | def to_json(self): 77 | return dict(name=self.name, is_admin=self.is_admin) 78 | 79 | @property 80 | def is_premium(self): 81 | return (self.account_type == ACCOUNT_PREMIUM) 82 | 83 | @property 84 | def is_admin(self): 85 | return (self.account_type == ACCOUNT_SUPERUSER) 86 | 87 | def __eq__(self, other): 88 | return type(self) is type(other) and self.id == other.id 89 | 90 | def __ne__(self, other): 91 | return not self.__eq__(other) 92 | 93 | class AnonymousUser(object): 94 | ''' 95 | This is the default object for representing an anonymous user. 96 | ''' 97 | def is_authenticated(self): 98 | return False 99 | 100 | def is_active(self): 101 | return False 102 | 103 | def is_anonymous(self): 104 | return True 105 | 106 | def get_id(self): 107 | return 108 | 109 | -------------------------------------------------------------------------------- /aleph/webui/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from aleph.webui import app 3 | from hashlib import sha256, sha512 4 | from pygeoip import GeoIP 5 | 6 | def hash_password(username, password): 7 | 8 | salt = sha512(app.secret_key+username).hexdigest() 9 | return sha256(password+salt).hexdigest() 10 | 11 | def geoip(): 12 | 13 | db_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../../', 'webui/resources/GeoLiteCity.dat') 14 | return GeoIP(db_path) 15 | -------------------------------------------------------------------------------- /aleph/webui/views/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 'general', 'users', 'samples' ] 2 | 3 | from aleph.webui import babel, app 4 | from aleph.utils import from_iso8601, humansize 5 | from flask import request, g 6 | from aleph.webui.utils import geoip 7 | 8 | @babel.localeselector 9 | def get_locale(): 10 | 11 | user = getattr(g, 'user', None) 12 | if user is not None and hasattr(user, 'locale'): 13 | if '-' in user.locale: 14 | return user.locale.split('-')[0] 15 | else: 16 | return user.locale 17 | 18 | locale = request.accept_languages.best_match(app.config.get('LANGUAGES').keys()) 19 | 20 | if '-' in locale: 21 | return locale.split('-')[0] 22 | 23 | return locale 24 | 25 | @babel.timezoneselector 26 | def get_timezone(): 27 | user = getattr(g, 'user', None) 28 | if user is not None and hasattr(user, 'timezone'): 29 | return user.timezone 30 | 31 | # Try to guess by Geo IP 32 | geo = geoip() 33 | tz = geo.time_zone_by_addr(request.remote_addr) 34 | if tz: 35 | return tz 36 | 37 | # Custom Filters 38 | def filter_strtoutc(value): 39 | return from_iso8601(value) 40 | 41 | app.jinja_env.filters['str2utc'] = filter_strtoutc 42 | app.jinja_env.filters['humansize'] = humansize 43 | -------------------------------------------------------------------------------- /aleph/webui/views/general.py: -------------------------------------------------------------------------------- 1 | from flask.ext.login import login_required, current_user 2 | from flask.ext.babel import gettext 3 | from elasticsearch import TransportError 4 | from flask import Blueprint, render_template, flash 5 | 6 | from aleph.constants import SAMPLE_STATUS_NEW, SAMPLE_STATUS_PROCESSING, SAMPLE_STATUS_PROCESSED 7 | from aleph.datastore import es 8 | from aleph.webui.views.samples import update_submissions 9 | from aleph.webui.models import Submission 10 | 11 | mod = Blueprint('general', __name__) 12 | 13 | @mod.route('/') 14 | @mod.route('/index') 15 | @login_required 16 | def index(): 17 | 18 | samples = [] 19 | sample_count = { 20 | 'total': 0, 21 | 'new': 0, 22 | 'processing': 0, 23 | 'ready': 0, 24 | } 25 | submissions = [] 26 | submission_samples = [] 27 | 28 | try: 29 | # Overall Counts 30 | sample_count = { 31 | 'total': es.count(), 32 | 'new': es.count('status:%d' % SAMPLE_STATUS_NEW), 33 | 'processing': es.count('status:%d' % SAMPLE_STATUS_PROCESSING), 34 | 'ready': es.count('status:%d' % SAMPLE_STATUS_PROCESSED), 35 | } 36 | 37 | # Latest submissions 38 | update_submissions(current_user.id) 39 | submissions = current_user.submissions.order_by(Submission.timestamp.desc()).limit(15).all() 40 | submission_samples = {} 41 | 42 | for item in submissions: 43 | if item.sample_uuid: 44 | submission_samples[item.sample_uuid] = es.get(item.sample_uuid) 45 | 46 | # Latest samples 47 | if sample_count['total'] > 0: 48 | es_samples = es.all(size=15) 49 | 50 | for s in es_samples['hits']['hits']: 51 | samples.append(s['_source']) 52 | 53 | except TransportError: 54 | flash(gettext('Error querying ElasticSearch database. Please check configuration.')) 55 | 56 | return render_template('general/index.html', sample_count=sample_count, submissions=submissions, submission_samples=submission_samples, samples=samples) 57 | -------------------------------------------------------------------------------- /aleph/webui/views/samples.py: -------------------------------------------------------------------------------- 1 | from flask.ext.babel import gettext 2 | from flask.ext.login import login_required, current_user 3 | from flask import Blueprint, render_template, request, g, abort, flash, redirect, url_for 4 | 5 | from werkzeug import secure_filename 6 | 7 | from elasticsearch import TransportError 8 | from sqlalchemy import and_ 9 | 10 | from math import ceil 11 | import hashlib, os 12 | 13 | from aleph.datastore import es 14 | from aleph.utils import humansize 15 | from aleph.webui import app 16 | from aleph.webui.forms import SubmitSampleForm 17 | from aleph.webui.models import Submission, User 18 | from aleph.webui.database import db 19 | 20 | 21 | mod = Blueprint('samples', __name__, url_prefix='/samples') 22 | 23 | @mod.route('/') 24 | @mod.route('/index') 25 | @mod.route('/index/') 26 | @login_required 27 | def index(page = 1): 28 | 29 | samples = [] 30 | es_samples = None 31 | page_offset = (app.config.get('ITEMS_PER_PAGE')*(page-1)) 32 | sample_count = 0 33 | 34 | try: 35 | if 'search' in request.args: 36 | # @@ FIXME <- INJECTION PRONE!!! 37 | query = request.args['search'] 38 | sample_count = es.count(query) 39 | if sample_count > 0: 40 | es_samples = es.lucene_search(query, start=page_offset, size=app.config.get('ITEMS_PER_PAGE')) 41 | else: 42 | sample_count = es.count() 43 | if sample_count > 0: 44 | es_samples = es.all(size=app.config.get('ITEMS_PER_PAGE'), start=page_offset) 45 | 46 | for s in es_samples['hits']['hits']: 47 | samples.append(s['_source']) 48 | except TransportError: 49 | flash(gettext('Error querying ElasticSearch database. Please check configuration.')) 50 | 51 | page_count = int(ceil(sample_count/app.config.get('ITEMS_PER_PAGE'))) 52 | if page_count <= 1: 53 | page_count = 1 54 | pages = range(1, page_count+1) 55 | return render_template('samples/index.html', sample_count=sample_count, samples=samples, pages=pages, page=page) 56 | 57 | 58 | @mod.route('/view/') 59 | @login_required 60 | def view(uuid): 61 | 62 | g.current_lang = 'pt' 63 | try: 64 | sample = es.get(uuid) 65 | xrefs = sample['xrefs'] 66 | xrefs_objs = {} 67 | for relation, rel_xrefs in xrefs.iteritems(): 68 | for xref in rel_xrefs: 69 | xrefs_objs[xref] = es.get(xref) 70 | 71 | return render_template('samples/view.html', sample=sample, xrefs_info=xrefs_objs) 72 | except TransportError, e: 73 | abort(404) 74 | 75 | def validate_submission(form): 76 | 77 | if not os.access(app.config.get('SAMPLE_SUBMIT_FOLDER'), os.W_OK): 78 | flash(gettext('Cannot write to folder: %(folderpath)s', folderpath=app.config.get('SAMPLE_SUBMIT_FOLDER'))) 79 | return False 80 | 81 | if form.sample.name not in request.files: 82 | flash(gettext('No file submitted.')) 83 | return False 84 | 85 | fh = request.files[form.sample.name] 86 | filename = secure_filename(fh.filename) 87 | file_data = fh.read() 88 | file_size = len(file_data) 89 | 90 | # Check size limits 91 | if file_size > app.config.get('SAMPLE_MAX_FILESIZE'): 92 | flash(gettext('Sample %(samplename)s (%(samplesize)s) is bigger than maximum file size allowed: %(maxsize)s', samplename=filename, samplesize=humansize(file_size), maxisze=humansize(app.config.get('SAMPLE_MAX_FILESIZE')))) 93 | return False 94 | 95 | if file_size < app.config.get('SAMPLE_MIN_FILESIZE'): 96 | flash(gettext('Sample %(samplename)s (%(samplesize)s) is smaller than minimum file size allowed: %(minsize)s', samplename=filename, samplesize=humansize(file_size), minsize=humansize(app.config.get('SAMPLE_MIN_FILESIZE')))) 97 | return False 98 | 99 | file_hash = hashlib.sha256(file_data).hexdigest() 100 | 101 | # Check if already submitted by this user 102 | exists = Submission.query.filter(and_(User.id == current_user.id, Submission.file_hash == file_hash)).first() 103 | if exists: 104 | flash(gettext('File already submitted.')) 105 | return False 106 | 107 | return True 108 | 109 | @mod.route('/submit', methods=['GET', 'POST']) 110 | @login_required 111 | def submit(): 112 | 113 | if not app.config.get('SAMPLE_SUBMIT_FOLDER'): 114 | if app.config.get('DEBUG'): 115 | flash(gettext('SAMPLE_SUBMIT_FOLDER is not set')) 116 | return redirect(url_for('samples.index')) 117 | else: 118 | abort('404') 119 | 120 | form = SubmitSampleForm() 121 | 122 | if form.validate_on_submit(): 123 | 124 | if not validate_submission(form): 125 | return render_template('samples/submit.html', form=form) 126 | else: 127 | fh = request.files[form.sample.name] 128 | fh.seek(0, 0) 129 | file_hash = hashlib.sha256(fh.read()).hexdigest() 130 | filename = secure_filename(fh.filename) 131 | 132 | submission = Submission() 133 | submission.user = current_user 134 | submission.filename = filename 135 | submission.file_hash = file_hash 136 | 137 | db.session.add(submission) 138 | db.session.commit() 139 | 140 | fh.seek(0, 0) 141 | fh.save(os.path.join(app.config.get('SAMPLE_SUBMIT_FOLDER'), filename)) 142 | 143 | flash(gettext('Sample submitted successfully')) 144 | return redirect(url_for('samples.submissions')) 145 | 146 | return render_template('samples/submit.html', form=form) 147 | 148 | def update_submissions(user_id): 149 | 150 | try: 151 | user = User.query.get(user_id) 152 | pending = user.submissions.filter(Submission.sample_uuid == None).all() 153 | for row in pending: 154 | result = es.search({"hashes.sha256": row.file_hash}) 155 | exists = ('hits' in result and result['hits']['total'] != 0) 156 | if exists: 157 | data = result['hits']['hits'][0]['_source'] 158 | row.sample_uuid = data['uuid'] 159 | db.session.add(row) 160 | 161 | db.session.commit() 162 | except TransportError: 163 | flash(gettext('Error querying ElasticSearch database. Please check configuration.')) 164 | 165 | 166 | @mod.route('/submissions') 167 | @mod.route('/submissions//') 168 | @login_required 169 | def submissions(page = 1): 170 | 171 | if not app.config.get('SAMPLE_SUBMIT_FOLDER'): 172 | if app.config.get('DEBUG'): 173 | flash(gettext('SAMPLE_SUBMIT_FOLDER is not set')) 174 | return redirect(url_for('samples.index')) 175 | else: 176 | abort('404') 177 | 178 | update_submissions(current_user.id) 179 | 180 | samples = {} 181 | submissions = current_user.submissions.paginate(page, app.config.get('ITEMS_PER_PAGE')) 182 | for item in submissions.items: 183 | if item.sample_uuid: 184 | samples[item.sample_uuid] = es.get(item.sample_uuid) 185 | 186 | return render_template('samples/submissions.html', submissions=submissions, samples=samples) 187 | 188 | 189 | -------------------------------------------------------------------------------- /aleph/webui/views/users.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, session, redirect, url_for, request, flash, g, abort 2 | from flask.ext.login import login_user, logout_user, current_user, login_required 3 | from flask.ext.babel import gettext, get_locale, get_timezone 4 | 5 | from aleph.webui import app, login_manager 6 | from aleph.webui.utils import hash_password 7 | from aleph.webui.email import send_email 8 | from aleph.webui.database import db 9 | from aleph.webui.models import User, AnonymousUser 10 | from aleph.webui.forms import LoginForm, NewUserForm, UserForm, BasicUserForm, ChangePasswordForm 11 | from aleph.constants import ACCOUNT_DISABLED, ACCOUNT_ENABLED 12 | 13 | from sqlalchemy import and_ 14 | from sqlalchemy.exc import OperationalError 15 | 16 | 17 | mod = Blueprint('users', __name__, url_prefix='/users') 18 | 19 | @mod.route('/') 20 | @mod.route('/index') 21 | @mod.route('/index/') 22 | @login_required 23 | def index(page = 1): 24 | 25 | if not current_user.is_admin: 26 | abort(401) 27 | 28 | users = User.query.paginate(1, app.config.get('ITEMS_PER_PAGE')) 29 | 30 | return render_template('users/index.html', users=users) 31 | 32 | @mod.route('/enable/') 33 | @login_required 34 | def enable(user_id): 35 | 36 | if not current_user.is_admin: 37 | abort(401) 38 | 39 | user = User.query.get(user_id) 40 | 41 | if not user: 42 | abort(404) 43 | 44 | user.active = ACCOUNT_ENABLED 45 | 46 | db.session.add(user) 47 | db.session.commit() 48 | flash(gettext('Account enabled')) 49 | return redirect(url_for('users.index')) 50 | 51 | @mod.route('/disable/') 52 | @login_required 53 | def disable(user_id): 54 | 55 | if not current_user.is_admin: 56 | abort(401) 57 | 58 | user = User.query.get(user_id) 59 | 60 | if not user: 61 | abort(404) 62 | 63 | user.active = ACCOUNT_DISABLED 64 | user.token = None 65 | 66 | db.session.add(user) 67 | db.session.commit() 68 | flash(gettext('Account disabled')) 69 | return redirect(url_for('users.index')) 70 | 71 | @mod.route('/add', methods=['POST','GET']) 72 | @login_required 73 | def add(): 74 | 75 | if not current_user.is_admin: 76 | abort(401) 77 | 78 | form = NewUserForm() 79 | 80 | if form.validate_on_submit(): 81 | 82 | user = User( 83 | login=form.login.data, 84 | email=form.email.data, 85 | password = hash_password(form.login.data, form.password.data) 86 | ) 87 | 88 | user.active = form.active.data 89 | user.account_type = form.account_type.data 90 | 91 | user.locale = form.locale.data 92 | user.timezone = form.timezone.data 93 | 94 | user.first_name = form.first_name.data 95 | user.last_name = form.last_name.data 96 | 97 | db.session.add(user) 98 | db.session.commit() 99 | 100 | flash(gettext(u'User added successfully')) 101 | 102 | return redirect(url_for('users.index')) 103 | else: 104 | form.timezone.data = str(get_timezone()) 105 | form.locale.data = str(get_locale()) 106 | 107 | return render_template('users/new.html', form=form) 108 | 109 | @mod.route('/edit/', methods=['POST','GET']) 110 | @login_required 111 | def edit(user_id): 112 | 113 | if not current_user.is_admin: 114 | abort(401) 115 | 116 | user = User.query.get(user_id) 117 | if not user: 118 | abort(404) 119 | 120 | form = UserForm() 121 | 122 | if form.validate_on_submit(): 123 | user.email = form.email.data 124 | user.login = form.login.data 125 | 126 | user.active = form.active.data 127 | user.account_type = form.account_type.data 128 | 129 | user.locale = form.locale.data 130 | user.timezone = form.timezone.data 131 | 132 | user.first_name = form.first_name.data 133 | user.last_name = form.last_name.data 134 | 135 | user.token = form.token.data 136 | user.api_key = form.api_key.data 137 | 138 | db.session.add(user) 139 | db.session.commit() 140 | 141 | flash(gettext(u'User updated successfully')) 142 | return redirect(url_for('users.index')) 143 | 144 | if request.method == 'GET': 145 | form.login.data = user.login 146 | form.email.data = user.email 147 | form.first_name.data = user.first_name 148 | form.last_name.data = user.last_name 149 | form.locale.data = user.locale 150 | form.timezone.data = user.timezone 151 | form.account_type.data = user.account_type 152 | form.active.data = user.active 153 | form.api_key.data = user.api_key 154 | form.token.data = user.token 155 | 156 | return render_template('users/edit.html', form=form, user=user) 157 | 158 | @mod.route('/settings', methods=['POST','GET']) 159 | @login_required 160 | def settings(): 161 | 162 | form = BasicUserForm() 163 | 164 | if form.validate_on_submit(): 165 | current_user.email = form.email.data 166 | 167 | current_user.locale = form.locale.data 168 | current_user.timezone = form.timezone.data 169 | 170 | current_user.first_name = form.first_name.data 171 | current_user.last_name = form.last_name.data 172 | 173 | db.session.add(current_user) 174 | db.session.commit() 175 | 176 | flash(gettext(u'Settings updated successfully')) 177 | return redirect(url_for('general.index')) 178 | 179 | if request.method == 'GET': 180 | form.email.data = current_user.email 181 | form.first_name.data = current_user.first_name 182 | form.last_name.data = current_user.last_name 183 | form.locale.data = current_user.locale 184 | form.timezone.data = current_user.timezone 185 | 186 | return render_template('users/settings.html', form=form, user=current_user) 187 | 188 | @mod.route('/change_password/', methods=['POST','GET']) 189 | @login_required 190 | def changepw(user_id): 191 | 192 | if not current_user.is_admin and user_id != current_user.id: 193 | abort(401) 194 | 195 | user = User.query.get(user_id) 196 | 197 | if not user: 198 | abort(404) 199 | 200 | form = ChangePasswordForm() 201 | 202 | if form.validate_on_submit(): 203 | current_pw = hash_password(user.login, form.current_password.data) 204 | if current_pw != user.password: 205 | flash(gettext('Current password doesn\'t match')) 206 | else: 207 | user.password = hash_password(user.login, form.password.data) 208 | db.session.add(user) 209 | db.session.commit() 210 | flash(gettext('Password changed successfully')) 211 | 212 | if current_user.is_admin: 213 | return redirect(url_for('users.index')) 214 | else: 215 | return redirect(url_for('general.index')) 216 | 217 | return render_template('users/change_password.html', form=form, user=user) 218 | 219 | @mod.route('/register', methods=['POST','GET']) 220 | def register(): 221 | 222 | if not app.config.get('ALLOW_REGISTRATIONS'): 223 | abort(404) 224 | 225 | form = NewUserForm() 226 | 227 | if form.validate_on_submit(): 228 | 229 | try: 230 | exists = User.query.filter(User.email == form.email.data).first() 231 | 232 | if exists: 233 | flash(gettext('Email address already registered')) 234 | else: 235 | user = User( 236 | login=form.login.data, 237 | email=form.email.data, 238 | password = hash_password(form.login.data, form.password.data), 239 | active = ACCOUNT_DISABLED 240 | ) 241 | 242 | user.first_name = form.first_name.data 243 | user.last_name = form.last_name.data 244 | 245 | user.locale = str(get_locale()) 246 | user.timezone = str(get_timezone()) 247 | 248 | db.session.add(user) 249 | db.session.commit() 250 | 251 | # Send email 252 | sent = send_email( 253 | gettext('Welcome to %(appname)s', appname = app.config.get('APP_TITLE')), 254 | app.config.get('MAIL_SENDER'), 255 | [form.email.data], 256 | render_template('users/mail_register.txt', user=user), 257 | render_template('users/mail_register.html', user=user), 258 | ) 259 | 260 | if sent: 261 | flash(gettext('Account created successfully. Please check your email for instructions on activating your account')) 262 | else: 263 | flash(gettext('Account created successfully but there were server-side errors while sending the email activation code. Your account needs to be manually activated.')) 264 | 265 | return redirect(url_for('users.login')) 266 | 267 | except OperationalError: 268 | if app.config.get('DEBUG'): 269 | flash(gettext('Error creating user. Database not set')) 270 | return redirect(url_for('users.login')) 271 | else: 272 | abort(500) 273 | 274 | return render_template('users/register.html', form=form, hide_sidebar=True, hide_header=True, class_body='bg-black', class_html ='bg-black') 275 | 276 | @mod.route('/activate/') 277 | def activate(token): 278 | 279 | if not app.config.get('ALLOW_REGISTRATIONS'): 280 | abort(404) 281 | 282 | user = User.query.filter(and_(User.active == ACCOUNT_DISABLED, User.token == token)).first() 283 | 284 | if not user: 285 | abort(404) 286 | 287 | user.active = ACCOUNT_ENABLED 288 | user.token = None 289 | 290 | db.session.add(user) 291 | db.session.commit() 292 | 293 | flash(gettext('Account activated. Please login below')) 294 | return redirect(url_for('users.login')) 295 | 296 | @mod.route('/login', methods=['POST', 'GET']) 297 | def login(): 298 | 299 | if g.user is not None and g.user.is_authenticated(): 300 | return redirect(url_for('general.index')) 301 | 302 | form = LoginForm() 303 | 304 | if form.validate_on_submit(): 305 | 306 | # Validate User 307 | user = User.query.filter(User.login == form.username.data).first() 308 | if not user: 309 | flash(gettext('Invalid credentials'), 'danger') 310 | else: 311 | password = hash_password(form.username.data, form.password.data) 312 | 313 | if user.password != password: 314 | flash(gettext('Invalid credentials'), 'danger') 315 | else: 316 | if login_user(user): 317 | flash(gettext('You have been successfully signed in'), 'success') 318 | session['remember_me'] = form.remember_me.data 319 | 320 | return redirect(url_for('general.index')) 321 | else: 322 | flash(gettext('Cannot sign in'),'danger') 323 | 324 | 325 | return render_template('users/login.html', form=form, hide_sidebar=True, hide_header=True, class_body='bg-black', class_html = 'bg-black') 326 | 327 | @login_required 328 | @mod.route('/logout') 329 | def logout(): 330 | logout_user() 331 | return redirect(url_for('general.index')) 332 | 333 | @login_manager.user_loader 334 | def load_user(userid): 335 | return User.query.get(int(userid)) 336 | 337 | @app.before_request 338 | def load_logged_user(): 339 | if not session.get('user_id'): 340 | g.user = AnonymousUser() 341 | else: 342 | g.user = load_user(session['user_id']) 343 | 344 | @login_manager.unauthorized_handler 345 | def unauthorized_callback(): 346 | return redirect(url_for('users.login')) 347 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /bin/aleph-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys, os 4 | 5 | # Fix path for importing modules 6 | CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) 7 | PACKAGE_DIR = os.path.abspath(os.path.join(CURRENT_DIR, os.pardir)) 8 | sys.path.append(PACKAGE_DIR) 9 | 10 | from aleph import AlephServer 11 | 12 | if __name__ == "__main__": 13 | 14 | app = AlephServer() 15 | app.run() 16 | -------------------------------------------------------------------------------- /bin/aleph-webui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$( cd $( dirname "${BASH_SOURCE[0]}" ) && pwd ) 4 | python $DIR/../webui/aleph-webui.py 5 | -------------------------------------------------------------------------------- /bin/db_create.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys, os 3 | 4 | # Fix path for importing modules 5 | CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) 6 | PACKAGE_DIR = os.path.abspath(os.path.join(CURRENT_DIR, os.pardir)) 7 | sys.path.append(PACKAGE_DIR) 8 | 9 | from aleph.webui.database import db 10 | from aleph.webui.models import * 11 | from aleph.webui.utils import hash_password 12 | from aleph.constants import ACCOUNT_SUPERUSER 13 | 14 | try: 15 | db.create_all() 16 | 17 | # Let's create an admin user 18 | u = User(login='admin', email='example@example.org', password=hash_password('admin', 'changeme12!')) 19 | u.account_type = ACCOUNT_SUPERUSER 20 | u.first_name = 'System' 21 | u.last_name = 'Administrator' 22 | db.session.add(u) 23 | db.session.commit() 24 | print "Database created successfully" 25 | except Exception, e: 26 | print "Error creating database: %s" % str(e) 27 | -------------------------------------------------------------------------------- /messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2014 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2014. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2014-09-11 13:14-0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 1.3\n" 19 | 20 | #: webui/templates/401.html:6 21 | msgid "Access denied" 22 | msgstr "" 23 | 24 | #: webui/templates/401.html:13 25 | msgid "You don't have access to this page." 26 | msgstr "" 27 | 28 | #: webui/templates/404.html:6 29 | msgid "Page not found" 30 | msgstr "" 31 | 32 | #: webui/templates/404.html:13 33 | msgid "We could not find the page you were looking for." 34 | msgstr "" 35 | 36 | #: webui/templates/404.html:14 37 | #, python-format 38 | msgid "" 39 | "Meanwhile, you may return to dashboard " 40 | "or try using the search form." 41 | msgstr "" 42 | 43 | #: webui/templates/header.html:10 44 | msgid "Toggle navigation" 45 | msgstr "" 46 | 47 | #: webui/templates/header.html:35 48 | msgid "Settings" 49 | msgstr "" 50 | 51 | #: webui/templates/header.html:38 52 | msgid "Sign out" 53 | msgstr "" 54 | 55 | #: webui/templates/layout.html:51 webui/templates/sidebar.html:29 56 | #: webui/templates/samples/index.html:2 57 | msgid "Samples" 58 | msgstr "" 59 | 60 | #: webui/templates/layout.html:54 webui/templates/sidebar.html:61 61 | msgid "Users" 62 | msgstr "" 63 | 64 | #: webui/templates/sidebar.html:8 65 | msgid "Search..." 66 | msgstr "" 67 | 68 | #: webui/templates/sidebar.html:19 webui/templates/general/index.html:2 69 | msgid "Dashboard" 70 | msgstr "" 71 | 72 | #: webui/templates/sidebar.html:34 webui/templates/general/index.html:142 73 | #: webui/templates/general/index.html:150 webui/templates/samples/index.html:46 74 | msgid "View" 75 | msgstr "" 76 | 77 | #: webui/templates/sidebar.html:36 webui/templates/samples/view.html:8 78 | msgid "Basic Information" 79 | msgstr "" 80 | 81 | #: webui/templates/sidebar.html:38 webui/templates/samples/view.html:40 82 | msgid "Cross-References" 83 | msgstr "" 84 | 85 | #: webui/templates/sidebar.html:40 webui/templates/general/index.html:102 86 | #: webui/templates/samples/index.html:21 webui/templates/samples/view.html:78 87 | msgid "Tags" 88 | msgstr "" 89 | 90 | #: webui/templates/sidebar.html:41 webui/templates/samples/view.html:95 91 | msgid "Origins" 92 | msgstr "" 93 | 94 | #: webui/templates/sidebar.html:42 webui/templates/samples/view.html:121 95 | msgid "Hashes & Checksums" 96 | msgstr "" 97 | 98 | #: webui/templates/sidebar.html:49 webui/templates/sidebar.html:65 99 | msgid "Browse" 100 | msgstr "" 101 | 102 | #: webui/templates/sidebar.html:50 webui/templates/samples/submissions.html:2 103 | #: webui/templates/samples/submissions.html:3 104 | msgid "My submissions" 105 | msgstr "" 106 | 107 | #: webui/templates/sidebar.html:51 webui/templates/sidebar.html:66 108 | msgid "Add new" 109 | msgstr "" 110 | 111 | #: webui/templates/general/index.html:3 112 | msgid "Aleph Web Interface" 113 | msgstr "" 114 | 115 | #: webui/templates/general/index.html:14 116 | msgid "Total Samples" 117 | msgstr "" 118 | 119 | #: webui/templates/general/index.html:21 webui/templates/general/index.html:40 120 | #: webui/templates/general/index.html:58 webui/templates/general/index.html:76 121 | msgid "More info" 122 | msgstr "" 123 | 124 | #: webui/templates/general/index.html:33 125 | msgid "Samples in queue" 126 | msgstr "" 127 | 128 | #: webui/templates/general/index.html:51 129 | msgid "Samples being processed right now" 130 | msgstr "" 131 | 132 | #: webui/templates/general/index.html:69 133 | msgid "Samples processed" 134 | msgstr "" 135 | 136 | #: webui/templates/general/index.html:85 137 | msgid "Latest samples" 138 | msgstr "" 139 | 140 | #: webui/templates/general/index.html:87 141 | msgid "View all samples" 142 | msgstr "" 143 | 144 | #: webui/templates/general/index.html:92 webui/templates/samples/index.html:8 145 | msgid "No samples on the database yet." 146 | msgstr "" 147 | 148 | #: webui/templates/general/index.html:96 webui/templates/samples/index.html:15 149 | #: webui/templates/samples/submissions.html:13 150 | #: webui/templates/samples/view.html:99 151 | msgid "Timestamp" 152 | msgstr "" 153 | 154 | #: webui/templates/general/index.html:97 webui/templates/general/index.html:140 155 | #: webui/templates/samples/index.html:16 156 | #: webui/templates/samples/submissions.html:14 157 | #: webui/templates/samples/view.html:13 158 | msgid "Status" 159 | msgstr "" 160 | 161 | #: webui/templates/general/index.html:98 webui/templates/samples/index.html:17 162 | msgid "UUID" 163 | msgstr "" 164 | 165 | #: webui/templates/general/index.html:99 webui/templates/samples/index.html:18 166 | msgid "Filenames" 167 | msgstr "" 168 | 169 | #: webui/templates/general/index.html:100 webui/templates/samples/index.html:19 170 | #: webui/templates/samples/view.html:19 webui/templates/samples/view.html:48 171 | msgid "MimeType" 172 | msgstr "" 173 | 174 | #: webui/templates/general/index.html:101 webui/templates/samples/index.html:20 175 | #: webui/templates/samples/view.html:17 webui/templates/samples/view.html:46 176 | msgid "Size" 177 | msgstr "" 178 | 179 | #: webui/templates/general/index.html:108 180 | #: webui/templates/general/index.html:147 181 | #: webui/templates/general/index.html:148 webui/templates/samples/index.html:28 182 | #: webui/templates/samples/submissions.html:23 183 | #: webui/templates/samples/submissions.html:24 184 | #: webui/templates/samples/view.html:14 185 | msgid "new" 186 | msgstr "" 187 | 188 | #: webui/templates/general/index.html:108 189 | #: webui/templates/general/index.html:148 webui/templates/samples/index.html:28 190 | #: webui/templates/samples/submissions.html:24 191 | #: webui/templates/samples/view.html:14 192 | msgid "processing" 193 | msgstr "" 194 | 195 | #: webui/templates/general/index.html:108 196 | #: webui/templates/general/index.html:148 webui/templates/samples/index.html:28 197 | #: webui/templates/samples/submissions.html:24 198 | #: webui/templates/samples/view.html:14 199 | msgid "ready" 200 | msgstr "" 201 | 202 | #: webui/templates/general/index.html:109 203 | #: webui/templates/general/index.html:150 204 | #: webui/templates/samples/submissions.html:27 205 | #, python-format 206 | msgid "View report for sample %(sampleuuid)s" 207 | msgstr "" 208 | 209 | #: webui/templates/general/index.html:129 210 | msgid "Latest submissions" 211 | msgstr "" 212 | 213 | #: webui/templates/general/index.html:131 214 | msgid "View all submissions" 215 | msgstr "" 216 | 217 | #: webui/templates/general/index.html:136 218 | #: webui/templates/samples/submissions.html:7 219 | msgid "No samples submitted." 220 | msgstr "" 221 | 222 | #: webui/templates/general/index.html:141 223 | #: webui/templates/samples/submissions.html:16 224 | #: webui/templates/samples/view.html:101 225 | msgid "Filename" 226 | msgstr "" 227 | 228 | #: webui/templates/samples/index.html:3 229 | msgid "Samples processed by Aleph" 230 | msgstr "" 231 | 232 | #: webui/templates/samples/index.html:10 233 | msgid "No samples matching your query." 234 | msgstr "" 235 | 236 | #: webui/templates/samples/index.html:22 webui/templates/samples/index.html:41 237 | #: webui/templates/users/index.html:17 238 | msgid "Action" 239 | msgstr "" 240 | 241 | #: webui/templates/samples/index.html:43 242 | msgid "Toggle Dropdown" 243 | msgstr "" 244 | 245 | #: webui/templates/samples/submissions.html:4 246 | msgid "Samples submitted by you" 247 | msgstr "" 248 | 249 | #: webui/templates/samples/submissions.html:15 250 | msgid "SHA256 Hash" 251 | msgstr "" 252 | 253 | #: webui/templates/samples/submissions.html:17 254 | #: webui/templates/samples/view.html:11 webui/templates/samples/view.html:45 255 | msgid "Sample UUID" 256 | msgstr "" 257 | 258 | #: webui/templates/samples/submissions.html:38 259 | msgid "Submit new sample" 260 | msgstr "" 261 | 262 | #: webui/templates/samples/submit.html:2 webui/templates/samples/submit.html:3 263 | #: webui/templates/samples/submit.html:9 264 | msgid "Submit sample" 265 | msgstr "" 266 | 267 | #: webui/templates/samples/submit.html:4 268 | msgid "Submit a new sample for analysis" 269 | msgstr "" 270 | 271 | #: webui/templates/samples/view.html:2 272 | msgid "Sample" 273 | msgstr "" 274 | 275 | #: webui/templates/samples/view.html:3 276 | msgid "Extended sample information" 277 | msgstr "" 278 | 279 | #: webui/templates/samples/view.html:15 280 | msgid "Added On" 281 | msgstr "" 282 | 283 | #: webui/templates/samples/view.html:21 webui/templates/samples/view.html:47 284 | msgid "Mime" 285 | msgstr "" 286 | 287 | #: webui/templates/samples/view.html:23 288 | msgid "Known filenames" 289 | msgstr "" 290 | 291 | #: webui/templates/samples/view.html:44 292 | msgid "Relation" 293 | msgstr "" 294 | 295 | #: webui/templates/samples/view.html:53 296 | msgid "Parent" 297 | msgstr "" 298 | 299 | #: webui/templates/samples/view.html:54 300 | #, python-format 301 | msgid "View parent sample %(uuid)s" 302 | msgstr "" 303 | 304 | #: webui/templates/samples/view.html:62 305 | msgid "Child" 306 | msgstr "" 307 | 308 | #: webui/templates/samples/view.html:63 309 | #, python-format 310 | msgid "View child sample %(uuid)s" 311 | msgstr "" 312 | 313 | #: webui/templates/samples/view.html:81 314 | msgid "This sample has no tags." 315 | msgstr "" 316 | 317 | #: webui/templates/samples/view.html:100 318 | msgid "Provider" 319 | msgstr "" 320 | 321 | #: webui/templates/samples/view.html:102 322 | msgid "Reference" 323 | msgstr "" 324 | 325 | #: webui/templates/samples/view.html:140 326 | msgid "Plugin Information" 327 | msgstr "" 328 | 329 | #: webui/templates/samples/view.html:159 webui/templates/samples/view.html:163 330 | #: webui/templates/samples/view.html:169 331 | #, python-format 332 | msgid "Search all samples for '%(keyname)s' with value '%(valuename)s'" 333 | msgstr "" 334 | 335 | #: webui/templates/users/change_password.html:2 336 | #: webui/templates/users/change_password.html:10 337 | #: webui/templates/users/settings.html:13 338 | msgid "Change password" 339 | msgstr "" 340 | 341 | #: webui/templates/users/change_password.html:3 342 | #: webui/templates/users/login.html:17 aleph/webui/forms.py:11 343 | #: aleph/webui/forms.py:50 aleph/webui/forms.py:58 344 | msgid "Password" 345 | msgstr "" 346 | 347 | #: webui/templates/users/edit.html:2 webui/templates/users/edit.html:3 348 | #: webui/templates/users/edit.html:17 349 | msgid "Edit user account" 350 | msgstr "" 351 | 352 | #: webui/templates/users/index.html:2 webui/templates/users/index.html:3 353 | msgid "System users" 354 | msgstr "" 355 | 356 | #: webui/templates/users/index.html:6 357 | msgid "No users on system yet." 358 | msgstr "" 359 | 360 | #: webui/templates/users/index.html:12 361 | msgid "ID" 362 | msgstr "" 363 | 364 | #: webui/templates/users/index.html:13 365 | msgid "Name" 366 | msgstr "" 367 | 368 | #: webui/templates/users/index.html:14 aleph/webui/forms.py:18 369 | msgid "Email" 370 | msgstr "" 371 | 372 | #: webui/templates/users/index.html:15 373 | msgid "Premium user" 374 | msgstr "" 375 | 376 | #: webui/templates/users/index.html:16 377 | msgid "Superuser" 378 | msgstr "" 379 | 380 | #: webui/templates/users/index.html:55 381 | msgid "Add new user" 382 | msgstr "" 383 | 384 | #: webui/templates/users/login.html:6 webui/templates/users/login.html:10 385 | msgid "Sign In" 386 | msgstr "" 387 | 388 | #: webui/templates/users/login.html:14 389 | msgid "User ID" 390 | msgstr "" 391 | 392 | #: webui/templates/users/login.html:21 393 | msgid "Remember me" 394 | msgstr "" 395 | 396 | #: webui/templates/users/login.html:25 397 | msgid "Sign me in" 398 | msgstr "" 399 | 400 | #: webui/templates/users/login.html:26 401 | msgid "Register a new account" 402 | msgstr "" 403 | 404 | #: webui/templates/users/mail_register.html:3 405 | #, python-format 406 | msgid "Hello %(firstname)s %(lastname)s" 407 | msgstr "" 408 | 409 | #: webui/templates/users/mail_register.html:5 aleph/webui/views/users.py:251 410 | #, python-format 411 | msgid "Welcome to %(appname)s" 412 | msgstr "" 413 | 414 | #: webui/templates/users/mail_register.html:7 415 | msgid "" 416 | "To activate your account click (or copy and paste into a browser) the " 417 | "link below:" 418 | msgstr "" 419 | 420 | #: webui/templates/users/mail_register.html:11 421 | msgid "Regards," 422 | msgstr "" 423 | 424 | #: webui/templates/users/new.html:2 webui/templates/users/new.html:3 425 | msgid "New user account" 426 | msgstr "" 427 | 428 | #: webui/templates/users/new.html:17 429 | msgid "Add user account" 430 | msgstr "" 431 | 432 | #: webui/templates/users/register.html:6 433 | msgid "Register" 434 | msgstr "" 435 | 436 | #: webui/templates/users/register.html:10 437 | msgid "Register new account" 438 | msgstr "" 439 | 440 | #: webui/templates/users/register.html:22 441 | msgid "Sign me up" 442 | msgstr "" 443 | 444 | #: webui/templates/users/register.html:23 445 | msgid "I already have a membership" 446 | msgstr "" 447 | 448 | #: webui/templates/users/settings.html:2 webui/templates/users/settings.html:3 449 | msgid "Edit user settings" 450 | msgstr "" 451 | 452 | #: webui/templates/users/settings.html:12 453 | msgid "Save settings" 454 | msgstr "" 455 | 456 | #: aleph/webui/__init__.py:24 457 | msgid "Please log in to access this page." 458 | msgstr "" 459 | 460 | #: aleph/webui/forms.py:10 aleph/webui/forms.py:46 461 | msgid "Username" 462 | msgstr "" 463 | 464 | #: aleph/webui/forms.py:15 465 | msgid "First Name" 466 | msgstr "" 467 | 468 | #: aleph/webui/forms.py:15 469 | msgid "First name must be between 2 and 30 characters long" 470 | msgstr "" 471 | 472 | #: aleph/webui/forms.py:16 473 | msgid "Last Name" 474 | msgstr "" 475 | 476 | #: aleph/webui/forms.py:16 477 | msgid "Last name must be between 2 and 255 characters long" 478 | msgstr "" 479 | 480 | #: aleph/webui/forms.py:28 481 | msgid "Language" 482 | msgstr "" 483 | 484 | #: aleph/webui/forms.py:29 485 | msgid "Timezone" 486 | msgstr "" 487 | 488 | #: aleph/webui/forms.py:31 aleph/webui/views/users.py:47 489 | msgid "Account enabled" 490 | msgstr "" 491 | 492 | #: aleph/webui/forms.py:34 493 | msgid "Regular account" 494 | msgstr "" 495 | 496 | #: aleph/webui/forms.py:35 497 | msgid "Premium account" 498 | msgstr "" 499 | 500 | #: aleph/webui/forms.py:36 501 | msgid "Superuser account" 502 | msgstr "" 503 | 504 | #: aleph/webui/forms.py:39 505 | msgid "Account Type" 506 | msgstr "" 507 | 508 | #: aleph/webui/forms.py:41 509 | msgid "Token" 510 | msgstr "" 511 | 512 | #: aleph/webui/forms.py:42 513 | msgid "API Key" 514 | msgstr "" 515 | 516 | #: aleph/webui/forms.py:46 517 | msgid "Only alphanumeric characters valid" 518 | msgstr "" 519 | 520 | #: aleph/webui/forms.py:52 aleph/webui/forms.py:60 521 | msgid "Passwords must match" 522 | msgstr "" 523 | 524 | #: aleph/webui/forms.py:54 aleph/webui/forms.py:62 525 | msgid "Confirm password" 526 | msgstr "" 527 | 528 | #: aleph/webui/forms.py:57 529 | msgid "Current password" 530 | msgstr "" 531 | 532 | #: aleph/webui/views/samples.py:73 533 | #, python-format 534 | msgid "Cannot write to folder: %(folderpath)s" 535 | msgstr "" 536 | 537 | #: aleph/webui/views/samples.py:77 538 | msgid "No file submitted." 539 | msgstr "" 540 | 541 | #: aleph/webui/views/samples.py:87 542 | #, python-format 543 | msgid "" 544 | "Sample %(samplename)s (%(samplesize)s) is bigger than maximum file size " 545 | "allowed: %(maxsize)s" 546 | msgstr "" 547 | 548 | #: aleph/webui/views/samples.py:91 549 | #, python-format 550 | msgid "" 551 | "Sample %(samplename)s (%(samplesize)s) is smaller than minimum file size " 552 | "allowed: %(minsize)s" 553 | msgstr "" 554 | 555 | #: aleph/webui/views/samples.py:99 556 | msgid "File already submitted." 557 | msgstr "" 558 | 559 | #: aleph/webui/views/samples.py:131 560 | msgid "Sample submitted successfully" 561 | msgstr "" 562 | 563 | #: aleph/webui/views/users.py:67 564 | msgid "Account disabled" 565 | msgstr "" 566 | 567 | #: aleph/webui/views/users.py:99 568 | msgid "User added successfully" 569 | msgstr "" 570 | 571 | #: aleph/webui/views/users.py:140 572 | msgid "User updated successfully" 573 | msgstr "" 574 | 575 | #: aleph/webui/views/users.py:175 576 | msgid "Settings updated successfully" 577 | msgstr "" 578 | 579 | #: aleph/webui/views/users.py:204 580 | msgid "Current password doesn't match" 581 | msgstr "" 582 | 583 | #: aleph/webui/views/users.py:209 584 | msgid "Password changed successfully" 585 | msgstr "" 586 | 587 | #: aleph/webui/views/users.py:231 588 | msgid "Email address already registered" 589 | msgstr "" 590 | 591 | #: aleph/webui/views/users.py:258 592 | msgid "" 593 | "Account created successfully. Please check your email for instructions on" 594 | " activating your account" 595 | msgstr "" 596 | 597 | #: aleph/webui/views/users.py:280 598 | msgid "Account activated. Please login below" 599 | msgstr "" 600 | 601 | #: aleph/webui/views/users.py:296 aleph/webui/views/users.py:301 602 | msgid "Invalid credentials" 603 | msgstr "" 604 | 605 | #: aleph/webui/views/users.py:305 606 | msgid "You have been successfully signed in" 607 | msgstr "" 608 | 609 | #: aleph/webui/views/users.py:310 610 | msgid "Cannot sign in" 611 | msgstr "" 612 | 613 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argparse==1.2.1 2 | Babel==1.3 3 | bitstring==3.1.3 4 | blinker==1.3 5 | celery==3.1.18 6 | cffi==0.8.6 7 | elasticsearch==1.2.0 8 | Flask-Babel==0.9 9 | Flask-Login==0.2.11 10 | Flask-Mail==0.9.0 11 | Flask-SQLAlchemy==2.0 12 | Flask-WTF==0.10.1 13 | Flask==0.10.1 14 | itsdangerous==0.24 15 | Jinja2==2.7.3 16 | MarkupSafe==0.23 17 | pefile==1.2.10-114 18 | pluginbase==0.3 19 | pycparser==2.10 20 | pygeoip==0.3.1 21 | pysqlite==2.6.3 22 | python-dateutil==2.2 23 | python-magic==0.4.6 24 | pytz==2014.4 25 | rarfile==2.7 26 | requests==2.3.0 27 | simplejson==3.6.2 28 | six==1.7.3 29 | speaklater==1.3 30 | SQLAlchemy==0.9.7 31 | ssdeep==3.1 32 | urllib3==1.9 33 | uuid==1.30 34 | virustotal==1.0.3 35 | Werkzeug==0.15.3 36 | wsgiref==0.1.2 37 | WTForms==2.0.1 38 | -------------------------------------------------------------------------------- /webui/aleph-webui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys, os 4 | 5 | # Fix path for importing modules 6 | CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) 7 | PACKAGE_DIR = os.path.abspath(os.path.join(CURRENT_DIR, os.pardir)) 8 | sys.path.append(PACKAGE_DIR) 9 | 10 | from aleph.webui import app 11 | 12 | if __name__ == "__main__": 13 | 14 | app.run('0.0.0.0',debug=app.config['DEBUG']) 15 | -------------------------------------------------------------------------------- /webui/resources/GeoLiteCity.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/resources/GeoLiteCity.dat -------------------------------------------------------------------------------- /webui/static/css/aleph.css: -------------------------------------------------------------------------------- 1 | ul.treeview-menu ul { 2 | font-size: 0.8em; 3 | list-style: none; 4 | } 5 | 6 | ul.treeview-menu ul li { 7 | margin-bottom: 5px; 8 | } 9 | ul.treeview-menu ul a { 10 | color: #3c8dbc; 11 | display: block; 12 | } 13 | ul.treeview-menu ul a:hover { 14 | color: #FFF; 15 | } 16 | 17 | .panel-body button i { 18 | margin-right: 5px; 19 | } 20 | 21 | .plugin-info dd, 22 | .plugin-info dt { 23 | margin-bottom: 10px; 24 | } 25 | .plugin-info dd ul { 26 | list-style: none; 27 | padding-left: 0px; 28 | width: 100%; 29 | } 30 | .plugin-info dd ul li { 31 | display: inline-block; 32 | margin-bottom: 5px !important; 33 | width: 33% 34 | } 35 | 36 | .plugin-info dd ul dl { 37 | margin-left: 0; 38 | padding: none; 39 | } 40 | 41 | .plugin-info dd ul dl dt { 42 | text-align: left; 43 | } 44 | .plugin-info dd ul dl dd { 45 | margin-left: 10%; 46 | } 47 | 48 | .plugin-info dd dl { 49 | clear: both; 50 | } 51 | a.logo { 52 | padding-top: 5px; 53 | font-size: 2em !important; 54 | } 55 | .user-header { 56 | height: 100px !important; 57 | } 58 | 59 | span.sample-processing { 60 | background-color: #FFCC33; 61 | } 62 | 63 | span.sample-ready { 64 | background-color: #33CCFF; 65 | } 66 | 67 | a.logo, 68 | .form-box a.brand { 69 | display: block; 70 | text-align: center; 71 | margin-bottom: 25px; 72 | text-shadow: 1px 1px 4px #000; 73 | font-family: Montserrat !important; 74 | text-transform: uppercase; 75 | font-size: 2.8em; 76 | color: #FFF; 77 | } 78 | 79 | h1,h2,h3,h4,h5 { 80 | } 81 | 82 | tr.disabled-user td { 83 | text-decoration: line-through; 84 | } 85 | .user-footer i { 86 | margin-right: 5px; 87 | } 88 | 89 | #error-box { 90 | text-align: center; 91 | position: absolute; 92 | top: 50%; 93 | margin-top: -220px; 94 | height: 400px; 95 | left: 50%; 96 | margin-left: -175px; 97 | width: 350px; 98 | } 99 | #error-box h2 { 100 | font-size: 8em; 101 | text-shadow: 1px 1px 8px #000; 102 | } 103 | #error-box h3 { 104 | margin: 30px 0px 10px 0px; 105 | } 106 | #error-box form { 107 | margin-top: 30px; 108 | } 109 | 110 | div.panel-body ul li { 111 | margin: 0; 112 | padding: 0; 113 | } 114 | div.panel-body ul { 115 | list-style: none; 116 | margin: 0; 117 | padding: 0; 118 | } 119 | #submit-sample-form input { 120 | padding: 10px; 121 | height: auto; 122 | } 123 | 124 | .align-right { 125 | text-align: right; 126 | } 127 | -------------------------------------------------------------------------------- /webui/static/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} -------------------------------------------------------------------------------- /webui/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/favicon.ico -------------------------------------------------------------------------------- /webui/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /webui/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /webui/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /webui/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /webui/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /webui/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /webui/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /webui/static/fonts/ionicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/ionicons.eot -------------------------------------------------------------------------------- /webui/static/fonts/ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/ionicons.ttf -------------------------------------------------------------------------------- /webui/static/fonts/ionicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/static/fonts/ionicons.woff -------------------------------------------------------------------------------- /webui/static/js/plugins/bootbox.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootbox.js v4.3.0 3 | * 4 | * http://bootboxjs.com/license.txt 5 | */ 6 | !function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d(a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),a.backdrop=a.backdrop?"static":!1,c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"
",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:!0,animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback():!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback(!1)},a.buttons.confirm.callback=function(){return a.callback(!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback(null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback(c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var c=b(n.dialog),d=c.find(".modal-dialog"),f=c.find(".modal-body"),i=a.buttons,j="",k={onEscape:a.onEscape};if(g(i,function(a,b){j+="",k[a]=b.callback}),f.find(".bootbox-body").html(a.message),a.animate===!0&&c.addClass("fade"),a.className&&c.addClass(a.className),"large"===a.size&&d.addClass("modal-lg"),"small"===a.size&&d.addClass("modal-sm"),a.title&&f.before(n.header),a.closeButton){var l=b(n.closeButton);a.title?c.find(".modal-header").prepend(l):l.css("margin-top","-10px").prependTo(f)}return a.title&&c.find(".modal-title").html(a.title),j.length&&(f.after(n.footer),c.find(".modal-footer").html(j)),c.on("hidden.bs.modal",function(a){a.target===this&&c.remove()}),c.on("shown.bs.modal",function(){c.find(".btn-primary:first").focus()}),c.on("escape.close.bb",function(a){k.onEscape&&e(a,c,k.onEscape)}),c.on("click",".modal-footer button",function(a){var d=b(this).data("bb-handler");e(a,c,k[d])}),c.on("click",".bootbox-close-button",function(a){e(a,c,k.onEscape)}),c.on("keyup",function(a){27===a.which&&c.trigger("escape.close.bb")}),b(a.container).append(c),c.modal({backdrop:a.backdrop,keyboard:!1,show:!1}),a.show&&c.modal("show"),c},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"確認"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.init=function(c){return a(c||b)},p}); 7 | -------------------------------------------------------------------------------- /webui/templates/401.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block variables %} 3 | {% set class_html = 'bg-black' %} 4 | {% set class_body = 'bg-black' %} 5 | {% endblock %} 6 | {% block title %}{{ _('Access denied') }}{% endblock %} 7 | {% block body %} 8 |
9 | ℵ {{ config.APP_TITLE }} 10 |

401

11 |

Access denied.

12 |

13 | {{ _('You don\'t have access to this page.') }} 14 |

15 | {% with messages = get_flashed_messages() %} 16 | {% if messages %} 17 |
18 | {% for message in messages %} 19 |

{{ message }}

20 | {% endfor %} 21 |
22 | {% endif %} 23 | {% endwith %} 24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /webui/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block variables %} 3 | {% set class_html = 'bg-black' %} 4 | {% set class_body = 'bg-black' %} 5 | {% endblock %} 6 | {% block title %}{{ _('Page not found') }}{% endblock %} 7 | {% block body %} 8 |
9 | ℵ {{ config.APP_TITLE }} 10 |

404

11 |

Oops! Page not found.

12 |

13 | {{ _('We could not find the page you were looking for.') }} 14 | {{ _('Meanwhile, you may return to dashboard or try using the search form.', dashboardlink = url_for('general.index')) }} 15 |

16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | {% with messages = get_flashed_messages() %} 25 | {% if messages %} 26 |
27 | {% for message in messages %} 28 |

{{ message }}

29 | {% endfor %} 30 |
31 | {% endif %} 32 | {% endwith %} 33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /webui/templates/_formhelpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field, silent=False, placeholder='') %} 2 |
3 | {% if not silent %}{{ field.label }}{% endif %} 4 | {% if not placeholder and silent %}{% set placeholder = field.label.text %}{% endif %} 5 | {{ field(placeholder=placeholder, class='form-control', **kwargs)|safe }} 6 |
7 | {% if field.errors %} 8 |
9 | This field has the following errors: 10 |
    11 | {% for error in field.errors %} 12 |
  • {{ error }}
  • 13 | {% endfor %} 14 |
15 |
16 | {% endif %} 17 | {% endmacro %} 18 | -------------------------------------------------------------------------------- /webui/templates/_paginationhelpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_pagination(pagination, endpoint) %} 2 | 13 | {% endmacro %} 14 | -------------------------------------------------------------------------------- /webui/templates/general/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('Dashboard') }}{% endblock %} 3 | {% block description %}{{ _('Aleph Web Interface') }}{% endblock %} 4 | {% block body %} 5 |
6 |
7 | 8 |
9 |
10 |

11 | {{ sample_count.total }} 12 |

13 |

14 | {{ _('Total Samples') }} 15 |

16 |
17 |
18 | 19 |
20 | 21 | {{ _('More info') }} 22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 |

30 | {{ sample_count.new }} 31 |

32 |

33 | {{ _('Samples in queue') }} 34 |

35 |
36 |
37 | 38 |
39 | 40 | {{ _('More info') }} 41 | 42 |
43 |
44 | 45 |
46 |
47 |

48 | {{ sample_count.processing }} 49 |

50 |

51 | {{ _('Samples being processed right now') }} 52 |

53 |
54 |
55 | 56 |
57 | 58 | {{ _('More info') }} 59 | 60 |
61 |
62 | 63 |
64 |
65 |

66 | {{ sample_count.ready }} 67 |

68 |

69 | {{ _('Samples processed') }} 70 |

71 |
72 |
73 | 74 |
75 | 76 | {{ _('More info') }} 77 | 78 |
79 |
80 |
81 |
82 | {% if config.get('SAMPLE_SUBMIT_FOLDER') %} 83 |
84 | {% else %} 85 |
86 | {% endif %} 87 |
88 |
89 |

{{ _('Latest samples') }}

90 | 93 |
94 |
95 | {% if samples|count == 0 %} 96 |

{{ _('No samples on the database yet.') }}

97 | {% else %} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {% for sample in samples %} 110 | 111 | 112 | 113 | 114 | 119 | 120 | 121 | 122 | 123 | {% endfor %} 124 | 125 |
{{ _('Timestamp') }}{{ _('Status') }}{{ _('UUID') }}{{ _('Filenames') }}{{ _('MimeType') }}{{ _('Size') }}{{ _('Tags') }}
{{ sample.timestamp|str2utc|datetimeformat('short') }}{% if sample.status == 0 %}{{ _('new') }}{% elif sample.status == 1%}{{ _('processing') }}{% elif sample.status == 2 %}{{ _('ready') }}{% endif %}{{ sample.uuid|truncate(10, True) }} 115 | {% for source in sample.sources %} 116 | {{ source.filename|truncate(30, '...') }} 117 | {% endfor %} 118 | {{ sample.mimetype }}{{ sample.size|humansize }}{{ ', '.join(sample.tags) }}
126 | {% endif %} 127 |
128 |
129 |
130 | {% if config.get('SAMPLE_SUBMIT_FOLDER') %} 131 |
132 |
133 |
134 |

{{ _('Latest submissions') }}

135 | 138 |
139 |
140 | {% if submissions|count == 0 %} 141 |

{{ _('No samples submitted.') }}

142 | {% else %} 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | {% for submission in submissions %} 151 | 152 | 154 | 155 | 156 | 157 | {% endfor %} 158 | 159 |
{{ _('Status') }}{{ _('Filename') }}{{ _('View') }}
{% if not submission.sample_uuid %}{{ _('new') }}{% else %} 153 | {% if submission_samples[submission.sample_uuid].status == 0 %}{{ _('new') }}{% elif submission_samples[submission.sample_uuid].status == 1%}{{ _('processing') }}{% elif submission_samples[submission.sample_uuid].status == 2 %}{{ _('ready') }}{% endif %}{% endif %}{{ submission.filename }}{% if submission.sample_uuid %}{{ _('View') }}{% endif %}
160 | {% endif %} 161 |
162 |
163 |
164 | {% endif %} 165 | 166 |
167 | {% endblock %} 168 | -------------------------------------------------------------------------------- /webui/templates/header.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 | 47 |
48 | -------------------------------------------------------------------------------- /webui/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | {% set class_html = class_html or '' %} 3 | {% set hide_header = hide_header or False %} 4 | {% set hide_sidebar = hide_sidebar or False %} 5 | {% set class_body = class_body or 'skin-black fixed' %} 6 | 7 | 8 | 9 | 10 | {% block title %}{% endblock %} | {{ config.APP_TITLE }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | {% if not hide_header %} 35 | {% include 'header.html' %} 36 | {% endif %} 37 | {% if not hide_sidebar %} 38 |
39 | {% include 'sidebar.html' %} 40 | 41 | 76 |
77 | {% endif %} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {% block javascript %} 88 | {% endblock %} 89 | 90 | 91 | -------------------------------------------------------------------------------- /webui/templates/samples/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{% if request.args.get('search') %}{{ _('Search Results') }}{% else %}{{ _('Samples') }}{% endif %}{% endblock %} 3 | {% block description %}{% if request.args.get('search') %}{{ request.args.get('search')|safe }}{% else %}{{ _('Samples processed by Aleph') }}{% endif %}{% endblock %} 4 | {% block body %} 5 |
6 |
7 | {% if sample_count == 0 %} 8 |

{{ _('No samples on the database yet.') }}

9 | {% elif samples|count == 0 %} 10 |

{{ _('No samples matching your query.') }}

11 | {% else %} 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for sample in samples %} 26 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 51 | 52 | {% endfor %} 53 | 54 |
{{ _('Timestamp') }}{{ _('Status') }}{{ _('UUID') }}{{ _('Filenames') }}{{ _('MimeType') }}{{ _('Size') }}{{ _('Tags') }}{{ _('Action') }}
{{ sample.timestamp|str2utc|datetimeformat }}{% if sample.status == 0 %}{{ _('new') }}{% elif sample.status == 1%}{{ _('processing') }}{% elif sample.status == 2 %}{{ _('ready') }}{% endif %}{{ sample.uuid|truncate(10, True) }} 31 | {% for source in sample.sources %} 32 | {{ source.filename|truncate(10, True) }} 33 | {% endfor %} 34 | {{ sample.mimetype }}{{ sample.size|humansize }}{{ ', '.join(sample.tags) }} 39 |
40 | 45 | 49 |
50 |
55 | {% endif %} 56 |
57 |
58 |
59 |
    60 | {% if page == 1 %} 61 |
  • «
  • 62 | {% else %} 63 |
  • «
  • 64 | {% endif %} 65 | {% for pg in pages %} 66 | {% if pg == page %} 67 |
  • {{ pg }} (current)
  • 68 | {% else %} 69 |
  • {{ pg }}
  • 70 | {% endif %} 71 | {% endfor %} 72 | 73 | {% if page >= pages|count %} 74 |
  • »
  • 75 | {% else %} 76 |
  • »
  • 77 | {% endif %} 78 |
79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /webui/templates/samples/submissions.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('My submissions') }}{% endblock %} 3 | {% block pagetitle %}{{ _('My submissions') }}{% endblock %} 4 | {% block description %}{{ _('Samples submitted by you') }}{% endblock %} 5 | {% block body %} 6 | {% if submissions.items|count == 0 %} 7 | 8 | {% else %} 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for submission in submissions.items %} 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | {% endfor %} 30 | 31 |
{{ _('Timestamp') }}{{ _('Status') }}{{ _('SHA256 Hash') }}{{ _('Filename') }}{{ _('Sample UUID') }}
{{ submission.timestamp|datetimeformat }}{% if not submission.sample_uuid %}{{ _('new') }}{% else %} 24 | {% if samples[submission.sample_uuid].status == 0 %}{{ _('new') }}{% elif samples[submission.sample_uuid].status == 1%}{{ _('processing') }}{% elif samples[submission.sample_uuid].status == 2 %}{{ _('ready') }}{% endif %}{% endif %}{{ submission.file_hash[0:6] }}...{{ submission.file_hash[-6:] }}{{ submission.filename }}{% if not submission.sample_uuid %}-{% else %}{{ submission.sample_uuid }}{% endif %}
32 |
33 |
34 | {% from "_paginationhelpers.html" import render_pagination %} 35 | {{ render_pagination(submissions, 'samples.submissions') }} 36 |
37 | {% endif %} 38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /webui/templates/samples/submit.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('Submit sample') }}{% endblock %} 3 | {% block pagetitle %}{{ _('Submit sample') }}{% endblock %} 4 | {% block description %}{{ _('Submit a new sample for analysis') }}{% endblock %} 5 | {% block body %} 6 |
7 | {% from "_formhelpers.html" import render_field %} 8 | {{ render_field(form.sample) }} 9 | 10 | {{ form.hidden_tag() }} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /webui/templates/samples/view.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('Sample') }} {{ sample.uuid }}{% endblock %} 3 | {% block description %}{{ _('Extended sample information') }}{% endblock %} 4 | {% block body %} 5 | 6 | 7 |
8 |
{{ _('Basic Information') }}
9 |
10 |
11 |
{{ _('Sample UUID') }}
12 |
{{ sample.uuid }}
13 |
{{ _('Status') }}
14 |
{% if sample.status == 0 %}{{ _('new') }}{% elif sample.status == 1%}{{ _('processing') }}{% elif sample.status == 2 %}{{ _('ready') }}{% endif %}
15 |
{{ _('Added On') }}
16 |
{{ sample.timestamp|str2utc|datetimeformat }}
17 |
{{ _('Size') }}
18 |
{{ sample.size|humansize }} ({{ sample.size }} bytes)
19 |
{{ _('MimeType') }}
20 |
{{ sample.mimetype }}
21 |
{{ _('Mime') }}
22 |
{{ sample.mime }}
23 |
{{ _('Known filenames') }}
24 |
25 |
    26 | {% for source in sample.sources %} 27 |
  • {{ source.filename }}
  • 28 | {% endfor %} 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 | {% if sample.xrefs.parent|count > 0 or sample.xrefs.child|count > 0 %} 38 | 39 |
40 |
{{ _('Cross-References') }}
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for ref in sample.xrefs.parent %} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% endfor %} 60 | {% for ref in sample.xrefs.child %} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% endfor %} 69 | 70 |
{{ _('Relation') }}{{ _('Sample UUID') }}{{ _('Size') }}{{ _('Mime') }}{{ _('MimeType') }}
{{ _('Parent') }}{{ ref }}{{ xrefs_info[ref].size|humansize }}{{ xrefs_info[ref].mimetype }}{{ xrefs_info[ref].mime }}
{{ _('Child') }}{{ ref }}{{ xrefs_info[ref].size|humansize }}{{ xrefs_info[ref].mimetype }}{{ xrefs_info[ref].mime }}
71 |
72 |
73 | 74 | {% endif %} 75 | 76 | 77 |
78 |
{{ _('Tags') }}
79 |
80 | {% if sample.tags|count == 0 %} 81 | {{ _('This sample has no tags.') }} 82 | {% else %} 83 | {% for tag in sample.tags %} 84 | 85 | {% endfor %} 86 | {% endif %} 87 | 88 |
89 |
90 | 91 | 92 | 93 | 94 |
95 |
{{ _('Origins') }}
96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {% for source in sample.sources %} 106 | 107 | 108 | 109 | 110 | 111 | 112 | {% endfor %} 113 | 114 |
{{ _('Timestamp') }}{{ _('Provider') }}{{ _('Filename') }}{{ _('Reference') }}
{{ source.timestamp|str2utc|datetimeformat }}{{ source.provider }}{{ source.filename }}{{ source.reference }}
115 |
116 |
117 | 118 | 119 | 120 |
121 |
{{ _('Hashes & Checksums') }}
122 |
123 |
124 |
MD5
125 |
{{ sample.hashes.md5 }}
126 |
SHA1
127 |
{{ sample.hashes.sha1 }}
128 |
SHA256
129 |
{{ sample.hashes.sha256 }}
130 |
SHA512
131 |
{{ sample.hashes.sha512 }}
132 |
CRC32
133 |
{{ sample.hashes.crc32 }}
134 |
SSDEEP
135 |
{{ sample.hashes.ssdeep }}
136 |
137 |
138 |
139 | 140 |

{{ _('Plugin Information') }}

141 | {% for plugin_name, plugin_data in sample.data.iteritems() %} 142 | 143 | 144 |
145 | 149 |
150 |
151 |
152 | {% for item_name, item_value in plugin_data.iteritems() %} 153 |
{{ item_name|replace('_',' ')|capitalize }}
154 |
155 | {% if item_value is not number and item_value is not string %} 156 | {% if item_value|count > 0 and item_value.0 is mapping %} 157 | 158 | 159 | {% for k,v in item_value.0.iteritems() %} 160 | 161 | {% endfor %} 162 | 163 | 164 | {% for item in item_value %} 165 | 166 | {% for k,v in item.iteritems() %} 167 | 168 | {% endfor %} 169 | 170 | {% endfor %} 171 | 172 |
{{ k|replace('_',' ')|capitalize }}
{{ v }}
173 | {% elif item_value is mapping %} 174 |
175 | {% for key,item in item_value.iteritems() %} 176 |
{{ key }}
177 |
178 | {% if item is string %}{{ item }}{% else %} 179 | 180 | {% if item.0 is mapping %} 181 | 182 | {% for k,v in item.0.iteritems() %} 183 | 184 | {% endfor %} 185 | 186 | {% endif %} 187 | {% for entry in item %} 188 | 189 | {% if entry is string %} 190 | 191 | {% else %} 192 | {% for k,v in entry.iteritems() %} 193 | 194 | {% endfor %} 195 | {% endif %} 196 | 197 | {% endfor %} 198 |
{{ k }}
{{ entry }}{{ v }}
199 | {% endif %} 200 |
201 | {% endfor %} 202 |
203 | {% else %} 204 |
    205 | {% for item in item_value %} 206 |
  • {{ item }}
  • 207 | {% endfor %} 208 |
209 | {% endif %} 210 | {% else %} 211 | {{ item_value }} 212 | {% endif %} 213 |
214 | {% endfor %} 215 |
216 |
217 |
218 |
219 | {% endfor %} 220 | 221 | {% endblock %} 222 | -------------------------------------------------------------------------------- /webui/templates/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 76 | -------------------------------------------------------------------------------- /webui/templates/users/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('Change password') }}{% endblock %} 3 | {% block pagetitle %}{{ _('Password') }} {{ user.first_name }} {{ user.last_name }} (#{{ user.id }}){% endblock %} 4 | {% block body %} 5 |
6 | {% from "_formhelpers.html" import render_field %} 7 | {{ render_field(form.current_password) }} 8 | {{ render_field(form.password) }} 9 | {{ render_field(form.confirm) }} 10 | 11 | {{ form.hidden_tag() }} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /webui/templates/users/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('Edit user account') }}{% endblock %} 3 | {% block pagetitle %}{{ _('Edit user account') }} {{ user.first_name }} {{ user.last_name }} (#{{ user.id }}){% endblock %} 4 | {% block body %} 5 |
6 | {% from "_formhelpers.html" import render_field %} 7 | {{ render_field(form.first_name) }} 8 | {{ render_field(form.last_name) }} 9 | {{ render_field(form.login) }} 10 | {{ render_field(form.email) }} 11 | {{ render_field(form.locale) }} 12 | {{ render_field(form.timezone) }} 13 | {{ render_field(form.token) }} 14 | {{ render_field(form.api_key) }} 15 | {{ render_field(form.account_type) }} 16 | {{ render_field(form.active) }} 17 | 18 | {{ form.hidden_tag() }} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /webui/templates/users/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('System users') }}{% endblock %} 3 | {% block pagetitle %}{{ _('System users') }}{% endblock %} 4 | {% block body %} 5 | {% if users.items|count == 0 %} 6 | 7 | {% else %} 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for user in users.items %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 45 | 46 | {% endfor %} 47 | 48 |
{{ _('ID') }}{{ _('Name') }}{{ _('Email') }}{{ _('Premium user') }}{{ _('Superuser') }}{{ _('Action') }}
{{ user.id }}{{ user.first_name }} {{ user.last_name }}{{ user.email }}{% if user.is_premium %}{% else %}{% endif %}{% if user.is_admin %}{% else %}{% endif %} 28 | 29 |
30 | 33 | 43 |
44 |
49 |
50 |
51 | {% from "_paginationhelpers.html" import render_pagination %} 52 | {{ render_pagination(users, 'users.index') }} 53 |
54 | {% endif %} 55 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /webui/templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block variables %} 3 | {% set class_html = 'bg-black' %} 4 | {% set class_body = 'bg-black' %} 5 | {% endblock %} 6 | {% block title %}{{ _('Sign In') }}{% endblock %} 7 | {% block body %} 8 |
9 | ℵ {{ config.APP_TITLE }} 10 |
{{ _('Sign In') }}
11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | {{ form.remember_me }} 21 | 22 |
23 |
24 | 29 | {{ form.hidden_tag() }} 30 |
31 | {% with messages = get_flashed_messages() %} 32 | {% if messages %} 33 |
34 | {% for message in messages %} 35 |

{{ message }}

36 | {% endfor %} 37 |
38 | {% endif %} 39 | {% endwith %} 40 | 41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /webui/templates/users/mail_register.html: -------------------------------------------------------------------------------- 1 |

ℵ {{ config.APP_TITLE }}

2 | 3 | {{ _('Hello %(firstname)s %(lastname)s', firstname=user.first_name, lastname=user.last_name) }} 4 | 5 |

{{ _('Welcome to %(appname)s', appname= config.APP_TITLE) }}

6 | 7 |

{{ _('To activate your account click (or copy and paste into a browser) the link below:') }}

8 | 9 |

{{ url_for('users.activate', token=user.token, _external=True) }}

10 | 11 |

{{ _('Regards,') }}
12 | {{ config.APP_TITLE }}

13 | -------------------------------------------------------------------------------- /webui/templates/users/mail_register.txt: -------------------------------------------------------------------------------- 1 | {{ _('Hello %(firstname)s %(lastname)s', firstname=user.first_name, lastname=user.last_name) }} 2 | 3 | {{ _('Welcome to %(appname)s', appname= config.APP_TITLE) }} 4 | 5 | {{ _('To activate your account click (or copy and paste into a browser) the link below:') }} 6 | 7 | {{ url_for('users.activate', token=user.token, _external=True) }} 8 | 9 | {{ _('Regards,') }} 10 | {{ config.APP_TITLE }} 11 | -------------------------------------------------------------------------------- /webui/templates/users/new.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('New user account') }}{% endblock %} 3 | {% block pagetitle %}{{ _('New user account') }}{% endblock %} 4 | {% block body %} 5 |
6 | {% from "_formhelpers.html" import render_field %} 7 | {{ render_field(form.first_name) }} 8 | {{ render_field(form.last_name) }} 9 | {{ render_field(form.login) }} 10 | {{ render_field(form.email) }} 11 | {{ render_field(form.password) }} 12 | {{ render_field(form.confirm) }} 13 | {{ render_field(form.locale) }} 14 | {{ render_field(form.timezone) }} 15 | {{ render_field(form.account_type) }} 16 | {{ render_field(form.active) }} 17 | 18 | {{ form.hidden_tag() }} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /webui/templates/users/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block variables %} 3 | {% set class_html = 'bg-black' %} 4 | {% set class_body = 'bg-black' %} 5 | {% endblock %} 6 | {% block title %}{{ _('Register') }}{% endblock %} 7 | {% block body %} 8 |
9 | ℵ {{ config.APP_TITLE }} 10 |
{{ _('Register new account') }}
11 |
12 |
13 | {% from "_formhelpers.html" import render_field %} 14 | {{ render_field(form.first_name, silent=True) }} 15 | {{ render_field(form.last_name, silent=True) }} 16 | {{ render_field(form.login, silent=True) }} 17 | {{ render_field(form.email, silent=True) }} 18 | {{ render_field(form.password, silent=True) }} 19 | {{ render_field(form.confirm, silent=True) }} 20 |
21 | 25 | {{ form.hidden_tag() }} 26 |
27 | 28 | {% with messages = get_flashed_messages() %} 29 | {% if messages %} 30 |
31 | {% for message in messages %} 32 |

{{ message }}

33 | {% endfor %} 34 |
35 | {% endif %} 36 | {% endwith %} 37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /webui/templates/users/settings.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ _('Edit user settings') }}{% endblock %} 3 | {% block pagetitle %}{{ _('Edit user settings') }}{% endblock %} 4 | {% block body %} 5 |
6 | {% from "_formhelpers.html" import render_field %} 7 | {{ render_field(form.first_name) }} 8 | {{ render_field(form.last_name) }} 9 | {{ render_field(form.email) }} 10 | {{ render_field(form.locale) }} 11 | {{ render_field(form.timezone) }} 12 | 13 | {{ _('Change password') }} 14 | {{ form.hidden_tag() }} 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /webui/translations/es/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/translations/es/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /webui/translations/es/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Spanish translations for PROJECT. 2 | # Copyright (C) 2014 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2014. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2014-09-11 13:14-0300\n" 11 | "PO-Revision-Date: 2014-09-11 14:04-0300\n" 12 | "Last-Translator: Jan Seidl \n" 13 | "Language-Team: es \n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 1.3\n" 19 | 20 | #: webui/templates/401.html:6 21 | msgid "Access denied" 22 | msgstr "Acceso denegado" 23 | 24 | #: webui/templates/401.html:13 25 | msgid "You don't have access to this page." 26 | msgstr "Usted no tiene acceso a esta página." 27 | 28 | #: webui/templates/404.html:6 29 | msgid "Page not found" 30 | msgstr "Página no encontrada" 31 | 32 | #: webui/templates/404.html:13 33 | msgid "We could not find the page you were looking for." 34 | msgstr "No hemos podido encontrar la página que estabas buscando." 35 | 36 | #: webui/templates/404.html:14 37 | #, python-format 38 | msgid "Meanwhile, you may return to dashboard or try using the search form." 39 | msgstr "Mientras tanto, puedes volver al tablero de mandos o intente utilizar el buscador." 40 | 41 | #: webui/templates/header.html:10 42 | msgid "Toggle navigation" 43 | msgstr "Navegación toggle" 44 | 45 | #: webui/templates/header.html:35 46 | msgid "Settings" 47 | msgstr "Ajustes" 48 | 49 | #: webui/templates/header.html:38 50 | msgid "Sign out" 51 | msgstr "Desconectar" 52 | 53 | #: webui/templates/layout.html:51 54 | #: webui/templates/sidebar.html:29 55 | #: webui/templates/samples/index.html:2 56 | msgid "Samples" 57 | msgstr "Muestra" 58 | 59 | #: webui/templates/layout.html:54 60 | #: webui/templates/sidebar.html:61 61 | msgid "Users" 62 | msgstr "Usuarios" 63 | 64 | #: webui/templates/sidebar.html:8 65 | msgid "Search..." 66 | msgstr "Buscar..." 67 | 68 | #: webui/templates/sidebar.html:19 69 | #: webui/templates/general/index.html:2 70 | msgid "Dashboard" 71 | msgstr "Panel de control" 72 | 73 | #: webui/templates/sidebar.html:34 74 | #: webui/templates/general/index.html:142 75 | #: webui/templates/general/index.html:150 76 | #: webui/templates/samples/index.html:46 77 | msgid "View" 78 | msgstr "Ver" 79 | 80 | #: webui/templates/sidebar.html:36 81 | #: webui/templates/samples/view.html:8 82 | msgid "Basic Information" 83 | msgstr "Datos Básicos" 84 | 85 | #: webui/templates/sidebar.html:38 86 | #: webui/templates/samples/view.html:40 87 | msgid "Cross-References" 88 | msgstr "Referencias cruzadas" 89 | 90 | #: webui/templates/sidebar.html:40 91 | #: webui/templates/general/index.html:102 92 | #: webui/templates/samples/index.html:21 93 | #: webui/templates/samples/view.html:78 94 | msgid "Tags" 95 | msgstr "Etiquetas" 96 | 97 | #: webui/templates/sidebar.html:41 98 | #: webui/templates/samples/view.html:95 99 | msgid "Origins" 100 | msgstr "Orígenes" 101 | 102 | #: webui/templates/sidebar.html:42 103 | #: webui/templates/samples/view.html:121 104 | msgid "Hashes & Checksums" 105 | msgstr "" 106 | 107 | #: webui/templates/sidebar.html:49 108 | #: webui/templates/sidebar.html:65 109 | msgid "Browse" 110 | msgstr "Explorar" 111 | 112 | #: webui/templates/sidebar.html:50 113 | #: webui/templates/samples/submissions.html:2 114 | #: webui/templates/samples/submissions.html:3 115 | msgid "My submissions" 116 | msgstr "Mis envíos" 117 | 118 | #: webui/templates/sidebar.html:51 119 | #: webui/templates/sidebar.html:66 120 | msgid "Add new" 121 | msgstr "Añadir nuevo" 122 | 123 | #: webui/templates/general/index.html:3 124 | msgid "Aleph Web Interface" 125 | msgstr "" 126 | 127 | #: webui/templates/general/index.html:14 128 | msgid "Total Samples" 129 | msgstr "Muestras totales" 130 | 131 | #: webui/templates/general/index.html:21 132 | #: webui/templates/general/index.html:40 133 | #: webui/templates/general/index.html:58 134 | #: webui/templates/general/index.html:76 135 | msgid "More info" 136 | msgstr "Más información" 137 | 138 | #: webui/templates/general/index.html:33 139 | msgid "Samples in queue" 140 | msgstr "Muestras en la cola" 141 | 142 | #: webui/templates/general/index.html:51 143 | msgid "Samples being processed right now" 144 | msgstr "Muestras siendo procesados en este momento" 145 | 146 | #: webui/templates/general/index.html:69 147 | msgid "Samples processed" 148 | msgstr "Muestras procesadas" 149 | 150 | #: webui/templates/general/index.html:85 151 | msgid "Latest samples" 152 | msgstr "Últimas muestras" 153 | 154 | #: webui/templates/general/index.html:87 155 | msgid "View all samples" 156 | msgstr "Ver todas las muestras" 157 | 158 | #: webui/templates/general/index.html:92 159 | #: webui/templates/samples/index.html:8 160 | msgid "No samples on the database yet." 161 | msgstr "No hay muestras en la base de datos." 162 | 163 | #: webui/templates/general/index.html:96 164 | #: webui/templates/samples/index.html:15 165 | #: webui/templates/samples/submissions.html:13 166 | #: webui/templates/samples/view.html:99 167 | msgid "Timestamp" 168 | msgstr "Marca de tiempo" 169 | 170 | #: webui/templates/general/index.html:97 171 | #: webui/templates/general/index.html:140 172 | #: webui/templates/samples/index.html:16 173 | #: webui/templates/samples/submissions.html:14 174 | #: webui/templates/samples/view.html:13 175 | msgid "Status" 176 | msgstr "" 177 | 178 | #: webui/templates/general/index.html:98 179 | #: webui/templates/samples/index.html:17 180 | msgid "UUID" 181 | msgstr "" 182 | 183 | #: webui/templates/general/index.html:99 184 | #: webui/templates/samples/index.html:18 185 | msgid "Filenames" 186 | msgstr "Nombres de archivo" 187 | 188 | #: webui/templates/general/index.html:100 189 | #: webui/templates/samples/index.html:19 190 | #: webui/templates/samples/view.html:19 191 | #: webui/templates/samples/view.html:48 192 | msgid "MimeType" 193 | msgstr "" 194 | 195 | #: webui/templates/general/index.html:101 196 | #: webui/templates/samples/index.html:20 197 | #: webui/templates/samples/view.html:17 198 | #: webui/templates/samples/view.html:46 199 | msgid "Size" 200 | msgstr "Tamaño" 201 | 202 | #: webui/templates/general/index.html:108 203 | #: webui/templates/general/index.html:147 204 | #: webui/templates/general/index.html:148 205 | #: webui/templates/samples/index.html:28 206 | #: webui/templates/samples/submissions.html:23 207 | #: webui/templates/samples/submissions.html:24 208 | #: webui/templates/samples/view.html:14 209 | msgid "new" 210 | msgstr "nuevo" 211 | 212 | #: webui/templates/general/index.html:108 213 | #: webui/templates/general/index.html:148 214 | #: webui/templates/samples/index.html:28 215 | #: webui/templates/samples/submissions.html:24 216 | #: webui/templates/samples/view.html:14 217 | msgid "processing" 218 | msgstr "procesamiento" 219 | 220 | #: webui/templates/general/index.html:108 221 | #: webui/templates/general/index.html:148 222 | #: webui/templates/samples/index.html:28 223 | #: webui/templates/samples/submissions.html:24 224 | #: webui/templates/samples/view.html:14 225 | msgid "ready" 226 | msgstr "listo" 227 | 228 | #: webui/templates/general/index.html:109 229 | #: webui/templates/general/index.html:150 230 | #: webui/templates/samples/submissions.html:27 231 | #, python-format 232 | msgid "View report for sample %(sampleuuid)s" 233 | msgstr "Ver informe de muestra %(sampleuuid)s" 234 | 235 | #: webui/templates/general/index.html:129 236 | msgid "Latest submissions" 237 | msgstr "Últimos envíos" 238 | 239 | #: webui/templates/general/index.html:131 240 | msgid "View all submissions" 241 | msgstr "Ver todos los envíos" 242 | 243 | #: webui/templates/general/index.html:136 244 | #: webui/templates/samples/submissions.html:7 245 | msgid "No samples submitted." 246 | msgstr "No hay muestras enviadas." 247 | 248 | #: webui/templates/general/index.html:141 249 | #: webui/templates/samples/submissions.html:16 250 | #: webui/templates/samples/view.html:101 251 | msgid "Filename" 252 | msgstr "Nombre del archivo" 253 | 254 | #: webui/templates/samples/index.html:3 255 | msgid "Samples processed by Aleph" 256 | msgstr "Muestras procesadas por Aleph" 257 | 258 | #: webui/templates/samples/index.html:10 259 | msgid "No samples matching your query." 260 | msgstr "No hay muestras coincidan con su consulta." 261 | 262 | #: webui/templates/samples/index.html:22 263 | #: webui/templates/samples/index.html:41 264 | #: webui/templates/users/index.html:17 265 | msgid "Action" 266 | msgstr "Acción" 267 | 268 | #: webui/templates/samples/index.html:43 269 | msgid "Toggle Dropdown" 270 | msgstr "" 271 | 272 | #: webui/templates/samples/submissions.html:4 273 | msgid "Samples submitted by you" 274 | msgstr "Las muestras enviadas por usted" 275 | 276 | #: webui/templates/samples/submissions.html:15 277 | msgid "SHA256 Hash" 278 | msgstr "" 279 | 280 | #: webui/templates/samples/submissions.html:17 281 | #: webui/templates/samples/view.html:11 282 | #: webui/templates/samples/view.html:45 283 | msgid "Sample UUID" 284 | msgstr "UUID Muestra" 285 | 286 | #: webui/templates/samples/submissions.html:38 287 | msgid "Submit new sample" 288 | msgstr "Enviar nueva muestra" 289 | 290 | #: webui/templates/samples/submit.html:2 291 | #: webui/templates/samples/submit.html:3 292 | #: webui/templates/samples/submit.html:9 293 | msgid "Submit sample" 294 | msgstr "Enviar la muestra" 295 | 296 | #: webui/templates/samples/submit.html:4 297 | msgid "Submit a new sample for analysis" 298 | msgstr "Enviar una nueva muestra para el análisis" 299 | 300 | #: webui/templates/samples/view.html:2 301 | msgid "Sample" 302 | msgstr "Muestra" 303 | 304 | #: webui/templates/samples/view.html:3 305 | msgid "Extended sample information" 306 | msgstr "Información de la muestra extendida" 307 | 308 | #: webui/templates/samples/view.html:15 309 | msgid "Added On" 310 | msgstr "Añadido en" 311 | 312 | #: webui/templates/samples/view.html:21 313 | #: webui/templates/samples/view.html:47 314 | msgid "Mime" 315 | msgstr "" 316 | 317 | #: webui/templates/samples/view.html:23 318 | msgid "Known filenames" 319 | msgstr "Nombres de archivo conocidos" 320 | 321 | #: webui/templates/samples/view.html:44 322 | msgid "Relation" 323 | msgstr "Relación" 324 | 325 | #: webui/templates/samples/view.html:53 326 | msgid "Parent" 327 | msgstr "Padres" 328 | 329 | #: webui/templates/samples/view.html:54 330 | #, python-format 331 | msgid "View parent sample %(uuid)s" 332 | msgstr "Ver muestra padres %(uuid)s" 333 | 334 | #: webui/templates/samples/view.html:62 335 | msgid "Child" 336 | msgstr "Niño" 337 | 338 | #: webui/templates/samples/view.html:63 339 | #, python-format 340 | msgid "View child sample %(uuid)s" 341 | msgstr "Ver muestra niños %(uuid)s" 342 | 343 | #: webui/templates/samples/view.html:81 344 | msgid "This sample has no tags." 345 | msgstr "Esta muestra no tiene etiquetas." 346 | 347 | #: webui/templates/samples/view.html:100 348 | msgid "Provider" 349 | msgstr "Proveedor" 350 | 351 | #: webui/templates/samples/view.html:102 352 | msgid "Reference" 353 | msgstr "Referencia" 354 | 355 | #: webui/templates/samples/view.html:140 356 | msgid "Plugin Information" 357 | msgstr "Información de Plugin" 358 | 359 | #: webui/templates/samples/view.html:159 360 | #: webui/templates/samples/view.html:163 361 | #: webui/templates/samples/view.html:169 362 | #, python-format 363 | msgid "Search all samples for '%(keyname)s' with value '%(valuename)s'" 364 | msgstr "Buscar todas las muestras para '%(keyname)s' con valor '%(valuename)s'" 365 | 366 | #: webui/templates/users/change_password.html:2 367 | #: webui/templates/users/change_password.html:10 368 | #: webui/templates/users/settings.html:13 369 | msgid "Change password" 370 | msgstr "Cambiar la contraseña" 371 | 372 | #: webui/templates/users/change_password.html:3 373 | #: webui/templates/users/login.html:17 374 | #: aleph/webui/forms.py:11 375 | #: aleph/webui/forms.py:50 376 | #: aleph/webui/forms.py:58 377 | msgid "Password" 378 | msgstr "Contraseña" 379 | 380 | #: webui/templates/users/edit.html:2 381 | #: webui/templates/users/edit.html:3 382 | #: webui/templates/users/edit.html:17 383 | msgid "Edit user account" 384 | msgstr "Editar cuenta de usuario" 385 | 386 | #: webui/templates/users/index.html:2 387 | #: webui/templates/users/index.html:3 388 | msgid "System users" 389 | msgstr "Usuarios del sistema" 390 | 391 | #: webui/templates/users/index.html:6 392 | msgid "No users on system yet." 393 | msgstr "No hay usuarios en el sistema." 394 | 395 | #: webui/templates/users/index.html:12 396 | msgid "ID" 397 | msgstr "" 398 | 399 | #: webui/templates/users/index.html:13 400 | msgid "Name" 401 | msgstr "Nombre" 402 | 403 | #: webui/templates/users/index.html:14 404 | #: aleph/webui/forms.py:18 405 | msgid "Email" 406 | msgstr "" 407 | 408 | #: webui/templates/users/index.html:15 409 | msgid "Premium user" 410 | msgstr "Usuario premium" 411 | 412 | #: webui/templates/users/index.html:16 413 | msgid "Superuser" 414 | msgstr "Superusuario" 415 | 416 | #: webui/templates/users/index.html:55 417 | msgid "Add new user" 418 | msgstr "Añadir un nuevo usuario" 419 | 420 | #: webui/templates/users/login.html:6 421 | #: webui/templates/users/login.html:10 422 | msgid "Sign In" 423 | msgstr "Ingresar" 424 | 425 | #: webui/templates/users/login.html:14 426 | msgid "User ID" 427 | msgstr "ID de usuario" 428 | 429 | #: webui/templates/users/login.html:21 430 | msgid "Remember me" 431 | msgstr "Acuérdate de mí" 432 | 433 | #: webui/templates/users/login.html:25 434 | msgid "Sign me in" 435 | msgstr "Me Ingresa" 436 | 437 | #: webui/templates/users/login.html:26 438 | msgid "Register a new account" 439 | msgstr "Registrar una cuenta nueva" 440 | 441 | #: webui/templates/users/mail_register.html:3 442 | #, python-format 443 | msgid "Hello %(firstname)s %(lastname)s" 444 | msgstr "Hola %(firstname)s %(lastname)s" 445 | 446 | #: webui/templates/users/mail_register.html:5 447 | #: aleph/webui/views/users.py:251 448 | #, python-format 449 | msgid "Welcome to %(appname)s" 450 | msgstr "Bienvenido a %(appname)s" 451 | 452 | #: webui/templates/users/mail_register.html:7 453 | msgid "To activate your account click (or copy and paste into a browser) the link below:" 454 | msgstr "Para activar su cuenta, haga clic (o copiar y pegar en el navegador) en el siguiente enlace:" 455 | 456 | #: webui/templates/users/mail_register.html:11 457 | msgid "Regards," 458 | msgstr "Saludos," 459 | 460 | #: webui/templates/users/new.html:2 461 | #: webui/templates/users/new.html:3 462 | msgid "New user account" 463 | msgstr "Nueva cuenta de usuario" 464 | 465 | #: webui/templates/users/new.html:17 466 | msgid "Add user account" 467 | msgstr "Google Translate" 468 | 469 | #: webui/templates/users/register.html:6 470 | msgid "Register" 471 | msgstr "Regístrate" 472 | 473 | #: webui/templates/users/register.html:10 474 | msgid "Register new account" 475 | msgstr "Regístrate una nueva cuenta" 476 | 477 | #: webui/templates/users/register.html:22 478 | msgid "Sign me up" 479 | msgstr "Registrarme" 480 | 481 | #: webui/templates/users/register.html:23 482 | msgid "I already have a membership" 483 | msgstr "Ya tengo una suscripción" 484 | 485 | #: webui/templates/users/settings.html:2 486 | #: webui/templates/users/settings.html:3 487 | msgid "Edit user settings" 488 | msgstr "Editar la configuración de usuario" 489 | 490 | #: webui/templates/users/settings.html:12 491 | msgid "Save settings" 492 | msgstr "Guardar la configuración" 493 | 494 | #: aleph/webui/__init__.py:24 495 | msgid "Please log in to access this page." 496 | msgstr "Por favor, inicie sesión para acceder a esta página." 497 | 498 | #: aleph/webui/forms.py:10 499 | #: aleph/webui/forms.py:46 500 | msgid "Username" 501 | msgstr "Nombre de usuario" 502 | 503 | #: aleph/webui/forms.py:15 504 | msgid "First Name" 505 | msgstr "Primer Nombre" 506 | 507 | #: aleph/webui/forms.py:15 508 | msgid "First name must be between 2 and 30 characters long" 509 | msgstr "Nombre debe tener entre 2 y 30 caracteres" 510 | 511 | #: aleph/webui/forms.py:16 512 | msgid "Last Name" 513 | msgstr "Apellido" 514 | 515 | #: aleph/webui/forms.py:16 516 | msgid "Last name must be between 2 and 255 characters long" 517 | msgstr "Apellidos debe tener entre 2 y 255 caracteres" 518 | 519 | #: aleph/webui/forms.py:28 520 | msgid "Language" 521 | msgstr "Idioma" 522 | 523 | #: aleph/webui/forms.py:29 524 | msgid "Timezone" 525 | msgstr "Zona Horaria" 526 | 527 | #: aleph/webui/forms.py:31 528 | #: aleph/webui/views/users.py:47 529 | msgid "Account enabled" 530 | msgstr "Cuenta habilitada" 531 | 532 | #: aleph/webui/forms.py:34 533 | msgid "Regular account" 534 | msgstr "Cuenta regular" 535 | 536 | #: aleph/webui/forms.py:35 537 | msgid "Premium account" 538 | msgstr "Cuenta premium" 539 | 540 | #: aleph/webui/forms.py:36 541 | msgid "Superuser account" 542 | msgstr "Cuenta de superusuario" 543 | 544 | #: aleph/webui/forms.py:39 545 | msgid "Account Type" 546 | msgstr "Tipo de cuenta" 547 | 548 | #: aleph/webui/forms.py:41 549 | msgid "Token" 550 | msgstr "" 551 | 552 | #: aleph/webui/forms.py:42 553 | msgid "API Key" 554 | msgstr "" 555 | 556 | #: aleph/webui/forms.py:46 557 | msgid "Only alphanumeric characters valid" 558 | msgstr "Caracteres alfanuméricos sólo válido" 559 | 560 | #: aleph/webui/forms.py:52 561 | #: aleph/webui/forms.py:60 562 | msgid "Passwords must match" 563 | msgstr "Las contraseñas deben coincidir" 564 | 565 | #: aleph/webui/forms.py:54 566 | #: aleph/webui/forms.py:62 567 | msgid "Confirm password" 568 | msgstr "Confirmar contraseña" 569 | 570 | #: aleph/webui/forms.py:57 571 | msgid "Current password" 572 | msgstr "Contraseña actual" 573 | 574 | #: aleph/webui/views/samples.py:73 575 | #, python-format 576 | msgid "Cannot write to folder: %(folderpath)s" 577 | msgstr "No se puede escribir en la carpeta: %(folderpath)s" 578 | 579 | #: aleph/webui/views/samples.py:77 580 | msgid "No file submitted." 581 | msgstr "No se presentó archivo." 582 | 583 | #: aleph/webui/views/samples.py:87 584 | #, python-format 585 | msgid "Sample %(samplename)s (%(samplesize)s) is bigger than maximum file size allowed: %(maxsize)s" 586 | msgstr "Muestra %(samplename)s (%(samplesize)s) es más grande que el tamaño máximo de archivo permitido: %(maxsize)s" 587 | 588 | #: aleph/webui/views/samples.py:91 589 | #, python-format 590 | msgid "Sample %(samplename)s (%(samplesize)s) is smaller than minimum file size allowed: %(minsize)s" 591 | msgstr "Muestra %(samplename)s (%(samplesize)s) es más pequeño que el tamaño de archivo mínimo permitido: %(minsize)s" 592 | 593 | #: aleph/webui/views/samples.py:99 594 | msgid "File already submitted." 595 | msgstr "Archivo ya presentado." 596 | 597 | #: aleph/webui/views/samples.py:131 598 | msgid "Sample submitted successfully" 599 | msgstr "Muestra enviado correctamente" 600 | 601 | #: aleph/webui/views/users.py:67 602 | msgid "Account disabled" 603 | msgstr "Cuenta deshabilitada" 604 | 605 | #: aleph/webui/views/users.py:99 606 | msgid "User added successfully" 607 | msgstr "Usuario añadido correctamente" 608 | 609 | #: aleph/webui/views/users.py:140 610 | msgid "User updated successfully" 611 | msgstr "Usuario actualizado correctamente" 612 | 613 | #: aleph/webui/views/users.py:175 614 | msgid "Settings updated successfully" 615 | msgstr "Ajustes actualizado correctamente" 616 | 617 | #: aleph/webui/views/users.py:204 618 | msgid "Current password doesn't match" 619 | msgstr "La contraseña actual no coincide" 620 | 621 | #: aleph/webui/views/users.py:209 622 | msgid "Password changed successfully" 623 | msgstr "Contraseña cambiada con éxito" 624 | 625 | #: aleph/webui/views/users.py:231 626 | msgid "Email address already registered" 627 | msgstr "Endereço de correo electrónico já cadastrado" 628 | 629 | #: aleph/webui/views/users.py:258 630 | msgid "Account created successfully. Please check your email for instructions on activating your account" 631 | msgstr "Cuenta creada con éxito. Por favor revise su correo electrónico para obtener instrucciones sobre cómo activar tu cuenta" 632 | 633 | #: aleph/webui/views/users.py:280 634 | msgid "Account activated. Please login below" 635 | msgstr "Cuenta activa. Por favor ingresa abajo" 636 | 637 | #: aleph/webui/views/users.py:296 638 | #: aleph/webui/views/users.py:301 639 | msgid "Invalid credentials" 640 | msgstr "Credenciales no válidas" 641 | 642 | #: aleph/webui/views/users.py:305 643 | msgid "You have been successfully signed in" 644 | msgstr "Usted ha sido suscrito con éxito" 645 | 646 | #: aleph/webui/views/users.py:310 647 | msgid "Cannot sign in" 648 | msgstr "No se puede ingressar" 649 | 650 | -------------------------------------------------------------------------------- /webui/translations/pt/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merces/aleph/4e2b1ef24eff513e82981e27fe7358bca8cc65e5/webui/translations/pt/LC_MESSAGES/messages.mo --------------------------------------------------------------------------------