class Invasion(object): def __init__(self): self.mongo = MongoHelper(MongoConfig.uri, db=MongoConfig.db, collection=MongoConfig.invasions_collection) def new(self, *activities) -> ObjectId: """ 新建入侵事件 返回入侵事件ID """ source_ip = activities[0]["form_data"]["source_ip"] title = "来自于{ip}的入侵事件".format(ip=source_ip) doc = { "title": title, "level": _get_max_level(list(map(lambda x: x["level"], activities))), "start_time": activities[0]["start_time"], "end_time": activities[-1]["end_time"], "source_ip": source_ip, "status": "pending" } return self.mongo.insert_one(doc).inserted_id def add_activity(self, invasion_id: ObjectId, activity_doc: dict): """ 向当前入侵事件添加一条新的威胁活动 """ query = { "_id": invasion_id } invasion = self.mongo.find_one(query=query) level = _get_max_level([invasion["level"], activity_doc["level"]]) end_time = activity_doc["end_time"] if end_time > invasion["end_time"]: self.update(invasion_id, { "$set": { "level": level, "end_time": end_time } }) else: self.update(invasion_id, { "$set": { "level": level } }) def find_record(self, source_ip, start_time, **kwargs): """ 根据 source_ip 查找一段时间内的相同来源的入侵事件 """ return self.mongo.find_one({ "source_ip": source_ip, "end_time": {"$gte": start_time + timedelta(hours=-main_config.merge_invasion_time)}, **kwargs }) def update(self, _id, doc): self.mongo.update_one({ "_id": _id }, doc=doc)
def init_sensitive_groups(domain): logger.info("init sensitive groups.") domain = get_netbios_domain(domain) ldap_search = LDAPSearch(domain) redis = RedisHelper() mongo = MongoHelper(uri=MongoConfig.uri, db=MongoConfig.db, collection=MongoConfig.settings_collection) sensitive_groups = [] for item in default_sensitive_groups(domain): if len(item["sid"]) > 0: sensitive_groups.append(item) else: entry = ldap_search.search_by_name(item["name"], attributes=["objectSid"]) if not entry or len( entry.entry_attributes_as_dict["objectSid"]) == 0: continue sid = entry.entry_attributes_as_dict["objectSid"][0] item["sid"] = sid sensitive_groups.append(item) logger.info(",".join(list(map(lambda x: x["name"], sensitive_groups)))) sensitive_entry = mongo.find_one({"name": "sensitive_entry"})["value"] sensitive_entry["group"] = sensitive_groups mongo.update_one({"name": "sensitive_entry"}, {"$set": { "value": sensitive_entry }}, upsert=True) redis.set_str_value("sensitive_entry" + REDIS_KEY_SUFFIX, simplejson.dumps(sensitive_entry))
def check_mongo_connection() -> bool: mongo = MongoHelper(MongoConfig.uri) if not mongo.check_connection(): logger.error( "Can't connect to the MongoDB, please reconfirm the settings.") return False logger.info("Connect to the MongoDB successfully, OK.") return True
def __init__(self, code: str, title: str, desc: str): self.code = code self.title = title self.desc = desc self.log = None self.krb = None self.domain = None self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.delay_run_collection)
def set_learning_end_time_setting(): value = move_n_days(datetime_utc_now_obj(), 10) logger.info("set learning end time: " + str(value)) name = "learning_end_time" redis = RedisHelper() mongo = MongoHelper(uri=MongoConfig.uri, db=MongoConfig.db, collection=MongoConfig.settings_collection) mongo.update_one(filter={"name": name}, doc={"$set": { "value": value }}, upsert=True) key = name + REDIS_KEY_SUFFIX redis.set_str_value(key, datetime_to_common_str(value))
def init_ldap_settings(domain, server, user, password): logger.info("init the ldap configuration.") if not server.startswith("ldap://"): server = "ldap://" + server mongo = MongoHelper(uri=MongoConfig.uri, db=MongoConfig.db, collection=MongoConfig.settings_collection) query = {"name": "ldap"} doc = { domain: { "server": server, "user": user, "password": password, "dn": get_dn_domain_name(domain) } } mongo.update_one(filter=query, doc={"$set": {"value": doc}}, upsert=True) redis = RedisHelper() redis.set_str_value("ldap" + REDIS_KEY_SUFFIX, simplejson.dumps(doc))
def init_default_settings(domain): logger.info("init other settings.") redis = RedisHelper() mongo = MongoHelper(uri=MongoConfig.uri, db=MongoConfig.db, collection=MongoConfig.settings_collection) for name, value in default_settings.items(): if name == "domain_list": value = [domain] mongo.update_one(filter={"name": name}, doc={"$set": { "value": value }}, upsert=True) key = name + REDIS_KEY_SUFFIX if name in [ "domain_list", "VPN_ip_part", "detail_file_share_white_list_setting" ]: if len(value) != 0: redis.set_list(key, *value) elif name in [ "raw_data_expire", "honeypot_account", "alarms_merge", "sensitive_entry", "kerberos" ]: redis.set_str_value(key, simplejson.dumps(value)) elif name in ["brute_force_max"]: redis.set_str_value(key, str(value)) elif isinstance(value, list): if len(value) > 0 and isinstance(value[0], dict): redis.set_str_value(key, simplejson.dumps(value)) else: if len(value) != 0: redis.set_list(key, *value) elif isinstance(value, str): redis.set_str_value(key, value) elif isinstance(value, dict): redis.set_str_value(key, simplejson.dumps(value)) elif isinstance(value, int): redis.set_str_value(key, str(value))
def load_settings(): """ 加载Mongo中保存的配置信息到redis中 """ mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.settings_collection) redis = RedisHelper() fetcher = mongo.find_all({}) for each in fetcher: key = each["name"] + "_setting" # 再录入 if isinstance(each["value"], list): if len(each["value"]) == 0: continue elif isinstance(each["value"][0], dict): redis.set_str_value(key, simplejson.dumps(each["value"])) continue redis.set_list(key, *each["value"]) elif isinstance(each["value"], dict): redis.set_str_value(key, simplejson.dumps(each["value"])) else: redis.set_str_value(key, each["value"])
def get_all_dc_names(domain: str): """ 将DC列表入库 """ domain = get_netbios_domain(domain) logger.info("Search all domain controllers using LDAP.") dc_name_list = [] ldap_search = LDAPSearch(domain) dc_list = ldap_search.search_domain_controller() for each in dc_list: dc_name = str(each["cn"]) dc_name_list.append(dc_name) mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.settings_collection) doc = {domain: dc_name_list} logger.info(",".join(dc_name_list)) logger.info( "domain controller count: {count}".format(count=len(dc_name_list))) logger.info("Save all domain controllers to settings.") mongo.update_one({"name": "dc_name_list"}, {"$set": {"value": doc}}, True) redis = RedisHelper() redis.set_str_value("dc_name_list" + REDIS_KEY_SUFFIX, simplejson.dumps(doc))
class Delegation(object): def __init__(self): self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.delegation_collection) def new_delegation_record(self, user: User, delegation_type: str, allowed_to=None): if delegation_type not in [ CONSTRAINED_DELEGATION, UNCONSTRAINED_DELEGATION, RES_BASED_CONSTRAINED_DELEGATION ]: raise NoSuchDelegationType() self.mongo.insert_one({ "name": user.user_name, "sid": user.user_sid, "domain": user.domain_name, "delegation_type": delegation_type, "allowed_to": allowed_to }) def find_constrained_delegation_by_sid(self, sid: str): return self.mongo.find_one({ "sid": sid, "delegation_type": CONSTRAINED_DELEGATION }) def find_res_constrained_delegation_by_name(self, name: str): return self.mongo.find_one({ "name": name, "delegation_type": RES_BASED_CONSTRAINED_DELEGATION }) def find_one_delegation(self, query: dict): return self.mongo.find_one(query) def update_delegation(self, sid, delegation_type, allowed_to): self.mongo.update_one({ "sid": sid, "delegation_type": delegation_type }, {"allowed_to": allowed_to})
def __init__(self, rules_table): self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, rules_table)
class MatchRules(object): def __init__(self, rules_table): self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, rules_table) def match(self, event_doc: dict): alert_code = event_doc["alert_code"] form_data = event_doc["form_data"] all_rules = self.mongo.find_all({"alert_code": alert_code}) for rule in all_rules: try: rule = Rule(rule) # 成功匹配规则 则返回被匹配上的规则ID if self._match_one(rule, form_data): return rule.id except Exception as e: return False return False def _match_one(self, rule: Rule, form_data): """ 一条忽略内容的多条规则,全部匹配才算成功 """ for rule_content in rule.rules: if rule_content.field_type == "string": if not self._match_string(form_data[rule_content.field_name], rule_content.value, rule_content.match_type): return False elif rule_content.field_type == "ip": if not self._match_ip(form_data[rule_content.field_name], rule_content.value): return False elif rule_content.field_type == "list": if not self._match_list(form_data[rule_content.field_name], rule_content.value, rule_content.match_type): return False return True def _match_string(self, alert_value, rule_value, match_type) -> bool: """ 匹配字符串,将告警字段内容和忽略规则的内容匹配 """ if match_type == "term": if isinstance(alert_value, list): for each in alert_value: if each == rule_value: return True return False else: return alert_value == rule_value # 正则匹配 elif match_type == "regex": if isinstance(alert_value, list): for each in alert_value: if re.match(rule_value, each): return True return False else: return True if re.match(rule_value, alert_value) else False def _match_ip(self, alert_ip, rule_ip) -> bool: """ 匹配IP类型 """ # CIDR if "/" in rule_ip: return IP(alert_ip) in IP(rule_ip) else: return alert_ip == rule_ip def _match_list(self, alert_value, rule_value, match_type): """ 匹配列表 """ for each in rule_value: if match_type == "ip" and self._match_ip(alert_value, each): return True elif match_type == "string" or match_type == "regex": if self._match_string(alert_value, each, match_type): return True return False
def __init__(self): self.alert_mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.alerts_collection) self.activity = Activity() self.ignore_rule = MatchRules(MongoConfig.ignore_collection) self.exclude_rule = MatchRules(MongoConfig.exclude_collection)
class Alert(object): def __init__(self): self.alert_mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.alerts_collection) self.activity = Activity() self.ignore_rule = MatchRules(MongoConfig.ignore_collection) self.exclude_rule = MatchRules(MongoConfig.exclude_collection) def generate(self, doc): """ 生成告警,发送邮件并入库 """ # 计算告警表单内容的唯一ID form_data_id = _get_form_data_md5(doc["form_data"]) doc["form_data_id"] = form_data_id # 误报排除规则过滤,不再产生记录 if self._auto_exclude(doc): return # 忽略规则过滤 doc = self._auto_ignore(doc) # 首先尝试合并相同来源且相同类型的告警到同一个威胁活动, 即unique_id重复的告警 if self._merge_alert(doc): return # 新增,生成威胁活动 activity_id = self.activity.new(doc) # 记录下威胁活动的ID以后,告警入库 doc["activity_id"] = activity_id self.alert_mongo.insert_one(doc) def _merge_alert(self, alert_doc): """ 合并告警到同一个威胁活动 """ # 查找是否已经生成了威胁活动,没有的话直接退出,然后新增 activity = self.activity.find_record(alert_doc["unique_id"], alert_doc["start_time"]) if not activity: return False alert_doc["activity_id"] = activity["_id"] # 尝试合并告警表单内容完全重复的告警,增加重复次数 if self._merge_repeat_count(alert_doc): self.activity.update(activity["_id"], { "$set": { "end_time": alert_doc["end_time"] } }) # 无完全重复,在该威胁活动下新增一条告警 else: self.activity.add_alert(activity, alert_doc) self.alert_mongo.insert_one(alert_doc) return True def _merge_repeat_count(self, alert_doc: dict) -> bool: """ 完全重复的告警内容 不再入库 直接统计次数 """ query = { "activity_id": alert_doc["activity_id"], "form_data_id": alert_doc["form_data_id"] } record = self.alert_mongo.find_one(query) if record: # 账号爆破特殊处理一下 if alert_doc["alert_code"] == "301": self.alert_mongo.update_one( {"_id": record["_id"]}, { "$set": { "form_data.brute_force_target_users": alert_doc["form_data"]["brute_force_target_users"] } } ) return True self.alert_mongo.update_one( {"_id": record["_id"]}, { "$inc": {"repeat_count": 1}, "$set": {"end_time": alert_doc["end_time"]} } ) return True else: return False def _auto_exclude(self, doc): """ 根据预先设定的规则,自动排除误报 和忽略的区别是,排除的误报不再产生记录,直接忽略 """ rule_id = self.exclude_rule.match(doc) if rule_id: return True else: return False def _auto_ignore(self, doc): """ 根据预先设定的规则,自动忽略告警 忽略的告警也需要合并 """ rule_id = self.ignore_rule.match(doc) if rule_id: doc["status"] = "auto_ignore" doc["ignore_rule_id"] = rule_id return doc
def __init__(self): self.mongo = MongoHelper(MongoConfig.uri, db=MongoConfig.db, collection=MongoConfig.invasions_collection)
def __init__(self): self.event_log_modules_map = None # self.traffic_kerberos_modules_map = None self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.delay_run_collection) self.alert = Alert()
class Engine(object): def __init__(self): self.event_log_modules_map = None # self.traffic_kerberos_modules_map = None self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.delay_run_collection) self.alert = Alert() def load(self): # 加载事件日志检测模块 logger.info("loading detect modules based on event_log") self.event_log_modules_map = self._load_module("event_log", "EVENT_ID") # 加载kerberos流量检测模块 # logger.info("loading detect modules based on traffic_kerberos") # self.traffic_kerberos_modules_map = self._load_module("traffic_kerberos", "MSG_TYPE") def start(self): """ 引擎启动主入口 """ self.load() # 启动消费者 c = Consumer() # 注册回调 logger.info("start MQ consumer and register callback func.") logger.info("status: main process running") c.run(self.do_analyze) def delay_run(self): """ 延迟检测 ** 请单进程运行! ** """ self.load() logger.info("status: delay process running") while True: time.sleep(5) data_list = self._get_delay_data() for data in data_list: alert_code = data["_delay_info"]["alert_code"] # if data["type"] == "krb5": # krb = Kerberos(data) # self._run_analyze(data=krb, data_type=krb.msg_type, modules_map=self.traffic_kerberos_modules_map, # alert_code=alert_code) if data["type"] == "wineventlog": log = Log(data) self._run_analyze(data=log, data_type=log.event_id, modules_map=self.event_log_modules_map, alert_code=alert_code) # 删除完成检测数据 self._clear_confirmed_data(data_list) def do_analyze(self, data: dict): # 解析krb5流量 # if data["type"] == "krb5": # krb = Kerberos(data) # if krb.msg_type not in self.traffic_kerberos_modules_map: # return # self._run_analyze(data=krb, data_type=krb.msg_type, modules_map=self.traffic_kerberos_modules_map) # 解析事件日志 if data["type"] == "wineventlog": if data["event_id"] == 4662: return if "event_data" not in data and data["event_id"] != 1100: return log = Log(data) if log.event_id not in self.event_log_modules_map: return self._run_analyze(data=log, data_type=log.event_id, modules_map=self.event_log_modules_map) def _run_analyze(self, data, data_type, modules_map: dict, alert_code=None): """ 运行检测模块 :param data: 数据字典 :param data_type: log.event_id 的值或者 krb.msg_type :param modules_map: 加载了检测模块的字典 :param alert_code: 可选,具体检测的告警代码,指定了之后只运行该模块 :return: """ module_list = modules_map[data_type] for module in module_list: code = module["code"] if alert_code and alert_code != code: continue m_object = module["object"] # 运行检测模块的语句 alert_doc = m_object.run(data) if alert_doc: # 存在问题,告警 self.alert.generate(alert_doc) def _load_module(self, name: str, data_type: str) -> dict: modules_map = {} def _register_module(d_type, m): if d_type not in modules_map: modules_map[d_type] = [m] else: modules_map[d_type].append(m) file_list = get_walk_files(project_dir + "/modules/detect/" + name) for f in file_list: f = f.replace(project_dir, ".") module_path, f = format_module_path(f) module = __import__(module_path, fromlist=[f]) logger.info("loaded module: " + module_path) data_types = getattr(module, data_type) assert isinstance(data_types, list) for d_type in data_types: _register_module( d_type, { "code": getattr(module, "ALERT_CODE") if hasattr( module, "ALERT_CODE") else None, "object": getattr(module, f)() }) return modules_map def _get_delay_data(self): query = {"_delay_info.time": {"$lte": datetime_now_obj()}} return [each for each in self.mongo.find_all(query)] def _clear_confirmed_data(self, data_list): id_list = [] for data in data_list: id_list.append(data["_id"]) query = {"_id": {"$in": id_list}} self.mongo.delete_many(query)
def __init__(self): self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.delegation_collection)
class DetectBase(object): def __init__(self, code: str, title: str, desc: str): self.code = code self.title = title self.desc = desc self.log = None self.krb = None self.domain = None self.mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.delay_run_collection) def init(self, log=None, krb=None): if log is None and krb is None: raise NoDataInitEvent() if log and krb: raise NoDataInitEvent() if log: assert isinstance(log, Log) self.domain = ".".join(log.dc_computer_name.split(".")[1:]) else: assert isinstance(krb, Kerberos) self.domain = krb.req.req_body.realm self.log = log self.krb = krb self._format_domain() def delay_confirm_krb(self, secs=10): doc = copy.deepcopy(self.krb.record) doc["_delay_info"] = { "time": move_n_sec(datetime_now_obj(), -secs), "data_type": "traffic_kerberos", "alert_code": self.code } self.mongo.insert_one(doc) def delay_confirm_log(self, secs=60): doc = copy.deepcopy(self.log.record) doc["_delay_info"] = { "time": move_n_sec(datetime_now_obj(), -secs), "data_type": "event_log", "alert_code": self.code } self.mongo.insert_one(doc) def _format_domain(self): if not self.domain: self.domain = "" self.netbios_name = "" self.fqdn_name = "" elif "." in self.domain: self.fqdn_name = self.domain self.netbios_name = _get_netbios_name(self.domain) elif self.domain and self.domain != "-": self.netbios_name = self.domain.upper() self.fqdn_name = _get_fqdn_name(self.domain) else: self.netbios_name = "" self.fqdn_name = "" def _get_unique_id(self, *args) -> str: _str = "" for each in args: _str += each return md5(_str) def _get_log_doc(self): if self.log is None: return {} return self.log.record def _get_krb_doc(self): if self.krb is None: return {} return self.krb.record def _get_dc_computer_name(self): if self.log: return self.log.dc_computer_name else: return self.krb.dc_host_name def _get_dc_hostname(self): if self.log: return self.log.dc_host_name else: return self.krb.dc_host_name def _get_detect_by(self): if self.log: return "event_log" else: return "krb_traffic" def _get_time(self): if self.log: return utc_to_datetime(self.log.utc_log_time) else: return utc_to_datetime(self.krb.utc_time) def _get_base_doc(self, **kwargs): return { "alert_code": self.code, "title": self.title, "description": self.desc, "classify": self._get_classify(), "dc_computer_name": self._get_dc_computer_name(), "dc_hostname": self._get_dc_hostname(), "domain": self.netbios_name, "status": "pending", "start_time": self._get_time(), "end_time": self._get_time(), "raw_log": self._get_log_doc(), "raw_krb": self._get_krb_doc(), "detect_by": self._get_detect_by(), "repeat_count": 1, **kwargs } def _get_classify(self): if self.code.startswith("1"): return "信息探测" elif self.code.startswith("2"): return "凭证盗取" elif self.code.startswith("3"): return "横向移动" elif self.code.startswith("4"): return "权限提升" elif self.code.startswith("5"): return "权限维持" elif self.code.startswith("6"): return "防御绕过" else: return "未知分类" def _get_workstation_by_source_ip(self, source_ip) -> str: """ 查找最近该IP认证主机名 返回主机名和域名 """ account_history = AccountHistory() return account_history.get_last_workstation_by_ip(source_ip) def _get_source_ip_by_workstation(self, source_ip) -> str: """ """ account_history = AccountHistory() return account_history.get_last_ip_by_workstation(source_ip) def _get_source_ip_by_logon_id(self, logon_id: str, user_name: str) -> str: id_term = get_term_statement("event_id", 4624) logon_id_term = get_term_statement("event_data.TargetLogonId.keyword", logon_id) user_term = get_term_statement("event_data.TargetUserName.keyword", user_name) query = { "query": get_must_statement(id_term, logon_id_term, user_term), "_source": ["event_data.IpAddress"], "size": 1 } es = ElasticHelper() rsp = es.search(body=query, index=ElasticConfig.event_log_index, doc_type=ElasticConfig.event_log_doc_type) if rsp and len(rsp["hits"]["hits"]) > 0: return rsp["hits"]["hits"][0]["_source"]["event_data"]["IpAddress"] else: return "unknown" @abstractmethod def _generate_alert_doc(self, **kwargs) -> dict: pass @abstractmethod def _get_level(self) -> str: pass
def __init__(self): self.activity_mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.activities_collection) self.invasion = Invasion()
class Activity(object): def __init__(self): self.activity_mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.activities_collection) self.invasion = Invasion() def new(self, activity_doc: dict) -> ObjectId: """ 新增威胁活动记录 同时尝试生成入侵事件记录 """ invasion_id = self._generate_invasion(activity_doc) if invasion_id: activity_doc["invasion_id"] = invasion_id return self.activity_mongo.insert_one(activity_doc).inserted_id def add_alert(self, activity_doc, alert_doc: dict): doc = { "$set": {} } if alert_doc["end_time"] > activity_doc["end_time"]: doc["$set"]["end_time"] = alert_doc["end_time"] if alert_doc["level"] > activity_doc["level"]: doc["$set"]["level"] = alert_doc["level"] if len(doc["$set"].keys()) == 0: return self.activity_mongo.update_one({ "_id": activity_doc["_id"] }, doc) def update(self, _id, doc): self.activity_mongo.update_one({ "_id": _id }, doc=doc) def find_record(self, uid, start_time): """ 根据 unique_id 查找一段时间内相同的威胁活动 """ return self.activity_mongo.find_one({ "unique_id": uid, "end_time": {"$gte": start_time + timedelta(hours=-main_config.merge_activity_time)} }) def _generate_invasion(self, activity_doc) -> ObjectId: """ 生成入侵事件 多个相同来源IP 不同类型的威胁活动 可以生成一个入侵事件 返回入侵事件的ID """ # 首先查找是否已存在对应的入侵事件,尝试合并 invasion_id = self._merge_activity(activity_doc) if invasion_id: assert isinstance(invasion_id, ObjectId) return invasion_id # 没有已存在的入侵事件,则按照条件,查找是否已存在不同类型的威胁活动 query = { "form_data.source_ip": activity_doc["form_data"]["source_ip"], "end_time": {"$gte": activity_doc["start_time"] + timedelta(hours=-main_config.merge_invasion_time)}, "alert_code": {"$ne": activity_doc["alert_code"]} } # 如果存在不同类型的威胁活动 another_activities = list(self.activity_mongo.find_all(query).sort("start_time", ASCENDING)) if len(another_activities) > 0: invasion_id = self.invasion.new(*another_activities, activity_doc) # 创建完入侵事件之后,将之前查询的到威胁活动全部加上对应的ID for activity in another_activities: self.update(activity["_id"], { "$set": { "invasion_id": invasion_id } }) def _merge_activity(self, activity_doc): source_ip = activity_doc["form_data"]["source_ip"] invasion = self.invasion.find_record(source_ip, activity_doc["start_time"]) if not invasion: return False # 向存在的入侵事件添加当前威胁活动 self.invasion.add_activity(invasion["_id"], activity_doc) return invasion["_id"]