├── requirements.txt ├── satintel.py ├── config └── settings.py ├── LICENSE ├── tests ├── test_orbital.py ├── test_sat_data.py └── test_security.py ├── modules ├── utils.py ├── orbital.py ├── cli.py ├── sat_data.py ├── security.py └── data_sources.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | sgp4>=2.21 3 | beautifulsoup4>=4.12.0 4 | python-dotenv>=1.0.0 5 | tabulate>=0.9.0 6 | pytest>=7.4.0 7 | colorama>=0.4.6 8 | lxml>=4.9.0 9 | numpy 10 | -------------------------------------------------------------------------------- /satintel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Sat hacker Tool - Satellite Intelligence Tool 4 | Herramienta de inteligencia satelital 5 | 6 | Versión: 2.0 - Cinn4mor0ll 7 | """ 8 | 9 | import sys 10 | from modules.cli import SatIntelCLI 11 | 12 | def main(): 13 | """Función principal.""" 14 | try: 15 | cli = SatIntelCLI() 16 | parser = cli.create_parser() 17 | args = parser.parse_args() 18 | cli.run(args) 19 | except Exception as e: 20 | print(f"Error crítico: {e}") 21 | sys.exit(1) 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | # URLs base de APIs 7 | CELESTRAK_BASE_URL = "https://celestrak.org" 8 | SPACETRACK_BASE_URL = "https://www.space-track.org" 9 | N2YO_BASE_URL = "https://api.n2yo.com/rest/v1/satellite" 10 | SATNOGS_BASE_URL = "https://db.satnogs.org/api" 11 | 12 | # Credenciales 13 | SPACETRACK_USERNAME = os.getenv("SPACETRACK_USERNAME") 14 | SPACETRACK_PASSWORD = os.getenv("SPACETRACK_PASSWORD") 15 | N2YO_API_KEY = os.getenv("N2YO_API_KEY", "25K5EB-9FM8MQ-BLDAGL-5B2J") # Clave demo 16 | 17 | # Configuración general 18 | LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") 19 | DEFAULT_LATITUDE = float(os.getenv("DEFAULT_LATITUDE", "20.67")) 20 | DEFAULT_LONGITUDE = float(os.getenv("DEFAULT_LONGITUDE", "-103.35")) 21 | 22 | # Constantes físicas 23 | EARTH_RADIUS_KM = 6371.0 24 | MIN_ELEVATION_DEGREES = 10.0 25 | 26 | # Timeouts y límites 27 | API_TIMEOUTS = { 28 | 'celestrak': 10, 29 | 'n2yo': 15, 30 | 'satnogs': 10, 31 | 'spacetrack': 15, 32 | 'scraping': 20 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Elisa Elias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_orbital.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | from modules.orbital import OrbitalCalculator 4 | 5 | class TestOrbitalCalculator: 6 | """Tests para OrbitalCalculator.""" 7 | 8 | def setup_method(self): 9 | """Configuración para cada test.""" 10 | # TLE de ejemplo para ISS 11 | self.tle_line1 = "1 25544U 98067A 24001.50000000 .00002182 00000-0 40864-4 0 9990" 12 | self.tle_line2 = "2 25544 51.6400 339.7900 0003835 356.8500 120.6800 15.48919103123456" 13 | self.calculator = OrbitalCalculator(self.tle_line1, self.tle_line2) 14 | 15 | def test_initialization_success(self): 16 | """Test inicialización exitosa.""" 17 | assert self.calculator.satellite is not None 18 | assert self.calculator.satellite.error == 0 19 | 20 | def test_initialization_invalid_tle(self): 21 | """Test inicialización con TLE inválido.""" 22 | with pytest.raises(ValueError): 23 | OrbitalCalculator("invalid", "tle") 24 | 25 | def test_get_position_at_time(self): 26 | """Test cálculo de posición.""" 27 | test_time = datetime(2024, 1, 1, 12, 0, 0) 28 | position = self.calculator.get_position_at_time(test_time) 29 | 30 | assert position is not None 31 | assert 'latitude' in position 32 | assert 'longitude' in position 33 | assert 'altitude_km' in position 34 | assert -90 <= position['latitude'] <= 90 35 | assert -180 <= position['longitude'] <= 180 36 | assert position['altitude_km'] > 0 37 | 38 | def test_get_orbital_elements(self): 39 | """Test extracción de elementos orbitales.""" 40 | elements = self.calculator.get_orbital_elements() 41 | 42 | assert 'inclination_deg' in elements 43 | assert 'eccentricity' in elements 44 | assert 'mean_motion_rev_per_day' in elements 45 | assert elements['satellite_number'] == 25544 46 | -------------------------------------------------------------------------------- /tests/test_sat_data.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import Mock, patch 3 | from modules.sat_data import SatelliteDataRetriever, parse_tle 4 | 5 | class TestSatelliteDataRetriever: 6 | """Tests para SatelliteDataRetriever.""" 7 | 8 | def setup_method(self): 9 | """Configuración para cada test.""" 10 | self.retriever = SatelliteDataRetriever() 11 | 12 | def test_parse_tle_valid(self): 13 | """Test parsing de TLE válido.""" 14 | tle_string = """ISS (ZARYA) 15 | 1 25544U 98067A 24001.50000000 .00002182 00000-0 40864-4 0 9990 16 | 2 25544 51.6400 339.7900 0003835 356.8500 120.6800 15.48919103123456""" 17 | 18 | name, line1, line2 = parse_tle(tle_string) 19 | 20 | assert name == "ISS (ZARYA)" 21 | assert line1.startswith("1 25544U") 22 | assert line2.startswith("2 25544") 23 | 24 | def test_parse_tle_invalid(self): 25 | """Test parsing de TLE inválido.""" 26 | with pytest.raises(ValueError): 27 | parse_tle("Invalid TLE") 28 | 29 | @patch('requests.Session.get') 30 | def test_get_tle_from_celestrak_success(self, mock_get): 31 | """Test recuperación exitosa de TLE desde Celestrak.""" 32 | mock_response = Mock() 33 | mock_response.text = """ISS (ZARYA) 34 | 1 25544U 98067A 24001.50000000 .00002182 00000-0 40864-4 0 9990 35 | 2 25544 51.6400 339.7900 0003835 356.8500 120.6800 15.48919103123456""" 36 | mock_response.raise_for_status.return_value = None 37 | mock_get.return_value = mock_response 38 | 39 | result = self.retriever.get_tle_from_celestrak(satellite_id=25544) 40 | 41 | assert result is not None 42 | assert "ISS (ZARYA)" in result 43 | assert "1 25544U" in result 44 | 45 | @patch('requests.Session.get') 46 | def test_get_tle_from_celestrak_not_found(self, mock_get): 47 | """Test cuando no se encuentra TLE en Celestrak.""" 48 | mock_response = Mock() 49 | mock_response.text = "" 50 | mock_response.raise_for_status.return_value = None 51 | mock_get.return_value = mock_response 52 | 53 | result = self.retriever.get_tle_from_celestrak(satellite_id=99999) 54 | 55 | assert result is None 56 | -------------------------------------------------------------------------------- /modules/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Tuple, Optional 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | def parse_tle(tle_string: str) -> Tuple[str, str, str]: 7 | """ 8 | Parse un string TLE en sus tres líneas componentes. 9 | 10 | Args: 11 | tle_string: String con el TLE completo 12 | 13 | Returns: 14 | Tupla con (nombre, línea1, línea2) 15 | """ 16 | lines = tle_string.strip().split('\n') 17 | if len(lines) >= 3: 18 | return lines[0].strip(), lines[1].strip(), lines[2].strip() 19 | else: 20 | raise ValueError("TLE debe contener al menos 3 líneas") 21 | 22 | def extract_norad_from_tle(tle_line1: str) -> int: 23 | """Extrae el NORAD ID de la primera línea del TLE.""" 24 | try: 25 | return int(tle_line1[2:7]) 26 | except: 27 | return 0 28 | 29 | def infer_satellite_info(satellite_name: str) -> dict: 30 | """Infiere información del satélite basado en su nombre.""" 31 | name_upper = satellite_name.upper() 32 | info = { 33 | 'inferred_country': 'UNKNOWN', 34 | 'inferred_type': 'UNKNOWN', 35 | 'inferred_purpose': 'UNKNOWN' 36 | } 37 | 38 | # Patrones de países 39 | country_patterns = { 40 | 'INTERNATIONAL': ['ISS', 'DRAGON', 'CYGNUS'], 41 | 'US': ['STARLINK', 'GPS', 'NOAA', 'GOES', 'LANDSAT'], 42 | 'RU': ['COSMOS', 'GLONASS'], 43 | 'CN': ['BEIDOU'], 44 | 'EU': ['GALILEO', 'SENTINEL'] 45 | } 46 | 47 | for country, patterns in country_patterns.items(): 48 | if any(pattern in name_upper for pattern in patterns): 49 | info['inferred_country'] = country 50 | break 51 | 52 | # Patrones de tipo/propósito 53 | type_patterns = { 54 | ('COMMUNICATION', 'Internet/Communications constellation'): ['STARLINK', 'ONEWEB', 'IRIDIUM'], 55 | ('NAVIGATION', 'Global Navigation Satellite System'): ['GPS', 'GLONASS', 'GALILEO', 'BEIDOU'], 56 | ('WEATHER', 'Weather monitoring and forecasting'): ['NOAA', 'GOES', 'METOP'], 57 | ('EARTH_OBSERVATION', 'Earth observation and remote sensing'): ['LANDSAT', 'SENTINEL', 'WORLDVIEW'], 58 | ('SPACE_STATION', 'International Space Station'): ['ISS'], 59 | ('MILITARY', 'Military/Intelligence satellite'): ['COSMOS'] 60 | } 61 | 62 | for (sat_type, purpose), patterns in type_patterns.items(): 63 | if any(pattern in name_upper for pattern in patterns): 64 | info['inferred_type'] = sat_type 65 | info['inferred_purpose'] = purpose 66 | break 67 | 68 | return info 69 | 70 | def safe_float_conversion(value: str, default: float = 0.0) -> float: 71 | """Convierte string a float de forma segura.""" 72 | try: 73 | return float(value) 74 | except (ValueError, TypeError): 75 | return default 76 | 77 | def safe_int_conversion(value: str, default: int = 0) -> int: 78 | """Convierte string a int de forma segura.""" 79 | try: 80 | return int(value) 81 | except (ValueError, TypeError): 82 | return default 83 | -------------------------------------------------------------------------------- /tests/test_security.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from modules.security import SPARTAAnalyzer 3 | 4 | class TestSPARTAAnalyzer: 5 | """Tests para SPARTAAnalyzer.""" 6 | 7 | def setup_method(self): 8 | """Configuración para cada test.""" 9 | self.analyzer = SPARTAAnalyzer() 10 | 11 | def test_analyze_satellite_communication(self): 12 | """Test análisis de satélite de comunicaciones.""" 13 | satellite_data = { 14 | 'spacetrack_info': { 15 | 'NORAD_CAT_ID': '12345', 16 | 'OBJECT_NAME': 'STARLINK-1234', 17 | 'COUNTRY': 'US' 18 | } 19 | } 20 | 21 | analysis = self.analyzer.analyze_satellite(satellite_data) 22 | 23 | assert analysis['satellite_category'] == 'communication' 24 | assert analysis['risk_level'] == 'low' 25 | assert 'command_and_control' in analysis['sparta_tactics'] 26 | 27 | def test_analyze_satellite_high_risk_country(self): 28 | """Test análisis de satélite de país de alto riesgo.""" 29 | satellite_data = { 30 | 'spacetrack_info': { 31 | 'NORAD_CAT_ID': '54321', 32 | 'OBJECT_NAME': 'MILITARY SAT', 33 | 'COUNTRY': 'RU' 34 | } 35 | } 36 | 37 | analysis = self.analyzer.analyze_satellite(satellite_data) 38 | 39 | assert analysis['risk_level'] == 'high' 40 | assert len(analysis['security_concerns']) > 0 41 | assert len(analysis['recommendations']) > 0 42 | 43 | def test_determine_satellite_category(self): 44 | """Test determinación de categoría de satélite.""" 45 | # Test satélite de comunicaciones 46 | spacetrack_info = {'OBJECT_NAME': 'STARLINK-1234', 'OBJECT_TYPE': 'PAYLOAD'} 47 | category = self.analyzer._determine_satellite_category(spacetrack_info) 48 | assert category == 'communication' 49 | 50 | # Test satélite de observación terrestre 51 | spacetrack_info = {'OBJECT_NAME': 'LANDSAT-8', 'OBJECT_TYPE': 'PAYLOAD'} 52 | category = self.analyzer._determine_satellite_category(spacetrack_info) 53 | assert category == 'earth_observation' 54 | 55 | def test_assess_country_risk(self): 56 | """Test evaluación de riesgo por país.""" 57 | assert self.analyzer._assess_country_risk('RU') == 'high' 58 | assert self.analyzer._assess_country_risk('US') == 'low' 59 | assert self.analyzer._assess_country_risk('XX') == 'medium' 60 | 61 | def test_generate_sparta_report(self): 62 | """Test generación de reporte SPARTA.""" 63 | analysis = { 64 | 'satellite_id': '25544', 65 | 'satellite_name': 'ISS', 66 | 'country_of_origin': 'US', 67 | 'satellite_category': 'space_station', 68 | 'risk_level': 'low', 69 | 'sparta_tactics': ['persistence', 'collection'], 70 | 'security_concerns': ['Test concern'], 71 | 'recommendations': ['Test recommendation'], 72 | 'confidence_score': 0.9 73 | } 74 | 75 | report = self.analyzer.generate_sparta_report(analysis) 76 | 77 | assert 'ANÁLISIS DE SEGURIDAD SPARTA' in report 78 | assert 'ISS' in report 79 | assert 'Test concern' in report 80 | assert 'Test recommendation' in report 81 | -------------------------------------------------------------------------------- /modules/orbital.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | from typing import List, Tuple, Dict, Optional 4 | from datetime import datetime, timedelta 5 | from sgp4.api import Satrec, jday 6 | from config.settings import EARTH_RADIUS_KM, MIN_ELEVATION_DEGREES 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class OrbitalCalculator: 11 | """Calculador de mecánica orbital""" 12 | 13 | def __init__(self, tle_line1: str, tle_line2: str): 14 | """Inicializa con datos TLE.""" 15 | try: 16 | self.satellite = Satrec.twoline2rv(tle_line1, tle_line2) 17 | if self.satellite.error != 0: 18 | raise ValueError(f"Error SGP4: {self.satellite.error}") 19 | logger.info("Calculador orbital inicializado") 20 | except Exception as e: 21 | logger.error(f"Error inicializando orbital: {e}") 22 | raise 23 | 24 | def get_current_position(self) -> Optional[Dict]: 25 | """Obtiene la posición actual del satélite.""" 26 | return self.get_position_at_time(datetime.utcnow()) 27 | 28 | def get_position_at_time(self, dt: datetime) -> Optional[Dict]: 29 | """Calcula posición en momento específico.""" 30 | try: 31 | jd, fr = jday(dt.year, dt.month, dt.day, 32 | dt.hour, dt.minute, dt.second) 33 | 34 | error, position, velocity = self.satellite.sgp4(jd, fr) 35 | if error != 0: 36 | return None 37 | 38 | lat, lon, alt = self._eci_to_geodetic(position, dt) 39 | speed = math.sqrt(sum(v**2 for v in velocity)) 40 | 41 | return { 42 | 'timestamp': dt.isoformat(), 43 | 'latitude': lat, 44 | 'longitude': lon, 45 | 'altitude_km': alt, 46 | 'velocity_km_s': speed, 47 | 'position_eci': position, 48 | 'velocity_eci': velocity 49 | } 50 | except Exception as e: 51 | logger.error(f"Error calculando posición: {e}") 52 | return None 53 | 54 | def calculate_passes(self, observer_lat: float, observer_lon: float, 55 | hours_ahead: int = 24) -> List[Dict]: 56 | """Calcula pases futuros sobre una ubicación.""" 57 | passes = [] 58 | start_time = datetime.utcnow() 59 | end_time = start_time + timedelta(hours=hours_ahead) 60 | 61 | current_time = start_time 62 | time_step = timedelta(minutes=5) 63 | current_pass = None 64 | 65 | while current_time <= end_time: 66 | position = self.get_position_at_time(current_time) 67 | if not position: 68 | current_time += time_step 69 | continue 70 | 71 | elevation, azimuth, distance = self._calculate_look_angles( 72 | observer_lat, observer_lon, 0.0, 73 | position['latitude'], position['longitude'], 74 | position['altitude_km'] 75 | ) 76 | 77 | if elevation >= MIN_ELEVATION_DEGREES: 78 | if not current_pass: 79 | current_pass = { 80 | 'start_time': current_time, 81 | 'max_elevation': elevation, 82 | 'max_elevation_time': current_time, 83 | 'points': [] 84 | } 85 | 86 | if elevation > current_pass['max_elevation']: 87 | current_pass['max_elevation'] = elevation 88 | current_pass['max_elevation_time'] = current_time 89 | 90 | current_pass['points'].append({ 91 | 'time': current_time, 92 | 'elevation': elevation, 93 | 'azimuth': azimuth, 94 | 'distance_km': distance 95 | }) 96 | else: 97 | if current_pass: 98 | current_pass['end_time'] = current_pass['points'][-1]['time'] 99 | current_pass['duration_minutes'] = ( 100 | current_pass['end_time'] - current_pass['start_time'] 101 | ).total_seconds() / 60.0 102 | passes.append(current_pass) 103 | current_pass = None 104 | 105 | current_time += time_step 106 | 107 | return passes 108 | 109 | def _eci_to_geodetic(self, position: Tuple[float, float, float], 110 | dt: datetime) -> Tuple[float, float, float]: 111 | """Convierte ECI a coordenadas geodésicas.""" 112 | x, y, z = position 113 | r = math.sqrt(x**2 + y**2 + z**2) 114 | 115 | lat = math.degrees(math.asin(z / r)) 116 | lon = math.degrees(math.atan2(y, x)) 117 | 118 | # Ajuste por rotación terrestre 119 | hours_since_j2000 = (dt - datetime(2000, 1, 1, 12, 0, 0)).total_seconds() / 3600.0 120 | lon_adjusted = lon - (15.04107 * hours_since_j2000) % 360 121 | 122 | if lon_adjusted > 180: 123 | lon_adjusted -= 360 124 | elif lon_adjusted < -180: 125 | lon_adjusted += 360 126 | 127 | alt = r - EARTH_RADIUS_KM 128 | return lat, lon_adjusted, alt 129 | 130 | def _calculate_look_angles(self, obs_lat: float, obs_lon: float, obs_alt: float, 131 | sat_lat: float, sat_lon: float, sat_alt: float) -> Tuple[float, float, float]: 132 | """Calcula ángulos de observación.""" 133 | obs_lat_rad = math.radians(obs_lat) 134 | obs_lon_rad = math.radians(obs_lon) 135 | sat_lat_rad = math.radians(sat_lat) 136 | sat_lon_rad = math.radians(sat_lon) 137 | 138 | # Coordenadas cartesianas 139 | obs_x = (EARTH_RADIUS_KM + obs_alt) * math.cos(obs_lat_rad) * math.cos(obs_lon_rad) 140 | obs_y = (EARTH_RADIUS_KM + obs_alt) * math.cos(obs_lat_rad) * math.sin(obs_lon_rad) 141 | obs_z = (EARTH_RADIUS_KM + obs_alt) * math.sin(obs_lat_rad) 142 | 143 | sat_x = (EARTH_RADIUS_KM + sat_alt) * math.cos(sat_lat_rad) * math.cos(sat_lon_rad) 144 | sat_y = (EARTH_RADIUS_KM + sat_alt) * math.cos(sat_lat_rad) * math.sin(sat_lon_rad) 145 | sat_z = (EARTH_RADIUS_KM + sat_alt) * math.sin(sat_lat_rad) 146 | 147 | dx, dy, dz = sat_x - obs_x, sat_y - obs_y, sat_z - obs_z 148 | distance = math.sqrt(dx**2 + dy**2 + dz**2) 149 | 150 | # Transformación a ENU 151 | sin_lat, cos_lat = math.sin(obs_lat_rad), math.cos(obs_lat_rad) 152 | sin_lon, cos_lon = math.sin(obs_lon_rad), math.cos(obs_lon_rad) 153 | 154 | east = -sin_lon * dx + cos_lon * dy 155 | north = -sin_lat * cos_lon * dx - sin_lat * sin_lon * dy + cos_lat * dz 156 | up = cos_lat * cos_lon * dx + cos_lat * sin_lon * dy + sin_lat * dz 157 | 158 | elevation = math.degrees(math.atan2(up, math.sqrt(east**2 + north**2))) 159 | azimuth = math.degrees(math.atan2(east, north)) 160 | if azimuth < 0: 161 | azimuth += 360 162 | 163 | return elevation, azimuth, distance 164 | -------------------------------------------------------------------------------- /modules/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | from typing import Dict, List, Tuple 5 | from datetime import datetime 6 | from tabulate import tabulate 7 | from colorama import init, Fore, Style 8 | 9 | from modules.data_sources import SatelliteDataManager 10 | from modules.orbital import OrbitalCalculator 11 | from modules.security import SatelliteRiskAnalyzer 12 | from modules.utils import parse_tle 13 | from config.settings import DEFAULT_LATITUDE, DEFAULT_LONGITUDE 14 | 15 | init(autoreset=True) 16 | logging.basicConfig(level=logging.INFO) 17 | logger = logging.getLogger(__name__) 18 | 19 | class SatIntelCLI: 20 | """Main CLI Interface""" 21 | 22 | def __init__(self): 23 | self.data_manager = SatelliteDataManager() 24 | self.risk_analyzer = SatelliteRiskAnalyzer() 25 | 26 | def create_parser(self) -> argparse.ArgumentParser: 27 | """Creates the argument parser.""" 28 | parser = argparse.ArgumentParser( 29 | description="SatIntel - Satellite Intelligence Tool", 30 | epilog=""" 31 | Examples: 32 | python satintel.py --id 25544 33 | python satintel.py --name "ISS" 34 | python satintel.py --id 25544 --passes --location "20.67,-103.35" 35 | python satintel.py --search "starlink" 36 | """ 37 | ) 38 | 39 | # Satellite identification 40 | id_group = parser.add_mutually_exclusive_group(required=True) 41 | id_group.add_argument('--id', type=int, help='NORAD ID of the satellite') 42 | id_group.add_argument('--name', type=str, help='Name of the satellite') 43 | id_group.add_argument('--search', type=str, help='Search satellites by name') 44 | 45 | # Analysis options 46 | parser.add_argument('--location', type=str, 47 | default=f"{DEFAULT_LATITUDE},{DEFAULT_LONGITUDE}", 48 | help='Observer location "lat,lon"') 49 | parser.add_argument('--passes', action='store_true', 50 | help='Calculate future passes') 51 | parser.add_argument('--hours', type=int, default=24, 52 | help='Hours to calculate passes for') 53 | 54 | # Output options 55 | parser.add_argument('--output', choices=['table', 'json', 'detailed'], 56 | default='detailed', help='Output format') 57 | parser.add_argument('--no-security', action='store_true', 58 | help='Omit SPARTA analysis') 59 | parser.add_argument('--verbose', '-v', action='store_true', 60 | help='Detailed output') 61 | 62 | return parser 63 | 64 | def run(self, args): 65 | """Runs the main application.""" 66 | try: 67 | if args.verbose: 68 | logging.basicConfig(level=logging.INFO) 69 | 70 | # Search case 71 | if args.search: 72 | self._handle_search(args.search) 73 | return 74 | 75 | # Get satellite data 76 | print(f"{Fore.CYAN} Retrieving satellite data...{Style.RESET_ALL}") 77 | 78 | satellite_data = self.data_manager.get_satellite_data( 79 | satellite_id=args.id, 80 | satellite_name=args.name 81 | ) 82 | 83 | if not satellite_data.get('sources_used'): 84 | print(f"{Fore.RED} No data found for the satellite{Style.RESET_ALL}") 85 | return 86 | 87 | # Display information 88 | self._display_satellite_info(satellite_data, args.output) 89 | 90 | # Display current position 91 | if satellite_data.get('tle'): 92 | self._display_current_position(satellite_data) 93 | 94 | # Calculate passes if requested 95 | if args.passes and satellite_data.get('tle'): 96 | observer_lat, observer_lon = self._parse_location(args.location) 97 | self._display_passes(satellite_data, observer_lat, observer_lon, args.hours) 98 | 99 | # Security analysis 100 | if not args.no_security: 101 | self._display_security_analysis(satellite_data) 102 | 103 | # Final summary 104 | sources_count = len(satellite_data.get('sources_used', [])) 105 | print(f"\n{Fore.GREEN} Query completed using {sources_count} sources{Style.RESET_ALL}") 106 | 107 | except KeyboardInterrupt: 108 | print(f"\n{Fore.YELLOW}Operation canceled{Style.RESET_ALL}") 109 | except Exception as e: 110 | print(f"{Fore.RED} Error: {e}{Style.RESET_ALL}") 111 | if args.verbose: 112 | import traceback 113 | traceback.print_exc() 114 | 115 | def _handle_search(self, search_term: str): 116 | """Handles satellite search - IMPROVED VERSION.""" 117 | print(f"{Fore.CYAN} Searching for satellites: '{search_term}'...{Style.RESET_ALL}") 118 | 119 | results = self.data_manager.search_satellites(search_term) 120 | 121 | if not results: 122 | print(f"{Fore.YELLOW} No satellites found with the term '{search_term}'{Style.RESET_ALL}") 123 | print(f"{Fore.CYAN} Suggestions:{Style.RESET_ALL}") 124 | print(f" • Try shorter terms: 'star' instead of 'starlink'") 125 | print(f" • Use common names: 'ISS', 'NOAA', 'GPS'") 126 | print(f" • Search by operator: 'SpaceX', 'NASA', 'USAF'") 127 | return 128 | 129 | # Group results by source 130 | results_by_source = {} 131 | for result in results: 132 | source = result.get('source', 'Unknown') 133 | if source not in results_by_source: 134 | results_by_source[source] = [] 135 | results_by_source[source].append(result) 136 | 137 | print(f"\n{Fore.GREEN} Found {len(results)} satellites from {len(results_by_source)} sources:{Style.RESET_ALL}\n") 138 | 139 | # Display results by source 140 | for source, source_results in results_by_source.items(): 141 | print(f"{Fore.CYAN} Results from {source} ({len(source_results)}):{Style.RESET_ALL}") 142 | 143 | table_data = [] 144 | for i, sat in enumerate(source_results[:10], 1): # Max 10 per source 145 | table_data.append([ 146 | i, 147 | sat.get('norad_id', 'N/A'), 148 | sat.get('name', 'N/A')[:45], # Truncate long names 149 | sat.get('operator', 'N/A')[:20], 150 | sat.get('status', 'N/A')[:15] 151 | ]) 152 | 153 | print(tabulate( 154 | table_data, 155 | headers=["#", "NORAD ID", "Name", "Operator", "Status"], 156 | tablefmt="grid" 157 | )) 158 | 159 | if len(source_results) > 10: 160 | print(f"{Fore.YELLOW}... and {len(source_results) - 10} more results from {source}{Style.RESET_ALL}") 161 | 162 | print() # Blank line between sources 163 | 164 | # Display final statistics 165 | print(f"{Fore.GREEN} Search completed. Total unique: {len(results)} satellites{Style.RESET_ALL}") 166 | 167 | # Suggest follow-up commands 168 | if results: 169 | first_result = results[0] 170 | norad_id = first_result.get('norad_id') 171 | if norad_id: 172 | print(f"\n{Fore.CYAN} For more details on the first result:{Style.RESET_ALL}") 173 | print(f" python satintel.py --id {norad_id}") 174 | 175 | def _display_satellite_info(self, satellite_data: Dict, output_format: str): 176 | """Displays satellite information.""" 177 | basic_info = satellite_data.get('basic_info', {}) 178 | mission_info = satellite_data.get('mission_info', {}) 179 | technical_info = satellite_data.get('technical_info', {}) 180 | orbital_info = satellite_data.get('orbital_info', {}) 181 | sources = ', '.join(satellite_data.get('sources_used', [])) 182 | 183 | # Basic information 184 | self._print_header("SATELLITE BASIC INFORMATION") 185 | print(f"{Fore.CYAN}Sources: {sources}{Style.RESET_ALL}\n") 186 | 187 | basic_data = [ 188 | ["NORAD ID", basic_info.get('norad_id', 'N/A')], 189 | ["Name", basic_info.get('name', 'N/A')], 190 | ["Operator", basic_info.get('operator', 'N/A')], 191 | ["Country", basic_info.get('countries', basic_info.get('inferred_country', 'N/A'))], 192 | ["Status", basic_info.get('status', 'N/A')], 193 | ["Launch", basic_info.get('launched', basic_info.get('launch_date', 'N/A'))], 194 | ["Website", basic_info.get('website', 'N/A')] 195 | ] 196 | 197 | self._print_table(basic_data, output_format) 198 | 199 | # Mission information 200 | if mission_info: 201 | self._print_header("MISSION INFORMATION") 202 | 203 | mission_data = [] 204 | if mission_info.get('description'): 205 | mission_data.append(["Description", mission_info['description'][:100] + "..."]) 206 | if mission_info.get('type'): 207 | mission_data.append(["Type", mission_info['type']]) 208 | if mission_info.get('orbit'): 209 | mission_data.append(["Orbit", mission_info['orbit']]) 210 | if mission_info.get('inferred_purpose'): 211 | mission_data.append(["Purpose", mission_info['inferred_purpose']]) 212 | 213 | if mission_data: 214 | self._print_table(mission_data, output_format) 215 | 216 | # Technical information 217 | if technical_info: 218 | self._print_header("TECHNICAL SPECIFICATIONS") 219 | 220 | tech_data = [] 221 | for key, value in technical_info.items(): 222 | if value: 223 | display_key = key.replace('_', ' ').title() 224 | if isinstance(value, list): 225 | value = ', '.join(str(v) for v in value) 226 | tech_data.append([display_key, str(value)]) 227 | 228 | if tech_data: 229 | self._print_table(tech_data, output_format) 230 | 231 | # Orbital elements 232 | if orbital_info: 233 | self._print_header("ORBITAL ELEMENTS") 234 | 235 | orbital_data = [ 236 | ["Inclination", f"{orbital_info.get('inclination', 0):.2f}°"], 237 | ["Eccentricity", f"{orbital_info.get('eccentricity', 0):.6f}"], 238 | ["Mean Motion", f"{orbital_info.get('mean_motion', 0):.6f} rev/day"], 239 | ["RAAN", f"{orbital_info.get('raan', 0):.2f}°"], 240 | ["Arg. Perigee", f"{orbital_info.get('arg_perigee', 0):.2f}°"] 241 | ] 242 | 243 | self._print_table(orbital_data, output_format) 244 | 245 | def _display_current_position(self, satellite_data: Dict): 246 | """Displays the current position of the satellite.""" 247 | try: 248 | tle = satellite_data.get('tle') 249 | if not tle: 250 | return 251 | 252 | name, line1, line2 = parse_tle(tle) 253 | calc = OrbitalCalculator(line1, line2) 254 | position = calc.get_current_position() 255 | 256 | if position: 257 | self._print_header("CURRENT POSITION") 258 | current_time = datetime.utcnow() 259 | 260 | position_data = [ 261 | ["UTC Time", current_time.strftime('%Y-%m-%d %H:%M:%S')], 262 | ["Latitude", f"{position['latitude']:.4f}°"], 263 | ["Longitude", f"{position['longitude']:.4f}°"], 264 | ["Altitude", f"{position['altitude_km']:.2f} km"], 265 | ["Velocity", f"{position['velocity_km_s']:.2f} km/s"] 266 | ] 267 | 268 | self._print_table(position_data, 'detailed') 269 | 270 | except Exception as e: 271 | print(f"{Fore.RED}Error calculating position: {e}{Style.RESET_ALL}") 272 | 273 | def _display_passes(self, satellite_data: Dict, observer_lat: float, 274 | observer_lon: float, hours: int): 275 | """Displays future passes.""" 276 | try: 277 | tle = satellite_data.get('tle') 278 | name, line1, line2 = parse_tle(tle) 279 | calc = OrbitalCalculator(line1, line2) 280 | 281 | self._print_header(f"FUTURE PASSES ({hours}h)") 282 | print(f"Location: {observer_lat:.2f}°, {observer_lon:.2f}°\n") 283 | 284 | passes = calc.calculate_passes(observer_lat, observer_lon, hours) 285 | 286 | if not passes: 287 | print(f"{Fore.YELLOW}No visible passes{Style.RESET_ALL}") 288 | return 289 | 290 | passes_data = [] 291 | for i, pass_info in enumerate(passes, 1): 292 | start_time = pass_info['start_time'].strftime('%m-%d %H:%M') 293 | duration = f"{pass_info['duration_minutes']:.1f} min" 294 | max_elev = f"{pass_info['max_elevation']:.1f}°" 295 | 296 | passes_data.append([i, start_time, duration, max_elev]) 297 | 298 | print(tabulate( 299 | passes_data, 300 | headers=["#", "Start", "Duration", "Max. Elev."], 301 | tablefmt="grid" 302 | )) 303 | 304 | except Exception as e: 305 | print(f"{Fore.RED}Error calculating passes: {e}{Style.RESET_ALL}") 306 | 307 | def _display_security_analysis(self, satellite_data: Dict): 308 | """Displays security analysis.""" 309 | try: 310 | analysis = self.risk_analyzer.analyze_satellite(satellite_data) 311 | report = self.risk_analyzer.generate_assessment_report(analysis) 312 | print(f"\n{Fore.RED}{report}{Style.RESET_ALL}") 313 | except Exception as e: 314 | print(f"{Fore.RED}Error in SPARTA analysis: {e}{Style.RESET_ALL}") 315 | 316 | def _print_header(self, title: str): 317 | """Prints a header.""" 318 | print(f"\n{Fore.CYAN}{Style.BRIGHT}{'=' * 60}") 319 | print(f"{title:^60}") 320 | print(f"{'=' * 60}{Style.RESET_ALL}\n") 321 | 322 | def _print_table(self, data: List, format_type: str): 323 | """Prints a data table.""" 324 | if format_type == 'table': 325 | print(tabulate(data, headers=["Field", "Value"], tablefmt="grid")) 326 | else: 327 | for field, value in data: 328 | if value and str(value) != 'N/A': 329 | print(f"{Fore.YELLOW}{field:.<25}{Style.RESET_ALL} {value}") 330 | print() 331 | 332 | def _parse_location(self, location_str: str) -> Tuple[float, float]: 333 | """Parses location in 'lat,lon' format.""" 334 | try: 335 | lat, lon = map(float, location_str.split(',')) 336 | return lat, lon 337 | except: 338 | raise ValueError(f"Invalid location format: {location_str}") 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SAT HACKER TOOL - Herramienta de Inteligencia Satelital 2 | 3 | Sat Hacker Tool es una herramienta de **línea de comandos** desarrollada en **Python** para la recopilación, análisis y evaluación de datos satelitales con enfoque en inteligencia de seguridad basada en el framework **SPARTA**. 4 | 5 | ----- 6 | 7 | ## 🚀 Características Principales 8 | 9 | * **📡 Recuperación de Datos Multi-Fuente:** 10 | 11 | * **Celestrak:** TLEs oficiales de NORAD/NASA 12 | * **N2YO API:** Seguimiento satelital en tiempo real 13 | * **SatNOGS DB:** Base de datos colaborativa de radioaficionados 14 | * **Web Scraping:** Información detallada de fuentes públicas 15 | 16 | * **🛰️ Análisis Orbital Preciso:** 17 | 18 | * Cálculos de posición en tiempo real usando **SGP4** 19 | * Predicción de pases sobre ubicaciones específicas 20 | * Elementos orbitales completos (inclinación, excentricidad, etc.) 21 | * Conversión automática de coordenadas ECI a geodésicas 22 | 23 | ## 🔒 Sistema de Evaluación de Riesgos 24 | 25 | SatIntel incluye un módulo de evaluación automatizada de riesgos 26 | que usa conceptos del framework SPARTA adaptados al dominio espacial: 27 | 28 | ### Características: 29 | - Evaluación heurística por país de origen 30 | - Categorización por tipo de misión 31 | - Scoring automático de riesgos potenciales 32 | - Recomendaciones generales de seguridad 33 | 34 | ### Limitaciones: 35 | Este sistema proporciona una evaluación inicial automatizada. 36 | Para análisis críticos de seguridad nacional, se recomienda 37 | validación por analistas especializados en inteligencia espacial. 38 | 39 | * **💻 Interfaz CLI:** 40 | 41 | * Comandos intuitivos con validación completa 42 | * Múltiples formatos de salida (tabla, JSON, detallado) 43 | * Salida coloreada y formateada 44 | * Manejo robusto de errores 45 | 46 | ----- 47 | 48 | ## 📋 Requisitos 49 | 50 | * **Python:** 3.8 o superior 51 | * **Conexión a Internet:** Para acceso a APIs y fuentes de datos 52 | * **Sistema Operativo:** Linux, macOS, Windows 53 | * **Memoria RAM:** Mínimo 512MB disponible 54 | 55 | ### 📦 Dependencias 56 | 57 | ```txt 58 | requests>=2.31.0 # Comunicación HTTP 59 | sgp4>=2.21 # Cálculos orbitales SGP4 60 | beautifulsoup4>=4.12.0 # Web scraping 61 | python-dotenv>=1.0.0 # Variables de entorno 62 | tabulate>=0.9.0 # Formateo de tablas 63 | pytest>=7.4.0 # Testing framework 64 | colorama>=0.4.6 # Colores en terminal 65 | lxml>=4.9.0 # Parser XML/HTML optimizado 66 | ``` 67 | 68 | ----- 69 | 70 | ## 🛠️ Instalación 71 | 72 | ### Instalación Rápida 73 | 74 | ```bash 75 | # 1. Clonar el repositorio 76 | git clone https://github.com/Elisaelias02/sat_hacker_tool 77 | cd sat_hacker_tool 78 | 79 | # 2. Crear entorno virtual (recomendado) 80 | python -m venv venv 81 | source venv/bin/activate # Linux/Mac 82 | # venv\Scripts\activate # Windows 83 | 84 | # 3. Instalar dependencias 85 | pip install -r requirements.txt 86 | 87 | # 4. Configurar variables de entorno (opcional) 88 | cp .env.example .env 89 | # Editar .env con tus credenciales 90 | ``` 91 | 92 | ### Verificar Instalación 93 | 94 | ```bash 95 | python satintel.py --id 25544 96 | ``` 97 | 98 | ----- 99 | 100 | ## ⚙️ Configuración 101 | 102 | ### Variables de Entorno (`.env`) 103 | 104 | ```env 105 | # Credenciales Space-Track.org (opcional pero recomendado) 106 | SPACETRACK_USERNAME=tu_usuario 107 | SPACETRACK_PASSWORD=tu_contraseña 108 | 109 | # API Key N2YO (registro gratuito recomendado) 110 | N2YO_API_KEY=tu_clave_n2yo 111 | 112 | # Configuración general 113 | LOG_LEVEL=INFO 114 | DEFAULT_LATITUDE=20.67 115 | DEFAULT_LONGITUDE=-103.35 116 | ``` 117 | 118 | ### Obtener Credenciales 119 | 120 | * **Space-Track.org (Opcional):** 121 | 122 | * Registrarse en [Space-Track.org](https://www.space-track.org) 123 | * Crear cuenta gratuita 124 | * Agregar credenciales al archivo `.env` 125 | 126 | * **N2YO API (Recomendado):** 127 | 128 | * Registrarse en [N2YO.com](https://www.n2yo.com) 129 | * Obtener API key gratuita (1000 requests/hora) 130 | * Agregar clave al archivo `.env` 131 | 132 | ----- 133 | 134 | ## 📖 Uso 135 | 136 | ### Comandos Básicos 137 | 138 | * **Consultar Satélite por NORAD ID:** 139 | 140 | ```bash 141 | python satintel.py --id 25544 142 | ``` 143 | 144 | * **Consultar Satélite por Nombre:** 145 | 146 | ```bash 147 | python satintel.py --name "ISS" 148 | ``` 149 | 150 | * **Buscar Satélites:** 151 | 152 | ```bash 153 | python satintel.py --search "starlink" 154 | python satintel.py --search "cosmos" 155 | ``` 156 | 157 | ### Análisis Orbital 158 | 159 | * **Calcular Pases Futuros:** 160 | ```bash 161 | 162 | # Pases sobre ubicación específica 163 | python satintel.py --id 25544 --passes --location "40.7128,-74.0060" # Nueva York 164 | 165 | # Personalizar parámetros 166 | python satintel.py --id 25544 --passes --hours 48 --location "19.4326,-99.1332" # Ciudad de México, 48 horas 167 | ``` 168 | 169 | ### Formatos de Salida 170 | 171 | * **Salida Detallada (por defecto):** 172 | 173 | ```bash 174 | python satintel.py --id 25544 175 | ``` 176 | 177 | * **Formato Tabla:** 178 | 179 | ```bash 180 | python satintel.py --id 25544 --output table 181 | ``` 182 | 183 | * **Formato JSON:** 184 | 185 | ```bash 186 | python satintel.py --id 25544 --output json 187 | ``` 188 | 189 | ### Opciones Avanzadas 190 | 191 | * **Omitir Análisis de Seguridad:** 192 | 193 | ```bash 194 | python satintel.py --id 25544 --no-security 195 | ``` 196 | 197 | * **Salida Detallada con Logs:** 198 | 199 | ```bash 200 | python satintel.py --id 25544 --verbose 201 | ``` 202 | 203 | * **Análisis Completo:** 204 | 205 | ```bash 206 | python satintel.py --id 25544 --passes --location "20.67,-103.35" --hours 24 --verbose 207 | ``` 208 | 209 | ----- 210 | 211 | ## 📊 Ejemplos de Salida 212 | 213 | ### Información Básica 214 | 215 | ``` 216 | ============================================================ 217 | INFORMACIÓN BÁSICA DEL SATÉLITE 218 | ============================================================ 219 | Fuentes: Celestrak, N2YO, SatNOGS 220 | NORAD ID................. 25544 221 | Nombre................... ISS (ZARYA) 222 | Operador................. NASA/Roscosmos/ESA/JAXA/CSA 223 | País..................... INTERNATIONAL 224 | Estado................... OPERATIONAL 225 | Lanzamiento.............. 1998-11-20 226 | Sitio Web................ https://www.nasa.gov/mission_pages/station/ 227 | ============================================================ 228 | INFORMACIÓN DE MISIÓN 229 | ============================================================ 230 | Descripción.............. International Space Station, habitable artificial satellite 231 | Tipo..................... Space Station 232 | Órbita................... LEO 233 | ============================================================ 234 | ESPECIFICACIONES TÉCNICAS 235 | ============================================================ 236 | Size..................... Large 237 | Freq Bands............... VHF, UHF, S-band 238 | Modes.................... FM, SSTV, Packet 239 | ============================================================ 240 | ELEMENTOS ORBITALES 241 | ============================================================ 242 | Inclinación.............. 51.64° 243 | Excentricidad............ 0.000123 244 | Movimiento Medio......... 15.488194 rev/día 245 | RAAN..................... 339.79° 246 | Arg. Perigeo............. 356.85° 247 | ============================================================ 248 | POSICIÓN ACTUAL 249 | ============================================================ 250 | Tiempo UTC............... 2025-09-14 15:30:45 251 | Latitud.................. 23.4567° 252 | Longitud................. -45.6789° 253 | Altitud.................. 408.12 km 254 | Velocidad................ 7.66 km/s 255 | ``` 256 | 257 | ### Análisis SPARTA 258 | 259 | ``` 260 | ============================================================ 261 | ANÁLISIS DE SEGURIDAD SPARTA 262 | ============================================================ 263 | Satélite: ISS (ZARYA) (ID: 25544) 264 | País: INTERNATIONAL 265 | Categoría: Estación Espacial 266 | Nivel de Riesgo: LOW 267 | Confianza: 85.0% 268 | 269 | PREOCUPACIONES DE SEGURIDAD: 270 | ⚠ Capacidades de vigilancia sobre territorio nacional 271 | ⚠ Recolección de inteligencia geoespacial 272 | ⚠ Satélite operacionalmente activo - capacidades en tiempo real 273 | 274 | RECOMENDACIONES: 275 | → Mantener vigilancia rutinaria 276 | → Auditar tráfico de comunicaciones sensibles 277 | → Evaluar exposición de infraestructura crítica 278 | ============================================================ 279 | ``` 280 | 281 | ### Pases Futuros 282 | 283 | ``` 284 | ============================================================ 285 | PASES FUTUROS (24h) 286 | ============================================================ 287 | Ubicación: 20.67°, -103.35° 288 | ┌───┬────────────┬──────────┬───────────┐ 289 | │ # │ Inicio │ Duración │ Elev. Máx │ 290 | ├───┼────────────┼──────────┼───────────┤ 291 | │ 1 │ 09-14 18:45│ 6.2 min │ 45.2° │ 292 | │ 2 │ 09-14 20:22│ 4.8 min │ 28.7° │ 293 | │ 3 │ 09-15 06:15│ 7.1 min │ 62.4° │ 294 | │ 4 │ 09-15 07:52│ 5.5 min │ 35.9° │ 295 | └───┴────────────┴──────────┴───────────┘ 296 | ``` 297 | 298 | ----- 299 | 300 | ## 🔍 Casos de Uso 301 | 302 | 1. **Análisis de la Estación Espacial Internacional:** 303 | 304 | ```bash 305 | python satintel.py --id 25544 --passes --location "19.4326,-99.1332" 306 | ``` 307 | 308 | * **Resultado:** Información completa, pases sobre Ciudad de México, análisis de seguridad bajo. 309 | 310 | 2. **Investigación de Constelación Starlink:** 311 | 312 | ```bash 313 | python satintel.py --search "starlink" 314 | ``` 315 | 316 | * **Resultado:** Lista de satélites Starlink activos con información de operador. 317 | 318 | 3. **Monitoreo de Satélites de Alto Riesgo:** 319 | 320 | ```bash 321 | python satintel.py --search "cosmos" 322 | ``` 323 | 324 | * **Resultado:** Satélites militares rusos con análisis SPARTA de alto riesgo. 325 | 326 | 4. **Análisis de Satélites Meteorológicos:** 327 | 328 | ```bash 329 | python satintel.py --name "NOAA" 330 | ``` 331 | 332 | * **Resultado:** Información de satélites meteorológicos estadounidenses. 333 | 334 | 5. **Estudio de Navegación Global:** 335 | 336 | ```bash 337 | python satintel.py --search "gps" 338 | python satintel.py --search "glonass" 339 | python satintel.py --search "galileo" 340 | ``` 341 | 342 | * **Resultado:** Comparación de sistemas de navegación global. 343 | 344 | ----- 345 | 346 | ## 📁 Estructura del Proyecto 347 | 348 | ``` 349 | satintel/ 350 | ├── satintel.py # Punto de entrada principal 351 | ├── modules/ 352 | │ ├── __init__.py 353 | │ ├── data_sources.py # Gestión de fuentes de datos 354 | │ ├── orbital.py # Cálculos de mecánica orbital 355 | │ ├── security.py # Análisis de seguridad SPARTA 356 | │ ├── cli.py # Interfaz de línea de comandos 357 | │ └── utils.py # Utilidades y funciones comunes 358 | ├── config/ 359 | │ ├── __init__.py 360 | │ └── settings.py # Configuración centralizada 361 | ├── tests/ 362 | │ ├── __init__.py 363 | │ ├── test_data_sources.py # Tests para fuentes de datos 364 | │ ├── test_orbital.py # Tests para cálculos orbitales 365 | │ └── test_security.py # Tests para análisis SPARTA 366 | ├── requirements.txt # Dependencias del proyecto 367 | ├── .env.example # Ejemplo de variables de entorno 368 | ├── .gitignore # Archivos ignorados por Git 369 | ├── LICENSE # Licencia del proyecto 370 | └── README.md # Este archivo 371 | ``` 372 | 373 | ----- 374 | 375 | ## 🧪 Testing 376 | 377 | ### Ejecutar Tests 378 | 379 | ```bash 380 | # Todos los tests 381 | pytest tests/ -v 382 | 383 | # Tests específicos 384 | pytest tests/test_data_sources.py -v 385 | pytest tests/test_orbital.py -v 386 | pytest tests/test_security.py -v 387 | 388 | # Tests con cobertura 389 | pytest tests/ --cov=modules --cov-report=html 390 | ``` 391 | 392 | ### Tests de Integración 393 | 394 | ```bash 395 | # Test básico de funcionamiento 396 | python satintel.py --id 25544 --no-security 397 | 398 | # Test de todas las fuentes 399 | python satintel.py --id 25544 --verbose 400 | 401 | # Test de búsqueda 402 | python satintel.py --search "test" 403 | ``` 404 | 405 | ----- 406 | 407 | ## 🌐 Fuentes de Datos 408 | 409 | * **🔴 Fuentes Primarias (Críticas):** 410 | 411 | * **Celestrak:** TLEs oficiales de NORAD/NASA. Actualización diaria. Confiabilidad 99.9%. Gratuito. 412 | * **N2YO API:** Seguimiento en tiempo real. 1000 requests/hora (gratuito). Confiabilidad 95%. 413 | 414 | * **🟡 Fuentes Secundarias (Importantes):** 415 | 416 | * **SatNOGS DB:** Base de datos colaborativa. Actualización continua. Confiabilidad 90%. Gratuito. 417 | * **Space-Track.org:** Catálogo oficial del Departamento de Defensa de EE.UU. Requiere registro y autenticación. Confiabilidad 99%. 418 | 419 | * **🟢 Fuentes Complementarias (Opcionales):** 420 | 421 | * Web Scraping de Gunter's Space Page, Orbit.ing-info.net y otras páginas públicas. 422 | 423 | ----- 424 | 425 | ## 🔒 Framework SPARTA 426 | 427 | Sat Hacker Tool implementa una versión adaptada del framework **SPARTA** para análisis de seguridad espacial, clasificando satélites por: 428 | 429 | * **Tácticas Identificadas:** Reconocimiento, Comando y Control, Recolección, Impacto y Persistencia. 430 | * **Evaluación de Riesgos:** Alto (RU, CN, KP, IR), Medio (Países no alineados), Bajo (US, CA, GB, FR, DE, JP, AU). 431 | * **Categorías de Satélites:** Comunicaciones, Observación Terrestre, Navegación, Meteorológicos, Científicos, Militares, Estaciones Espaciales. 432 | 433 | ----- 434 | 435 | ## ⚠️ Limitaciones y Consideraciones 436 | 437 | * **Técnicas:** Los cálculos orbitales son aproximaciones; la precisión depende de la actualidad de los datos TLE. 438 | * **Legales:** Respetar los términos de uso de las APIs. No usar para actividades ilegales. 439 | * **Éticas:** El análisis SPARTA es para fines educativos y de investigación. Respetar la privacidad y soberanía nacional. 440 | 441 | ----- 442 | 443 | ## 🐛 Resolución de Problemas 444 | 445 | ### Problemas Comunes 446 | 447 | * **`ModuleNotFoundError: No module named 'requests'`:** 448 | 449 | * **Solución:** `pip install -r requirements.txt` 450 | 451 | * **`WARNING: Credenciales de Space-Track no configuradas`:** 452 | 453 | * **Solución:** Configurar las credenciales en `.env` o usar `--no-security` para omitir la fuente. 454 | 455 | * **`ValueError: Formato de ubicación inválido`:** 456 | 457 | * **Solución:** Usar el formato `"lat,lon"` (ej: `"20.67,-103.35"`). 458 | 459 | * **`❌ No se encontraron datos para el satélite`:** 460 | 461 | * **Solución:** Verificar el NORAD ID o el nombre del satélite. 462 | 463 | ----- 464 | 465 | ## 🤝 Contribuciones 466 | 467 | Las contribuciones son bienvenidas. Para contribuir: 468 | 469 | 1. Hacer **fork** del repositorio. 470 | 2. Crear una rama para la nueva funcionalidad (`git checkout -b feature/nueva-funcionalidad`). 471 | 3. Hacer **commit** de los cambios. 472 | 4. Hacer **push** a la rama. 473 | 5. Crear un **Pull Request**. 474 | 475 | ----- 476 | 477 | ## 📄 Licencia 478 | 479 | Este proyecto está bajo la **Licencia MIT**. Ver [LICENSE](https://www.google.com/search?q=LICENSE) para más detalles. 480 | 481 | ----- 482 | 483 | ## 🔗 Enlaces Útiles 484 | 485 | * **APIs y Fuentes de Datos:** 486 | 487 | * [Celestrak](https://celestrak.org) - TLEs oficiales 488 | * [N2YO API](https://www.n2yo.com/api/) - API de seguimiento satelital 489 | * [SatNOGS DB](https://db.satnogs.org) - Base de datos colaborativa 490 | * [Space-Track.org](https://www.space-track.org) - Catálogo oficial 491 | 492 | * **Documentación Técnica:** 493 | 494 | * [SGP4 Library](https://pypi.org/project/sgp4/) - Cálculos orbitales 495 | * [SPARTA Framework](https://www.google.com/search?q=SPARTA+framework) - Framework de análisis 496 | * [TLE Format](https://en.wikipedia.org/wiki/Two-line_element_set) - Formato TLE 497 | 498 | * **Herramientas Relacionadas:** 499 | 500 | * [Gpredict](http://gpredict.oz9aec.net/) - Software de seguimiento satelital 501 | * [Orbitron](https://www.google.com/search?q=https://www.stoff.pl/orbitron/) - Tracker satelital para Windows 502 | * [ISS Tracker](https://www.google.com/search?q=https://spotthestation.nasa.gov/tracking_map.cfm) - Rastreador oficial de la ISS 503 | 504 | ----- 505 | 506 | ## 📞 Soporte 507 | 508 | * **Reportar Bugs:** Crear un [issue en GitHub](https://www.google.com/search?q=https://github.com/tu-usuario/satintel/issues) con la descripción del problema, comando ejecutado, salida de error completa, sistema operativo y versión de Python. 509 | * **Solicitar Features:** Crear un [issue](https://www.google.com/search?q=https://github.com/tu-usuario/satintel/issues) con la descripción de la funcionalidad deseada, casos de uso y beneficios esperados. 510 | * **Preguntas Generales:** Revisar este README, consultar issues existentes o crear un nuevo issue si es necesario. 511 | 512 | ----- 513 | 514 | **⚠️ Disclaimer:** Esta herramienta está destinada únicamente para fines educativos, de investigación y análisis de seguridad legítimos. El uso indebido de esta herramienta es responsabilidad del usuario. La autora no se hace responsable del mal uso de la información obtenida. 515 | 516 | **🌟 Agradecimientos:** Agradezco a todas las organizaciones que proporcionan datos satelitales abiertos y a mi gatita Sky que me acompaño en el desarrollo de este proyecto. 517 | 518 | **Sat HACKER Tool v1.0** - Desarrollado por Cinn4mor0ll (Elisa Elias) para la comunidad de inteligencia espacial. 519 | -------------------------------------------------------------------------------- /modules/sat_data.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | from typing import Dict, List, Optional, Tuple 4 | from bs4 import BeautifulSoup 5 | import json 6 | import time 7 | import os 8 | from dotenv import load_dotenv 9 | 10 | # Cargar variables de entorno AQUÍ directamente 11 | load_dotenv() 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | # URLs y constantes 16 | CELESTRAK_BASE_URL = "https://celestrak.org" 17 | SPACETRACK_BASE_URL = "https://www.space-track.org" 18 | 19 | CELESTRAK_TLE_URLS = { 20 | "all": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=active&FORMAT=tle", 21 | "stations": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle", 22 | "visual": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=visual&FORMAT=tle", 23 | "weather": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=weather&FORMAT=tle", 24 | "noaa": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=noaa&FORMAT=tle", 25 | "goes": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=goes&FORMAT=tle", 26 | "resource": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=resource&FORMAT=tle", 27 | "cubesat": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=tle", 28 | "other": f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP=other&FORMAT=tle" 29 | } 30 | 31 | class SatelliteDataRetriever: 32 | """Clase para recuperar datos satelitales desde múltiples fuentes.""" 33 | 34 | def __init__(self): 35 | self.session = requests.Session() 36 | self.session.headers.update({ 37 | 'User-Agent': 'SatIntel/1.0 (Educational Purpose)' 38 | }) 39 | self._spacetrack_authenticated = False 40 | 41 | # Obtener credenciales directamente aquí 42 | self.spacetrack_username = os.getenv("SPACETRACK_USERNAME") 43 | self.spacetrack_password = os.getenv("SPACETRACK_PASSWORD") 44 | 45 | # Debug 46 | logger.info(f"Username loaded: {bool(self.spacetrack_username)}") 47 | logger.info(f"Password loaded: {bool(self.spacetrack_password)}") 48 | 49 | def get_tle_from_celestrak(self, satellite_id: int = None, 50 | satellite_name: str = None) -> Optional[str]: 51 | """ 52 | Recupera TLE desde Celestrak. 53 | 54 | Args: 55 | satellite_id: NORAD ID del satélite 56 | satellite_name: Nombre del satélite 57 | 58 | Returns: 59 | String con el TLE en formato de 3 líneas o None si no se encuentra 60 | """ 61 | # ... (código que se te proporcionó) ... 62 | try: 63 | if satellite_id: 64 | url = f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?CATNR={satellite_id}&FORMAT=tle" 65 | elif satellite_name: 66 | url = f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?NAME={satellite_name}&FORMAT=tle" 67 | else: 68 | raise ValueError("Debe proporcionar satellite_id o satellite_name") 69 | 70 | logger.info(f"Recuperando TLE desde Celestrak: {url}") 71 | response = self.session.get(url, timeout=10) 72 | response.raise_for_status() 73 | 74 | tle_data = response.text.strip() 75 | if len(tle_data.split('\n')) >= 3: 76 | return tle_data 77 | else: 78 | logger.warning(f"TLE no encontrado para {satellite_id or satellite_name}") 79 | return None 80 | 81 | except requests.RequestException as e: 82 | logger.error(f"Error al recuperar TLE desde Celestrak: {e}") 83 | return None 84 | except Exception as e: 85 | logger.error(f"Error inesperado: {e}") 86 | return None 87 | 88 | def authenticate_spacetrack(self) -> bool: 89 | """ 90 | Autentica con Space-Track.org. 91 | 92 | Returns: 93 | True si la autenticación es exitosa, False en caso contrario 94 | """ 95 | # ... (código que se te proporcionó) ... 96 | if not self.spacetrack_username or not self.spacetrack_password: 97 | logger.warning("Credenciales de Space-Track no configuradas") 98 | logger.warning(f"Username: {bool(self.spacetrack_username)}, Password: {bool(self.spacetrack_password)}") 99 | return False 100 | 101 | try: 102 | login_url = f"{SPACETRACK_BASE_URL}/ajaxauth/login" 103 | login_data = { 104 | 'identity': self.spacetrack_username, 105 | 'password': self.spacetrack_password 106 | } 107 | 108 | logger.info("Autenticando con Space-Track.org...") 109 | logger.info(f"URL de login: {login_url}") 110 | 111 | response = self.session.post(login_url, data=login_data, timeout=10) 112 | 113 | logger.info(f"Respuesta de autenticación: {response.status_code}") 114 | logger.info(f"Cookies recibidas: {len(response.cookies)}") 115 | 116 | # Space-Track devuelve diferentes códigos según el resultado 117 | if response.status_code == 200: 118 | # Verificar si realmente estamos autenticados haciendo una consulta de prueba 119 | test_url = f"{SPACETRACK_BASE_URL}/basicspacedata/query/class/satcat/NORAD_CAT_ID/25544/format/json/limit/1" 120 | test_response = self.session.get(test_url, timeout=10) 121 | 122 | if test_response.status_code == 200: 123 | self._spacetrack_authenticated = True 124 | logger.info("Autenticación exitosa con Space-Track.org") 125 | return True 126 | else: 127 | logger.error(f"Autenticación falló - test query status: {test_response.status_code}") 128 | return False 129 | else: 130 | logger.error(f"Autenticación falló - status code: {response.status_code}") 131 | logger.error(f"Respuesta: {response.text[:200]}") 132 | return False 133 | 134 | except requests.RequestException as e: 135 | logger.error(f"Error de conexión durante autenticación: {e}") 136 | return False 137 | except Exception as e: 138 | logger.error(f"Error inesperado durante autenticación: {e}") 139 | return False 140 | 141 | def get_satellite_info_spacetrack(self, satellite_id: int) -> Optional[Dict]: 142 | """ 143 | Recupera información detallada del satélite desde Space-Track. 144 | 145 | Args: 146 | satellite_id: NORAD ID del satélite 147 | 148 | Returns: 149 | Diccionario con información del satélite o None 150 | """ 151 | # ... (código que se te proporcionó) ... 152 | if not self._spacetrack_authenticated: 153 | if not self.authenticate_spacetrack(): 154 | return None 155 | 156 | try: 157 | # Consulta básica de información del catálogo 158 | query_url = (f"{SPACETRACK_BASE_URL}/basicspacedata/query/class/satcat/" 159 | f"NORAD_CAT_ID/{satellite_id}/format/json") 160 | 161 | logger.info(f"Consultando información desde Space-Track para ID: {satellite_id}") 162 | response = self.session.get(query_url, timeout=15) 163 | response.raise_for_status() 164 | 165 | data = response.json() 166 | if data and len(data) > 0: 167 | return data[0] # Retorna el primer resultado 168 | else: 169 | logger.warning(f"No se encontró información para satélite ID: {satellite_id}") 170 | return None 171 | 172 | except requests.RequestException as e: 173 | logger.error(f"Error al consultar Space-Track: {e}") 174 | return None 175 | except json.JSONDecodeError as e: 176 | logger.error(f"Error al decodificar respuesta JSON: {e}") 177 | return None 178 | 179 | def search_satellite_by_name(self, satellite_name: str) -> List[Dict]: 180 | """ 181 | Busca satélites por nombre en Space-Track. 182 | 183 | Args: 184 | satellite_name: Nombre del satélite a buscar 185 | 186 | Returns: 187 | Lista de diccionarios con información de satélites encontrados 188 | """ 189 | # ... (código que se te proporcionó) ... 190 | if not self._spacetrack_authenticated: 191 | if not self.authenticate_spacetrack(): 192 | return [] 193 | 194 | try: 195 | # Búsqueda por nombre en el catálogo 196 | query_url = (f"{SPACETRACK_BASE_URL}/basicspacedata/query/class/satcat/" 197 | f"OBJECT_NAME/{satellite_name}~~*/format/json") 198 | 199 | logger.info(f"Buscando satélites con nombre: {satellite_name}") 200 | response = self.session.get(query_url, timeout=15) 201 | response.raise_for_status() 202 | 203 | data = response.json() 204 | return data if data else [] 205 | 206 | except requests.RequestException as e: 207 | logger.error(f"Error en búsqueda por nombre: {e}") 208 | return [] 209 | except json.JSONDecodeError as e: 210 | logger.error(f"Error al decodificar respuesta JSON: {e}") 211 | return [] 212 | 213 | def get_satellite_launches_info(self, satellite_id: int) -> Optional[Dict]: 214 | """ 215 | Obtiene información de lanzamiento desde fuentes públicas. 216 | 217 | Args: 218 | satellite_id: NORAD ID del satélite 219 | 220 | Returns: 221 | Diccionario con información de lanzamiento o None 222 | """ 223 | # ... (código que se te proporcionó) ... 224 | try: 225 | # Buscar en N2YO para información adicional (fuente pública) 226 | n2yo_url = f"https://www.n2yo.com/satellite/?s={satellite_id}" 227 | 228 | logger.info(f"Buscando información adicional para ID: {satellite_id}") 229 | response = self.session.get(n2yo_url, timeout=10) 230 | response.raise_for_status() 231 | 232 | soup = BeautifulSoup(response.content, 'html.parser') 233 | 234 | # Extraer información básica (esto puede variar según el HTML actual) 235 | info = {} 236 | 237 | # Buscar tabla de información del satélite 238 | tables = soup.find_all('table') 239 | for table in tables: 240 | rows = table.find_all('tr') 241 | for row in rows: 242 | cells = row.find_all(['td', 'th']) 243 | if len(cells) >= 2: 244 | key = cells[0].get_text(strip=True) 245 | value = cells[1].get_text(strip=True) 246 | info[key] = value 247 | 248 | return info if info else None 249 | 250 | except requests.RequestException as e: 251 | logger.error(f"Error al obtener información adicional: {e}") 252 | return None 253 | except Exception as e: 254 | logger.error(f"Error al procesar información adicional: {e}") 255 | return None 256 | 257 | def get_comprehensive_satellite_data(self, satellite_id: int = None, 258 | satellite_name: str = None) -> Dict: 259 | """ 260 | Recupera datos completos del satélite desde múltiples fuentes. 261 | 262 | Args: 263 | satellite_id: NORAD ID del satélite 264 | satellite_name: Nombre del satélite 265 | 266 | Returns: 267 | Diccionario con toda la información disponible 268 | """ 269 | # ... (código que se te proporcionó) ... 270 | result = { 271 | 'tle': None, 272 | 'spacetrack_info': None, 273 | 'additional_info': None, 274 | 'search_results': [], 275 | 'errors': [], 276 | 'celestrak_data': None 277 | } 278 | 279 | # Si solo tenemos nombre, buscar primero el ID 280 | if satellite_name and not satellite_id: 281 | search_results = self.search_satellite_by_name(satellite_name) 282 | result['search_results'] = search_results 283 | 284 | if search_results: 285 | satellite_id = int(search_results[0].get('NORAD_CAT_ID', 0)) 286 | logger.info(f"Encontrado ID {satellite_id} para nombre '{satellite_name}'") 287 | 288 | # Recuperar TLE desde Celestrak 289 | if satellite_id: 290 | result['tle'] = self.get_tle_from_celestrak(satellite_id=satellite_id) 291 | elif satellite_name: 292 | result['tle'] = self.get_tle_from_celestrak(satellite_name=satellite_name) 293 | 294 | # Si tenemos TLE, extraer información básica 295 | if result['tle']: 296 | try: 297 | lines = result['tle'].strip().split('\n') 298 | if len(lines) >= 3: 299 | sat_name = lines[0].strip() 300 | line1 = lines[1].strip() 301 | line2 = lines[2].strip() 302 | 303 | # Extraer información del TLE 304 | result['celestrak_data'] = { 305 | 'satellite_name': sat_name, 306 | 'norad_id': satellite_id or self._extract_norad_from_tle(line1), 307 | 'classification': line1[7:8], 308 | 'international_designator': line1[9:17].strip(), 309 | 'epoch_year': int(line1[18:20]), 310 | 'epoch_day': float(line1[20:32]), 311 | 'inclination': float(line2[8:16]), 312 | 'raan': float(line2[17:25]), 313 | 'eccentricity': float('0.' + line2[26:33]), 314 | 'arg_perigee': float(line2[34:42]), 315 | 'mean_anomaly': float(line2[43:51]), 316 | 'mean_motion': float(line2[52:63]), 317 | 'revolution_number': int(line2[63:68]) 318 | } 319 | 320 | # Inferir información adicional del nombre 321 | result['celestrak_data'].update(self._infer_satellite_info(sat_name)) 322 | 323 | except Exception as e: 324 | logger.error(f"Error procesando TLE: {e}") 325 | 326 | # Intentar Space-Track solo si las credenciales están disponibles 327 | if self.spacetrack_username and self.spacetrack_password: 328 | logger.info("Intentando recuperar datos desde Space-Track...") 329 | try: 330 | # Si solo tenemos nombre, buscar primero el ID 331 | if satellite_name and not satellite_id: 332 | search_results = self.search_satellite_by_name(satellite_name) 333 | result['search_results'] = search_results 334 | 335 | if search_results: 336 | satellite_id = int(search_results[0].get('NORAD_CAT_ID', 0)) 337 | logger.info(f"Encontrado ID {satellite_id} para nombre '{satellite_name}'") 338 | 339 | # Recuperar información desde Space-Track 340 | if satellite_id: 341 | result['spacetrack_info'] = self.get_satellite_info_spacetrack(satellite_id) 342 | 343 | except Exception as e: 344 | logger.warning(f"Error al acceder Space-Track (continuando con Celestrak): {e}") 345 | result['errors'].append(f"Space-Track error: {e}") 346 | else: 347 | logger.info("Credenciales de Space-Track no disponibles, usando solo Celestrak") 348 | 349 | # Información adicional de fuentes públicas 350 | if satellite_id: 351 | try: 352 | result['additional_info'] = self.get_satellite_launches_info(satellite_id) 353 | except Exception as e: 354 | logger.warning(f"Error obteniendo información adicional: {e}") 355 | 356 | return result 357 | 358 | def _extract_norad_from_tle(self, tle_line1: str) -> int: 359 | """Extrae el NORAD ID de la primera línea del TLE.""" 360 | try: 361 | return int(tle_line1[2:7]) 362 | except: 363 | return 0 364 | 365 | def _infer_satellite_info(self, satellite_name: str) -> Dict: 366 | """Infiere información del satélite basado en su nombre.""" 367 | name_upper = satellite_name.upper() 368 | info = { 369 | 'inferred_country': 'UNKNOWN', 370 | 'inferred_type': 'UNKNOWN', 371 | 'inferred_purpose': 'UNKNOWN' 372 | } 373 | 374 | # Inferir país basado en patrones de nombres 375 | if any(pattern in name_upper for pattern in ['ISS', 'DRAGON', 'CYGNUS']): 376 | info['inferred_country'] = 'INTERNATIONAL' 377 | elif any(pattern in name_upper for pattern in ['STARLINK', 'GPS', 'NOAA', 'GOES']): 378 | info['inferred_country'] = 'US' 379 | elif any(pattern in name_upper for pattern in ['COSMOS', 'GLONASS']): 380 | info['inferred_country'] = 'RU' 381 | elif 'BEIDOU' in name_upper: 382 | info['inferred_country'] = 'CN' 383 | elif 'GALILEO' in name_upper: 384 | info['inferred_country'] = 'EU' 385 | 386 | # Inferir tipo/propósito 387 | if any(pattern in name_upper for pattern in ['STARLINK', 'ONEWEB', 'IRIDIUM']): 388 | info['inferred_type'] = 'COMMUNICATION' 389 | info['inferred_purpose'] = 'Internet/Communications constellation' 390 | elif any(pattern in name_upper for pattern in ['GPS', 'GLONASS', 'GALILEO', 'BEIDOU']): 391 | info['inferred_type'] = 'NAVIGATION' 392 | info['inferred_purpose'] = 'Global Navigation Satellite System' 393 | elif any(pattern in name_upper for pattern in ['NOAA', 'GOES', 'METOP']): 394 | info['inferred_type'] = 'WEATHER' 395 | info['inferred_purpose'] = 'Weather monitoring and forecasting' 396 | elif any(pattern in name_upper for pattern in ['LANDSAT', 'SENTINEL', 'WORLDVIEW']): 397 | info['inferred_type'] = 'EARTH_OBSERVATION' 398 | info['inferred_purpose'] = 'Earth observation and remote sensing' 399 | elif 'ISS' in name_upper: 400 | info['inferred_type'] = 'SPACE_STATION' 401 | info['inferred_purpose'] = 'International Space Station' 402 | elif 'COSMOS' in name_upper: 403 | info['inferred_type'] = 'MILITARY' 404 | info['inferred_purpose'] = 'Military/Intelligence satellite' 405 | 406 | return info 407 | 408 | 409 | def parse_tle(tle_string: str) -> Tuple[str, str, str]: 410 | """ 411 | Parse un string TLE en sus tres líneas componentes. 412 | 413 | Args: 414 | tle_string: String con el TLE completo 415 | 416 | Returns: 417 | Tupla con (nombre, línea1, línea2) 418 | """ 419 | lines = tle_string.strip().split('\n') 420 | if len(lines) >= 3: 421 | return lines[0].strip(), lines[1].strip(), lines[2].strip() 422 | else: 423 | raise ValueError("TLE debe contener al menos 3 líneas") 424 | -------------------------------------------------------------------------------- /modules/security.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, List 3 | from datetime import datetime 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | class SatelliteRiskAnalyzer: 8 | """ 9 | Analizador de riesgos satelitales usando evaluación heurística automatizada. 10 | 11 | IMPORTANTE: Este sistema realiza análisis automatizado basado en heurísticas 12 | y datos públicos. NO es un análisis de TTPs documentadas como SPARTA/MITRE. 13 | Es una herramienta de screening inicial que requiere validación humana 14 | para evaluaciones críticas de seguridad. 15 | 16 | Inspirado en conceptos del framework SPARTA adaptados al dominio espacial. 17 | """ 18 | 19 | SATELLITE_CATEGORIES = { 20 | 'communication': 'Comunicaciones', 21 | 'earth_observation': 'Observación Terrestre', 22 | 'navigation': 'Navegación', 23 | 'weather': 'Meteorológico', 24 | 'scientific': 'Científico', 25 | 'military': 'Militar', 26 | 'space_station': 'Estación Espacial', 27 | 'unknown': 'Desconocido' 28 | } 29 | 30 | # Clasificación heurística de riesgo por país (basada en políticas espaciales públicas) 31 | RISK_COUNTRIES = { 32 | 'high': ['RU', 'CN', 'KP', 'IR'], # Países con políticas espaciales agresivas 33 | 'medium': ['PK', 'IN', 'IL', 'BR'], # Países con capacidades espaciales en desarrollo 34 | 'low': ['US', 'CA', 'GB', 'FR', 'DE', 'JP', 'AU', 'NL', 'SE', 'NO'] # Aliados tradicionales 35 | } 36 | 37 | def __init__(self): 38 | """Inicializa el analizador de riesgos.""" 39 | logger.info("SatelliteRiskAnalyzer inicializado") 40 | logger.info("NOTA: Este es un sistema de evaluación heurística, no análisis de TTPs documentadas") 41 | 42 | def analyze_satellite(self, satellite_data: Dict) -> Dict: 43 | """ 44 | Realiza evaluación automatizada de riesgos usando heurísticas. 45 | 46 | Args: 47 | satellite_data: Datos del satélite obtenidos de múltiples fuentes 48 | 49 | Returns: 50 | Diccionario con evaluación de riesgos heurística 51 | 52 | IMPORTANTE: Este análisis es automatizado y debe ser validado 53 | por analistas especializados para casos críticos. 54 | """ 55 | basic_info = satellite_data.get('basic_info', {}) 56 | mission_info = satellite_data.get('mission_info', {}) 57 | 58 | analysis = { 59 | 'analysis_type': 'automated_heuristic_assessment', 60 | 'framework_inspiration': 'SPARTA concepts adapted for space domain', 61 | 'disclaimer': 'Initial screening - requires human validation for critical assessments', 62 | 'timestamp': datetime.utcnow().isoformat(), 63 | 'satellite_id': basic_info.get('norad_id'), 64 | 'satellite_name': basic_info.get('name', ''), 65 | 'country': self._determine_country(basic_info), 66 | 'category': self._determine_category(basic_info, mission_info), 67 | 'risk_level': 'unknown', 68 | 'risk_factors': [], 69 | 'potential_concerns': [], 70 | 'general_recommendations': [], 71 | 'confidence_score': self._calculate_confidence(satellite_data), 72 | 'data_sources': satellite_data.get('sources_used', []) 73 | } 74 | 75 | # Evaluación heurística de riesgo 76 | analysis['risk_level'] = self._assess_risk_heuristic(analysis['country']) 77 | analysis['risk_factors'] = self._identify_risk_factors( 78 | analysis['category'], analysis['country'], basic_info 79 | ) 80 | 81 | # Generar preocupaciones potenciales (no confirmadas) 82 | analysis['potential_concerns'] = self._generate_potential_concerns( 83 | analysis['category'], analysis['risk_level'], basic_info 84 | ) 85 | 86 | # Generar recomendaciones generales 87 | analysis['general_recommendations'] = self._generate_general_recommendations( 88 | analysis['category'], analysis['risk_level'] 89 | ) 90 | 91 | return analysis 92 | 93 | def _determine_country(self, basic_info: Dict) -> str: 94 | """Determina país de origen basado en datos disponibles.""" 95 | return (basic_info.get('countries') or 96 | basic_info.get('inferred_country') or 97 | 'UNKNOWN') 98 | 99 | def _determine_category(self, basic_info: Dict, mission_info: Dict) -> str: 100 | """Determina categoría del satélite basado en información disponible.""" 101 | # Prioridad a datos reales de fuentes oficiales 102 | sat_type = mission_info.get('type', '').upper() 103 | if 'COMMUNICATION' in sat_type: 104 | return 'communication' 105 | elif 'EARTH' in sat_type or 'OBSERVATION' in sat_type: 106 | return 'earth_observation' 107 | elif 'NAVIGATION' in sat_type: 108 | return 'navigation' 109 | elif 'WEATHER' in sat_type or 'METEOROLOGY' in sat_type: 110 | return 'weather' 111 | elif 'SCIENTIFIC' in sat_type or 'RESEARCH' in sat_type: 112 | return 'scientific' 113 | elif 'MILITARY' in sat_type or 'DEFENSE' in sat_type: 114 | return 'military' 115 | elif 'STATION' in sat_type: 116 | return 'space_station' 117 | 118 | # Fallback a información inferida del nombre 119 | inferred_type = mission_info.get('inferred_type', '').lower() 120 | return inferred_type if inferred_type != 'unknown' else 'unknown' 121 | 122 | def _assess_risk_heuristic(self, country: str) -> str: 123 | """ 124 | Evaluación heurística de riesgo basada en país de origen. 125 | 126 | NOTA: Esta es una clasificación simplificada basada en políticas 127 | espaciales públicas conocidas. No constituye una evaluación 128 | de inteligencia formal. 129 | """ 130 | country_upper = country.upper() 131 | 132 | if any(c in country_upper for c in self.RISK_COUNTRIES['high']): 133 | return 'high' 134 | elif any(c in country_upper for c in self.RISK_COUNTRIES['medium']): 135 | return 'medium' 136 | elif any(c in country_upper for c in self.RISK_COUNTRIES['low']): 137 | return 'low' 138 | else: 139 | return 'medium' # Default para países no clasificados 140 | 141 | def _identify_risk_factors(self, category: str, country: str, basic_info: Dict) -> List[str]: 142 | """Identifica factores de riesgo basados en características observables.""" 143 | risk_factors = [] 144 | 145 | # Factores por categoría 146 | if category == 'military': 147 | risk_factors.append("Satélite de propósito militar declarado") 148 | elif category == 'earth_observation': 149 | risk_factors.append("Capacidades de observación terrestre") 150 | elif category == 'communication': 151 | risk_factors.append("Capacidades de comunicaciones") 152 | elif category == 'navigation': 153 | risk_factors.append("Infraestructura de navegación crítica") 154 | 155 | # Factores por país 156 | if country.upper() in self.RISK_COUNTRIES['high']: 157 | risk_factors.append("País con políticas espaciales agresivas conocidas") 158 | 159 | # Factores por operador 160 | operator = basic_info.get('operator', '') 161 | if operator and any(word in operator.upper() for word in ['MILITARY', 'DEFENSE', 'ARMY', 'NAVY']): 162 | risk_factors.append("Operador militar o de defensa identificado") 163 | 164 | # Factores por estado 165 | status = basic_info.get('status', '') 166 | if 'OPERATIONAL' in status.upper() or 'ACTIVE' in status.upper(): 167 | risk_factors.append("Satélite operacionalmente activo") 168 | 169 | return risk_factors 170 | 171 | def _generate_potential_concerns(self, category: str, risk_level: str, basic_info: Dict) -> List[str]: 172 | """ 173 | Genera lista de preocupaciones potenciales basadas en capacidades inferidas. 174 | 175 | NOTA: Estas son preocupaciones teóricas basadas en capacidades típicas 176 | del tipo de satélite, no en inteligencia específica verificada. 177 | """ 178 | concerns = [] 179 | 180 | # Preocupaciones por categoría (basadas en capacidades típicas) 181 | category_concerns = { 182 | 'communication': [ 183 | "Potencial para interceptación de comunicaciones", 184 | "Posible uso como canal de comunicación encubierto", 185 | "Capacidad de relevo para comunicaciones sensibles" 186 | ], 187 | 'earth_observation': [ 188 | "Capacidades de vigilancia sobre territorio nacional", 189 | "Potencial recolección de inteligencia geoespacial", 190 | "Monitoreo de infraestructura crítica desde el espacio" 191 | ], 192 | 'navigation': [ 193 | "Riesgo de interferencia con sistemas de navegación críticos", 194 | "Potencial para spoofing de señales GNSS", 195 | "Dependencia de infraestructura de navegación extranjera" 196 | ], 197 | 'military': [ 198 | "Capacidades militares espaciales no transparentes", 199 | "Potencial para operaciones de guerra espacial", 200 | "Tecnologías de doble uso no declaradas" 201 | ], 202 | 'weather': [ 203 | "Recolección de datos meteorológicos con posibles aplicaciones militares" 204 | ], 205 | 'space_station': [ 206 | "Plataforma para actividades espaciales múltiples", 207 | "Capacidad de servicing de otros satélites" 208 | ] 209 | } 210 | 211 | concerns.extend(category_concerns.get(category, [])) 212 | 213 | # Preocupaciones adicionales por nivel de riesgo heurístico 214 | if risk_level == 'high': 215 | concerns.extend([ 216 | "País de origen con historial de actividades espaciales agresivas", 217 | "Potencial uso para actividades de inteligencia no declaradas", 218 | "Falta de transparencia en capacidades y misión declarada" 219 | ]) 220 | elif risk_level == 'medium': 221 | concerns.extend([ 222 | "Capacidades espaciales en desarrollo con transparencia limitada" 223 | ]) 224 | 225 | # Agregar disclaimer 226 | if concerns: 227 | concerns.insert(0, "NOTA: Estas son preocupaciones teóricas basadas en capacidades típicas") 228 | 229 | return concerns 230 | 231 | def _generate_general_recommendations(self, category: str, risk_level: str) -> List[str]: 232 | """Genera recomendaciones generales de seguridad.""" 233 | recommendations = [] 234 | 235 | # Recomendaciones por nivel de riesgo 236 | if risk_level == 'high': 237 | recommendations.extend([ 238 | "Monitorear actividad satelital mediante fuentes abiertas", 239 | "Evaluar dependencias críticas de servicios relacionados", 240 | "Considerar implementación de sistemas redundantes" 241 | ]) 242 | elif risk_level == 'medium': 243 | recommendations.extend([ 244 | "Mantener vigilancia rutinaria de actividades", 245 | "Verificar capacidades declaradas versus observadas" 246 | ]) 247 | else: # low risk 248 | recommendations.extend([ 249 | "Monitoreo rutinario según protocolos estándar" 250 | ]) 251 | 252 | # Recomendaciones por categoría 253 | category_recommendations = { 254 | 'communication': [ 255 | "Auditar patrones de tráfico de comunicaciones sensibles", 256 | "Implementar protocolos de comunicación seguros" 257 | ], 258 | 'earth_observation': [ 259 | "Evaluar exposición de infraestructura crítica a observación", 260 | "Implementar contramedidas de ocultación si es necesario" 261 | ], 262 | 'navigation': [ 263 | "Implementar sistemas de navegación alternativos/redundantes", 264 | "Monitorear integridad de señales GNSS" 265 | ], 266 | 'military': [ 267 | "Realizar análisis detallado de capacidades mediante OSINT", 268 | "Coordinar con agencias de inteligencia especializadas" 269 | ] 270 | } 271 | 272 | recommendations.extend(category_recommendations.get(category, [])) 273 | 274 | # Recomendación general importante 275 | recommendations.append( 276 | "IMPORTANTE: Validar este análisis con analistas de inteligencia especializados" 277 | ) 278 | 279 | return recommendations 280 | 281 | def _calculate_confidence(self, satellite_data: Dict) -> float: 282 | """ 283 | Calcula nivel de confianza del análisis basado en calidad y cantidad de datos. 284 | 285 | Factores que afectan la confianza: 286 | - Número de fuentes de datos 287 | - Completitud de información básica 288 | - Disponibilidad de información de misión 289 | - Calidad de datos orbitales 290 | """ 291 | score = 0.0 292 | 293 | # Puntos por número de fuentes (max 0.4) 294 | sources = satellite_data.get('sources_used', []) 295 | score += min(len(sources) * 0.1, 0.4) 296 | 297 | # Puntos por completitud de datos básicos (max 0.3) 298 | basic_info = satellite_data.get('basic_info', {}) 299 | if basic_info.get('norad_id'): 300 | score += 0.1 301 | if basic_info.get('name'): 302 | score += 0.1 303 | if basic_info.get('countries') or basic_info.get('operator'): 304 | score += 0.1 305 | 306 | # Puntos por información de misión (max 0.2) 307 | mission_info = satellite_data.get('mission_info', {}) 308 | if mission_info.get('type') or mission_info.get('description'): 309 | score += 0.2 310 | 311 | # Puntos por datos orbitales (max 0.1) 312 | if satellite_data.get('tle'): 313 | score += 0.1 314 | 315 | return min(score, 1.0) 316 | 317 | def generate_assessment_report(self, analysis: Dict) -> str: 318 | """ 319 | Genera reporte de evaluación de riesgos en formato texto. 320 | 321 | NOTA: Cambiado de 'generate_report' para ser más específico 322 | sobre el tipo de análisis que se está realizando. 323 | """ 324 | lines = [ 325 | "=" * 60, 326 | "EVALUACIÓN AUTOMATIZADA DE RIESGOS SATELITALES", 327 | "=" * 60, 328 | "TIPO DE ANÁLISIS: Heurístico automatizado", 329 | "INSPIRADO EN: Conceptos del framework SPARTA", 330 | "LIMITACIÓN: Requiere validación por analistas especializados", 331 | "", 332 | f"Satélite: {analysis['satellite_name']} (ID: {analysis['satellite_id']})", 333 | f"País de Origen: {analysis['country']}", 334 | f"Categoría: {self.SATELLITE_CATEGORIES.get(analysis['category'], 'Desconocido')}", 335 | f"Nivel de Riesgo Heurístico: {analysis['risk_level'].upper()}", 336 | f"Confianza del Análisis: {analysis['confidence_score']:.1%}", 337 | f"Fuentes de Datos: {', '.join(analysis['data_sources'])}", 338 | "" 339 | ] 340 | 341 | # Factores de riesgo identificados 342 | if analysis['risk_factors']: 343 | lines.append("FACTORES DE RIESGO IDENTIFICADOS:") 344 | for factor in analysis['risk_factors']: 345 | lines.append(f" • {factor}") 346 | lines.append("") 347 | 348 | # Preocupaciones potenciales 349 | if analysis['potential_concerns']: 350 | lines.append("PREOCUPACIONES POTENCIALES:") 351 | for concern in analysis['potential_concerns']: 352 | lines.append(f" ⚠ {concern}") 353 | lines.append("") 354 | 355 | # Recomendaciones generales 356 | if analysis['general_recommendations']: 357 | lines.append("RECOMENDACIONES GENERALES:") 358 | for rec in analysis['general_recommendations']: 359 | lines.append(f" → {rec}") 360 | lines.append("") 361 | 362 | # Disclaimer final 363 | lines.extend([ 364 | "DISCLAIMER IMPORTANTE:", 365 | "Este análisis es una evaluación heurística automatizada basada", 366 | "en datos públicos y no constituye una evaluación formal de", 367 | "inteligencia. Para decisiones críticas de seguridad nacional,", 368 | "consulte con analistas especializados en inteligencia espacial.", 369 | "=" * 60 370 | ]) 371 | 372 | return "\n".join(lines) 373 | 374 | 375 | # Clase de compatibilidad (para no romper código existente) 376 | class SecurityAnalyzer(SatelliteRiskAnalyzer): 377 | """ 378 | Clase de compatibilidad para mantener interfaz existente. 379 | 380 | DEPRECATED: Use SatelliteRiskAnalyzer para nuevas implementaciones. 381 | """ 382 | 383 | def __init__(self): 384 | super().__init__() 385 | logger.warning("SecurityAnalyzer está deprecated. Use SatelliteRiskAnalyzer.") 386 | 387 | def analyze_satellite(self, satellite_data: Dict) -> Dict: 388 | """Método de compatibilidad.""" 389 | analysis = super().analyze_satellite(satellite_data) 390 | 391 | # Mapear a formato anterior para compatibilidad 392 | return { 393 | 'satellite_id': analysis['satellite_id'], 394 | 'satellite_name': analysis['satellite_name'], 395 | 'country': analysis['country'], 396 | 'category': analysis['category'], 397 | 'risk_level': analysis['risk_level'], 398 | 'concerns': analysis['potential_concerns'], 399 | 'recommendations': analysis['general_recommendations'], 400 | 'confidence': analysis['confidence_score'] 401 | } 402 | 403 | def generate_report(self, analysis: Dict) -> str: 404 | """Método de compatibilidad.""" 405 | # Si recibimos análisis en formato nuevo, convertir 406 | if 'potential_concerns' in analysis: 407 | return self.generate_assessment_report(analysis) 408 | 409 | # Si recibimos análisis en formato anterior, usar formato anterior 410 | lines = [ 411 | "=" * 60, 412 | "EVALUACIÓN AUTOMATIZADA DE RIESGOS SATELITALES", 413 | "=" * 60, 414 | f"Satélite: {analysis['satellite_name']} (ID: {analysis['satellite_id']})", 415 | f"País: {analysis['country']}", 416 | f"Categoría: {self.SATELLITE_CATEGORIES.get(analysis['category'], 'Desconocido')}", 417 | f"Nivel de Riesgo: {analysis['risk_level'].upper()}", 418 | f"Confianza: {analysis['confidence']:.1%}", 419 | "" 420 | ] 421 | 422 | if analysis['concerns']: 423 | lines.append("PREOCUPACIONES DE SEGURIDAD:") 424 | for concern in analysis['concerns']: 425 | lines.append(f" ⚠ {concern}") 426 | lines.append("") 427 | 428 | if analysis['recommendations']: 429 | lines.append("RECOMENDACIONES:") 430 | for rec in analysis['recommendations']: 431 | lines.append(f" → {rec}") 432 | lines.append("") 433 | 434 | lines.append("=" * 60) 435 | return "\n".join(lines) 436 | -------------------------------------------------------------------------------- /modules/data_sources.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import json 4 | from typing import Dict, List, Optional 5 | from bs4 import BeautifulSoup 6 | from config.settings import * 7 | from modules.utils import infer_satellite_info, extract_norad_from_tle 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | class SatelliteDataManager: 12 | """Gestor unificado de fuentes de datos satelitales.""" 13 | 14 | def __init__(self): 15 | self.session = requests.Session() 16 | self.session.headers.update({ 17 | 'User-Agent': 'SatIntel/1.0 (Educational Purpose)' 18 | }) 19 | logger.info("SatelliteDataManager inicializado") 20 | self.n2yo_api_key = N2YO_API_KEY # Se corrige para que sea una variable de la clase. 21 | 22 | def get_satellite_data(self, satellite_id: int = None, 23 | satellite_name: str = None) -> Dict: 24 | """ 25 | Punto de entrada principal para obtener datos satelitales. 26 | 27 | Args: 28 | satellite_id: NORAD ID del satélite 29 | satellite_name: Nombre del satélite 30 | 31 | Returns: 32 | Diccionario con datos consolidados 33 | """ 34 | result = { 35 | 'basic_info': {}, 36 | 'mission_info': {}, 37 | 'technical_info': {}, 38 | 'orbital_info': {}, 39 | 'tle': None, 40 | 'sources_used': [], 41 | 'errors': [], 42 | 'raw_data': {} 43 | } 44 | 45 | logger.info(f"Recuperando datos para: ID={satellite_id}, Nombre={satellite_name}") 46 | 47 | # 1. Obtener TLE desde Celestrak (prioritario) 48 | tle_data = self._get_celestrak_data(satellite_id, satellite_name) 49 | if tle_data: 50 | result['tle'] = tle_data['tle'] 51 | result['orbital_info'] = tle_data['orbital_elements'] 52 | result['sources_used'].append('Celestrak') 53 | result['raw_data']['celestrak'] = tle_data 54 | 55 | # Extraer ID si solo teníamos nombre 56 | if not satellite_id and tle_data.get('norad_id'): 57 | satellite_id = tle_data['norad_id'] 58 | 59 | # 2. Obtener datos de N2YO 60 | n2yo_data = self._get_n2yo_data(satellite_id) 61 | if n2yo_data: 62 | result['basic_info'].update(n2yo_data['basic_info']) 63 | result['sources_used'].append('N2YO') 64 | result['raw_data']['n2yo'] = n2yo_data 65 | 66 | # 3. Obtener datos de SatNOGS 67 | satnogs_data = self._get_satnogs_data(satellite_id) 68 | if satnogs_data: 69 | result['basic_info'].update(satnogs_data['basic_info']) 70 | result['mission_info'].update(satnogs_data['mission_info']) 71 | result['technical_info'].update(satnogs_data['technical_info']) 72 | result['sources_used'].append('SatNOGS') 73 | result['raw_data']['satnogs'] = satnogs_data 74 | 75 | # 4. Consolidar información 76 | self._consolidate_data(result) 77 | 78 | logger.info(f"Datos recuperados de {len(result['sources_used'])} fuentes") 79 | return result 80 | 81 | def _get_celestrak_data(self, satellite_id: int = None, 82 | satellite_name: str = None) -> Optional[Dict]: 83 | """Recupera datos desde Celestrak.""" 84 | try: 85 | if satellite_id: 86 | url = f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?CATNR={satellite_id}&FORMAT=tle" 87 | elif satellite_name: 88 | url = f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?NAME={satellite_name}&FORMAT=tle" 89 | else: 90 | return None 91 | 92 | logger.info(f"Consultando Celestrak: {url}") 93 | response = self.session.get(url, timeout=API_TIMEOUTS['celestrak']) 94 | response.raise_for_status() 95 | 96 | tle_text = response.text.strip() 97 | if len(tle_text.split('\n')) >= 3: 98 | lines = tle_text.split('\n') 99 | name = lines[0].strip() 100 | line1 = lines[1].strip() 101 | line2 = lines[2].strip() 102 | 103 | # Extraer elementos orbitales 104 | orbital_elements = { 105 | 'norad_id': extract_norad_from_tle(line1), 106 | 'classification': line1[7:8], 107 | 'international_designator': line1[9:17].strip(), 108 | 'epoch_year': int(line1[18:20]), 109 | 'epoch_day': float(line1[20:32]), 110 | 'inclination': float(line2[8:16]), 111 | 'raan': float(line2[17:25]), 112 | 'eccentricity': float('0.' + line2[26:33]), 113 | 'arg_perigee': float(line2[34:42]), 114 | 'mean_anomaly': float(line2[43:51]), 115 | 'mean_motion': float(line2[52:63]), 116 | 'revolution_number': int(line2[63:68]) 117 | } 118 | 119 | # Información inferida 120 | inferred = infer_satellite_info(name) 121 | 122 | return { 123 | 'tle': tle_text, 124 | 'satellite_name': name, 125 | 'norad_id': orbital_elements['norad_id'], 126 | 'orbital_elements': orbital_elements, 127 | 'inferred_info': inferred 128 | } 129 | 130 | return None 131 | 132 | except Exception as e: 133 | logger.error(f"Error en Celestrak: {e}") 134 | return None 135 | 136 | def _get_n2yo_data(self, satellite_id: int) -> Optional[Dict]: 137 | """Recupera datos desde N2YO API.""" 138 | if not satellite_id: 139 | return None 140 | 141 | try: 142 | url = f"{N2YO_BASE_URL}/tle/{satellite_id}&apiKey={self.n2yo_api_key}" 143 | 144 | logger.info(f"Consultando N2YO API: {satellite_id}") 145 | response = self.session.get(url, timeout=API_TIMEOUTS['n2yo']) 146 | response.raise_for_status() 147 | 148 | data = response.json() 149 | 150 | if 'info' in data: 151 | return { 152 | 'basic_info': { 153 | 'norad_id': data['info'].get('satid'), 154 | 'name': data['info'].get('satname'), 155 | 'launch_date': data['info'].get('launchDate'), 156 | 'decay_date': data['info'].get('decayDate') 157 | } 158 | } 159 | 160 | return None 161 | 162 | except Exception as e: 163 | logger.error(f"Error en N2YO: {e}") 164 | return None 165 | 166 | def _get_satnogs_data(self, satellite_id: int) -> Optional[Dict]: 167 | """Recupera datos desde SatNOGS DB.""" 168 | if not satellite_id: 169 | return None 170 | 171 | try: 172 | url = f"{SATNOGS_BASE_URL}/satellites/?norad_cat_id={satellite_id}" 173 | 174 | logger.info(f"Consultando SatNOGS: {satellite_id}") 175 | response = self.session.get(url, timeout=API_TIMEOUTS['satnogs']) 176 | response.raise_for_status() 177 | 178 | data = response.json() 179 | 180 | if data and len(data) > 0: 181 | sat = data[0] 182 | return { 183 | 'basic_info': { 184 | 'name': sat.get('name'), 185 | 'operator': sat.get('operator'), 186 | 'countries': sat.get('countries'), 187 | 'launched': sat.get('launched'), 188 | 'status': sat.get('status'), 189 | 'website': sat.get('website') 190 | }, 191 | 'mission_info': { 192 | 'description': sat.get('description'), 193 | 'type': sat.get('type'), 194 | 'orbit': sat.get('orbit') 195 | }, 196 | 'technical_info': { 197 | 'size': sat.get('size'), 198 | 'freq_bands': [band.get('name') for band in sat.get('freq_bands', [])], 199 | 'modes': [mode.get('name') for mode in sat.get('modes', [])] 200 | } 201 | } 202 | 203 | return None 204 | 205 | except Exception as e: 206 | logger.error(f"Error en SatNOGS: {e}") 207 | return None 208 | 209 | def _consolidate_data(self, result: Dict): 210 | """Consolida y limpia los datos obtenidos.""" 211 | # Limpiar valores vacíos 212 | for section in ['basic_info', 'mission_info', 'technical_info', 'orbital_info']: 213 | result[section] = {k: v for k, v in result[section].items() 214 | if v and v != 'N/A' and v != ''} 215 | 216 | # Agregar información inferida si falta datos 217 | if 'celestrak' in result['raw_data']: 218 | celestrak_data = result['raw_data']['celestrak'] 219 | inferred = celestrak_data.get('inferred_info', {}) 220 | 221 | if not result['basic_info'].get('countries'): 222 | result['basic_info']['inferred_country'] = inferred.get('inferred_country') 223 | if not result['mission_info'].get('type'): 224 | result['mission_info']['inferred_type'] = inferred.get('inferred_type') 225 | if not result['mission_info'].get('purpose'): 226 | result['mission_info']['inferred_purpose'] = inferred.get('inferred_purpose') 227 | 228 | # Agregar nombre y NORAD ID desde Celestrak si no están 229 | if not result['basic_info'].get('name'): 230 | result['basic_info']['name'] = celestrak_data.get('satellite_name') 231 | if not result['basic_info'].get('norad_id'): 232 | result['basic_info']['norad_id'] = celestrak_data.get('norad_id') 233 | 234 | def search_satellites(self, search_term: str) -> List[Dict]: 235 | """ 236 | Busca satélites por nombre en múltiples fuentes - VERSIÓN CORREGIDA. 237 | 238 | Args: 239 | search_term: Término de búsqueda 240 | 241 | Returns: 242 | Lista de resultados de múltiples fuentes 243 | """ 244 | results = [] 245 | search_term_clean = search_term.strip().lower() 246 | 247 | logger.info(f"Buscando '{search_term}' en múltiples fuentes...") 248 | 249 | # 1. Buscar en SatNOGS (más completo) 250 | satnogs_results = self._search_satnogs(search_term_clean) 251 | results.extend(satnogs_results) 252 | 253 | # 2. Buscar en Celestrak por nombre 254 | celestrak_results = self._search_celestrak(search_term_clean) 255 | results.extend(celestrak_results) 256 | 257 | # 3. Buscar usando N2YO si tenemos API key 258 | if hasattr(self, 'n2yo_api_key') and self.n2yo_api_key and self.n2yo_api_key != "25K5EB-9FM8MQ-BLDAGL-5B2J": 259 | n2yo_results = self._search_n2yo(search_term_clean) 260 | results.extend(n2yo_results) 261 | 262 | # 4. Eliminar duplicados basado en NORAD ID 263 | unique_results = self._remove_duplicate_satellites(results) 264 | 265 | logger.info(f"Encontrados {len(unique_results)} satélites únicos") 266 | return unique_results 267 | 268 | def _search_satnogs(self, search_term: str) -> List[Dict]: 269 | """Busca en SatNOGS DB.""" 270 | results = [] 271 | try: 272 | # Múltiples estrategias de búsqueda en SatNOGS 273 | search_urls = [ 274 | f"{SATNOGS_BASE_URL}/satellites/?name__icontains={search_term}", 275 | f"{SATNOGS_BASE_URL}/satellites/?operator__icontains={search_term}", 276 | ] 277 | 278 | for url in search_urls: 279 | try: 280 | logger.info(f"Consultando SatNOGS: {url}") 281 | response = self.session.get(url, timeout=API_TIMEOUTS['satnogs']) 282 | 283 | if response.status_code == 200: 284 | data = response.json() 285 | for sat in data[:15]: # Limitar resultados por fuente 286 | if sat.get('norad_cat_id'): # Solo satélites con NORAD ID válido 287 | results.append({ 288 | 'source': 'SatNOGS', 289 | 'norad_id': sat.get('norad_cat_id'), 290 | 'name': sat.get('name', '').strip(), 291 | 'operator': sat.get('operator', '').strip(), 292 | 'countries': sat.get('countries', '').strip(), 293 | 'status': sat.get('status', '').strip(), 294 | 'launched': sat.get('launched', ''), 295 | 'description': sat.get('description', '').strip()[:100] 296 | }) 297 | else: 298 | logger.warning(f"SatNOGS respuesta: {response.status_code}") 299 | 300 | except requests.RequestException as e: 301 | logger.warning(f"Error en consulta SatNOGS: {e}") 302 | continue 303 | 304 | except Exception as e: 305 | logger.error(f"Error general en búsqueda SatNOGS: {e}") 306 | 307 | return results 308 | 309 | def _search_celestrak(self, search_term: str) -> List[Dict]: 310 | """Busca en Celestrak usando múltiples categorías.""" 311 | results = [] 312 | 313 | try: 314 | # Categorías principales de Celestrak para buscar 315 | categories = ['stations', 'visual', 'active', 'weather', 'noaa', 'goes', 'resource'] 316 | 317 | for category in categories: 318 | try: 319 | url = f"{CELESTRAK_BASE_URL}/NORAD/elements/gp.php?GROUP={category}&FORMAT=tle" 320 | logger.info(f"Consultando Celestrak categoría: {category}") 321 | 322 | response = self.session.get(url, timeout=API_TIMEOUTS['celestrak']) 323 | 324 | if response.status_code == 200: 325 | tle_data = response.text.strip() 326 | satellites_found = self._parse_tle_search(tle_data, search_term) 327 | results.extend(satellites_found) 328 | 329 | # Si encontramos suficientes resultados, no seguir buscando 330 | if len(results) >= 20: 331 | break 332 | 333 | except requests.RequestException as e: 334 | logger.warning(f"Error consultando Celestrak {category}: {e}") 335 | continue 336 | 337 | except Exception as e: 338 | logger.error(f"Error general en búsqueda Celestrak: {e}") 339 | 340 | return results[:15] # Limitar resultados 341 | 342 | def _search_n2yo(self, search_term: str) -> List[Dict]: 343 | """Busca usando N2YO API si está disponible.""" 344 | results = [] 345 | 346 | try: 347 | # N2YO no tiene búsqueda por nombre directa, 348 | # pero podemos intentar con satélites populares conocidos 349 | popular_satellites = { 350 | 'iss': 25544, 351 | 'hubble': 20580, 352 | 'starlink': [44713, 44714, 44715], # Algunos IDs de Starlink 353 | 'noaa': [43013, 37849, 28654], # NOAA satellites 354 | 'goes': [41866, 36411, 29155], # GOES satellites 355 | 'cosmos': [32037, 32038, 32039], # Algunos Cosmos 356 | 'gps': [32711, 35752, 38833] # Algunos GPS 357 | } 358 | 359 | # Buscar coincidencias en términos populares 360 | for keyword, sat_ids in popular_satellites.items(): 361 | if keyword in search_term.lower(): 362 | if isinstance(sat_ids, list): 363 | ids_to_check = sat_ids 364 | else: 365 | ids_to_check = [sat_ids] 366 | 367 | for sat_id in ids_to_check: 368 | try: 369 | url = f"{N2YO_BASE_URL}/tle/{sat_id}&apiKey={self.n2yo_api_key}" 370 | response = self.session.get(url, timeout=API_TIMEOUTS['n2yo']) 371 | 372 | if response.status_code == 200: 373 | data = response.json() 374 | if 'info' in data: 375 | results.append({ 376 | 'source': 'N2YO', 377 | 'norad_id': sat_id, 378 | 'name': data['info'].get('satname', '').strip(), 379 | 'operator': 'N/A', 380 | 'status': 'N/A', 381 | 'launch_date': data['info'].get('launchDate', '') 382 | }) 383 | 384 | except Exception as e: 385 | logger.warning(f"Error consultando N2YO ID {sat_id}: {e}") 386 | continue 387 | 388 | except Exception as e: 389 | logger.error(f"Error en búsqueda N2YO: {e}") 390 | 391 | return results 392 | 393 | def _parse_tle_search(self, tle_data: str, search_term: str) -> List[Dict]: 394 | """Parse TLE data y busca coincidencias con el término de búsqueda.""" 395 | results = [] 396 | 397 | try: 398 | lines = tle_data.split('\n') 399 | 400 | # Procesar TLEs en grupos de 3 líneas 401 | for i in range(0, len(lines) - 2, 3): 402 | if i + 2 < len(lines): 403 | name_line = lines[i].strip() 404 | line1 = lines[i + 1].strip() 405 | line2 = lines[i + 2].strip() 406 | 407 | # Verificar si el nombre contiene el término de búsqueda 408 | if (search_term in name_line.lower() and 409 | line1.startswith('1 ') and 410 | line2.startswith('2 ')): 411 | 412 | try: 413 | # Extraer NORAD ID de la primera línea TLE 414 | norad_id = int(line1[2:7]) 415 | 416 | results.append({ 417 | 'source': 'Celestrak', 418 | 'norad_id': norad_id, 419 | 'name': name_line.strip(), 420 | 'operator': 'N/A', 421 | 'status': 'Active', 422 | 'classification': line1[7:8], 423 | 'tle_available': True 424 | }) 425 | 426 | except (ValueError, IndexError) as e: 427 | logger.warning(f"Error procesando TLE: {e}") 428 | continue 429 | 430 | except Exception as e: 431 | logger.error(f"Error parseando TLE para búsqueda: {e}") 432 | 433 | return results 434 | 435 | def _remove_duplicate_satellites(self, results: List[Dict]) -> List[Dict]: 436 | """Elimina satélites duplicados basado en NORAD ID.""" 437 | seen_ids = set() 438 | unique_results = [] 439 | 440 | for satellite in results: 441 | norad_id = satellite.get('norad_id') 442 | if norad_id and norad_id not in seen_ids: 443 | seen_ids.add(norad_id) 444 | unique_results.append(satellite) 445 | 446 | # Ordenar por relevancia (SatNOGS primero, luego por nombre) 447 | def sort_key(sat): 448 | source_priority = {'SatNOGS': 0, 'Celestrak': 1, 'N2YO': 2} 449 | return (source_priority.get(sat.get('source', ''), 3), sat.get('name', '').lower()) 450 | 451 | unique_results.sort(key=sort_key) 452 | return unique_results 453 | --------------------------------------------------------------------------------