# Copyright 2015-present Palo Alto Networks, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os.path import os import shutil import sqlite3 import time from tempfile import NamedTemporaryFile import yaml import filelock import ujson as json from gevent import sleep from flask import request, jsonify from .mmrpc import MMRpcClient from .aaa import MMBlueprint from .logger import LOG __all__ = ['BLUEPRINT'] LOCK_TIMEOUT = 3000 BLUEPRINT = MMBlueprint('configdata', __name__, url_prefix='/config/data') def _safe_remove(path, g=None): try: os.remove(path) except: LOG.exception('Exception removing {}'.format(path)) class _CDataYaml(object): def __init__(self, cpath, datafilename): self.cpath = cpath self.datafilename = datafilename def read(self): fdfname = self.datafilename+'.yml' lockfname = os.path.join(self.cpath, fdfname+'.lock') lock = filelock.FileLock(lockfname) os.listdir(self.cpath) if fdfname not in os.listdir(self.cpath): return jsonify(error={ 'message': 'Unknown config data file' }), 400 try: with lock.acquire(timeout=10): with open(os.path.join(self.cpath, fdfname), 'r') as f: result = yaml.safe_load(f) except Exception as e: return jsonify(error={ 'message': 'Error loading config data file: %s' % str(e) }), 500 return jsonify(result=result) def create(self): tdir = os.path.dirname(os.path.join(self.cpath, self.datafilename)) if not os.path.samefile(self.cpath, tdir): return jsonify(error={'msg': 'Wrong config data filename'}), 400 fdfname = os.path.join(self.cpath, self.datafilename+'.yml') lockfname = fdfname+'.lock' lock = filelock.FileLock(lockfname) try: body = request.get_json() except Exception as e: return jsonify(error={'message': str(e)}), 400 try: with lock.acquire(timeout=10): with open(fdfname, 'w') as f: yaml.safe_dump(body, stream=f) except Exception as e: return jsonify(error={ 'message': str(e) }), 500 def append(self): tdir = os.path.dirname(os.path.join(self.cpath, self.datafilename)) if not os.path.samefile(self.cpath, tdir): return jsonify(error={'msg': 'Wrong config data filename'}), 400 cdfname = os.path.join(self.cpath, self.datafilename+'.yml') lockfname = cdfname+'.lock' lock = filelock.FileLock(lockfname) try: with lock.acquire(timeout=10): if not os.path.isfile(cdfname): config_data_file = [] else: with open(cdfname, 'r') as f: config_data_file = yaml.safe_load(f) if type(config_data_file) != list: raise RuntimeError('Config data file is not a list') body = request.get_json() if body is None: return jsonify(error={ 'message': 'No record in request' }), 400 config_data_file.append(body) with open(cdfname, 'w') as f: yaml.safe_dump(config_data_file, stream=f) except Exception as e: return jsonify(error={ 'message': 'Error appending to config data file: %s' % str(e) }), 500 class _CDataLocalDB(object): def __init__(self, cpath, datafilename): self.cpath = cpath self.datafilename = datafilename self.full_path = os.path.join(self.cpath, self.datafilename) def read(self): tdir = os.path.dirname(self.full_path) if not os.path.samefile(self.cpath, tdir): return jsonify(error={'msg': 'Wrong config data filename'}), 400 result = [] if not os.path.isfile(self.full_path+'.db'): return jsonify(result=[]) try: conn = sqlite3.connect(self.full_path+'.db') for row in conn.execute('select * from indicators'): indicator = json.loads(row[2]) indicator['indicator'] = row[0] indicator['type'] = row[1] indicator['_expiration_ts'] = row[3] indicator['_update_ts'] = row[4] result.append(indicator) sleep(0) finally: conn.close() return jsonify(result=result) def create(self): return jsonify(error=dict(message='Method not allowed on localdb files')), 400 def _parse_text_data(self, data): result = [] state = 'INIT' indicator = {} attribute = None for line in iter(data.splitlines()): if len(line) > 0 and line[0] == '#': continue if state == 'INIT': line = line.strip() if len(line) == 0: continue indicator['type'] = line state = 'TYPE' continue if state == 'TYPE': line = line.strip() if len(line) == 0: continue indicator['indicator'] = line state = 'INDICATOR' continue if state == 'INDICATOR': line = line.strip() if len(line) == 0: result.append(indicator) indicator = {} state = 'INIT' continue attribute = line state = 'ATTRIBUTE' continue if state == 'ATTRIBUTE': line = line.strip() indicator[attribute] = line if attribute == 'confidence': if not line.isdigit(): LOG.error('Invalid confidence value: {!r}'.format(line)) return None indicator[attribute] = int(line) elif attribute == 'ttl': if line.isdigit(): indicator[attribute] = int(line) else: indicator[attribute] = 'disabled' state = 'INDICATOR' continue if state == 'INDICATOR': result.append(indicator) state = 'INIT' if state != 'INIT': LOG.error('Error parsing indicators, state: {}'.format(state)) return None if len(result) == 0: return None return result def append(self): tdir = os.path.dirname(self.full_path) if not os.path.samefile(self.cpath, tdir): return jsonify(error={'msg': 'Wrong config data filename'}), 400 record = request.get_json() if record is None: record = self._parse_text_data(request.data) if record is None: return jsonify(error={ 'message': 'No valid record in request' }), 400 indicators = [record] if isinstance(record, list): indicators = record now = int(time.time()*1000) updates = [] for en, entry in enumerate(indicators): indicator = entry.pop('indicator', None) if indicator is None: return jsonify(error={ 'message': 'entry {}: indicator field is missing'.format(en) }), 400 type_ = entry.pop('type', None) if type_ is None: return jsonify(error={ 'message': 'entry {}: type field is missing'.format(en) }), 400 expiration_ts = entry.pop('ttl', None) if expiration_ts is not None: if isinstance(expiration_ts, int): expiration_ts = (expiration_ts*1000+now) else: expiration_ts = 'disabled' updates.append(( indicator, type_, json.dumps(entry), expiration_ts, now, json.dumps([indicator, type_, entry]) )) try: conn = sqlite3.connect(self.full_path+'.db') with conn: conn.execute('''create table if not exists indicators (indicator text, type text, attributes text, expiration_ts integer, update_ts integer, content text, primary key(indicator, type));''') conn.execute('''create index if not exists updateIndex on indicators(update_ts);''') conn.executemany('''insert or replace into indicators (indicator, type, attributes, expiration_ts, update_ts, content) values (?, ?, ?, ?, ?, ?); ''', updates) finally: conn.close() class _CDataUploadOnly(object): def __init__(self, extension, cpath, datafilename): self.extension = extension self.cpath = cpath self.datafilename = datafilename def read(self): fdfname = '{}.{}'.format(self.datafilename, self.extension) os.listdir(self.cpath) if fdfname not in os.listdir(self.cpath): return jsonify(error={ 'message': 'Unknown config data file' }), 400 return jsonify(result='ok') def create(self): tdir = os.path.dirname(os.path.join(self.cpath, self.datafilename)) if not os.path.samefile(self.cpath, tdir): return jsonify(error={'msg': 'Wrong config data filename'}), 400 fdfname = os.path.join(self.cpath, '{}.{}'.format(self.datafilename, self.extension)) if 'file' not in request.files: return jsonify(error={'messsage': 'No file'}), 400 file = request.files['file'] if file.filename == '': return jsonify(error={'message': 'No file'}), 400 tf = NamedTemporaryFile(prefix='mm-extension-upload', delete=False) try: file.save(tf) tf.close() shutil.move(tf.name, fdfname) finally: _safe_remove(tf.name) class _CDataCertificate(_CDataUploadOnly): def __init__(self, cpath, datafilename): super(_CDataCertificate, self).__init__( extension='crt', cpath=cpath, datafilename=datafilename ) class _CDataPrivateKey(_CDataUploadOnly): def __init__(self, cpath, datafilename): super(_CDataPrivateKey, self).__init__( extension='pem', cpath=cpath, datafilename=datafilename ) # API for working with side configs and dynamic data files @BLUEPRINT.route('/<datafilename>', methods=['GET'], read_write=False) def get_config_data(datafilename): cpath = os.path.dirname(os.environ.get('MM_CONFIG')) datafiletype = request.values.get('t', 'yaml') if datafiletype == 'yaml': return _CDataYaml(cpath, datafilename).read() elif datafiletype == 'cert': return _CDataCertificate(cpath, datafilename).read() elif datafiletype == 'pkey': return _CDataPrivateKey(cpath, datafilename).read() elif datafiletype == 'localdb': return _CDataLocalDB(cpath, datafilename).read() return jsonify(error=dict(message='Unknown data file type')), 400 @BLUEPRINT.route('/<datafilename>', methods=['PUT'], read_write=True) def save_config_data(datafilename): cpath = os.path.dirname(os.environ.get('MM_CONFIG')) datafiletype = request.values.get('t', 'yaml') if datafiletype == 'yaml': result = _CDataYaml(cpath, datafilename).create() elif datafiletype == 'cert': result = _CDataCertificate(cpath, datafilename).create() elif datafiletype == 'pkey': result = _CDataPrivateKey(cpath, datafilename).create() elif datafiletype == 'localdb': result = _CDataLocalDB(cpath, datafilename).create() else: return jsonify(error=dict(message='Unknown data file type')), 400 if result is None: hup = request.args.get('h', None) if hup is not None: MMRpcClient.send_cmd(hup, 'hup', {'source': 'minemeld-web'}) return jsonify(result='ok'), 200 return result @BLUEPRINT.route('/<datafilename>/append', methods=['POST'], read_write=True) def append_config_data(datafilename): cpath = os.path.dirname(os.environ.get('MM_CONFIG')) datafiletype = request.values.get('t', 'yaml') if datafiletype == 'yaml': result = _CDataYaml(cpath, datafilename).append() elif datafiletype == 'localdb': result = _CDataLocalDB(cpath, datafilename).append() else: return jsonify(error=dict(message='Unknown data file type')), 400 if result is None: hup = request.args.get('h', None) if hup is not None: MMRpcClient.send_cmd(hup, 'hup', {'source': 'minemeld-web'}) return jsonify(result='ok'), 200 return result