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