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