├── .gitignore ├── LICENSE ├── README.md ├── Vagrantfile ├── app ├── __init__.py ├── forms.py ├── models.py ├── my_datatables.py ├── static │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ └── fixes.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── jquery-2.1.3.js │ │ └── npm.js ├── templates │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── event_add.html │ ├── event_view.html │ ├── feed_config.html │ ├── index.html │ ├── indicator_edit.html │ ├── indicator_pending.html │ ├── indicator_search.html │ ├── macros │ │ └── form.html │ ├── modals │ │ ├── form_config_edit.html │ │ ├── form_event_edit.html │ │ ├── form_event_ioc_add.html │ │ ├── form_event_note_add.html │ │ └── form_mitigation_add_edit.html │ ├── table_view.html │ └── type_edit.html ├── utils.py └── views.py ├── config.py ├── db_create.py ├── db_migrate.py ├── db_populate.py ├── doc ├── ostip_addevent.png ├── ostip_addindicator.png ├── ostip_datatype_config.png ├── ostip_eventview.png ├── ostip_eventview2.png └── ostip_home.png ├── feeder ├── __init__.py ├── collect.py ├── feed.json.example ├── feed.py ├── logentry.py └── parse.py ├── requirements.txt ├── run.py ├── scripts ├── __init__.py ├── install-redis.sh ├── test_api.py └── update.sql └── tasks.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | #virtenv 7 | bin/ 8 | include/ 9 | lib/ 10 | local/ 11 | pip-selfcheck.json 12 | 13 | #app specific stuff 14 | app.db 15 | db_repository/ 16 | .idea/ 17 | .DS_Store 18 | redis-stable/ 19 | *.log 20 | feeder/feed.json 21 | 22 | 23 | celerybeat-schedule 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | To set up: 2 | git clone https://github.com/kx499/ostip.git 3 | virtualenv ostip 4 | cd ostip 5 | bin/pip install -r requirements.txt 6 | scripts/install-redis.sh 7 | ./db_create.py 8 | 9 | running: 10 | ../redis-stable/src/redis-server # Note this is started in install-redis.sh, but in subsequent runs, it's required. 11 | bin/celery -A tasks.celery worker --loglevel=info --beat 12 | ./run.py 13 | 14 | Note: if not running on localhost, add host=0.0.0.0 to app.run() in run.py, or use ./run.py --prod 15 | 16 | On Debian or Ubuntu systems, you will need to `sudo apt install git python-virtualenv python-pip python-dev` 17 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure(2) do |config| 2 | config.vm.box = "ubuntu/trusty64" 3 | config.vm.network "forwarded_port", guest: 5000, host: 5000 4 | config.vm.provision "shell", inline: <<-SHELL 5 | sudo apt-get update 6 | sudo apt-get install -y git python-virtualenv python-pip python-dev 7 | mkdir -p /opt 8 | cd /opt 9 | git clone https://github.com/kx499/ostip.git 10 | virtualenv ostip 11 | cd ostip 12 | bin/pip install -r requirements.txt 13 | scripts/install-redis.sh --DoNotStartRedis 14 | SHELL 15 | config.vm.provision "shell", run: 'always', inline: <<-SHELL 16 | cd /opt 17 | redis-stable/src/redis-server & 18 | cd /opt/ostip 19 | mkdir -p tmp 20 | touch tmp/ostip_access.log 21 | sudo chown -R vagrant:vagrant /opt/ostip 22 | SHELL 23 | config.vm.provision "shell", inline: <<-SHELL 24 | cd /opt/ostip 25 | ./db_create.py 26 | touch feeder/feed.json 27 | if [ -z `cat feeder/feed.json` ]; then echo "[]" > feeder/feed.json ; fi 28 | SHELL 29 | config.vm.provision "shell", run: 'always', inline: <<-SHELL 30 | cd /opt/ostip 31 | mkdir -p tmp 32 | touch tmp/ostip_access.log 33 | sudo chown -R vagrant:vagrant /opt/ostip 34 | sudo -u vagrant -- bin/celery -A tasks.celery worker --loglevel=info --beat & 35 | sudo -u vagrant -- ./run.py --prod & 36 | SHELL 37 | end 38 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | from celery import Celery 4 | 5 | app = Flask(__name__) 6 | app.config.from_object('config') 7 | db = SQLAlchemy(app) 8 | 9 | 10 | from app import views, models 11 | 12 | if not app.debug: 13 | import logging 14 | from logging.handlers import RotatingFileHandler 15 | 16 | #acces logs 17 | a_logger = logging.getLogger('werkzeug') 18 | handler = RotatingFileHandler('tmp/ostip_access.log', 'a', 1 * 1024 * 1024, 10) 19 | a_logger.addHandler(handler) 20 | 21 | #error/app info logs 22 | file_handler = RotatingFileHandler('tmp/ostip.log', 'a', 1 * 1024 * 1024, 10) 23 | file_handler.setFormatter(logging.Formatter('%(asctime)s %(module)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) 24 | app.logger.setLevel(logging.INFO) 25 | file_handler.setLevel(logging.INFO) 26 | app.logger.addHandler(file_handler) 27 | app.logger.info('OSTIP startup') 28 | 29 | def create_celery_app(app): 30 | app = app 31 | celery = Celery(__name__, broker=app.config['CELERY_BROKER_URL']) 32 | celery.conf.update(app.config) 33 | Taskbase = celery.Task 34 | 35 | class ContextTask(Taskbase): 36 | abstract = True 37 | 38 | def __call__(self, *args, **kwargs): 39 | with app.app_context(): 40 | return Taskbase.__call__(self, *args, **kwargs) 41 | 42 | celery.Task = ContextTask 43 | return celery 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import Form 2 | from wtforms import StringField, IntegerField, SelectField, BooleanField 3 | from wtforms.validators import DataRequired 4 | from wtforms.widgets import TextArea, HiddenInput 5 | from wtforms.ext.sqlalchemy.fields import QuerySelectField 6 | from .models import Source, Tlp, Level, Itype, Control, Status, Likelihood, Event, Destination 7 | 8 | 9 | class FeedConfigForm(Form): 10 | index_id = IntegerField(widget=HiddenInput()) 11 | name = StringField('Name', validators=[DataRequired()]) 12 | frequency = StringField('Frequency', validators=[DataRequired()]) 13 | event = QuerySelectField('Event', 14 | query_factory=lambda: Event.query.join(Source).filter(Source.name == 'Feed'), 15 | get_label='name') 16 | module = details = StringField('Modules', widget=TextArea(), validators=[DataRequired()]) 17 | 18 | 19 | 20 | class EventForm(Form): 21 | name = StringField('Name', validators=[DataRequired()]) 22 | details = StringField('Details', widget=TextArea(), validators=[DataRequired()]) 23 | confidence = SelectField('Confidence', coerce=int, validators=[DataRequired()]) 24 | source = QuerySelectField('Source', query_factory=lambda: Source.query, get_label='name') 25 | tlp = QuerySelectField('TLP', query_factory=lambda: Tlp.query, get_label='name') 26 | impact = QuerySelectField('Potential Impact', query_factory=lambda: Level.query, get_label='name') 27 | likelihood = QuerySelectField('Likelihood', query_factory=lambda: Likelihood.query, get_label='name') 28 | status = QuerySelectField('Status', query_factory=lambda: Status.query, get_label='name') 29 | 30 | 31 | class IndicatorForm(Form): 32 | event_id = IntegerField(widget=HiddenInput()) 33 | ioc = StringField('IOC', validators=[DataRequired()]) 34 | comment = StringField('Comment') 35 | control = QuerySelectField('Control Path', query_factory=lambda: Control.query, get_label='name') 36 | itype = QuerySelectField('Data Type', query_factory=lambda: Itype.query, get_label='name') 37 | 38 | 39 | class IndicatorEditForm(Form): 40 | event_id = IntegerField(widget=HiddenInput()) 41 | comment = StringField('Comment') 42 | control = QuerySelectField('Control Path', query_factory=lambda: Control.query, get_label='name') 43 | enrich = StringField('Enrich Summary') 44 | enrich_full = StringField('Enrich Details', widget=TextArea()) 45 | update_enrich = BooleanField('Update Enrichment') 46 | 47 | class MitigationForm(Form): 48 | mit_id = IntegerField(widget=HiddenInput()) 49 | description = StringField('Description', validators=[DataRequired()]) 50 | ttl = SelectField('TTL', coerce=int) 51 | destination = QuerySelectField('Destination', query_factory=lambda: Destination.query, get_label='name') 52 | pending = BooleanField('Pending') 53 | active = BooleanField('Active') 54 | 55 | 56 | 57 | class NoteForm(Form): 58 | event_id = IntegerField(widget=HiddenInput()) 59 | details = StringField('Note', widget=TextArea(), validators=[DataRequired()]) 60 | 61 | 62 | class ControlForm(Form): 63 | name = StringField('Name', validators=[DataRequired()]) 64 | 65 | 66 | class ItypeForm(Form): 67 | field_id = IntegerField() 68 | field_name = StringField('Name', validators=[DataRequired()]) 69 | field_regex = StringField('Regex') 70 | 71 | 72 | class LevelForm(Form): 73 | name = StringField('Name', validators=[DataRequired()]) 74 | 75 | 76 | class LikelihoodForm(Form): 77 | name = StringField('Name', validators=[DataRequired()]) 78 | 79 | 80 | class SourceForm(Form): 81 | name = StringField('Name', validators=[DataRequired()]) 82 | 83 | 84 | class StatusForm(Form): 85 | name = StringField('Name', validators=[DataRequired()]) 86 | 87 | class TlpForm(Form): 88 | name = StringField('Name', validators=[DataRequired()]) 89 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from app import db 4 | import datetime 5 | from sqlalchemy.ext.hybrid import hybrid_property 6 | from sqlalchemy.sql import select, func 7 | 8 | 9 | class Event(db.Model): 10 | __tablename__ = "event" 11 | id = db.Column(db.Integer, primary_key=True) 12 | name = db.Column(db.String(64), index=True, unique=True, nullable=False) 13 | details = db.Column(db.Text()) 14 | created = db.Column(db.DateTime, nullable=False) 15 | confidence = db.Column(db.Integer, nullable=False) 16 | status_id = db.Column(db.Integer, db.ForeignKey('status.id'), nullable=False) 17 | source_id = db.Column(db.Integer, db.ForeignKey('source.id'), nullable=False) 18 | tlp_id = db.Column(db.Integer, db.ForeignKey('tlp.id'), nullable=False) 19 | impact_id = db.Column(db.Integer, db.ForeignKey('level.id'), nullable=False) 20 | likelihood_id = db.Column(db.Integer, db.ForeignKey('likelihood.id'), nullable=False) 21 | 22 | source = db.relationship('Source', foreign_keys=source_id) 23 | tlp = db.relationship('Tlp', foreign_keys=tlp_id) 24 | impact = db.relationship('Level', foreign_keys=impact_id) 25 | likelihood = db.relationship('Likelihood', foreign_keys=likelihood_id) 26 | status = db.relationship('Status', foreign_keys=status_id) 27 | 28 | indicators = db.relationship('Indicator', backref='event', lazy='dynamic') 29 | rel_events = db.relationship('Links', backref='event', lazy='dynamic') 30 | notes = db.relationship('Note', backref='event', lazy='dynamic') 31 | 32 | @hybrid_property 33 | def indicator_count(self): 34 | return self.indicators.count() 35 | 36 | @indicator_count.expression 37 | def indicator_count(cls): 38 | return (select([func.count(Indicator.id)]). 39 | where(Indicator.event_id == cls.id). 40 | label("indicator_count") 41 | ) 42 | 43 | def __init__(self, name, details, source, tlp, impact, likelihood, confidence=50): 44 | self.name = name 45 | self.details = details 46 | self.confidence = confidence 47 | self.source = source 48 | self.tlp = tlp 49 | self.impact = impact 50 | self.likelihood = likelihood 51 | self.status = Status.query.get(1) 52 | self.created = datetime.datetime.utcnow() 53 | 54 | def as_dict(self): 55 | return '%s' % {c.name: getattr(self, c.name) for c in self.__table__.columns} 56 | 57 | def __repr__(self): 58 | return '' % (self.name) 59 | 60 | 61 | class Indicator(db.Model): 62 | __tablename__ = "indicator" 63 | id = db.Column(db.Integer, primary_key=True) 64 | ioc = db.Column(db.String(64), index=True, nullable=False) 65 | comment = db.Column(db.String(255)) 66 | enrich = db.Column(db.String(255)) 67 | enrich_full = db.Column(db.Text()) 68 | first_seen = db.Column(db.DateTime, nullable=False) 69 | last_seen = db.Column(db.DateTime, index=True, nullable=False) 70 | pending = db.Column(db.Boolean, nullable=False) 71 | event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) 72 | control_id = db.Column(db.Integer, db.ForeignKey('control.id'), nullable=False) 73 | itype_id = db.Column(db.Integer, db.ForeignKey('itype.id'), nullable=False) 74 | 75 | control = db.relationship('Control', foreign_keys=control_id) 76 | itype = db.relationship('Itype', foreign_keys=itype_id) 77 | rel_indicators = db.relationship('Links', backref='indicator', lazy='dynamic') 78 | mitigations = db.relationship('Mitigation', backref='indicator', lazy='dynamic') 79 | 80 | __table_args__ = (db.UniqueConstraint("ioc", "event_id", "itype_id", "control_id"), ) 81 | 82 | @hybrid_property 83 | def rel_list(self): 84 | return ','.join([str(i.rel_event_id) for i in self.rel_indicators]) 85 | 86 | def __init__(self, event_id, ioc, comment, control, itype, pending=False, enrich=None, enrich_full=None): 87 | self.event_id = event_id 88 | self.ioc = ioc 89 | self.comment = comment 90 | self.control = control 91 | self.itype = itype 92 | self.pending = pending 93 | self.enrich = enrich 94 | self.enrich_full = enrich_full 95 | self.first_seen = datetime.datetime.utcnow() 96 | self.last_seen = datetime.datetime.utcnow() 97 | 98 | 99 | def as_dict(self): 100 | return '%s' % {c.name: getattr(self, c.name) for c in self.__table__.columns} 101 | 102 | def __repr__(self): 103 | return '' % (self.ioc) 104 | 105 | 106 | class Links(db.Model): 107 | __tablename__ = "links" 108 | id = db.Column(db.Integer, primary_key=True) 109 | event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) 110 | indicator_id = db.Column(db.Integer, db.ForeignKey('indicator.id'), nullable=False) 111 | rel_event_id = db.Column(db.Integer, nullable=False) 112 | rel_indicator_id = db.Column(db.Integer, nullable=False) 113 | 114 | def __init__(self, event_id, indicator_id, rel_event_id, rel_indicator_id): 115 | self.event_id = event_id 116 | self.indicator_id = indicator_id 117 | self.rel_event_id = rel_event_id 118 | self.rel_indicator_id = rel_indicator_id 119 | 120 | def __repr__(self): 121 | return ' %r:%r>' % (self.event_id, self.indicator_id, self.rel_event_id, self.rel_indicator_id) 122 | 123 | class Note(db.Model): 124 | __tablename__ = "note" 125 | id = db.Column(db.Integer, primary_key=True) 126 | created = db.Column(db.DateTime, nullable=False) 127 | details = db.Column(db.Text()) 128 | event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) 129 | 130 | def __init__(self, event_id, details): 131 | self.details= details 132 | self.event_id = event_id 133 | self.created = datetime.datetime.utcnow() 134 | 135 | def __repr__(self): 136 | return '' % (self.details) 137 | 138 | 139 | class Mitigation(db.Model): 140 | __tablename__ = "mitigation" 141 | id = db.Column(db.Integer, primary_key=True) 142 | description = db.Column(db.String(255), nullable=False) 143 | created = db.Column(db.DateTime, nullable=False) 144 | ttl = db.Column(db.Integer, nullable=False) 145 | destination_id = db.Column(db.Integer, db.ForeignKey('destination.id'), nullable=False) 146 | indicator_id = db.Column(db.Integer, db.ForeignKey('indicator.id'), nullable=False) 147 | pending = db.Column(db.Boolean, nullable=False) 148 | active = db.Column(db.Boolean, nullable=False) 149 | 150 | destination = db.relationship('Destination', foreign_keys=destination_id) 151 | 152 | 153 | def __init__(self, destination_id, ttl, description): 154 | self.created = datetime.datetime.utcnow() 155 | self.pending = True 156 | self.active = True 157 | self.destination_id = destination_id 158 | self.ttl = ttl 159 | self.description = description 160 | 161 | 162 | def __repr__(self): 163 | return '' % (self.id) 164 | 165 | 166 | class Destination(db.Model): 167 | __tablename__ = "destination" 168 | id = db.Column(db.Integer, primary_key=True) 169 | name = db.Column(db.String(64), nullable=False) 170 | description = db.Column(db.String(255)) 171 | formatter = db.Column(db.String(64), nullable=False) 172 | 173 | __table_args__ = (db.UniqueConstraint("name"),) 174 | 175 | def __repr__(self): 176 | return '' % (self.name) 177 | 178 | 179 | class Tlp(db.Model): 180 | __tablename__ = "tlp" 181 | id = db.Column(db.Integer, primary_key=True) 182 | name = db.Column(db.String(64)) 183 | 184 | def __repr__(self): 185 | return '' % (self.name) 186 | 187 | 188 | class Level (db.Model): 189 | __tablename__ = "level" 190 | id = db.Column(db.Integer, primary_key=True) 191 | name = db.Column(db.String(64)) 192 | 193 | def __repr__(self): 194 | return '' % (self.name) 195 | 196 | 197 | class Likelihood (db.Model): 198 | __tablename__ = "likelihood" 199 | id = db.Column(db.Integer, primary_key=True) 200 | name = db.Column(db.String(64)) 201 | 202 | def __repr__(self): 203 | return '' % (self.name) 204 | 205 | 206 | class Source(db.Model): 207 | __tablename__ = "source" 208 | id = db.Column(db.Integer, primary_key=True) 209 | name = db.Column(db.String(64)) 210 | 211 | def __repr__(self): 212 | return '' % (self.name) 213 | 214 | 215 | class Itype(db.Model): 216 | __tablename__ = "itype" 217 | id = db.Column(db.Integer, primary_key=True) 218 | name = db.Column(db.String(64)) 219 | regex = db.Column(db.String(255)) 220 | 221 | def __init__(self, name, regex): 222 | self.name = name 223 | self.regex = regex 224 | 225 | def __repr__(self): 226 | return '' % (self.name) 227 | 228 | 229 | class Control(db.Model): 230 | __tablename__ = "control" 231 | id = db.Column(db.Integer, primary_key=True) 232 | name = db.Column(db.String(64)) 233 | 234 | def __repr__(self): 235 | return '' % (self.name) 236 | 237 | 238 | class Status(db.Model): 239 | __tablename__ = "status" 240 | id = db.Column(db.Integer, primary_key=True) 241 | name = db.Column(db.String(64)) 242 | 243 | def __repr__(self): 244 | return '' % (self.name) -------------------------------------------------------------------------------- /app/my_datatables.py: -------------------------------------------------------------------------------- 1 | """Main entry file, definition of ColumnDT and DataTables.""" 2 | import sys 3 | 4 | from sqlalchemy.sql.expression import asc, desc, nullsfirst, nullslast 5 | from sqlalchemy.sql import or_, and_, text 6 | from sqlalchemy.orm.properties import RelationshipProperty 7 | from sqlalchemy.orm.collections import InstrumentedList 8 | from sqlalchemy.sql.expression import cast 9 | from sqlalchemy import String 10 | 11 | from collections import namedtuple 12 | from logging import getLogger 13 | 14 | log = getLogger(__file__) 15 | 16 | if sys.version_info > (3, 0): 17 | unicode = str 18 | 19 | nullsMethods = { 20 | 'nullsfirst': nullsfirst, 21 | 'nullslast': nullslast 22 | } 23 | 24 | REGEX_OP = { 25 | 'mysql': 'regexp', 26 | 'postgresql': '~', 27 | } 28 | 29 | ColumnTuple = namedtuple( 30 | 'ColumnDT', 31 | ['column_name', 'mData', 'search_like', 'filter', 'searchable', 32 | 'filterarg', 'nulls_order']) 33 | 34 | 35 | def get_attr(sqla_object, attribute): 36 | """Return the value of an attribute of an SQLAlchemy entity.""" 37 | output = sqla_object 38 | for x in attribute.split('.'): 39 | if type(output) is InstrumentedList: 40 | output = ', '.join([getattr(elem, x) for elem in output]) 41 | else: 42 | output = getattr(output, x, None) 43 | return output 44 | 45 | 46 | def clean_regex(regex): 47 | ''' 48 | escape any regex special characters other than alternation | 49 | 50 | :param regex: regex from datatables interface 51 | :type regex: str 52 | :rtype: str with regex to use with database 53 | ''' 54 | # copy for return 55 | ret_regex = str(regex) 56 | 57 | 58 | 59 | # these characters are escaped (all except alternation | and escape \) 60 | # see http://www.regular-expressions.info/refquick.html 61 | escape_chars = '[^$.?*+(){}' 62 | 63 | # remove any escape chars 64 | ret_regex = ret_regex.replace('\\','') 65 | 66 | # escape any characters which are used by regex 67 | # could probably concoct something incomprehensible using re.sub() but 68 | # prefer to write clear code with this loop 69 | # note expectation that no characters have already been escaped 70 | for c in escape_chars: 71 | ret_regex = ret_regex.replace(c,'\\'+c) 72 | 73 | # remove any double alternations until these don't exist any more 74 | while True: 75 | old_regex = ret_regex 76 | ret_regex = ret_regex.replace('||', '|') 77 | if old_regex == ret_regex: break 78 | 79 | # if last char is alternation | remove it because this 80 | # will cause operational error 81 | # this can happen as user is typing in global search box 82 | while len(ret_regex) >= 1 and ret_regex[-1] == '|': 83 | ret_regex = ret_regex[:-1] 84 | 85 | # and back to the caller 86 | return ret_regex 87 | 88 | class InvalidParameter(Exception): 89 | 90 | """Class defining an invalid parameter exception.""" 91 | 92 | pass 93 | 94 | 95 | class ColumnDT(ColumnTuple): 96 | 97 | """Class defining a DataTables Column with a ColumnTuple. 98 | 99 | :param column_name: name of the column as defined by the SQLAlchemy model 100 | :type column_name: str 101 | :param mData: name of the mData property as defined in the 102 | DataTables javascript options (default None) 103 | :type mData: str 104 | :param search_like: is the search made in the middle of the column value. 105 | If not, the column value must equals to the search value (default True) 106 | :type search_like: bool 107 | :param filter: the method needed to be executed on the cell 108 | values of the column 109 | as an equivalent of a jinja2 filter (default None) 110 | :type filter: a callable object 111 | :param searchable: enable or disable a column to be searchable 112 | server-side. (default True) 113 | :type searchable: bool 114 | :param filterarg: type of argument for filter function 115 | :type filterarg: string: 'cell' or 'row'. 'cell' is default 116 | :param nulls_order: define a sort order for the NULL values. Possible 117 | values: nullsfirst, nullslast. (default None) 118 | :type nulls_order: str 119 | 120 | :returns: a ColumnDT object 121 | """ 122 | 123 | def __new__(cls, column_name, mData=None, search_like=True, 124 | filter=str, searchable=True, filterarg='cell', 125 | nulls_order=None): 126 | """Set default values for mData and filter. 127 | 128 | On creation, sets default None values for mData and string value for 129 | filter (cause: Object representation is not JSON serializable). 130 | """ 131 | # check if allowed value 132 | if nulls_order and nulls_order not in ['nullsfirst', 'nullslast']: 133 | raise ValueError('`%s` is not an allowed value for nulls_order.' 134 | % nulls_order) 135 | 136 | return super(ColumnDT, cls).__new__( 137 | cls, column_name, mData, search_like, filter, searchable, 138 | filterarg, nulls_order) 139 | 140 | 141 | class DataTables: 142 | 143 | """Class defining a DataTables object. 144 | 145 | :param request: request containing the GET values, specified by the 146 | datatable for filtering, sorting and paging 147 | :type request: pyramid.request 148 | :param sqla_object: your SQLAlchemy table object 149 | :type sqla_object: sqlalchemy.ext.declarative.DeclarativeMeta 150 | :param query: the query wanted to be seen in the the table 151 | :type query: sqlalchemy.orm.query.Query 152 | :param columns: columns specification for the datatables 153 | :type columns: list 154 | 155 | :returns: a DataTables object 156 | """ 157 | 158 | def __init__(self, request, sqla_object, query, columns, dialect=None): 159 | """Initialize object and run the query.""" 160 | self.request_values, self.legacy = DataTables.prepare_arguments( 161 | request) 162 | self.sqla_object = sqla_object 163 | self.query = query 164 | self.columns = columns 165 | self.results = None 166 | self.dialect = dialect 167 | 168 | # total in the table after filtering 169 | self.cardinality_filtered = 0 170 | 171 | # total in the table unfiltered 172 | self.cardinality = 0 173 | 174 | self.run() 175 | 176 | @classmethod 177 | def prepare_arguments(cls, request): 178 | """Prepare DataTables with default arguments.""" 179 | request_values = dict() 180 | legacy = False 181 | for key, value in request.items(): 182 | try: 183 | request_values[key] = int(value) 184 | except ValueError: 185 | if value in ('true', 'false'): 186 | request_values[key] = value == 'true' 187 | else: # assume string 188 | request_values[key] = value 189 | 190 | # check if DT is older than 1.10.x 191 | if request_values.get('sEcho'): 192 | legacy = True 193 | 194 | return request_values, legacy 195 | 196 | def output_result(self): 197 | """Output results in the format needed by DataTables.""" 198 | output = {} 199 | 200 | if self.legacy: 201 | echo = 'sEcho' 202 | totalRecords = 'iTotalRecords' 203 | totalDisplayRecords = 'iTotalDisplayRecords' 204 | data = 'aaData' 205 | else: 206 | echo = 'draw' 207 | totalRecords = 'recordsTotal' 208 | totalDisplayRecords = 'recordsFiltered' 209 | data = 'data' 210 | 211 | output[echo] = str(int(self.request_values[echo])) 212 | output[totalRecords] = str(self.cardinality) 213 | output[totalDisplayRecords] = str(self.cardinality_filtered) 214 | 215 | output[data] = self.results 216 | 217 | return output 218 | 219 | def run(self): 220 | """Launch filtering, sorting and paging to output results.""" 221 | # count before filtering 222 | self.cardinality = self.query.count() 223 | 224 | # the term entered in the datatable's search box 225 | self.filtering() 226 | 227 | # field chosen to sort on 228 | self.sorting() 229 | 230 | # pages have a 'start' and 'length' attributes 231 | self.paging() 232 | 233 | # fetch the result of the queries 234 | self.results = self.query.all() 235 | 236 | # return formatted results with correct filters applied 237 | formatted_results = [] 238 | for i in range(len(self.results)): 239 | row = dict() 240 | for j in range(len(self.columns)): 241 | col = self.columns[j] 242 | if col.filter: 243 | if col.filterarg == 'cell': 244 | tmp_row = get_attr(self.results[i], col.column_name) 245 | if sys.version_info < (3, 0) \ 246 | and hasattr(tmp_row, 'encode'): 247 | tmp_row = tmp_row.encode('utf-8') 248 | tmp_row = col.filter(tmp_row) 249 | elif col.filterarg == 'row': 250 | tmp_row = col.filter(self.results[i]) 251 | else: 252 | raise InvalidParameter( 253 | "invalid filterarg %s for \ column_name %s: \ 254 | filterarg must be 'row' or 'cell'" 255 | % col.filterarg, col.column_name) 256 | else: 257 | tmp_row = get_attr(self.results[i], col.column_name) 258 | row[col.mData if col.mData else str(j)] = tmp_row 259 | formatted_results.append(row) 260 | 261 | self.results = formatted_results 262 | 263 | def filtering(self): 264 | """Construct the query: filtering. 265 | 266 | Add filtering(LIKE) on all columns when the datatable's search 267 | box is used. 268 | """ 269 | if self.legacy: 270 | # see http://legacy.datatables.net/usage/server-side 271 | searchValue = self.request_values.get('sSearch') 272 | searchRegex = self.request_values.get('bRegex') 273 | searchableColumn = 'bSearchable_%s' 274 | searchableColumnValue = 'sSearch_%s' 275 | searchableColumnRegex = 'bRegex_%s' 276 | else: 277 | searchValue = self.request_values.get('search[value]') 278 | searchRegex = self.request_values.get('search[regex]') 279 | searchableColumn = 'columns[%s][searchable]' 280 | searchableColumnValue = 'columns[%s][search][value]' 281 | searchableColumnRegex = 'columns[%s][search][regex]' 282 | 283 | condition = None 284 | 285 | def search(idx, col): 286 | # FIXME: @hybrid properties that reference json or similar columns 287 | tmp_column_name = col.column_name.split('.') 288 | for tmp_name in tmp_column_name: 289 | # This handles the x.y.z.a option 290 | if tmp_column_name.index(tmp_name) == 0: 291 | obj = getattr(self.sqla_object, tmp_name) 292 | parent = self.sqla_object 293 | elif isinstance(obj.property, RelationshipProperty): 294 | # otherwise try and see if we can percolate down the list 295 | # for relationships of relationships. 296 | parent = obj.property.mapper.class_ 297 | obj = getattr(parent, tmp_name) 298 | 299 | # Ex: hybrid_property or property 300 | if not hasattr(obj, 'property'): 301 | sqla_obj = parent 302 | column_name = tmp_name 303 | # Ex: ForeignKey 304 | elif isinstance(obj.property, RelationshipProperty): 305 | # Ex: address.description 306 | sqla_obj = obj.mapper.class_ 307 | column_name = tmp_name 308 | if not column_name: 309 | # find first primary key 310 | column_name = obj.property.table.primary_key.columns \ 311 | .values()[0].name 312 | else: 313 | sqla_obj = parent 314 | column_name = tmp_name 315 | return sqla_obj, column_name 316 | 317 | if searchValue: 318 | conditions = [] 319 | 320 | # only need to call this once 321 | regex = clean_regex(searchValue) 322 | 323 | # loop through columns looking for global search value 324 | for idx, col in enumerate(self.columns): 325 | if self.request_values.get(searchableColumn % idx) in ( 326 | True, 'true') and col.searchable: 327 | sqla_obj, column_name = search(idx, col) 328 | # regex takes precedence 329 | if (searchRegex in ( True, 'true') 330 | and self.dialect in REGEX_OP 331 | and len(regex) >= 1): 332 | conditions.append(cast( 333 | get_attr(sqla_obj, column_name), String) 334 | .op(REGEX_OP[self.dialect])(regex)) 335 | # use like 336 | else: 337 | conditions.append(cast( 338 | get_attr(sqla_obj, column_name), String) 339 | .ilike('%%%s%%' % searchValue)) 340 | condition = or_(*conditions) 341 | conditions = [] 342 | for idx, col in enumerate(self.columns): 343 | search_value2 = self.request_values.get( 344 | searchableColumnValue % idx) 345 | 346 | if search_value2: 347 | sqla_obj, column_name = search(idx, col) 348 | 349 | # regex takes precedence over search_like 350 | regex = clean_regex(search_value2) 351 | if (self.request_values.get(searchableColumnRegex % idx) 352 | in ( True, 'true') and 353 | self.dialect in REGEX_OP and 354 | len(regex) >= 1): 355 | conditions.append(cast( 356 | get_attr(sqla_obj, column_name), String) 357 | .op(REGEX_OP[self.dialect])(regex)) 358 | elif col.search_like: 359 | conditions.append(cast( 360 | get_attr(sqla_obj, column_name), String) 361 | .ilike('%%%s%%' % search_value2)) 362 | else: 363 | conditions.append(cast( 364 | get_attr(sqla_obj, column_name), String) 365 | .__eq__(search_value2)) 366 | 367 | if condition is not None: 368 | condition = and_(condition, and_(*conditions)) 369 | else: 370 | condition = and_(*conditions) 371 | 372 | if condition is not None: 373 | self.query = self.query.filter(condition) 374 | # count after filtering 375 | self.cardinality_filtered = self.query.count() 376 | else: 377 | self.cardinality_filtered = self.cardinality 378 | 379 | def sorting(self): 380 | """Construct the query: sorting. 381 | 382 | Add sorting(ORDER BY) on the columns needed to be applied on. 383 | """ 384 | sorting = [] 385 | 386 | Order = namedtuple('order', ['name', 'dir', 'nullsOrder']) 387 | 388 | if self.legacy: 389 | columnOrder = 'iSortCol_%s' 390 | dirOrder = 'sSortDir_%s' 391 | else: 392 | columnOrder = 'order[%s][column]' 393 | dirOrder = 'order[%s][dir]' 394 | 395 | i = 0 396 | if self.request_values.get(columnOrder % i) is not None: 397 | sorting.append( 398 | Order( 399 | self.columns[ 400 | int(self.request_values[columnOrder % i])].column_name, 401 | self.request_values[dirOrder % i], 402 | self.columns[ 403 | int(self.request_values[columnOrder % i])] 404 | .nulls_order)) 405 | 406 | for sort in sorting: 407 | tmp_sort_name = sort.name.split('.') 408 | for tmp_name in tmp_sort_name: 409 | # iterate over the list so we can support things like x.y.z.a 410 | if tmp_sort_name.index(tmp_name) == 0: 411 | obj = getattr(self.sqla_object, tmp_name) 412 | parent = self.sqla_object 413 | elif isinstance(obj.property, RelationshipProperty): 414 | # otherwise try and see if we can percolate down the list 415 | # for relationships of relationships. 416 | parent = obj.property.mapper.class_ 417 | obj = getattr(parent, tmp_name) 418 | 419 | if not hasattr(obj, 'property'): # hybrid_property or property 420 | sort_name = tmp_name 421 | if hasattr(parent, '__tablename__'): 422 | tablename = parent.__tablename__ 423 | else: 424 | tablename = parent.__table__.name 425 | tablename = None 426 | # Ex: ForeignKey 427 | elif isinstance(obj.property, RelationshipProperty): 428 | # Ex: address.description => description => 429 | # addresses.description 430 | sort_name = tmp_name 431 | if not sort_name: 432 | # Find first primary key 433 | sort_name = obj.property.table.primary_key.columns \ 434 | .values()[0].name 435 | tablename = obj.property.table.name 436 | else: # -> ColumnProperty 437 | sort_name = tmp_name 438 | 439 | if hasattr(parent, '__tablename__'): 440 | tablename = parent.__tablename__ 441 | else: 442 | tablename = parent.__table__.name 443 | if tablename: 444 | sort_name = text('%s.%s' % (tablename, sort_name)) 445 | else: 446 | sort_name = getattr(parent, sort_name) 447 | 448 | 449 | ordering = asc(sort_name) if sort.dir == 'asc' else desc(sort_name) 450 | 451 | if sort.nullsOrder: 452 | ordering = nullsMethods[sort.nullsOrder](ordering) 453 | 454 | self.query = self.query.order_by(ordering) 455 | 456 | def paging(self): 457 | """Construct the query: paging. 458 | 459 | Slice the results in order to limit rows showed on the page, and 460 | paginate the rest. 461 | """ 462 | pages = namedtuple('pages', ['start', 'length']) 463 | 464 | if self.legacy: 465 | displayStart = 'iDisplayStart' 466 | displayLength = 'iDisplayLength' 467 | else: 468 | displayStart = 'start' 469 | displayLength = 'length' 470 | 471 | if (self.request_values[displayStart] != '') \ 472 | and (self.request_values[displayLength] != -1): 473 | pages.start = int(self.request_values[displayStart]) 474 | pages.length = int(self.request_values[displayLength]) 475 | 476 | offset = pages.start + pages.length 477 | self.query = self.query.slice(pages.start, offset) 478 | -------------------------------------------------------------------------------- /app/static/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /app/static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /app/static/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA","sourcesContent":["/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} -------------------------------------------------------------------------------- /app/static/css/fixes.css: -------------------------------------------------------------------------------- 1 | .dl-horizontal dd { 2 | margin-left: 90; 3 | } 4 | 5 | .dl-horizontal dt { 6 | width:auto; 7 | } -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/app/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/app/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/app/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/app/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/static/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |

File Not Found

6 |

Back

7 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |

An unexpected error has occurred

6 |

The administrator has been notified. Sorry for the inconvenience!

7 |

Back

8 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if title %} 4 | {{ title }} - OSTIP 5 | {% else %} 6 | Welcome to OSTIP 7 | {% endif %} 8 | 9 | 10 | 11 | {% block extra_stylesheets %} {% endblock %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 59 | 60 |
61 | {% with messages = get_flashed_messages() %} 62 | {% if messages %} 63 | {% for message in messages %} 64 | 65 | {% endfor %} 66 | {% endif %} 67 | {% endwith %} 68 | {% block content %}{% endblock %} 69 |
70 | 71 |
72 |
73 |
74 |

OSTIP Example App

75 |
76 |
77 | {% block extra_javascripts %} {% endblock %} 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/templates/event_add.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% import "macros/form.html" as forms %} 4 | {% block content %} 5 |

Add Event

6 |
7 |
8 |
9 | {{ forms.render_field(form.status) }} 10 |
11 |
12 |
13 |
14 | {{ forms.render_field(form.name) }} 15 |
16 |
17 |
18 |
19 | {{ forms.render_field(form.details) }} 20 |
21 |
22 |
23 |
24 | {{ forms.render_field(form.tlp) }} 25 |
26 |
27 | {{ forms.render_field(form.impact) }} 28 |
29 |
30 |
31 |
32 | {{ forms.render_field(form.source) }} 33 |
34 |
35 | {{ forms.render_field(form.likelihood) }} 36 |
37 |
38 |
39 |
40 | {{ forms.render_field(form.confidence) }} 41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 | {{ form.hidden_tag() }} 49 |
50 | {% endblock %} 51 | 52 | -------------------------------------------------------------------------------- /app/templates/event_view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "macros/form.html" as forms %} 3 | 4 | {% block extra_stylesheets %} 5 | 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 | {% include "modals/form_event_ioc_add.html" %} 13 | {% include "modals/form_event_edit.html" %} 14 | {% include "modals/form_event_note_add.html" %} 15 | 16 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
Name
40 |
{{ event.name }}
41 |
Details
42 |
{{ event.details }}
43 |
44 |
45 |
46 |
47 |
Created
48 |
{{ event.created.strftime('%Y-%m-%d') }}
49 |
Status
50 |
{{ event.status.name }}
51 |
Confidence
52 |
{{ event.confidence }}
53 |
Source
54 |
{{ event.source.name }}
55 |
TLP
56 |
{{ event.tlp.name }}
57 |
Impact
58 |
{{ event.impact.name }}
59 |
Likelihood
60 |
{{ event.likelihood.name }}
61 |
62 |
63 |
64 |
65 |
66 | 67 |
68 |
69 | 70 |
71 |
72 |
73 |
74 |

Associated Indicators

75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
#DataTypeControlCommentEnrichFirst SeenLast SeenRelated Event
91 |
92 |
93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 |

Analyst Notes

101 | {% for note in event.notes %} 102 |
103 |
104 |

{{ note.created.strftime('%Y-%m-%d %H:%M') }}

105 |
106 |
107 | {% autoescape false %} 108 | {{ note.details }} 109 | {% endautoescape %} 110 |
111 |
112 | {% endfor %} 113 |
114 |
115 | {% endblock %} 116 | 117 | {% block extra_javascripts %} 118 | 119 | 120 | 121 | 158 | {% endblock %} 159 | -------------------------------------------------------------------------------- /app/templates/feed_config.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | {% include "modals/form_config_edit.html" %} 5 | 6 |
7 |
8 |

{{ title }}

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for dt in data %} 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | {% endfor %} 32 |
NameFrequencyEventConfigActions
{{ dt['name'] }}{{ dt['frequency'] }}{{ dt['event_name'] }}
{{ dt['modules'] }}
27 | 28 | 29 |
33 |
34 |
35 | {% endblock %} 36 | 37 | {% block extra_javascripts %} 38 | 101 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_stylesheets %} 4 | 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

Recent Events

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
#NameStatusSourceTLPConfidenceCreation dateIndicators
28 |
29 |
30 | {% endblock %} 31 | 32 | {% block extra_javascripts %} 33 | 34 | 35 | 36 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /app/templates/indicator_edit.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% import "macros/form.html" as forms %} 4 | {% block content %} 5 | 6 | {% include "modals/form_mitigation_add_edit.html" %} 7 |
8 |
9 |

View/Edit Indicator

10 |
11 |
12 |
13 |
14 |
Value
15 |
{{ indicator.ioc }}
16 |
Data Type
17 |
{{ indicator.itype.name }}
18 |
Event
19 |
{{ indicator.event.name }}
20 |
First Seen
21 |
{{ indicator.first_seen }}
22 |
Last Seen
23 |
{{ indicator.last_seen }}
24 |
25 |
26 |
27 |
28 |
29 | {{ forms.render_field(form.comment) }} 30 |
31 |
32 |
33 |
34 | {{ forms.render_field(form.control) }} 35 |
36 |
37 |
38 |
39 | {{ forms.render_field(form.enrich) }} 40 |
41 |
42 |
43 |
44 | {{ forms.render_field(form.enrich_full, rows=20) }} 45 |
46 |
47 |
48 |
49 | {{ forms.render_field(form.update_enrich) }} 50 |
51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 | {{ form.hidden_tag() }} 59 |
60 |
61 |
62 |
63 |
64 |

Mitigations

65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {% for m in indicator.mitigations %} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 89 | 90 | {% endfor %} 91 |
#CreatedDeviceTTLDescriptionPending
{{ m.id}}{{ m.created }}{{ m.destination.name }}{{ m.ttl }}{{ m.description }}{{ m.pending }} 86 | 87 | 88 |
92 |
93 |
94 |
95 |
96 |

Research

97 |
98 |
99 | 100 | 101 | 159 | {% endblock %} 160 | 161 | -------------------------------------------------------------------------------- /app/templates/indicator_pending.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_stylesheets %} 4 | 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

Pending Indicators

13 |
14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
18 | 19 | IndicatorTypeControlCommentEnrichCreation dateEvent IdEvent
32 | 33 |
34 |
35 |
36 | {% endblock %} 37 | 38 | {% block extra_javascripts %} 39 | 40 | 41 | 42 | 92 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/indicator_search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_stylesheets %} 4 | 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

Pending Indicators

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
IdIndicatorTypeControlCommentEnrichCreation dateEvent IdEvent
29 |
30 |
31 | {% endblock %} 32 | 33 | {% block extra_javascripts %} 34 | 35 | 36 | 37 | 60 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/macros/form.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 | {{ field.label }} 3 | {{ field(class_="form-control form-control-warning", **kwargs) }} 4 | {% for error in field.errors %} 5 |
[{{ error }}]
6 | 7 | {% endfor %} 8 | {% endmacro %} -------------------------------------------------------------------------------- /app/templates/modals/form_config_edit.html: -------------------------------------------------------------------------------- 1 | {% import "macros/form.html" as forms %} 2 | -------------------------------------------------------------------------------- /app/templates/modals/form_event_edit.html: -------------------------------------------------------------------------------- 1 | {% import "macros/form.html" as forms %} 2 | -------------------------------------------------------------------------------- /app/templates/modals/form_event_ioc_add.html: -------------------------------------------------------------------------------- 1 | {% import "macros/form.html" as forms %} 2 | -------------------------------------------------------------------------------- /app/templates/modals/form_event_note_add.html: -------------------------------------------------------------------------------- 1 | {% import "macros/form.html" as forms %} 2 | -------------------------------------------------------------------------------- /app/templates/modals/form_mitigation_add_edit.html: -------------------------------------------------------------------------------- 1 | {% import "macros/form.html" as forms %} 2 | -------------------------------------------------------------------------------- /app/templates/table_view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "macros/form.html" as forms %} 3 | {% block content %} 4 | 38 | 39 |
40 | {% for title in fields.keys() %} 41 |
42 |

{{ title }}

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {% for row in fields[title][0] %} 53 | 54 | 55 | 56 | 60 | 61 | {% endfor %} 62 |
#NameActions
{{ row.id }}{{ row.name }} 57 | 58 | 59 |
63 |
64 | {% endfor %} 65 |
66 | 67 | 106 | {% endblock %} 107 | -------------------------------------------------------------------------------- /app/templates/type_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "macros/form.html" as forms %} 3 | {% block content %} 4 | 43 | 44 |
45 |
46 |

{{ title }}

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {% for dt in data_types %} 58 | 59 | 60 | 61 | 62 | 66 | 67 | {% endfor %} 68 |
#NameRegexActions
{{ dt.id }}{{ dt.name }}{{ dt.regex }} 63 | 64 | 65 |
69 |
70 |
71 | 72 | 119 | {% endblock %} 120 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from app import app 3 | from .models import Indicator, Control, Itype, Links, Event 4 | from feeder.logentry import ResultsDict 5 | from whois import whois 6 | from ipwhois import IPWhois 7 | import pprint 8 | import re 9 | 10 | 11 | def _load_related_data(data): 12 | ioc = {} 13 | items = Indicator.query.filter_by(event_id=data.event_id).all() 14 | [ioc.update({item.ioc: item.id}) for item in items] 15 | 16 | data_types = {} 17 | d_items = Itype.query.all() 18 | [data_types.update({d_item.name: [d_item, d_item.regex]}) for d_item in d_items] 19 | 20 | control = Control.query.filter_by(name=data.control).first() 21 | if not control: 22 | raise Exception("Control not found") 23 | 24 | for dt in data.data_types.keys(): 25 | data_type = data_types.get(dt) 26 | if not data_type: 27 | raise Exception("Data Type not found") 28 | 29 | return ioc, control, data_types 30 | 31 | 32 | def _correlate(indicator_list): 33 | # not yet implemented 34 | for ind_id, ev_id, val in indicator_list: 35 | for i in Indicator.query.filter_by(ioc=val).all(): 36 | if i.id != ind_id: 37 | link = Links(ev_id, ind_id, i.event_id, i.id) 38 | link2 = Links(i.event_id, i.id, ev_id, ind_id) 39 | db.session.add(link) 40 | db.session.add(link2) 41 | db.session.commit() 42 | 43 | 44 | def _enrich_data(data_type, data, pend=True): 45 | results = 'Not implemented yet' 46 | full = 'Not implemented yet' 47 | if pend: 48 | if data_type == 'ipv4': 49 | obj = IPWhois(data) 50 | q = obj.lookup_rdap(depth=1) 51 | net = q.get('network', {}) 52 | results = '%s|%s' % (net.get('name'), net.get('cidr')) 53 | full = pprint.pformat(q) 54 | elif data_type == 'domain': 55 | q = whois(data) 56 | results = '%s|%s|%s' % (q.get('registrar'), q.get('name'), q.get('emails')) 57 | full = q.text 58 | 59 | return results, full 60 | 61 | def _valid_json(fields, data_dict): 62 | if all(k in data_dict for k in fields): 63 | for field in fields: 64 | if re.search('_id$', field): 65 | try: 66 | int(data_dict[field]) 67 | except Exception: 68 | return False 69 | return True 70 | 71 | return False 72 | 73 | 74 | def _add_indicators(results, pending=False, enrich_it=False): 75 | reasons = [] 76 | inserted_indicators = [] 77 | failed_indicators = [] 78 | updated_indicators = [] 79 | if not isinstance(results, ResultsDict): 80 | app.logger.warn('Bad object passed to _add_indicators') 81 | reasons.append('Bad object passed to _add_indicators') 82 | return {'success':len(inserted_indicators), 'failed':len(failed_indicators), 'reason':';'.join(reasons)} 83 | 84 | if not Event.query.get(results.event_id): 85 | app.logger.warn('Event ID %s doesnt exist' % results.event_id) 86 | reasons.append('Event ID %s doesnt exist' % results.event_id) 87 | return {'success':len(inserted_indicators), 'failed':len(failed_indicators), 'reason':';'.join(reasons)} 88 | 89 | ioc_list, cont_obj, all_data_types = _load_related_data(results) 90 | for data_type in results.data_types.keys(): 91 | type_array = all_data_types.get(data_type) 92 | if not type_array: 93 | app.logger.warn("Bulk Indicator: Non-existent data type: %s can't process" % data_type) 94 | reasons.append('Bad Data Type') 95 | failed_indicators.append([0, results.event_id, [i for i in results.data_types.get(data_type, {}).keys()]]) 96 | continue 97 | regex = type_array[1] 98 | if regex: 99 | regex = re.compile(type_array[1]) 100 | type_obj = type_array[0] 101 | indicators = results.data_types.get(data_type) 102 | for i in indicators.keys(): 103 | val = i 104 | dt = indicators[i]['date'] 105 | desc = indicators[i]['description'] 106 | ind_id = ioc_list.get(val) 107 | if ind_id: 108 | ind = Indicator.query.get(ind_id) 109 | ind.last_seen = dt 110 | updated_indicators.append([ind_id, results.event_id, val]) 111 | else: 112 | if (regex and regex.match(val)) or regex is None: 113 | enrich, enrich_full = _enrich_data(data_type, val, pending|enrich_it) 114 | ind = Indicator(results.event_id, val, desc, cont_obj, type_obj, pending, enrich, enrich_full) 115 | db.session.add(ind) 116 | db.session.flush() 117 | ind_id = ind.id 118 | inserted_indicators.append([ind_id, results.event_id, val]) 119 | else: 120 | reasons.append('Validation Failed') 121 | failed_indicators.append([0, results.event_id, val]) 122 | 123 | # commit and correlate 124 | try: 125 | db.session.commit() 126 | if not pending: 127 | _correlate(inserted_indicators) 128 | except Exception, e: 129 | db.session.rollback() 130 | app.logger.warn('Error committing indicators: %s' % e) 131 | reasons.append('Commit Failed') 132 | failed_indicators += inserted_indicators 133 | inserted_indicators = [] 134 | 135 | return {'success':len(inserted_indicators) + len(updated_indicators), 'failed':len(failed_indicators), 'reason':';'.join(reasons)} 136 | 137 | 138 | def filter_query(query, conditions): 139 | model_class = query._entities[0].mapper._identity_class # returns the query's Model 140 | join_class = query._join_entities[0].mapper._identity_class 141 | for cond in conditions: 142 | key = cond.get('field') 143 | op = cond.get('operator') 144 | value = cond.get('val') 145 | 146 | column = getattr(model_class, key, None) or getattr(join_class, key, None) 147 | if not column: 148 | raise Exception('Invalid filter column: %s' % key) 149 | if op == 'in': 150 | filt = column.in_(value.split(',')) 151 | else: 152 | try: 153 | attr = filter(lambda e: hasattr(column, e % op), ['%s', '%s_', '__%s__'])[0] % op 154 | except IndexError: 155 | raise Exception('Invalid filter operator: %s' % op) 156 | if value == 'null': 157 | value = None 158 | filt = getattr(column, attr)(value) 159 | print filt 160 | query = query.filter(filt) 161 | return query -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, flash, redirect, request, jsonify, json, escape 2 | from sqlalchemy.exc import IntegrityError 3 | from app import app 4 | from .forms import EventForm, IndicatorForm, NoteForm, ItypeForm, FeedConfigForm, IndicatorEditForm, MitigationForm 5 | from feeder.logentry import ResultsDict 6 | from .models import Event, Indicator, Itype, Control, Level, Likelihood, Source, Status, Tlp, Note, Mitigation, db 7 | from .utils import _valid_json, _add_indicators, _correlate, _enrich_data, filter_query 8 | from .my_datatables import ColumnDT, DataTables 9 | 10 | 11 | def _count(chain): 12 | ret = chain.count() 13 | if ret: 14 | return ret 15 | else: 16 | return chain 17 | 18 | 19 | 20 | 21 | 22 | @app.route('/') 23 | @app.route('/index') 24 | def index(): 25 | return render_template('index.html', title='Home') 26 | 27 | 28 | @app.route('/event/add', methods=['GET', 'POST']) 29 | def event_add(): 30 | form = EventForm() 31 | form.confidence.choices = [(i, '%s' % i) for i in xrange(0, 100, 5)] 32 | if form.validate_on_submit(): 33 | ev = Event(form.name.data, 34 | form.details.data, 35 | form.source.data, 36 | form.tlp.data, 37 | form.impact.data, 38 | form.likelihood.data, 39 | form.confidence.data) 40 | db.session.add(ev) 41 | try: 42 | db.session.commit() 43 | flash('Event added') 44 | except IntegrityError: 45 | db.session.rollback() 46 | flash('Commit Error') 47 | return redirect('/index') 48 | print(form.errors) 49 | return render_template('event_add.html', 50 | title='Add Event', 51 | form=form) 52 | 53 | 54 | @app.route('/event/view/', methods=['GET', 'POST']) 55 | def event_view(event_id): 56 | def _indicator_add(form): 57 | res_dict = ResultsDict(form.event_id.data, 58 | form.control.data.name) 59 | res_dict.new_ind(data_type=form.itype.data.name, 60 | indicator=form.ioc.data, 61 | date=None, 62 | description=form.comment.data) 63 | r = _add_indicators(res_dict, False, True) 64 | print r 65 | if r.get('success', 0) == 1: 66 | res = '"%s" indicator submitted' % form.ioc.data 67 | else: 68 | res = '"%s" indicator not submitted: %s' % (form.ioc.data, r.get('reason', 'N/A')) 69 | return res 70 | 71 | def _note_add(form): 72 | note = Note(form.event_id.data, form.details.data) 73 | db.session.add(note) 74 | try: 75 | db.session.commit() 76 | res = 'note submitted' 77 | except IntegrityError: 78 | db.session.rollback() 79 | res = 'note not submitted' 80 | return res 81 | 82 | def _event_edit(form, event): 83 | form.populate_obj(event) 84 | try: 85 | db.session.commit() 86 | res = '"%s" Event Updated' % event.id 87 | except IntegrityError: 88 | db.session.rollback() 89 | res = '"%s" Event Not Updated' % event.id 90 | return res 91 | 92 | ev = Event.query.get(event_id) 93 | ind_form = IndicatorForm() 94 | ind_form.event_id.data = event_id 95 | ev_form = EventForm(obj=ev) 96 | ev_form.confidence.choices = [(i, '%s' % i) for i in xrange(0, 100, 5)] 97 | nt_form = NoteForm() 98 | nt_form.event_id.data = event_id 99 | if ind_form.validate_on_submit(): 100 | flash(_indicator_add(ind_form)) 101 | return redirect('/event/view/%s' % event_id) 102 | elif ev_form.validate_on_submit(): 103 | flash(_event_edit(ev_form, ev)) 104 | return redirect('/event/view/%s' % event_id) 105 | elif nt_form.validate_on_submit(): 106 | flash(_note_add(nt_form)) 107 | return redirect('/event/view/%s' % event_id) 108 | return render_template('event_view.html', 109 | title='View Event', 110 | event=ev, 111 | form=ind_form, 112 | ev_form=ev_form, 113 | nt_form=nt_form) 114 | 115 | 116 | @app.route('/indicator/pending/view', methods=['GET', 'POST']) 117 | def indicator_pending(): 118 | if request.method == 'POST': 119 | update_list = [int(i) for i in request.form.getlist('selected')] 120 | del_list = [int(i) for i in request.form.getlist('not_selected')] 121 | 122 | upd_query = db.session.query(Indicator).filter(Indicator.id.in_(update_list)) 123 | upd_query.update({'pending':False}, synchronize_session=False) 124 | del_query = db.session.query(Indicator).filter(Indicator.id.in_(del_list)) 125 | del_query.delete(synchronize_session=False) 126 | try: 127 | db.session.commit() 128 | flash('Indicators updated') 129 | except IntegrityError: 130 | db.session.rollback() 131 | flash('Commit Error') 132 | return redirect('/indicator/pending/view') 133 | 134 | ioc_query = Indicator.query.with_entities(Indicator.id, Indicator.event_id, Indicator.ioc) 135 | ioc_list = ioc_query.filter(Indicator.id.in_(update_list)).all() 136 | _correlate(ioc_list) 137 | return redirect('/indicator/pending/view') 138 | 139 | 140 | return render_template('indicator_pending.html', title='Pending Indicators') 141 | 142 | 143 | @app.route('/indicator/edit//', methods=['GET', 'POST']) 144 | def indicator_edit(ind_id, action): 145 | i = Indicator.query.get(ind_id) 146 | form = IndicatorEditForm(obj=i) 147 | m_form = MitigationForm() 148 | m_form.ttl.choices = [(c, '%s' % c) for c in [0,7,30,90]] 149 | if request.method == 'POST' and action=='view' and form.validate_on_submit(): 150 | if request.form['submit'] == 'edit': 151 | i.comment = form.comment.data 152 | i.control = form.control.data 153 | if form.update_enrich.data: 154 | i.enrich, i.enrich_full = _enrich_data(i.itype.name, i.ioc, True) 155 | db.session.add(i) 156 | elif request.form['submit'] == 'delete': 157 | db.session.delete(i) 158 | try: 159 | db.session.commit() 160 | flash("Indicator updated successfully") 161 | except Exception, e: 162 | db.session.rollback() 163 | app.logger.warn("Error committing - rolled back %s" % e) 164 | flash("Error committing - rolled back") 165 | return redirect('/indicator/edit/%s/view' % ind_id) 166 | elif request.method == 'POST' and action.startswith('mitigation') and m_form.validate_on_submit(): 167 | if action == 'mitigation_add': 168 | print m_form.destination.data.id, m_form.ttl.data, m_form.description.data 169 | mit = Mitigation(m_form.destination.data.id, m_form.ttl.data, m_form.description.data) 170 | i.mitigations.append(mit) 171 | elif action == 'mitigation_edit': 172 | mit = Mitigation.query.get(m_form.mit_id.data) 173 | if not mit: 174 | flash("Mitigation doesn't exist") 175 | return redirect('/indicator/edit/%s/view' % ind_id) 176 | mit.ttl = m_form.ttl.data 177 | mit.destination_id = m_form.destination.data.id 178 | mit.description = m_form.description.data 179 | db.session.add(mit) 180 | elif action == 'mitigation_del': 181 | print 'hit here' 182 | mit = Mitigation.query.get(m_form.mit_id.data) 183 | db.session.delete(mit) 184 | else: 185 | flash("Knock off the funny business....") 186 | return redirect('/indicator/edit/%s/view' % ind_id) 187 | 188 | try: 189 | db.session.commit() 190 | flash("Mitigation successfully updated/added") 191 | except Exception, e: 192 | db.session.rollback() 193 | app.logger.warn("Error committing mitigation - rolled back %s" % e) 194 | flash("Error committing - rolled back") 195 | return redirect('/indicator/edit/%s/view' % ind_id) 196 | print m_form.errors 197 | return render_template('indicator_edit.html', title='Edit Indicator', form=form, m_form=m_form, indicator=i) 198 | 199 | 200 | @app.route('/indicator/search/view', methods=['GET', 'POST']) 201 | def indicator_search(): 202 | return render_template('indicator_search.html', title='Search Indicators') 203 | 204 | 205 | @app.route('/admin/data_types/', methods=['GET', 'POST']) 206 | def admin_data_types(action): 207 | form = ItypeForm() 208 | print '%s' % request.form 209 | if action == 'view': 210 | data_types = Itype.query.all() 211 | return render_template('type_edit.html', title='View/Edit Fields', data_types=data_types, form=form) 212 | elif action == 'add': 213 | dt = Itype(request.form['field_name'], request.form['field_regex']) 214 | if dt.regex == 'None' or dt.regex == '': 215 | dt.regex = None 216 | db.session.add(dt) 217 | elif action == 'edit': 218 | dt = Itype.query.get(request.form['field_id']) 219 | dt.name = request.form['field_name'] 220 | dt.regex = request.form['field_regex'] 221 | if dt.regex == 'None' or dt.regex == '': 222 | dt.regex = None 223 | db.session.add(dt) 224 | elif action == 'delete': 225 | dt = Itype.query.get(request.form['field_id']) 226 | db.session.delete(dt) 227 | else: 228 | return redirect('/admin/data_types/view') 229 | try: 230 | db.session.commit() 231 | flash('Data Types Updated') 232 | except IntegrityError: 233 | db.session.rollback() 234 | flash('Commit Error') 235 | return redirect('/admin/data_types/view') 236 | 237 | 238 | @app.route('/admin/table/view', methods=['GET', 'POST']) 239 | def view_fields(): 240 | fields = {'Impact': [Level.query.all(), 'impact'], 241 | 'Likelihood': [Likelihood.query.all(), 'likelihood'], 242 | 'Data Source': [Source.query.all(), 'source'], 243 | 'Event Status': [Status.query.all(), 'status'], 244 | 'TLP': [Tlp.query.all(), 'tlp'], 245 | 'Controls': [Control.query.all(), 'control'] 246 | } 247 | return render_template('table_view.html', title='View/Edit Fields', fields=fields) 248 | 249 | 250 | @app.route('/admin/table//', methods=['POST']) 251 | def view_edit_table(table_name, action): 252 | objects = {'impact': Level(), 253 | 'likelihood': Likelihood(), 254 | 'source': Source(), 255 | 'status': Status(), 256 | 'tlp': Tlp(), 257 | 'control': Control()} 258 | if table_name in objects.keys(): 259 | base_obj = objects[table_name] 260 | else: 261 | return redirect('/fields/view') 262 | 263 | if action == 'add': 264 | base_obj.name = request.form['field_name'] 265 | db.session.add(base_obj) 266 | elif action == 'edit': 267 | item = base_obj.query.get(request.form['field_id']) 268 | item.name = request.form['field_name'] 269 | db.session.add(item) 270 | elif action == 'delete': 271 | item = base_obj.query.get(request.form['field_id']) 272 | db.session.delete(item) 273 | else: 274 | return redirect('/admin/table/view') 275 | try: 276 | db.session.commit() 277 | flash('Data Updated') 278 | except IntegrityError: 279 | db.session.rollback() 280 | flash('Commit Error') 281 | 282 | return redirect('/admin/table/view') 283 | 284 | 285 | @app.route('/feeds/config/', methods=['GET', 'POST']) 286 | def feed_config(action): 287 | with open(app.config.get('FEED_CONFIG')) as F: 288 | data = F.read() 289 | data = json.loads(data.replace('\\', '\\\\')) 290 | if action == 'view': 291 | form_edit = FeedConfigForm() 292 | for d in data: 293 | e = Event.query.get(d['event_id']) 294 | if not e: 295 | pass 296 | d['event_name'] = Event.query.get(d['event_id']).name 297 | d['modules'] = json.dumps(d['modules'], indent=4, sort_keys=True).replace('\\\\', '\\') 298 | return render_template('feed_config.html', title='Feed Config', data=data, form_edit=form_edit) 299 | 300 | elif action == 'edit': 301 | try: 302 | idx = int(request.form['index_id']) 303 | evt_id = int(request.form['event']) 304 | except Exception: 305 | flash('Error converting IDs to integers') 306 | return redirect('/feeds/config/view') 307 | d = data[idx] 308 | d['name'] = request.form['name'] 309 | d['frequency'] = request.form['frequency'] 310 | d['event_id'] = evt_id 311 | modules = request.form['module'] 312 | if modules: 313 | try: 314 | d['modules'] = json.loads(modules.replace('\\', '\\\\')) 315 | except Exception, e: 316 | flash('Invalid JSON for module config') 317 | return redirect('/feeds/config/view') 318 | 319 | elif action == 'add': 320 | try: 321 | evt_id = int(request.form['event']) 322 | except Exception: 323 | flash('Error converting ID to integer') 324 | return redirect('/feeds/config/view') 325 | d = dict() 326 | d['name'] = request.form['name'] 327 | d['frequency'] = request.form['frequency'] 328 | d['event_id'] = evt_id 329 | modules = request.form['module'] 330 | if modules: 331 | try: 332 | d['modules'] = json.loads(modules.replace('\\', '\\\\')) 333 | except Exception, e: 334 | flash('Invalid JSON for module config') 335 | return redirect('/feeds/config/view') 336 | data.append(d) 337 | 338 | elif action == 'del': 339 | try: 340 | idx = int(request.form['index_id']) 341 | except Exception: 342 | flash('Error converting IDs to integers') 343 | return redirect('/feeds/config/view') 344 | data.pop(idx) 345 | 346 | try: 347 | json_data = json.dumps(data, indent=4, sort_keys=True) 348 | with open(app.config.get('FEED_CONFIG'), 'w') as F: 349 | F.write(json_data.replace('\\\\', '\\')) 350 | except Exception, e: 351 | flash('Error writing file: %s' % e) 352 | 353 | return redirect('/feeds/config/view') 354 | 355 | 356 | ### 357 | # DataTables Ajax Endpoints 358 | ### 359 | @app.route('/indicator//data/') 360 | def pending_data(status, event_id): 361 | """Return server side data.""" 362 | # defining columns 363 | columns = [] 364 | columns.append(ColumnDT('id')) 365 | columns.append(ColumnDT('ioc')) 366 | columns.append(ColumnDT('itype.name')) 367 | columns.append(ColumnDT('control.name')) 368 | columns.append(ColumnDT('comment')) 369 | columns.append(ColumnDT('enrich')) 370 | columns.append(ColumnDT('first_seen')) 371 | 372 | base_query = db.session.query(Indicator).join(Control).join(Itype) 373 | 374 | if status == 'pending': 375 | columns.append(ColumnDT('event_id')) 376 | columns.append(ColumnDT('event.name')) 377 | query = base_query.join(Event).filter(Indicator.pending == True) 378 | elif status == 'search': 379 | columns.append(ColumnDT('event_id')) 380 | columns.append(ColumnDT('event.name')) 381 | query = base_query.join(Event).filter(Indicator.pending == False) 382 | elif status == 'approved': 383 | columns.append(ColumnDT('last_seen')) 384 | columns.append(ColumnDT('rel_list')) 385 | query = base_query.filter(Indicator.event_id == event_id).filter(Indicator.pending == False ) 386 | else: 387 | query = base_query.filter(Indicator.pending == True) 388 | 389 | rowTable = DataTables(request.args, Indicator, query, columns) 390 | 391 | #xss catch just to be safe 392 | res = rowTable.output_result() 393 | for item in res['data']: 394 | for k,v in item.iteritems(): 395 | item[k] = escape(v) 396 | 397 | return jsonify(res) 398 | 399 | 400 | @app.route('/event//data') 401 | def event_data(status): 402 | """Return server side data.""" 403 | # defining columns 404 | columns = [] 405 | columns.append(ColumnDT('id')) 406 | columns.append(ColumnDT('name')) 407 | columns.append(ColumnDT('status.name')) 408 | columns.append(ColumnDT('source.name')) 409 | columns.append(ColumnDT('tlp.name')) 410 | columns.append(ColumnDT('confidence')) 411 | columns.append(ColumnDT('created')) 412 | columns.append(ColumnDT('indicator_count')) 413 | 414 | base_query = db.session.query(Event).join(Source).join(Tlp).join(Status) 415 | 416 | if status in ['New', 'Open', 'Resolved']: 417 | query = base_query.filter(Status.name == status) 418 | else: 419 | query = base_query 420 | 421 | rowTable = DataTables(request.args, Event, query, columns) 422 | 423 | #xss catch just to be safe 424 | res = rowTable.output_result() 425 | for item in res['data']: 426 | for k,v in item.iteritems(): 427 | item[k] = escape(v) 428 | 429 | return jsonify(res) 430 | 431 | 432 | ### 433 | # API Calls 434 | ### 435 | @app.route('/api/event/add', methods=['POST']) 436 | def api_event_add(): 437 | req_keys = ('name', 'details', 'confidence', 'source', 'tlp', 'impact', 'likelihood') 438 | 439 | try: 440 | pld = request.get_json(silent=True) 441 | except Exception, e: 442 | return json.dumps({'results': 'error', 'data': '%s' % e}) 443 | 444 | if _valid_json(req_keys, pld): 445 | impact = Level.query.filter(Level.name == pld['impact']).first() 446 | likelihood = Likelihood.query.filter(Likelihood.name == pld['likelihood']).first() 447 | source = Source.query.filter(Source.name == pld['source']).first() 448 | tlp = Tlp.query.filter(Tlp.name == pld['tlp']).first() 449 | if not (impact and likelihood and source and tlp): 450 | return json.dumps({'results': 'error', 'data': 'impact, likelihood, source, or tlp not found'}) 451 | 452 | try: 453 | confidence = int(pld['confidence']) 454 | if confidence < 0 or confidence > 100: 455 | raise Exception 456 | except Exception, e: 457 | return json.dumps({'results': 'error', 'data': 'confidence was not a number between 0 and 100'}) 458 | 459 | ev = Event(pld['name'], pld['details'], source, tlp, impact, likelihood, confidence) 460 | db.session.add(ev) 461 | try: 462 | db.session.commit() 463 | except IntegrityError: 464 | db.session.rollback() 465 | return json.dumps({'results': 'error', 'data': 'Integrity error - Rolled back'}) 466 | return json.dumps({'results':'success', 'data':{'event_id': ev.id}}) 467 | else: 468 | return json.dumps({'results': 'error', 'data': 'bad json'}) 469 | 470 | 471 | @app.route('/api/indicator/bulk_add', methods=['POST']) 472 | def indicator_bulk_add(): 473 | req_keys = ('control', 'data_type', 'event_id', 'pending', 'data') 474 | 475 | try: 476 | pld = request.get_json(silent=True) 477 | except Exception, e: 478 | return json.dumps({'results': 'error', 'data': '%s' % e}) 479 | 480 | if _valid_json(req_keys, pld): 481 | # load related stuff 482 | res_dict = ResultsDict(pld['event_id'], pld['control']) 483 | for val, desc in pld['data']: 484 | res_dict.new_ind(data_type=pld['data_type'], 485 | indicator=val, 486 | date=None, 487 | description=desc) 488 | 489 | results = _add_indicators(res_dict, pld['pending']) 490 | return json.dumps({'results': 'success', 'data': results}) 491 | else: 492 | return json.dumps({'results': 'error', 'data': 'bad json'}) 493 | 494 | 495 | @app.route('/api/indicator/get', methods=['POST']) 496 | def api_indicator_get(): 497 | req_keys = ('conditions',) 498 | req_keys2 = ('field', 'operator', 'val') 499 | try: 500 | pld = request.get_json(silent=True) 501 | except Exception, e: 502 | return json.dumps({'results': 'error', 'data': '%s' % e}) 503 | 504 | if not _valid_json(req_keys, pld): 505 | return json.dumps({'results': 'error', 'data': 'Invalid json'}) 506 | 507 | for item in pld.get('conditions'): 508 | if not _valid_json(req_keys2, item): 509 | return json.dumps({'results': 'error', 'data': 'Invalid json'}) 510 | 511 | q = filter_query(Indicator.query.join(Event), pld.get('conditions')) 512 | 513 | 514 | 515 | @app.errorhandler(404) 516 | def not_found_error(error): 517 | return render_template('404.html'), 404 518 | 519 | 520 | @app.errorhandler(500) 521 | def internal_error(error): 522 | db.session.rollback() 523 | return render_template('500.html'), 500 524 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | 4 | WTF_CSRF_ENABLED = True 5 | SECRET_KEY = 'you-will-never-guess' 6 | 7 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') 8 | SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') 9 | SQLALCHEMY_TRACK_MODIFICATIONS = False 10 | 11 | CELERY_BROKER_URL = 'redis://127.0.0.1:6379' 12 | CELERY_BROKER_BACKEND = 'redis://127.0.0.1:6379' 13 | 14 | FEED_CONFIG = os.path.join(basedir, 'feeder/feed.json') 15 | -------------------------------------------------------------------------------- /db_create.py: -------------------------------------------------------------------------------- 1 | #!bin/python 2 | from migrate.versioning import api 3 | from config import SQLALCHEMY_DATABASE_URI 4 | from config import SQLALCHEMY_MIGRATE_REPO 5 | from app import db 6 | import os.path 7 | db.create_all() 8 | if not os.path.exists(SQLALCHEMY_MIGRATE_REPO): 9 | api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository') 10 | api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 11 | else: 12 | api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, api.version(SQLALCHEMY_MIGRATE_REPO)) 13 | -------------------------------------------------------------------------------- /db_migrate.py: -------------------------------------------------------------------------------- 1 | #!bin/python 2 | import imp 3 | from migrate.versioning import api 4 | from app import db 5 | from config import SQLALCHEMY_DATABASE_URI 6 | from config import SQLALCHEMY_MIGRATE_REPO 7 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 8 | migration = SQLALCHEMY_MIGRATE_REPO + ('/versions/%03d_migration.py' % (v+1)) 9 | tmp_module = imp.new_module('old_model') 10 | old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 11 | exec(old_model, tmp_module.__dict__) 12 | script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, tmp_module.meta, db.metadata) 13 | open(migration, "wt").write(script) 14 | api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 15 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 16 | print('New migration saved as ' + migration) 17 | print('Current database version: ' + str(v)) 18 | -------------------------------------------------------------------------------- /db_populate.py: -------------------------------------------------------------------------------- 1 | #!bin/python 2 | from app import db 3 | from app.models import Control, Level, Itype, Tlp, Source, Likelihood, Status, Destination 4 | 5 | controls = ["Inbound", "Outbound"] 6 | levels = ["None", "Low", "Medium", "High"] 7 | tlps = ["White", "Green", "Amber", "Red"] 8 | types = [["ipv4","^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"], 9 | ["domain", "^(?:[-A-Za-z0-9]+\.)+[A-Za-z]{2,12}$"], 10 | ["md5", "^[a-fA-F0-9]{32}$"], 11 | ["sha256", "^[A-Fa-f0-9]{64}$"], 12 | ["url", "^(http[s]?://(?:[-A-Za-z0-9]+\.)+[A-Za-z]{2,12}(/[^\s]+)?|(?:[-A-Za-z0-9]+\.)+[A-Za-z]{2,12}/[^\s]*)$"]] 13 | sources = ["Feed", "Email List", "Blog"] 14 | statuses = ["New", "Open", "Resolved"] 15 | destinations = [["pan", "Palo Alto", "fmtPan"], ["json", "Generic Json", "fmtJson"]] 16 | 17 | for i in controls: 18 | obj = Control() 19 | obj.name = i 20 | db.session.add(obj) 21 | db.session.commit() 22 | 23 | for i in levels: 24 | obj = Level() 25 | obj2 = Likelihood() 26 | obj.name = i 27 | obj2.name = i 28 | db.session.add(obj) 29 | db.session.add(obj2) 30 | db.session.commit() 31 | 32 | 33 | for i in tlps: 34 | obj = Tlp() 35 | obj.name = i 36 | db.session.add(obj) 37 | db.session.commit() 38 | 39 | for i in types: 40 | obj = Itype(i[0], i[1]) 41 | db.session.add(obj) 42 | db.session.commit() 43 | 44 | for i in sources: 45 | obj = Source() 46 | obj.name = i 47 | db.session.add(obj) 48 | db.session.commit() 49 | 50 | for i in statuses: 51 | obj = Status() 52 | obj.name = i 53 | db.session.add(obj) 54 | db.session.commit() 55 | 56 | for i in destinations: 57 | obj = Destination() 58 | obj.name = i[0] 59 | obj.description = i[1] 60 | obj.formatter = i[2] 61 | db.session.add(obj) 62 | db.session.commit() 63 | 64 | -------------------------------------------------------------------------------- /doc/ostip_addevent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/doc/ostip_addevent.png -------------------------------------------------------------------------------- /doc/ostip_addindicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/doc/ostip_addindicator.png -------------------------------------------------------------------------------- /doc/ostip_datatype_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/doc/ostip_datatype_config.png -------------------------------------------------------------------------------- /doc/ostip_eventview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/doc/ostip_eventview.png -------------------------------------------------------------------------------- /doc/ostip_eventview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/doc/ostip_eventview2.png -------------------------------------------------------------------------------- /doc/ostip_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/doc/ostip_home.png -------------------------------------------------------------------------------- /feeder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/feeder/__init__.py -------------------------------------------------------------------------------- /feeder/collect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import requests 3 | import itertools 4 | import re 5 | import datetime 6 | from app import app 7 | 8 | 9 | class GetHttp: 10 | 11 | # "config": { 12 | # "url": "https://sslbl.abuse.ch/blacklist/sslipblacklist.csv", 13 | # "ignore_regex": "^#", 14 | # "user_agent": "OSTIP", 15 | # "referer": null, 16 | # "timeout": 20, 17 | # "verify_cert": true 18 | # } 19 | # for dates you can embed < date format > in url string. so <%m_%d_%Y> would = 10_15_2016 20 | # Returns an itertools.ifilter() object - essentially a generator 21 | 22 | def __init__(self, config): 23 | self.url = config.get('url') 24 | self.ignore_regex = config.get('ignore_regex') 25 | self.user_agent = config.get('user_agent', 'OSTIP') 26 | self.referer = config.get('referer') 27 | self.timeout = config.get('timeout', 20) 28 | self.verify_cert = config.get('verify_cert', True) 29 | 30 | if self.ignore_regex: 31 | self.ignore_regex = re.compile(self.ignore_regex) 32 | 33 | if self.url: 34 | m = re.search('\<(?P[^\>]+)\>', self.url) 35 | if m: 36 | fmt = m.groupdict().get('fmt', '') 37 | try: 38 | dt_str = datetime.datetime.now().strftime(fmt) 39 | self.url = re.sub('\<[^\>]+\>', dt_str, self.url) 40 | except: 41 | app.logger.warn('Error in date string replacement') 42 | raise 43 | 44 | 45 | 46 | 47 | def get(self): 48 | rkwargs = dict( 49 | stream=True, 50 | verify=self.verify_cert, 51 | timeout=self.timeout, 52 | headers={ 'User-Agent': self.user_agent or 'OSTIP' } 53 | ) 54 | print self.verify_cert 55 | if self.referer: 56 | rkwargs['headers']['referer'] = self.referer 57 | 58 | r = requests.get( 59 | self.url, 60 | **rkwargs 61 | ) 62 | 63 | try: 64 | r.raise_for_status() 65 | except: 66 | app.logger.warn('%s - exception in request: %s %s', self.url, r.status_code, r.content) 67 | raise 68 | app.logger.info('%s - url loaded: %s', self.url, self.referer) 69 | result = r.iter_lines() 70 | if self.ignore_regex: 71 | result = itertools.ifilter( 72 | lambda x: self.ignore_regex.match(x) is None, 73 | result 74 | ) 75 | 76 | return result 77 | -------------------------------------------------------------------------------- /feeder/feed.json.example: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "SSL IPBL", 4 | "frequency": "0", 5 | "event_id": 1, 6 | "modules": { 7 | "collect": { 8 | "name": "collect.GetHttp", 9 | "config": { 10 | "url": "https://sslbl.abuse.ch/blacklist/sslipblacklist.csv", 11 | "ignore_regex": "^#", 12 | "user_agent": "OSTIP", 13 | "referer": null, 14 | "timeout": 20, 15 | "verify_cert": true 16 | } 17 | }, 18 | "parse": { 19 | "name": "parse.ParseCsv", 20 | "config":{ 21 | "fieldnames": ["indicator_ipv4", "port", "desc_1"], 22 | "data_types": ["ipv4"], 23 | "control": "Inbound" 24 | } 25 | } 26 | } 27 | }, 28 | { 29 | "name": "alienvault", 30 | "frequency": "0,4,8,12,16,20", 31 | "event_id": 4, 32 | "modules": { 33 | "collect": { 34 | "name": "collect.GetHttp", 35 | "config": { 36 | "url": "https://reputation.alienvault.com/reputation.data", 37 | "ignore_regex": "^#", 38 | "user_agent": "OSTIP", 39 | "referer": null, 40 | "timeout": 20, 41 | "verify_cert": true 42 | } 43 | }, 44 | "parse": { 45 | "name": "parse.ParseText", 46 | "config":{ 47 | "regex": "^(?P[^\#]+)\#\d\#\d\#(?P[^\#]+)\#", 48 | "data_types": ["ipv4"], 49 | "control": "Inbound" 50 | } 51 | } 52 | } 53 | } 54 | ] -------------------------------------------------------------------------------- /feeder/feed.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from app.utils import _add_indicators, _valid_json 4 | from app import app 5 | import datetime 6 | 7 | basedir = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | FEED_CONFIG = app.config.get('FEED_CONFIG') 10 | 11 | 12 | def _dynamic_load(class_name): 13 | if '.' not in class_name: 14 | raise ValueError('invalid absolute classname %s' % class_name) 15 | 16 | modname, class_name = class_name.rsplit('.', 1) 17 | t = __import__(modname, globals(), locals(), [class_name]) 18 | cls = getattr(t, class_name) 19 | return cls 20 | 21 | 22 | class Feed: 23 | 24 | def __init__(self): 25 | try: 26 | with open(FEED_CONFIG) as F: 27 | json_data = F.read() 28 | json_data = json_data.replace('\\', '\\\\') 29 | self.configs = json.loads(json_data) 30 | except Exception, e: 31 | app.logger.warn('Error Loading File: %s' % e) 32 | 33 | def process_all(self, config_to_process=None): 34 | results = {} 35 | fields = ['name', 'frequency', 'event_id', 'modules'] 36 | date_hour = datetime.datetime.now().hour 37 | for config in self.configs: 38 | if not _valid_json(fields, config): 39 | app.logger.warn('Bad config from feed.json') 40 | continue 41 | if config_to_process: 42 | if not config.get('name') == config_to_process: 43 | continue 44 | app.logger.info('Processing Feed: %s' % config['name']) 45 | modules = config.get('modules') 46 | event_id = config.get('event_id') 47 | freq = config.get('frequency').split(',') 48 | if not ('*' in freq or date_hour in freq): 49 | continue 50 | 51 | if 'parse' in modules.keys() and 'collect' in modules.keys(): 52 | try: 53 | coll_cls = _dynamic_load(modules['collect'].get('name')) 54 | parse_cls = _dynamic_load(modules['parse'].get('name')) 55 | except Exception, e: 56 | app.logger.warn('error loading classes: %s' % e) 57 | continue 58 | 59 | collect_config = modules['collect'].get('config') 60 | parse_config = modules['parse'].get('config') 61 | if not collect_config and not parse_config: 62 | app.logger.warn('error loading module configs') 63 | continue 64 | 65 | collector = coll_cls(collect_config) 66 | data = collector.get() 67 | 68 | if not data: 69 | app.logger.warn('error loading data from collector') 70 | continue 71 | 72 | parser = parse_cls(parse_config, event_id, data) 73 | logs = parser.run() 74 | #results[config.get('name', 'n/a')] = logs 75 | results[config.get('name','n/a')] = _add_indicators(logs) 76 | elif 'custom' in modules.keys(): 77 | pass 78 | else: 79 | app.logger.warn('Bad config from feed.json in modules') 80 | continue 81 | app.logger.info('Processing Results: %s' % results) 82 | return results 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /feeder/logentry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import datetime 3 | 4 | 5 | class ResultsDict: 6 | 7 | def __init__(self, event_id, control): 8 | self.event_id = event_id 9 | self.control = control 10 | self.data_types = dict() 11 | # data_types dict looks like 12 | # {'ipv4':{'1.1.1.1':{'data':datetime_obj, 'description':'blah'}, '2.2.2.2':{... }, 'domain':{..... 13 | 14 | def new_ind(self, data_type, indicator, date, description): 15 | if not data_type in self.data_types.keys(): 16 | self.data_types[data_type] = dict() 17 | dt_obj = self.data_types.get(data_type) 18 | ioc = dt_obj.get(indicator) 19 | if ioc: 20 | ioc['date'] = date or ioc['date'] 21 | if description and ioc['description'] and len(ioc['description']) > 0: 22 | ioc['description'] += '; %s' % description 23 | else: 24 | ioc['description'] = None 25 | else: 26 | dt_obj[indicator] = dict(date=date or datetime.datetime.utcnow(),description = description) 27 | 28 | def as_dict(self): 29 | return dict(event_id=self.event_id, control=self.control, data_types=self.data_types) 30 | 31 | 32 | -------------------------------------------------------------------------------- /feeder/parse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logging 3 | import re 4 | import itertools 5 | import datetime 6 | import csv 7 | from logentry import ResultsDict 8 | from app import app 9 | 10 | 11 | class ParseCsv: 12 | 13 | # Example config will look like this 14 | # Note indicator_ is ioc field, date is date and optional, desc_ are desc columns and optional 15 | # fieldnames, data_types, control are required fields 16 | # "config": { 17 | # "fieldnames": ["date", "indicator_ipv4", "desc_1", "desc_2"], 18 | # "data_types": ["ipv4"] 19 | # "control": "inbound" 20 | # "delimiter": ",", 21 | # "doublequote": T|F, 22 | # "escapechar": "", 23 | # "quotechar": "", 24 | # "skipinitialspace": T|F 25 | # } 26 | # data is a generator or a list of lines 27 | 28 | def __init__(self, config, event, data): 29 | 30 | self.fieldnames = config.get('fieldnames') 31 | self.data_types = config.get('data_types') 32 | self.control = config.get('control') 33 | self.event = event 34 | self.data = data 35 | self.dialect = { 36 | 'delimiter': config.get('delimiter', ','), 37 | 'doublequote': config.get('doublequote', True), 38 | 'escapechar': config.get('escapechar', None), 39 | 'quotechar': config.get('quotechar', '"'), 40 | 'skipinitialspace': config.get('skipinitialspace', False) 41 | } 42 | 43 | def run(self): 44 | count = 0 45 | app.logger.info("Processing ParseCsv") 46 | reader = csv.DictReader( 47 | self.data, 48 | fieldnames=self.fieldnames, 49 | **self.dialect 50 | ) 51 | 52 | results = ResultsDict(self.event, self.control) 53 | for row in reader: 54 | for data_type in self.data_types: 55 | desc_val = [] 56 | ioc = row.get('indicator_%s' % data_type) 57 | if not ioc: 58 | continue 59 | for i in xrange(1,10): 60 | tmp = row.get('desc_%s' % i) 61 | if tmp: 62 | desc_val.append(tmp) 63 | log_date = row.get('date') 64 | results.new_ind(data_type=data_type, 65 | indicator=ioc, 66 | date=log_date, 67 | description=';'.join(desc_val)) 68 | count += 1 69 | app.logger.info("ParseCsv processed %s rows", count) 70 | return results 71 | 72 | 73 | class ParseText: 74 | # Example config will look like this 75 | # Regex will need to have named capture groups the name are as follows: 76 | # indicator_ is ioc field, 77 | # date is date and optional, 78 | # desc_ are desc columns and optional 79 | # data_types, control, and regex are required in json config 80 | # "config": { 81 | # "data_types": ["ipv4"] 82 | # "control": "inbound" 83 | # "regex": "^(?P[^,]+),(?P[^,]+),(?P[^,]+)" 84 | # } 85 | # data is a generator or a list of lines 86 | 87 | def __init__(self, config, event, data): 88 | self.data_types = config.get('data_types', []) 89 | self.control = config.get('control') 90 | self.regex = config.get('regex') 91 | self.event = event 92 | self.data = data 93 | 94 | def run(self): 95 | count = 0 96 | app.logger.info("Processing ParseText") 97 | rex = re.compile(self.regex) 98 | results = ResultsDict(self.event, self.control) 99 | for row in self.data: 100 | m = rex.search(row) 101 | if not m: 102 | app.logger.warn("Row did not match regex: %s", row) 103 | continue 104 | matches = m.groupdict() 105 | for data_type in self.data_types: 106 | desc_val = [] 107 | desc = None 108 | ioc = matches.get('indicator_%s' % data_type) 109 | if not ioc: 110 | app.logger.warn("no indicator found for: %s", data_type) 111 | continue 112 | for i in xrange(1,10): 113 | tmp = matches.get('desc_%s' % i) 114 | if tmp: 115 | desc_val.append(tmp) 116 | log_date = matches.get('date') 117 | if len(desc_val) > 0: 118 | print 'val: %s' % desc_val 119 | desc = ';'.join(desc_val) 120 | results.new_ind(data_type=data_type, 121 | indicator=ioc, 122 | date=log_date, 123 | description=desc) 124 | count += 1 125 | app.logger.info("ParseText processed %s lines", count) 126 | return results 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==1.4.9 2 | anyjson==0.3.3 3 | Babel==2.3.4 4 | billiard==3.3.0.23 5 | blinker==1.4 6 | celery==3.1.23 7 | click==6.6 8 | coverage==4.2 9 | decorator==4.0.10 10 | dnspython==1.14.0 11 | Flask==0.11.1 12 | Flask-Babel==0.11.1 13 | Flask-Login==0.3.2 14 | Flask-Mail==0.9.1 15 | Flask-SQLAlchemy==2.1 16 | Flask-WhooshAlchemy==0.56 17 | Flask-WTF==0.12 18 | flipflop==1.0 19 | future==0.15.2 20 | ipaddr==2.1.11 21 | ipwhois==0.14.0 22 | itsdangerous==0.24 23 | Jinja2==2.8 24 | kombu==3.0.35 25 | MarkupSafe==0.23 26 | pbr==1.10.0 27 | python-whois==0.6.3 28 | pytz==2016.6.1 29 | redis==2.10.5 30 | requests==2.11.1 31 | six==1.10.0 32 | SQLAlchemy==1.0.14 33 | sqlalchemy-datatables==0.4.0 34 | sqlalchemy-migrate==0.10.0 35 | sqlparse==0.2.1 36 | Tempita==0.5.2 37 | Werkzeug==0.11.10 38 | Whoosh==2.7.4 39 | WTForms==2.1 40 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!bin/python 2 | from app import app 3 | import argparse 4 | 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument("--prod", help="increase output verbosity", action="store_true") 7 | args = parser.parse_args() 8 | 9 | ip = '127.0.0.1' 10 | if args.prod: 11 | ip = '0.0.0.0' 12 | 13 | app.run(debug=False, host=ip) 14 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx499-zz/ostip/f1e98786876fa8e1ea6175bd7726a1f60becec45/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/install-redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd ../ 3 | if [ ! -d redis-stable/src ]; then 4 | curl -O http://download.redis.io/redis-stable.tar.gz 5 | tar xvzf redis-stable.tar.gz 6 | rm redis-stable.tar.gz 7 | fi 8 | cd redis-stable 9 | make 10 | if [ -z "$1" ]; then 11 | src/redis-server 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/test_api.py: -------------------------------------------------------------------------------- 1 | #!../bin/python 2 | import requests 3 | 4 | 5 | 6 | indicator_json = { 7 | "event_id": 3, 8 | "control": "Inbound", 9 | "data_type": "ipv4", 10 | "pending": True, 11 | "data": [ ["6.1.1.4", "test ip"], ["6.1.1.8", "test ip"], ["6.1.1.9", "test ip"], ["6.1.1.10", "test ip"] ] 12 | } 13 | 14 | event_json = { 15 | "name": "Test API event", 16 | "details": "Some text", 17 | "confidence": 90, 18 | "source": "Blog", 19 | "tlp": "Amber", 20 | "impact": "Low", 21 | "likelihood":"Low" 22 | } 23 | 24 | 25 | ind_get_json = { 26 | "conditions": [{ 27 | "field": "confidence", # confidence, ioc, last_seen, first_seen, desc 28 | "operator": "gt", # eq, lt, gt, like 29 | "val": 50 30 | }] 31 | } 32 | 33 | res = requests.post('http://localhost:5000/api/indicator/bulk_add', json=indicator_json) 34 | print 'Indicator Add Results:' 35 | if res.ok: 36 | print res.json() 37 | else: 38 | print 'Something bad happened' 39 | 40 | res = requests.post('http://localhost:5000/api/event/add', json=event_json) 41 | print 'Event Add Results:' 42 | if res.ok: 43 | print res.json() 44 | else: 45 | print 'Something bad happened' 46 | 47 | res = requests.post('http://localhost:5000/api/indicator/get', json=ind_get_json) 48 | print 'Event Add Results:' 49 | if res.ok: 50 | print res.json() 51 | else: 52 | print 'Something bad happened' 53 | -------------------------------------------------------------------------------- /scripts/update.sql: -------------------------------------------------------------------------------- 1 | PRAGMA foreign_keys=off; 2 | 3 | BEGIN TRANSACTION; 4 | 5 | ALTER TABLE indicator RENAME TO old_indicator; 6 | 7 | CREATE TABLE indicator ( 8 | id INTEGER NOT NULL, 9 | ioc VARCHAR(64) NOT NULL, 10 | comment VARCHAR(255), 11 | enrich VARCHAR(255), 12 | first_seen DATETIME NOT NULL, 13 | last_seen DATETIME NOT NULL, 14 | pending BOOLEAN NOT NULL, 15 | event_id INTEGER NOT NULL, 16 | control_id INTEGER NOT NULL, 17 | itype_id INTEGER NOT NULL, 18 | PRIMARY KEY (id), 19 | UNIQUE (ioc, event_id, itype_id, control_id), 20 | CHECK (pending IN (0, 1)), 21 | FOREIGN KEY(event_id) REFERENCES event (id), 22 | FOREIGN KEY(control_id) REFERENCES control (id), 23 | FOREIGN KEY(itype_id) REFERENCES itype (id) 24 | ); 25 | 26 | INSERT INTO indicator SELECT * FROM old_indicator; 27 | 28 | COMMIT; 29 | 30 | PRAGMA foreign_keys=on; -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | from app import db, app 2 | from celery.signals import task_postrun 3 | from app import create_celery_app 4 | from celery.task.schedules import crontab 5 | from celery.decorators import periodic_task 6 | from celery.utils.log import get_task_logger 7 | from datetime import datetime 8 | from feeder.feed import Feed 9 | import requests 10 | 11 | 12 | celery = create_celery_app(app) 13 | logger = get_task_logger(__name__) 14 | 15 | 16 | @celery.task 17 | def send_event_notification(): 18 | ''' send mail to the recipients ''' 19 | print "sent the mail" 20 | 21 | @periodic_task(run_every=(crontab(hour="*", minute="1", day_of_week="*"))) 22 | def pull_feeds(): 23 | logger.info("Start task") 24 | now = datetime.now() 25 | f = Feed() 26 | results = f.process_all() 27 | logger.info("pulled feeds") 28 | logger.info("Task finished: total inserted %i" % len(results)) 29 | 30 | @task_postrun.connect 31 | def close_session(*args, **kwargs): 32 | db.session.remove() --------------------------------------------------------------------------------