misp_analyzer.py hinzugefügt
This commit is contained in:
parent
d1630f3e0c
commit
a2c008e97d
258
misp_analyzer.py
Normal file
258
misp_analyzer.py
Normal file
@ -0,0 +1,258 @@
|
||||
"""Index analyzer plugin for MISP."""
|
||||
|
||||
import logging
|
||||
import ntpath
|
||||
import re
|
||||
import requests
|
||||
|
||||
from flask import current_app
|
||||
from timesketch.lib.analyzers import interface
|
||||
from timesketch.lib.analyzers import manager
|
||||
|
||||
|
||||
logger = logging.getLogger("timesketch.analyzers.misp")
|
||||
|
||||
|
||||
class MispAnalyzer(interface.BaseAnalyzer):
|
||||
"""Analyzer for MISP."""
|
||||
|
||||
NAME = "misp_analyzer"
|
||||
DISPLAY_NAME = "MISP"
|
||||
DESCRIPTION = "Mark events using MISP"
|
||||
|
||||
def __init__(self, index_name, sketch_id, timeline_id=None, **kwargs):
|
||||
"""Initialize the Analyzer."""
|
||||
super().__init__(index_name, sketch_id, timeline_id=timeline_id)
|
||||
self.misp_url = current_app.config.get("MISP_URL")
|
||||
self.misp_api_key = current_app.config.get("MISP_API_KEY")
|
||||
self.total_event_counter = 0
|
||||
self.result_dict = {}
|
||||
self._query_string = kwargs.get("query_string")
|
||||
self._attr = kwargs.get("attr")
|
||||
self._timesketch_attr = kwargs.get("timesketch_attr")
|
||||
self.ip_pattern = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b')
|
||||
|
||||
@staticmethod
|
||||
def get_kwargs():
|
||||
"""Get kwargs for the analyzer."""
|
||||
to_query = [
|
||||
{
|
||||
"query_string": "md5_hash:*",
|
||||
"attr": "md5",
|
||||
"timesketch_attr": "md5_hash",
|
||||
},
|
||||
{
|
||||
"query_string": "sha1_hash:*",
|
||||
"attr": "sha1",
|
||||
"timesketch_attr": "sha1_hash",
|
||||
},
|
||||
{
|
||||
"query_string": "sha256_hash:*",
|
||||
"attr": "sha256",
|
||||
"timesketch_attr": "sha256_hash",
|
||||
},
|
||||
{
|
||||
"query_string": "filename:*",
|
||||
"attr": "filename",
|
||||
"timesketch_attr": "filename",
|
||||
},
|
||||
{
|
||||
"query_string": "message:*",
|
||||
"attr": "ip-src",
|
||||
"timesketch_attr": "message",
|
||||
},
|
||||
{
|
||||
"query_string": "message:*",
|
||||
"attr": "ip-dst",
|
||||
"timesketch_attr": "message",
|
||||
},
|
||||
{
|
||||
"query_string": "source_ip:*",
|
||||
"attr": "ip-src",
|
||||
"timesketch_attr": "source_ip",
|
||||
},
|
||||
]
|
||||
return to_query
|
||||
|
||||
def _is_valid_ip(self, ip_str):
|
||||
"""Validate IP address."""
|
||||
try:
|
||||
import ipaddress
|
||||
ip_str = ip_str.strip()
|
||||
ipaddress.ip_address(ip_str)
|
||||
# Filter out invalid ranges
|
||||
if ip_str.startswith(('0.', '127.', '255.255.255.255')):
|
||||
return False
|
||||
return True
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
def _is_valid_hash(self, hash_str, hash_type):
|
||||
"""Validate hash format."""
|
||||
if not hash_str:
|
||||
return False
|
||||
hash_str = hash_str.strip().lower()
|
||||
|
||||
if hash_type == "md5":
|
||||
return len(hash_str) == 32 and all(c in '0123456789abcdef' for c in hash_str)
|
||||
elif hash_type == "sha1":
|
||||
return len(hash_str) == 40 and all(c in '0123456789abcdef' for c in hash_str)
|
||||
elif hash_type == "sha256":
|
||||
return len(hash_str) == 64 and all(c in '0123456789abcdef' for c in hash_str)
|
||||
|
||||
return False
|
||||
|
||||
def query_misp_single(self, value, attr):
|
||||
"""Query MISP for a single value."""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.misp_url}/attributes/restSearch/",
|
||||
json={"returnFormat": "json", "value": value, "type": attr},
|
||||
headers={"Authorization": self.misp_api_key},
|
||||
verify=False,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return []
|
||||
|
||||
data = response.json()
|
||||
return data.get("response", {}).get("Attribute", [])
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def mark_event(self, event, result, attr):
|
||||
"""Add MISP intelligence to event."""
|
||||
try:
|
||||
if attr.startswith("ip-"):
|
||||
msg = "MISP: Malicious IP - "
|
||||
else:
|
||||
msg = "MISP: Known indicator - "
|
||||
|
||||
event_info = result[0].get("Event", {}).get("info", "Unknown")
|
||||
msg += event_info
|
||||
|
||||
if len(result) > 1:
|
||||
msg += f" (+{len(result)-1} more)"
|
||||
|
||||
event.add_comment(msg)
|
||||
event.add_tags([f"MISP-{attr}", "threat-intel"])
|
||||
event.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking event: {e}")
|
||||
|
||||
def query_misp(self, query, attr, timesketch_attr):
|
||||
"""Extract indicators and query MISP."""
|
||||
events = self.event_stream(query_string=query, return_fields=[timesketch_attr])
|
||||
query_list = []
|
||||
events_list = []
|
||||
processed = 0
|
||||
|
||||
# Extract indicators from events
|
||||
for event in events:
|
||||
processed += 1
|
||||
if processed > 5000: # Reasonable limit
|
||||
break
|
||||
|
||||
loc = event.source.get(timesketch_attr)
|
||||
if not loc:
|
||||
continue
|
||||
|
||||
events_list.append(event)
|
||||
indicators = []
|
||||
|
||||
# Extract based on attribute type - STRICT VALIDATION
|
||||
if attr.startswith("ip-") and timesketch_attr == "message":
|
||||
# Extract IPs from message field
|
||||
ip_matches = self.ip_pattern.findall(str(loc))
|
||||
indicators = [ip for ip in ip_matches if self._is_valid_ip(ip)]
|
||||
|
||||
elif attr.startswith("ip-") and timesketch_attr in ["source_ip", "src_ip", "client_ip"]:
|
||||
# Direct IP field
|
||||
if self._is_valid_ip(str(loc)):
|
||||
indicators = [str(loc)]
|
||||
|
||||
elif attr in ["md5", "sha1", "sha256"]:
|
||||
# Hash fields
|
||||
if self._is_valid_hash(str(loc), attr):
|
||||
indicators = [str(loc)]
|
||||
|
||||
elif attr == "filename":
|
||||
# Filename extraction
|
||||
filename = ntpath.basename(str(loc))
|
||||
if filename and len(filename) > 1:
|
||||
indicators = [filename]
|
||||
|
||||
# Add valid indicators to query list
|
||||
for indicator in indicators:
|
||||
if indicator not in query_list:
|
||||
query_list.append(indicator)
|
||||
self.result_dict[f"{attr}:{indicator}"] = []
|
||||
|
||||
logger.info(f"Extracted {len(query_list)} {attr} indicators from {processed} events")
|
||||
|
||||
if not query_list:
|
||||
return
|
||||
|
||||
# Query MISP for each indicator
|
||||
for indicator in query_list:
|
||||
result = self.query_misp_single(indicator, attr)
|
||||
if result:
|
||||
self.result_dict[f"{attr}:{indicator}"] = result
|
||||
logger.info(f"MISP hit: {indicator}")
|
||||
|
||||
# Mark matching events
|
||||
for event in events_list:
|
||||
loc = event.source.get(timesketch_attr)
|
||||
if not loc:
|
||||
continue
|
||||
|
||||
# Re-extract indicators from this event
|
||||
event_indicators = []
|
||||
|
||||
if attr.startswith("ip-") and timesketch_attr == "message":
|
||||
ip_matches = self.ip_pattern.findall(str(loc))
|
||||
event_indicators = [ip for ip in ip_matches if self._is_valid_ip(ip)]
|
||||
elif attr.startswith("ip-") and timesketch_attr in ["source_ip", "src_ip", "client_ip"]:
|
||||
if self._is_valid_ip(str(loc)):
|
||||
event_indicators = [str(loc)]
|
||||
elif attr in ["md5", "sha1", "sha256"]:
|
||||
if self._is_valid_hash(str(loc), attr):
|
||||
event_indicators = [str(loc)]
|
||||
elif attr == "filename":
|
||||
filename = ntpath.basename(str(loc))
|
||||
if filename:
|
||||
event_indicators = [filename]
|
||||
|
||||
# Check if any indicator has MISP match
|
||||
for indicator in event_indicators:
|
||||
key = f"{attr}:{indicator}"
|
||||
if key in self.result_dict and self.result_dict[key]:
|
||||
self.total_event_counter += 1
|
||||
self.mark_event(event, self.result_dict[key], attr)
|
||||
break # Only mark once per event
|
||||
|
||||
# Create view if we found matches
|
||||
if self.total_event_counter > 0:
|
||||
self.sketch.add_view(
|
||||
view_name="MISP Threat Intelligence",
|
||||
analyzer_name=self.NAME,
|
||||
query_string='tag:"MISP-*" OR tag:"threat-intel"',
|
||||
)
|
||||
|
||||
def run(self):
|
||||
"""Entry point for the analyzer."""
|
||||
if not self.misp_url or not self.misp_api_key:
|
||||
return "No MISP configuration found"
|
||||
|
||||
try:
|
||||
self.query_misp(self._query_string, self._attr, self._timesketch_attr)
|
||||
return f"[{self._timesketch_attr}] MISP Match: {self.total_event_counter}"
|
||||
except Exception as e:
|
||||
logger.error(f"MISP analyzer error: {e}")
|
||||
return f"[{self._timesketch_attr}] MISP Error: {str(e)}"
|
||||
|
||||
|
||||
manager.AnalysisManager.register_analyzer(MispAnalyzer)
|
Reference in New Issue
Block a user