misp_analyzer.py aktualisiert
This commit is contained in:
parent
812bc2376d
commit
e9745c0c86
131
misp_analyzer.py
131
misp_analyzer.py
@ -31,6 +31,8 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
self._attr = kwargs.get("attr")
|
self._attr = kwargs.get("attr")
|
||||||
self._timesketch_attr = kwargs.get("timesketch_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')
|
self.ip_pattern = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b')
|
||||||
|
# Track marked events to prevent duplicates
|
||||||
|
self.marked_events = set()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_kwargs():
|
def get_kwargs():
|
||||||
@ -58,17 +60,12 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"query_string": "message:*",
|
"query_string": "message:*",
|
||||||
"attr": "ip-src",
|
"attr": "ip", # Generic IP instead of ip-src/ip-dst
|
||||||
"timesketch_attr": "message",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"query_string": "message:*",
|
|
||||||
"attr": "ip-dst",
|
|
||||||
"timesketch_attr": "message",
|
"timesketch_attr": "message",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"query_string": "source_ip:*",
|
"query_string": "source_ip:*",
|
||||||
"attr": "ip-src",
|
"attr": "ip",
|
||||||
"timesketch_attr": "source_ip",
|
"timesketch_attr": "source_ip",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -80,7 +77,6 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
ip_str = ip_str.strip()
|
ip_str = ip_str.strip()
|
||||||
ipaddress.ip_address(ip_str)
|
ipaddress.ip_address(ip_str)
|
||||||
# Filter out invalid ranges
|
|
||||||
if ip_str.startswith(('0.', '127.', '255.255.255.255')):
|
if ip_str.startswith(('0.', '127.', '255.255.255.255')):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@ -103,41 +99,96 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def query_misp_single(self, value, attr):
|
def query_misp_single(self, value, attr):
|
||||||
"""Query MISP for a single value."""
|
"""Query MISP for a single value - ENHANCED for cross-org visibility."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Query both ip-src and ip-dst for IPs, include cross-org events
|
||||||
|
if attr == "ip":
|
||||||
|
search_types = ["ip-src", "ip-dst"]
|
||||||
|
else:
|
||||||
|
search_types = [attr]
|
||||||
|
|
||||||
|
for search_type in search_types:
|
||||||
try:
|
try:
|
||||||
|
# Include events from other organizations
|
||||||
|
payload = {
|
||||||
|
"returnFormat": "json",
|
||||||
|
"value": value,
|
||||||
|
"type": search_type,
|
||||||
|
# Include events from all organizations with proper distribution
|
||||||
|
"enforceWarninglist": False, # Don't filter known-good IPs
|
||||||
|
"includeDecayScore": False, # Skip decay scores for speed
|
||||||
|
"includeFullModel": False, # Skip full model for speed
|
||||||
|
"decayingModel": [], # No decaying model filters
|
||||||
|
"excludeDecayed": False, # Include older indicators
|
||||||
|
# Distribution levels: 0=Own org, 1=Community, 2=Connected, 3=All, 5=Inherit
|
||||||
|
"distribution": [0, 1, 2, 3, 5] # Include all distribution levels
|
||||||
|
}
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.misp_url}/attributes/restSearch/",
|
f"{self.misp_url}/attributes/restSearch/",
|
||||||
json={"returnFormat": "json", "value": value, "type": attr},
|
json=payload,
|
||||||
headers={"Authorization": self.misp_api_key},
|
headers={"Authorization": self.misp_api_key},
|
||||||
verify=False,
|
verify=False,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code == 200:
|
||||||
return []
|
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return data.get("response", {}).get("Attribute", [])
|
attributes = data.get("response", {}).get("Attribute", [])
|
||||||
|
results.extend(attributes)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
continue
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def mark_event(self, event, result, attr):
|
def mark_event(self, event, result, attr):
|
||||||
"""Add MISP intelligence to event."""
|
"""Add MISP intelligence to event - FIXED to prevent duplicates."""
|
||||||
try:
|
try:
|
||||||
if attr.startswith("ip-"):
|
# Check if event already marked
|
||||||
msg = "MISP: Malicious IP - "
|
event_id = event.source.get('_id', '')
|
||||||
|
if event_id in self.marked_events:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.marked_events.add(event_id)
|
||||||
|
|
||||||
|
# Show organization info for cross-org awareness
|
||||||
|
if attr == "ip":
|
||||||
|
msg = "MISP: Malicious IP detected - "
|
||||||
else:
|
else:
|
||||||
msg = "MISP: Known indicator - "
|
msg = "MISP: Known indicator - "
|
||||||
|
|
||||||
event_info = result[0].get("Event", {}).get("info", "Unknown")
|
# Collect unique events and organizations
|
||||||
msg += event_info
|
events_info = {}
|
||||||
|
orgs_info = set()
|
||||||
|
|
||||||
if len(result) > 1:
|
for misp_attr in result:
|
||||||
msg += f" (+{len(result)-1} more)"
|
event_info = misp_attr.get("Event", {})
|
||||||
|
event_id = event_info.get("id", "")
|
||||||
|
event_desc = event_info.get("info", "Unknown")
|
||||||
|
org_name = event_info.get("Orgc", {}).get("name", "Unknown Org")
|
||||||
|
|
||||||
|
events_info[event_id] = f'"{event_desc}"'
|
||||||
|
orgs_info.add(org_name)
|
||||||
|
|
||||||
|
# Build message with org info
|
||||||
|
event_descriptions = list(events_info.values())[:2] # First 2 events
|
||||||
|
msg += " | ".join(event_descriptions)
|
||||||
|
|
||||||
|
if len(result) > 2:
|
||||||
|
msg += f" | +{len(result)-2} more"
|
||||||
|
|
||||||
|
# Add organization information
|
||||||
|
if len(orgs_info) > 1:
|
||||||
|
msg += f" | Orgs: {', '.join(list(orgs_info)[:3])}"
|
||||||
|
elif orgs_info:
|
||||||
|
org_name = list(orgs_info)[0]
|
||||||
|
if org_name != "Unknown Org":
|
||||||
|
msg += f" | Org: {org_name}"
|
||||||
|
|
||||||
event.add_comment(msg)
|
event.add_comment(msg)
|
||||||
event.add_tags([f"MISP-{attr}", "threat-intel"])
|
event.add_tags([f"MISP-{attr}", "threat-intel", "cross-org-intel"])
|
||||||
event.commit()
|
event.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -145,7 +196,7 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
|
|
||||||
def query_misp(self, query, attr, timesketch_attr):
|
def query_misp(self, query, attr, timesketch_attr):
|
||||||
"""Extract indicators and query MISP."""
|
"""Extract indicators and query MISP."""
|
||||||
events = self.event_stream(query_string=query, return_fields=[timesketch_attr])
|
events = self.event_stream(query_string=query, return_fields=[timesketch_attr, '_id'])
|
||||||
query_list = []
|
query_list = []
|
||||||
events_list = []
|
events_list = []
|
||||||
processed = 0
|
processed = 0
|
||||||
@ -153,7 +204,7 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
# Extract indicators from events
|
# Extract indicators from events
|
||||||
for event in events:
|
for event in events:
|
||||||
processed += 1
|
processed += 1
|
||||||
if processed > 5000: # Reasonable limit
|
if processed > 5000:
|
||||||
break
|
break
|
||||||
|
|
||||||
loc = event.source.get(timesketch_attr)
|
loc = event.source.get(timesketch_attr)
|
||||||
@ -163,24 +214,20 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
events_list.append(event)
|
events_list.append(event)
|
||||||
indicators = []
|
indicators = []
|
||||||
|
|
||||||
# Extract based on attribute type - STRICT VALIDATION
|
# Extract based on attribute type
|
||||||
if attr.startswith("ip-") and timesketch_attr == "message":
|
if attr == "ip" and timesketch_attr == "message":
|
||||||
# Extract IPs from message field
|
|
||||||
ip_matches = self.ip_pattern.findall(str(loc))
|
ip_matches = self.ip_pattern.findall(str(loc))
|
||||||
indicators = [ip for ip in ip_matches if self._is_valid_ip(ip)]
|
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"]:
|
elif attr == "ip" and timesketch_attr in ["source_ip", "src_ip", "client_ip"]:
|
||||||
# Direct IP field
|
|
||||||
if self._is_valid_ip(str(loc)):
|
if self._is_valid_ip(str(loc)):
|
||||||
indicators = [str(loc)]
|
indicators = [str(loc)]
|
||||||
|
|
||||||
elif attr in ["md5", "sha1", "sha256"]:
|
elif attr in ["md5", "sha1", "sha256"]:
|
||||||
# Hash fields
|
|
||||||
if self._is_valid_hash(str(loc), attr):
|
if self._is_valid_hash(str(loc), attr):
|
||||||
indicators = [str(loc)]
|
indicators = [str(loc)]
|
||||||
|
|
||||||
elif attr == "filename":
|
elif attr == "filename":
|
||||||
# Filename extraction
|
|
||||||
filename = ntpath.basename(str(loc))
|
filename = ntpath.basename(str(loc))
|
||||||
if filename and len(filename) > 1:
|
if filename and len(filename) > 1:
|
||||||
indicators = [filename]
|
indicators = [filename]
|
||||||
@ -201,7 +248,12 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
result = self.query_misp_single(indicator, attr)
|
result = self.query_misp_single(indicator, attr)
|
||||||
if result:
|
if result:
|
||||||
self.result_dict[f"{attr}:{indicator}"] = result
|
self.result_dict[f"{attr}:{indicator}"] = result
|
||||||
logger.info(f"MISP hit: {indicator}")
|
# Log organization diversity
|
||||||
|
orgs = set()
|
||||||
|
for r in result:
|
||||||
|
org = r.get("Event", {}).get("Orgc", {}).get("name", "Unknown")
|
||||||
|
orgs.add(org)
|
||||||
|
logger.info(f"MISP hit: {indicator} ({len(result)} indicators from {len(orgs)} orgs)")
|
||||||
|
|
||||||
# Mark matching events
|
# Mark matching events
|
||||||
for event in events_list:
|
for event in events_list:
|
||||||
@ -209,13 +261,18 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
if not loc:
|
if not loc:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check if event already processed
|
||||||
|
event_id = event.source.get('_id', '')
|
||||||
|
if event_id in self.marked_events:
|
||||||
|
continue
|
||||||
|
|
||||||
# Re-extract indicators from this event
|
# Re-extract indicators from this event
|
||||||
event_indicators = []
|
event_indicators = []
|
||||||
|
|
||||||
if attr.startswith("ip-") and timesketch_attr == "message":
|
if attr == "ip" and timesketch_attr == "message":
|
||||||
ip_matches = self.ip_pattern.findall(str(loc))
|
ip_matches = self.ip_pattern.findall(str(loc))
|
||||||
event_indicators = [ip for ip in ip_matches if self._is_valid_ip(ip)]
|
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"]:
|
elif attr == "ip" and timesketch_attr in ["source_ip", "src_ip", "client_ip"]:
|
||||||
if self._is_valid_ip(str(loc)):
|
if self._is_valid_ip(str(loc)):
|
||||||
event_indicators = [str(loc)]
|
event_indicators = [str(loc)]
|
||||||
elif attr in ["md5", "sha1", "sha256"]:
|
elif attr in ["md5", "sha1", "sha256"]:
|
||||||
@ -237,9 +294,9 @@ class MispAnalyzer(interface.BaseAnalyzer):
|
|||||||
# Create view if we found matches
|
# Create view if we found matches
|
||||||
if self.total_event_counter > 0:
|
if self.total_event_counter > 0:
|
||||||
self.sketch.add_view(
|
self.sketch.add_view(
|
||||||
view_name="MISP Threat Intelligence",
|
view_name="MISP Cross-Org Threat Intel",
|
||||||
analyzer_name=self.NAME,
|
analyzer_name=self.NAME,
|
||||||
query_string='tag:"MISP-*" OR tag:"threat-intel"',
|
query_string='tag:"MISP-*" OR tag:"threat-intel" OR tag:"cross-org-intel"',
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
Reference in New Issue
Block a user