add scripts
This commit is contained in:
		
							parent
							
								
									930614410c
								
							
						
					
					
						commit
						4fd7bc0fbc
					
				
							
								
								
									
										19
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# Copyright 2022 Google Inc. All rights reserved.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					"""Contrib Analyzer module."""
 | 
				
			||||||
 | 
					from timesketch.lib.analyzers.contrib import bigquery_matcher
 | 
				
			||||||
 | 
					from timesketch.lib.analyzers.contrib import misp_analyzer
 | 
				
			||||||
 | 
					from timesketch.lib.analyzers.contrib import misp_ip_analyzer
 | 
				
			||||||
 | 
					from timesketch.lib.analyzers.contrib import shodan_analyzer
 | 
				
			||||||
 | 
					from timesketch.lib.analyzers.contrib import hashlookup_analyzer
 | 
				
			||||||
							
								
								
									
										500
									
								
								misp_ip_analyzer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										500
									
								
								misp_ip_analyzer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,500 @@
 | 
				
			|||||||
 | 
					"""Index analyzer plugin for MISP - Simple and reliable for large-scale processing."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import ntpath
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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):
 | 
				
			||||||
 | 
					    """Simple, reliable MISP Analyzer for large-scale processing."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    NAME = "misp_ip_analyzer"
 | 
				
			||||||
 | 
					    DISPLAY_NAME = "MISP-IP"
 | 
				
			||||||
 | 
					    DESCRIPTION = "Mark events using MISP - Simple and Reliable"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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.include_community = kwargs.get("include_community", False)
 | 
				
			||||||
 | 
					        self.chunk_size = kwargs.get("chunk_size", 1000)
 | 
				
			||||||
 | 
					        self.max_retries = kwargs.get("max_retries", 2)
 | 
				
			||||||
 | 
					        self.request_delay = kwargs.get("request_delay", 0.5)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.ip_pattern = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.processed_indicators = set()
 | 
				
			||||||
 | 
					        self.failed_indicators = set()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.stats = {
 | 
				
			||||||
 | 
					            'events_processed': 0,
 | 
				
			||||||
 | 
					            'indicators_found': 0,
 | 
				
			||||||
 | 
					            'api_calls': 0,
 | 
				
			||||||
 | 
					            'api_timeouts': 0,
 | 
				
			||||||
 | 
					            'events_marked': 0,
 | 
				
			||||||
 | 
					            'community_hits': 0,
 | 
				
			||||||
 | 
					            'own_org_hits': 0,
 | 
				
			||||||
 | 
					            'total_correlations': 0,
 | 
				
			||||||
 | 
					            'multi_event_correlations': 0
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get_kwargs():
 | 
				
			||||||
 | 
					        """Get kwargs for the analyzer."""
 | 
				
			||||||
 | 
					        to_query = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "query_string": "md5_hash:*",
 | 
				
			||||||
 | 
					                "attr": "md5",
 | 
				
			||||||
 | 
					                "timesketch_attr": "md5_hash",
 | 
				
			||||||
 | 
					                "include_community": True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "query_string": "sha1_hash:*",
 | 
				
			||||||
 | 
					                "attr": "sha1",
 | 
				
			||||||
 | 
					                "timesketch_attr": "sha1_hash",
 | 
				
			||||||
 | 
					                "include_community": True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "query_string": "sha256_hash:*",
 | 
				
			||||||
 | 
					                "attr": "sha256",
 | 
				
			||||||
 | 
					                "timesketch_attr": "sha256_hash",
 | 
				
			||||||
 | 
					                "include_community": True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "query_string": "filename:*",
 | 
				
			||||||
 | 
					                "attr": "filename",
 | 
				
			||||||
 | 
					                "timesketch_attr": "filename",
 | 
				
			||||||
 | 
					                "include_community": True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "query_string": "source_ip:*",
 | 
				
			||||||
 | 
					                "attr": "ip-src",
 | 
				
			||||||
 | 
					                "timesketch_attr": "source_ip",
 | 
				
			||||||
 | 
					                "include_community": True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        return to_query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _is_valid_ip(self, ip_str):
 | 
				
			||||||
 | 
					        """Simple IP validation."""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            import ipaddress
 | 
				
			||||||
 | 
					            ip_str = ip_str.strip()
 | 
				
			||||||
 | 
					            ipaddress.ip_address(ip_str)
 | 
				
			||||||
 | 
					            if ip_str.startswith(('0.', '127.', '255.255.255.255', '10.', '192.168.', '172.')):
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        except (ValueError, AttributeError):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _is_valid_hash(self, hash_str, hash_type):
 | 
				
			||||||
 | 
					        """Simple hash validation."""
 | 
				
			||||||
 | 
					        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, retry_count=0):
 | 
				
			||||||
 | 
					        """Query MISP for a single value."""
 | 
				
			||||||
 | 
					        if value in self.failed_indicators:
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            # For IP searches, query both ip-src and ip-dst
 | 
				
			||||||
 | 
					            search_types = []
 | 
				
			||||||
 | 
					            if attr.startswith("ip-"):
 | 
				
			||||||
 | 
					                search_types = ["ip-src", "ip-dst"]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                search_types = [attr]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            all_results = []
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for search_type in search_types:
 | 
				
			||||||
 | 
					                payload = {
 | 
				
			||||||
 | 
					                    "returnFormat": "json", 
 | 
				
			||||||
 | 
					                    "value": value, 
 | 
				
			||||||
 | 
					                    "type": search_type,
 | 
				
			||||||
 | 
					                    "enforceWarninglist": False,
 | 
				
			||||||
 | 
					                    "includeEventTags": True,
 | 
				
			||||||
 | 
					                    "includeContext": True,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if self.include_community:
 | 
				
			||||||
 | 
					                    payload.update({
 | 
				
			||||||
 | 
					                        "distribution": [0, 1, 2, 3, 5],
 | 
				
			||||||
 | 
					                        "includeEventUuid": True,
 | 
				
			||||||
 | 
					                        "includeCorrelations": True,
 | 
				
			||||||
 | 
					                        "includeDecayScore": False,
 | 
				
			||||||
 | 
					                        "includeFullModel": False,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    payload["distribution"] = [0]
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                self.stats['api_calls'] += 1
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                response = requests.post(
 | 
				
			||||||
 | 
					                    f"{self.misp_url}/attributes/restSearch/",
 | 
				
			||||||
 | 
					                    json=payload,
 | 
				
			||||||
 | 
					                    headers={"Authorization": self.misp_api_key},
 | 
				
			||||||
 | 
					                    verify=False,
 | 
				
			||||||
 | 
					                    timeout=45,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if response.status_code == 200:
 | 
				
			||||||
 | 
					                    data = response.json()
 | 
				
			||||||
 | 
					                    attributes = data.get("response", {}).get("Attribute", [])
 | 
				
			||||||
 | 
					                    all_results.extend(attributes)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                time.sleep(0.1)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return all_results
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
 | 
				
			||||||
 | 
					            self.stats['api_timeouts'] += 1
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if retry_count < self.max_retries:
 | 
				
			||||||
 | 
					                wait_time = (retry_count + 1) * 2
 | 
				
			||||||
 | 
					                logger.warning(f"Timeout for {value}, retrying in {wait_time}s (attempt {retry_count + 1})")
 | 
				
			||||||
 | 
					                time.sleep(wait_time)
 | 
				
			||||||
 | 
					                return self.query_misp_single(value, attr, retry_count + 1)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                logger.error(f"Max retries exceeded for {value}: {e}")
 | 
				
			||||||
 | 
					                self.failed_indicators.add(value)
 | 
				
			||||||
 | 
					                return []
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            logger.debug(f"Error querying MISP for {value}: {e}")
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def mark_event(self, event, result, attr):
 | 
				
			||||||
 | 
					        """Mark event with MISP intelligence including event links."""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            if attr.startswith("ip-"):
 | 
				
			||||||
 | 
					                msg = "MISP: Malicious IP"
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                msg = "MISP: Known indicator"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            unique_events = {}
 | 
				
			||||||
 | 
					            orgs = set()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for res in result:
 | 
				
			||||||
 | 
					                event_info = res.get("Event", {})
 | 
				
			||||||
 | 
					                event_id = event_info.get("id")
 | 
				
			||||||
 | 
					                event_desc = event_info.get("info", "Unknown")
 | 
				
			||||||
 | 
					                org_name = event_info.get("Orgc", {}).get("name", "Unknown")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if event_id and event_id not in unique_events:
 | 
				
			||||||
 | 
					                    unique_events[event_id] = {
 | 
				
			||||||
 | 
					                        'description': event_desc,
 | 
				
			||||||
 | 
					                        'url': f"{self.misp_url}/events/view/{event_id}"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if org_name != "Unknown":
 | 
				
			||||||
 | 
					                    orgs.add(org_name)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            unique_event_list = list(unique_events.values())
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if len(unique_event_list) == 1:
 | 
				
			||||||
 | 
					                event_data = unique_event_list[0]
 | 
				
			||||||
 | 
					                short_desc = event_data['description'][:50] + "..." if len(event_data['description']) > 50 else event_data['description']
 | 
				
			||||||
 | 
					                msg += f" | Event: {short_desc} | Link: {event_data['url']}"
 | 
				
			||||||
 | 
					            elif len(unique_event_list) > 1:
 | 
				
			||||||
 | 
					                msg += f" | {len(unique_event_list)} Events:"
 | 
				
			||||||
 | 
					                for i, event_data in enumerate(unique_event_list[:2]):
 | 
				
			||||||
 | 
					                    short_desc = event_data['description'][:40] + "..." if len(event_data['description']) > 40 else event_data['description']
 | 
				
			||||||
 | 
					                    msg += f" [{i+1}] {short_desc} ({event_data['url']})"
 | 
				
			||||||
 | 
					                    if i < len(unique_event_list) - 1 and i < 1:
 | 
				
			||||||
 | 
					                        msg += " |"
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if len(unique_event_list) > 2:
 | 
				
			||||||
 | 
					                    msg += f" | +{len(unique_event_list)-2} more events"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if self.include_community and orgs:
 | 
				
			||||||
 | 
					                if len(orgs) > 1:
 | 
				
			||||||
 | 
					                    msg += f" | Sources: {', '.join(list(orgs)[:2])}"
 | 
				
			||||||
 | 
					                    if len(orgs) > 2:
 | 
				
			||||||
 | 
					                        msg += f" +{len(orgs)-2} more"
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    msg += f" | Source: {list(orgs)[0]}"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if len(result) > 1:
 | 
				
			||||||
 | 
					                msg += f" | {len(result)} total correlations"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            tags = [f"MISP-{attr}", "threat-intel"]
 | 
				
			||||||
 | 
					            if self.include_community:
 | 
				
			||||||
 | 
					                tags.append("community-intel")
 | 
				
			||||||
 | 
					            if len(unique_event_list) > 1:
 | 
				
			||||||
 | 
					                tags.append("multi-event-correlation")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            event.add_comment(msg)
 | 
				
			||||||
 | 
					            event.add_tags(tags)
 | 
				
			||||||
 | 
					            event.commit()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            self.stats['events_marked'] += 1
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.info(f"Marked event with {len(unique_event_list)} unique MISP events")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            logger.error(f"Error marking event: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def extract_indicators_from_chunk(self, events_chunk, attr, timesketch_attr):
 | 
				
			||||||
 | 
					        """Extract indicators from a chunk of events."""
 | 
				
			||||||
 | 
					        chunk_indicators = []
 | 
				
			||||||
 | 
					        events_with_indicators = []
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for event in events_chunk:
 | 
				
			||||||
 | 
					            self.stats['events_processed'] += 1
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            loc = event.source.get(timesketch_attr)
 | 
				
			||||||
 | 
					            if not loc:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            indicators = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if attr.startswith("ip-") and timesketch_attr == "message":
 | 
				
			||||||
 | 
					                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"]:
 | 
				
			||||||
 | 
					                if self._is_valid_ip(str(loc)):
 | 
				
			||||||
 | 
					                    indicators = [str(loc)]
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					            elif attr in ["md5", "sha1", "sha256"]:
 | 
				
			||||||
 | 
					                if self._is_valid_hash(str(loc), attr):
 | 
				
			||||||
 | 
					                    indicators = [str(loc)]
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					            elif attr == "filename":
 | 
				
			||||||
 | 
					                filename = ntpath.basename(str(loc))
 | 
				
			||||||
 | 
					                if filename and len(filename) > 1:
 | 
				
			||||||
 | 
					                    indicators = [filename]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if indicators:
 | 
				
			||||||
 | 
					                events_with_indicators.append((event, indicators))
 | 
				
			||||||
 | 
					                chunk_indicators.extend(indicators)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return events_with_indicators, chunk_indicators
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def process_chunk(self, events_chunk, attr, timesketch_attr):
 | 
				
			||||||
 | 
					        """Process a chunk of events."""
 | 
				
			||||||
 | 
					        events_with_indicators, chunk_indicators = self.extract_indicators_from_chunk(
 | 
				
			||||||
 | 
					            events_chunk, attr, timesketch_attr
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if not chunk_indicators:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        unique_indicators = list(dict.fromkeys(chunk_indicators))
 | 
				
			||||||
 | 
					        new_indicators = [ind for ind in unique_indicators if ind not in self.processed_indicators]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if not new_indicators:
 | 
				
			||||||
 | 
					            self.check_existing_matches(events_with_indicators, attr)
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info(f"Processing {len(new_indicators)} new {attr} indicators from {len(events_chunk)} events")
 | 
				
			||||||
 | 
					        self.stats['indicators_found'] += len(new_indicators)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for indicator in new_indicators:
 | 
				
			||||||
 | 
					            if indicator in self.failed_indicators:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            result = self.query_misp_single(indicator, attr)
 | 
				
			||||||
 | 
					            if result:
 | 
				
			||||||
 | 
					                self.result_dict[f"{attr}:{indicator}"] = result
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                self.stats['total_correlations'] += len(result)
 | 
				
			||||||
 | 
					                unique_events = set()
 | 
				
			||||||
 | 
					                for res in result:
 | 
				
			||||||
 | 
					                    event_id = res.get("Event", {}).get("id")
 | 
				
			||||||
 | 
					                    if event_id:
 | 
				
			||||||
 | 
					                        unique_events.add(event_id)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if len(unique_events) > 1:
 | 
				
			||||||
 | 
					                    self.stats['multi_event_correlations'] += 1
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                orgs = set()
 | 
				
			||||||
 | 
					                events = set()
 | 
				
			||||||
 | 
					                for res in result:
 | 
				
			||||||
 | 
					                    org = res.get("Event", {}).get("Orgc", {}).get("name", "Unknown")
 | 
				
			||||||
 | 
					                    event_info = res.get("Event", {}).get("info", "Unknown")[:50]
 | 
				
			||||||
 | 
					                    orgs.add(org)
 | 
				
			||||||
 | 
					                    events.add(event_info)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if len(orgs) > 1 or any(org not in ["Unknown", "Your Organization"] for org in orgs):
 | 
				
			||||||
 | 
					                    self.stats['community_hits'] += 1
 | 
				
			||||||
 | 
					                    logger.info(f"Community MISP hit: {indicator} | {len(result)} correlations | "
 | 
				
			||||||
 | 
					                              f"Events: {', '.join(list(events)[:2])} | Sources: {', '.join(list(orgs)[:3])}")
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    self.stats['own_org_hits'] += 1
 | 
				
			||||||
 | 
					                    logger.info(f"Own org MISP hit: {indicator} | {len(result)} correlations | "
 | 
				
			||||||
 | 
					                              f"Events: {', '.join(list(events)[:2])}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            self.processed_indicators.add(indicator)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            time.sleep(self.request_delay)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.check_existing_matches(events_with_indicators, attr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_community_connectivity(self):
 | 
				
			||||||
 | 
					        """Test if community feeds are accessible."""
 | 
				
			||||||
 | 
					        if not self.include_community:
 | 
				
			||||||
 | 
					            return "Community search disabled"
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            test_payload = {
 | 
				
			||||||
 | 
					                "returnFormat": "json",
 | 
				
			||||||
 | 
					                "distribution": [1, 2, 3],
 | 
				
			||||||
 | 
					                "limit": 1,
 | 
				
			||||||
 | 
					                "enforceWarninglist": False,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            response = requests.post(
 | 
				
			||||||
 | 
					                f"{self.misp_url}/attributes/restSearch/",
 | 
				
			||||||
 | 
					                json=test_payload,
 | 
				
			||||||
 | 
					                headers={"Authorization": self.misp_api_key},
 | 
				
			||||||
 | 
					                verify=False,
 | 
				
			||||||
 | 
					                timeout=30,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if response.status_code == 200:
 | 
				
			||||||
 | 
					                data = response.json()
 | 
				
			||||||
 | 
					                attributes = data.get("response", {}).get("Attribute", [])
 | 
				
			||||||
 | 
					                if attributes:
 | 
				
			||||||
 | 
					                    orgs = set()
 | 
				
			||||||
 | 
					                    for attr in attributes[:5]:
 | 
				
			||||||
 | 
					                        org = attr.get("Event", {}).get("Orgc", {}).get("name", "Unknown")
 | 
				
			||||||
 | 
					                        orgs.add(org)
 | 
				
			||||||
 | 
					                    return f"Community access OK - {len(attributes)} indicators from {len(orgs)} orgs: {', '.join(list(orgs)[:3])}"
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    return "Community access OK but no community indicators found"
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return f"Community test failed: HTTP {response.status_code}"
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            return f"Community test error: {e}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_existing_matches(self, events_with_indicators, attr):
 | 
				
			||||||
 | 
					        """Check events against existing MISP results."""
 | 
				
			||||||
 | 
					        for event, indicators in events_with_indicators:
 | 
				
			||||||
 | 
					            for indicator in indicators:
 | 
				
			||||||
 | 
					                key = f"{attr}:{indicator}"
 | 
				
			||||||
 | 
					                if key in self.result_dict and self.result_dict[key]:
 | 
				
			||||||
 | 
					                    self.mark_event(event, self.result_dict[key], attr)
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def query_misp(self, query, attr, timesketch_attr):
 | 
				
			||||||
 | 
					        """Process events in chunks."""
 | 
				
			||||||
 | 
					        logger.info(f"Starting MISP analysis for {attr} in {timesketch_attr}")
 | 
				
			||||||
 | 
					        logger.info(f"Community search: {'enabled' if self.include_community else 'disabled'}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        events_stream = self.event_stream(
 | 
				
			||||||
 | 
					            query_string=query, 
 | 
				
			||||||
 | 
					            return_fields=[timesketch_attr, '_id']
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        current_chunk = []
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            for event in events_stream:
 | 
				
			||||||
 | 
					                current_chunk.append(event)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if len(current_chunk) >= self.chunk_size:
 | 
				
			||||||
 | 
					                    self.process_chunk(current_chunk, attr, timesketch_attr)
 | 
				
			||||||
 | 
					                    current_chunk = []
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if self.stats['events_processed'] % 10000 == 0:
 | 
				
			||||||
 | 
					                        logger.info(f"Progress: {self.stats['events_processed']} events processed, "
 | 
				
			||||||
 | 
					                                  f"{self.stats['events_marked']} marked, "
 | 
				
			||||||
 | 
					                                  f"{self.stats['api_calls']} API calls, "
 | 
				
			||||||
 | 
					                                  f"{self.stats['api_timeouts']} timeouts")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if current_chunk:
 | 
				
			||||||
 | 
					                self.process_chunk(current_chunk, attr, timesketch_attr)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            logger.error(f"Error during chunk processing: {e}")
 | 
				
			||||||
 | 
					            raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.stats['events_marked'] > 0:
 | 
				
			||||||
 | 
					            view_name = "MISP Threat Intelligence"
 | 
				
			||||||
 | 
					            if self.include_community:
 | 
				
			||||||
 | 
					                view_name += " (Community)"
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            self.sketch.add_view(
 | 
				
			||||||
 | 
					                view_name=view_name,
 | 
				
			||||||
 | 
					                analyzer_name=self.NAME,
 | 
				
			||||||
 | 
					                query_string='tag:"MISP-*" OR tag:"threat-intel"',
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            correlation_count = sum(1 for key, results in self.result_dict.items() 
 | 
				
			||||||
 | 
					                                  if results and len(results) > 1)
 | 
				
			||||||
 | 
					            if correlation_count > 0:
 | 
				
			||||||
 | 
					                self.sketch.add_view(
 | 
				
			||||||
 | 
					                    view_name="MISP Multi-Event Correlations",
 | 
				
			||||||
 | 
					                    analyzer_name=self.NAME,
 | 
				
			||||||
 | 
					                    query_string='tag:"multi-event-correlation"',
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self):
 | 
				
			||||||
 | 
					        """Entry point for the analyzer."""
 | 
				
			||||||
 | 
					        if not self.misp_url or not self.misp_api_key:
 | 
				
			||||||
 | 
					            return "No MISP configuration found"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        start_time = time.time()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if self.include_community:
 | 
				
			||||||
 | 
					            community_status = self.test_community_connectivity()
 | 
				
			||||||
 | 
					            logger.info(f"Community connectivity test: {community_status}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.query_misp(self._query_string, self._attr, self._timesketch_attr)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            elapsed = time.time() - start_time
 | 
				
			||||||
 | 
					            success_rate = ((self.stats['api_calls'] - self.stats['api_timeouts']) / 
 | 
				
			||||||
 | 
					                          max(1, self.stats['api_calls']) * 100)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            result = (f"[{self._timesketch_attr}] MISP Analysis Complete: "
 | 
				
			||||||
 | 
					                     f"{self.stats['events_marked']}/{self.stats['events_processed']} events marked | "
 | 
				
			||||||
 | 
					                     f"{self.stats['api_calls']} API calls ({success_rate:.1f}% success) | ")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if self.include_community:
 | 
				
			||||||
 | 
					                result += f"Community hits: {self.stats['community_hits']}, Own org: {self.stats['own_org_hits']} | "
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            result += f"Total correlations: {self.stats['total_correlations']}"
 | 
				
			||||||
 | 
					            if self.stats['multi_event_correlations'] > 0:
 | 
				
			||||||
 | 
					                result += f", Multi-event: {self.stats['multi_event_correlations']}"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            result += f" | {elapsed:.0f}s"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.info(result)
 | 
				
			||||||
 | 
					            return result
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
							
								
								
									
										130
									
								
								shodan_analyzer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								shodan_analyzer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					from timesketch.lib.analyzers import interface
 | 
				
			||||||
 | 
					from timesketch.lib.analyzers import manager
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					import ipaddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ShodanEnrichmentAnalyzer(interface.BaseAnalyzer):
 | 
				
			||||||
 | 
					    """Analyzer to enrich IP addresses with Shodan data."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    NAME = 'shodan_enrichment'
 | 
				
			||||||
 | 
					    DISPLAY_NAME = 'Shodan IP Enrichment'
 | 
				
			||||||
 | 
					    DESCRIPTION = 'Enriches source IP addresses with Shodan historical data'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def __init__(self, index_name, sketch_id, timeline_id=None):
 | 
				
			||||||
 | 
					        super().__init__(index_name, sketch_id, timeline_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get API key from config or environment
 | 
				
			||||||
 | 
					        config = self.get_config()
 | 
				
			||||||
 | 
					        self.shodan_api_key = config.get('api_key', '')
 | 
				
			||||||
 | 
					        self.max_time_diff_hours = config.get('max_time_diff_hours', 24)
 | 
				
			||||||
 | 
					        self.rate_limit_delay = config.get('rate_limit_delay', 1)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if not self.shodan_api_key:
 | 
				
			||||||
 | 
					            self.logger.error("Shodan API key not configured")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def run(self):
 | 
				
			||||||
 | 
					        """Main analyzer logic."""
 | 
				
			||||||
 | 
					        if not self.shodan_api_key:
 | 
				
			||||||
 | 
					            return "Shodan API key not configured"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        query = {
 | 
				
			||||||
 | 
					            'query': {
 | 
				
			||||||
 | 
					                'bool': {
 | 
				
			||||||
 | 
					                    'must': [
 | 
				
			||||||
 | 
					                        {'exists': {'field': 'source_ip'}},
 | 
				
			||||||
 | 
					                        {'bool': {'must_not': [{'term': {'__ts_analyzer_shodan_enrichment': True}}]}}
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        events = self.event_stream(query_dsl=query, return_fields=['source_ip', 'timestamp'])
 | 
				
			||||||
 | 
					        processed_count = 0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for event in events:
 | 
				
			||||||
 | 
					            source_ip = event.source.get('source_ip')
 | 
				
			||||||
 | 
					            timestamp = event.source.get('timestamp')
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if source_ip and self._is_public_ip(source_ip):
 | 
				
			||||||
 | 
					                shodan_data = self._get_shodan_data(source_ip)
 | 
				
			||||||
 | 
					                if shodan_data:
 | 
				
			||||||
 | 
					                    self._enrich_event(event, shodan_data)
 | 
				
			||||||
 | 
					                    processed_count += 1
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                # Rate limiting
 | 
				
			||||||
 | 
					                import time
 | 
				
			||||||
 | 
					                time.sleep(self.rate_limit_delay)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return f"Processed {processed_count} events with Shodan data"
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _get_shodan_data(self, ip):
 | 
				
			||||||
 | 
					        """Fetch Shodan data for IP."""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            url = f"https://api.shodan.io/shodan/host/{ip}"
 | 
				
			||||||
 | 
					            params = {
 | 
				
			||||||
 | 
					                'key': self.shodan_api_key,
 | 
				
			||||||
 | 
					                'history': 'true'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            response = requests.get(url, params=params, timeout=10)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if response.status_code == 200:
 | 
				
			||||||
 | 
					                return response.json()
 | 
				
			||||||
 | 
					            elif response.status_code == 404:
 | 
				
			||||||
 | 
					                self.logger.debug(f'No Shodan data found for {ip}')
 | 
				
			||||||
 | 
					                return None
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                self.logger.warning(f'Shodan API error for {ip}: {response.status_code}')
 | 
				
			||||||
 | 
					                return None
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					        except requests.exceptions.RequestException as e:
 | 
				
			||||||
 | 
					            self.logger.warning(f'Request error for {ip}: {e}')
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        except json.JSONDecodeError as e:
 | 
				
			||||||
 | 
					            self.logger.warning(f'JSON decode error for {ip}: {e}')
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _enrich_event(self, event, shodan_data):
 | 
				
			||||||
 | 
					        """Add Shodan data to the event."""
 | 
				
			||||||
 | 
					        enrichment = {
 | 
				
			||||||
 | 
					            'shodan_ports': shodan_data.get('ports', []),
 | 
				
			||||||
 | 
					            'shodan_org': shodan_data.get('org', ''),
 | 
				
			||||||
 | 
					            'shodan_isp': shodan_data.get('isp', ''),
 | 
				
			||||||
 | 
					            'shodan_country': shodan_data.get('location', {}).get('country_name', ''),
 | 
				
			||||||
 | 
					            'shodan_city': shodan_data.get('location', {}).get('city', ''),
 | 
				
			||||||
 | 
					            'shodan_hostnames': shodan_data.get('hostnames', []),
 | 
				
			||||||
 | 
					            'shodan_last_update': shodan_data.get('last_update', ''),
 | 
				
			||||||
 | 
					            '__ts_analyzer_shodan_enrichment': True
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Add service banners from latest scan
 | 
				
			||||||
 | 
					        if shodan_data.get('data'):
 | 
				
			||||||
 | 
					            latest_scan = shodan_data['data'][0]  # Most recent scan
 | 
				
			||||||
 | 
					            enrichment['shodan_services'] = []
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for service in shodan_data.get('data', []):
 | 
				
			||||||
 | 
					                service_info = {
 | 
				
			||||||
 | 
					                    'port': service.get('port'),
 | 
				
			||||||
 | 
					                    'protocol': service.get('transport', 'tcp'),
 | 
				
			||||||
 | 
					                    'service': service.get('product', ''),
 | 
				
			||||||
 | 
					                    'version': service.get('version', ''),
 | 
				
			||||||
 | 
					                    'timestamp': service.get('timestamp', '')
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                enrichment['shodan_services'].append(service_info)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        event.add_attributes(enrichment)
 | 
				
			||||||
 | 
					        event.add_tags(['shodan-enriched'])
 | 
				
			||||||
 | 
					        event.commit()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _is_public_ip(self, ip):
 | 
				
			||||||
 | 
					        """Check if IP is public (not RFC1918 private ranges)."""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            ip_obj = ipaddress.ip_address(ip)
 | 
				
			||||||
 | 
					            return ip_obj.is_global
 | 
				
			||||||
 | 
					        except (ValueError, ipaddress.AddressValueError):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Register the analyzer
 | 
				
			||||||
 | 
					manager.AnalysisManager.register_analyzer(ShodanEnrichmentAnalyzer)
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user