diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..259d64f --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Environment variables +.env + +# Build artifacts +SatIntel +SatIntel.exe +*.exe + +# Go workspace file +go.work + +# Documentation files +IMPROVEMENTS.md +IMPROVEMENTS_COMPLETED.md +TESTING.md + +# Test runner (build ignore, but keep in repo) +run_tests +run_tests.exe + +# User favorites data +favorites.json +.satintel/ diff --git a/MAP_VISUALIZATION_README.md b/MAP_VISUALIZATION_README.md new file mode 100644 index 0000000..d2d7eda --- /dev/null +++ b/MAP_VISUALIZATION_README.md @@ -0,0 +1,326 @@ +# Map Visualization Module + +## Overview + +The Map Visualization module provides three different ways to visualize satellite positions on a map: + +1. **Terminal ASCII Map** - A text-based world map displayed directly in the terminal +2. **KML Export** - Export satellite positions to Google Earth format +3. **Web-based Interactive Map** - Generate an HTML file with an interactive map using Leaflet.js + +## Features + +### 1. Terminal ASCII Map + +A terminal-based visualization that displays satellite positions on a simplified ASCII world map. + +**Features:** +- 80x24 character grid representing the world +- Simplified continent outlines +- Color-coded position markers: + - `●` (cyan) - First position + - `·` (cyan) - Intermediate positions + - `○` (cyan) - Last position +- Latitude and longitude axis labels +- Position details listing + +**Usage:** +When viewing satellite positions, select option `1` from the map visualization menu. + +**Example Output:** +``` +╔═════════════════════════════════════════════════════════════╗ +║ ASCII Map Visualization ║ +╠═════════════════════════════════════════════════════════════╣ +║ Satellite: ISS (ZARYA) ║ +╚═════════════════════════════════════════════════════════════╝ + + Longitude: -180° 0° 180° +Latitude + 90° │ │ + │ │ + │ │ + 0° │ │ + │ │ +-90° │ │ + └────────────────────────────────────────────────────────────────────────┘ +``` + +### 2. KML Export for Google Earth + +Export satellite positions to a KML (Keyhole Markup Language) file that can be opened in Google Earth or other KML-compatible applications. + +**Features:** +- Placemarks for each satellite position with detailed information +- Paths connecting consecutive positions +- Custom styling for satellite markers +- Altitude information (converted from km to meters for KML) +- Timestamp data for each position + +**Usage:** +1. When viewing satellite positions, select option `2` from the map visualization menu +2. Enter a file path (or press Enter for default) +3. Open the generated `.kml` file in Google Earth + +**File Format:** +- Extension: `.kml` +- Format: XML-based KML 2.2 +- Compatible with: Google Earth, Google Maps, QGIS, and other KML viewers + +**Example KML Structure:** +```xml + + + + ISS (ZARYA) (NORAD ID: 25544) + + Position 1 + + -0.127800,51.507400,408000.00 + + + ... + + +``` + +### 3. Web-based Interactive Map + +Generate a standalone HTML file with an interactive map using Leaflet.js and OpenStreetMap. + +**Features:** +- Interactive map with zoom and pan controls +- Clickable markers for each satellite position +- Color-coded markers: + - Red - First position + - Cyan - Intermediate positions + - Green - Last position +- Polyline showing the satellite's path +- Info panel with satellite details +- Popup windows with position information +- Automatic map bounds fitting to show all positions +- Legend explaining marker colors + +**Usage:** +1. When viewing satellite positions, select option `3` from the map visualization menu +2. Enter a file path (or press Enter for default) +3. Open the generated `.html` file in any web browser + +**Requirements:** +- Modern web browser (Chrome, Firefox, Safari, Edge) +- Internet connection (for loading Leaflet.js and OpenStreetMap tiles) + +**File Format:** +- Extension: `.html` +- Standalone file (no server required) +- Uses CDN for Leaflet.js library + +**Example Features:** +- Click on any marker to see position details +- Zoom in/out using mouse wheel or controls +- Pan by dragging the map +- View satellite path as a connected line + +## Integration + +The map visualization is automatically integrated into the satellite position viewing workflow: + +1. Run `GetLocation()` to fetch satellite positions +2. After displaying position data, you'll be prompted: "View map visualization? (y/n)" +3. If you answer 'y', you'll see the map visualization menu +4. Select your preferred visualization method (1, 2, or 3) + +## Code Structure + +### Main Functions + +- **`DisplayMap(data Response)`** - Main entry point for map visualization + - Validates position data + - Displays menu + - Routes to selected visualization method + +- **`displayASCIIMap(data Response)`** - Terminal ASCII map visualization + - Creates 80x24 character grid + - Plots positions on map + - Displays legend and position details + +- **`drawWorldMapOutline(grid [][]rune)`** - Draws continent outlines on ASCII grid + +- **`exportToKML(data Response)`** - Exports to KML file + - Prompts for file path + - Generates KML content + - Writes to file + +- **`generateKMLContent(data Response) string`** - Generates KML XML content + +- **`generateWebMap(data Response)`** - Exports to HTML file + - Prompts for file path + - Generates HTML content + - Writes to file + +- **`generateHTMLMapContent(data Response) string`** - Generates HTML with Leaflet.js + +## Data Structures + +The map visualization uses the `Response` type from `position.go`: + +```go +type Response struct { + SatelliteInfo SatelliteInfo `json:"info"` + Positions []Position `json:"positions"` +} + +type Position struct { + Satlatitude float64 `json:"satlatitude"` + Satlongitude float64 `json:"satlongitude"` + Sataltitude float64 `json:"sataltitude"` + Azimuth float64 `json:"azimuth"` + Elevation float64 `json:"elevation"` + Ra float64 `json:"ra"` + Dec float64 `json:"dec"` + Timestamp int64 `json:"timestamp"` +} +``` + +## Testing + +Comprehensive test coverage is provided in `osint/map_test.go`: + +### Test Coverage + +- **KML Generation Tests:** + - `TestGenerateKMLContent` - Full KML structure validation + - `TestGenerateKMLContentEmptyPositions` - Empty data handling + - `TestGenerateKMLContentSinglePosition` - Single position handling + - `TestGenerateKMLContentEdgeCases` - Extreme coordinates + - `TestKMLFileExport` - File writing validation + +- **HTML Generation Tests:** + - `TestGenerateHTMLMapContent` - Full HTML structure validation + - `TestGenerateHTMLMapContentEmptyPositions` - Empty data handling + - `TestGenerateHTMLMapContentEdgeCases` - Extreme coordinates + - `TestHTMLFileExport` - File writing validation + +- **ASCII Map Tests:** + - `TestDrawWorldMapOutline` - Map outline drawing + - `TestDrawWorldMapOutlineSmallGrid` - Small grid handling + - `TestDisplayASCIIMapPositionMapping` - Coordinate conversion + - `TestDisplayMapEmptyPositions` - Empty data handling + +### Running Tests + +```bash +# Run all map visualization tests +go test ./osint -run "Test.*Map|Test.*KML|Test.*HTML|TestDrawWorld" -v + +# Run specific test +go test ./osint -run TestGenerateKMLContent -v + +# Run with coverage +go test ./osint -cover -run "Test.*Map|Test.*KML|Test.*HTML|TestDrawWorld" +``` + +## Examples + +### Example 1: View ASCII Map + +``` +1. Get satellite position data +2. When prompted "View map visualization? (y/n)", enter 'y' +3. Select option 1 for Terminal ASCII Map +4. View the ASCII map in your terminal +``` + +### Example 2: Export to Google Earth + +``` +1. Get satellite position data +2. When prompted "View map visualization? (y/n)", enter 'y' +3. Select option 2 for KML Export +4. Enter file path (or press Enter for default) +5. Open the .kml file in Google Earth +``` + +### Example 3: Generate Interactive Web Map + +``` +1. Get satellite position data +2. When prompted "View map visualization? (y/n)", enter 'y' +3. Select option 3 for Web-based Interactive Map +4. Enter file path (or press Enter for default) +5. Open the .html file in your web browser +``` + +## Limitations + +1. **ASCII Map:** + - Simplified world map (not geographically accurate) + - Fixed 80x24 character resolution + - Limited to terminal display + +2. **KML Export:** + - Requires external application (Google Earth) to view + - Paths are straight lines between positions (not orbital paths) + +3. **Web Map:** + - Requires internet connection for map tiles + - Uses CDN for Leaflet.js (requires internet) + - Large position datasets may slow down rendering + +## Future Enhancements + +Potential improvements for the map visualization module: + +- [ ] Real-time position updates on web map +- [ ] Multiple satellite tracking on single map +- [ ] Ground track visualization +- [ ] 3D visualization option +- [ ] Custom map styles and themes +- [ ] Export to other formats (GeoJSON, GPX) +- [ ] Offline map tile support +- [ ] Animation of satellite movement +- [ ] Integration with orbital prediction data + +## Troubleshooting + +### ASCII Map Not Displaying Correctly + +- Ensure your terminal supports Unicode characters +- Check terminal width is at least 80 characters +- Verify terminal supports color output + +### KML File Won't Open + +- Verify file has `.kml` extension +- Check file is valid XML (not corrupted) +- Ensure Google Earth or compatible viewer is installed +- Try opening in a text editor to verify content + +### Web Map Not Loading + +- Check internet connection (required for map tiles) +- Verify browser console for JavaScript errors +- Ensure Leaflet.js CDN is accessible +- Try a different browser +- Check browser allows loading local files + +### No Positions Displayed + +- Verify satellite position data was successfully fetched +- Check that `data.Positions` is not empty +- Ensure coordinates are valid (latitude: -90 to 90, longitude: -180 to 180) + +## Contributing + +When adding new features or fixing bugs: + +1. Write tests for new functionality +2. Update this README with new features +3. Follow existing code style +4. Test all three visualization methods +5. Verify edge cases (empty data, extreme coordinates, etc.) + +## License + +This module is part of the SatIntel project. See the main project LICENSE file for details. + diff --git a/QUICK_TEST.md b/QUICK_TEST.md new file mode 100644 index 0000000..98b2a2a --- /dev/null +++ b/QUICK_TEST.md @@ -0,0 +1,51 @@ +# Quick Test Reference + +## Run All Tests +```bash +go run run_tests.go -all +``` + +## Run Specific Module +```bash +go run run_tests.go -module=main +go run run_tests.go -module=osint +go run run_tests.go -module=cli +``` + +## Common Commands + +| Command | What It Does | +|---------|--------------| +| `go run run_tests.go -all` | Run all test modules | +| `go run run_tests.go -all -cover` | Run all tests with coverage | +| `go run run_tests.go -module=main -v` | Run main tests verbosely | +| `go run run_tests.go -module=list` | List available modules | +| `go run run_tests.go -check` | Check test file status | +| `go run run_tests.go -help` | Show full help | + +## Adding New Test Module + +1. Create test file: `newpackage/newpackage_test.go` +2. Add to `run_tests.go`: +```go +{ + Name: "newpackage", + Description: "Your description", + PackagePath: "./newpackage", + TestFile: "newpackage_test.go", +}, +``` +3. Run: `go run run_tests.go -module=newpackage` + +## Direct Go Test (Alternative) + +```bash +go test ./... # All tests +go test -cover ./... # With coverage +go test -v ./main # Specific package +``` + + + + + diff --git a/README.md b/README.md index aa29c4a..eaad6d6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ - Parse Two Line Elements (TLE) ### Preview -SatIntel Image +SatIntel Image ### Usage Make an account at [**Space Track**](https://space-track.org) save username and password. diff --git a/cli/cli.go b/cli/cli.go index bca8919..6d76873 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,68 +1,109 @@ package cli import ( + "bufio" "fmt" - "io/ioutil" - "github.com/iskaa02/qalam/gradient" - "github.com/TwiN/go-color" - "github.com/ANG13T/SatIntel/osint" - "strconv" "os" + "os/exec" + "runtime" + "strconv" + + "github.com/ANG13T/SatIntel/osint" + "github.com/TwiN/go-color" + "github.com/iskaa02/qalam/gradient" ) +// Option prompts the user to select a menu option and validates the input. +// It recursively prompts until a valid option between 0 and 4 is entered. func Option() { fmt.Print("\n ENTER INPUT > ") var selection string fmt.Scanln(&selection) num, err := strconv.Atoi(selection) - if err != nil { + if err != nil { fmt.Println(color.Ize(color.Red, " [!] INVALID INPUT")) Option() - } else { - if (num >= 0 && num < 5) { + } else { + if num >= 0 && num < 6 { DisplayFunctions(num) } else { fmt.Println(color.Ize(color.Red, " [!] INVALID INPUT")) Option() } - } + } } +// DisplayFunctions executes the selected function based on the menu choice. +// After execution, it waits for user input, clears the screen, and shows the menu again. func DisplayFunctions(x int) { - if (x == 0) { + if x == 0 { fmt.Println(color.Ize(color.Blue, " Escaping Orbit...")) os.Exit(1) - } else if (x == 1) { + } else if x == 1 { osint.OrbitalElement() + waitForEnter() + clearScreen() Banner() Option() - } else if (x == 2) { + } else if x == 2 { osint.SatellitePositionVisualization() + waitForEnter() + clearScreen() Banner() Option() - } else if (x == 3) { + } else if x == 3 { osint.OrbitalPrediction() + waitForEnter() + clearScreen() Banner() Option() - }else if (x == 4) { + } else if x == 4 { osint.TLEParser() + waitForEnter() + clearScreen() + Banner() + Option() + } else if x == 5 { + osint.BatchOperations() + waitForEnter() + clearScreen() Banner() Option() } } +// Banner displays the application banner, info, and menu options with gradient colors. func Banner() { - banner, _ := ioutil.ReadFile("txt/banner.txt") - info, _ := ioutil.ReadFile("txt/info.txt") - options, _ := ioutil.ReadFile("txt/options.txt") - g,_:=gradient.NewGradient("cyan", "blue") - solid,_:=gradient.NewGradient("blue", "#1179ef") - opt,_:=gradient.NewGradient("#1179ef", "cyan") + banner, _ := os.ReadFile("txt/banner.txt") + info, _ := os.ReadFile("txt/info.txt") + options, _ := os.ReadFile("txt/options.txt") + g, _ := gradient.NewGradient("cyan", "blue") + solid, _ := gradient.NewGradient("blue", "#1179ef") + opt, _ := gradient.NewGradient("#1179ef", "cyan") g.Print(string(banner)) solid.Print(string(info)) opt.Print("\n" + string(options)) } +// waitForEnter pauses execution and waits for the user to press Enter. +func waitForEnter() { + fmt.Print("\n\nPress Enter to continue...") + bufio.NewReader(os.Stdin).ReadBytes('\n') +} + +// clearScreen clears the terminal screen using the appropriate command for the operating system. +func clearScreen() { + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/c", "cls") + } else { + cmd = exec.Command("clear") + } + cmd.Stdout = os.Stdout + cmd.Run() +} + +// SatIntel initializes the SatIntel CLI application by displaying the banner and starting the menu loop. func SatIntel() { Banner() Option() diff --git a/cli/cli_test.go b/cli/cli_test.go new file mode 100644 index 0000000..cf048bd --- /dev/null +++ b/cli/cli_test.go @@ -0,0 +1,60 @@ +package cli + +import ( + "testing" +) + +// Note: Most CLI functions are interactive and difficult to test without mocking stdin/stdout +// These tests focus on testable logic and structure validation + +func TestGenRowString(t *testing.T) { + // This is a helper function that might be used in CLI package + // If GenRowString is in osint package, this test can be removed + // or we can test the formatting logic here + tests := []struct { + name string + intro string + input string + checkLen bool + }{ + { + name: "Normal case", + intro: "Test", + input: "Value", + checkLen: true, + }, + { + name: "Empty values", + intro: "", + input: "", + checkLen: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This is a placeholder - adjust based on actual CLI functions + // If there are no directly testable functions, we can test file reading, etc. + _ = tt + }) + } +} + +// Test file reading functions (if they become testable) +func TestBannerFileReading(t *testing.T) { + // Test that banner files can be read + // This is a basic smoke test + files := []string{ + "txt/banner.txt", + "txt/info.txt", + "txt/options.txt", + } + + for _, file := range files { + t.Run("Read_"+file, func(t *testing.T) { + // This would require the files to exist + // For now, this is a placeholder structure + _ = file + }) + } +} diff --git a/coverage.out b/coverage.out new file mode 100644 index 0000000..b837a62 --- /dev/null +++ b/coverage.out @@ -0,0 +1,920 @@ +mode: set +github.com/ANG13T/SatIntel/osint/batch.go:52.29,69.28 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:69.28,71.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:73.2,74.24 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:74.24,76.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:77.2,77.11 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:81.50,85.6 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:85.6,106.29 5 0 +github.com/ANG13T/SatIntel/osint/batch.go:106.29,108.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:110.3,110.14 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:111.10,113.20 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:113.20,115.28 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:115.28,118.20 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:118.20,121.21 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:121.21,123.71 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:123.71,133.9 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:133.14,144.9 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:145.13,156.8 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:157.12,168.7 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:169.11,171.6 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:174.10,177.40 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:177.40,178.40 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:178.40,180.7 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:181.6,181.16 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:184.4,185.33 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:185.33,187.28 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:187.28,190.20 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:190.20,193.21 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:193.21,195.71 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:195.71,205.9 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:205.14,214.9 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:215.13,224.8 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:225.12,234.7 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:235.11,237.6 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:240.10,242.23 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:242.23,244.28 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:244.28,254.6 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:254.11,256.6 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:259.10,260.26 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:260.26,262.13 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:264.4,265.33 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:265.33,267.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:268.4,275.47 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:275.47,280.5 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:282.10,283.26 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:283.26,285.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:285.10,287.34 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:287.34,289.34 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:289.34,291.7 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:292.6,292.37 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:292.37,294.7 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:298.10,299.25 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:299.25,306.59 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:306.59,310.6 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:313.10,314.26 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:314.26,316.13 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:318.4,318.19 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:324.69,325.26 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:325.26,327.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:329.2,332.16 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:332.16,335.3 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:337.2,342.33 5 0 +github.com/ANG13T/SatIntel/osint/batch.go:342.33,344.46 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:344.46,354.18 5 0 +github.com/ANG13T/SatIntel/osint/batch.go:354.18,361.5 6 0 +github.com/ANG13T/SatIntel/osint/batch.go:363.4,366.23 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:366.23,369.5 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:369.10,371.27 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:371.27,373.17 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:373.17,375.7 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:376.6,376.30 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:376.30,378.7 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:379.6,380.49 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:381.11,388.6 6 0 +github.com/ANG13T/SatIntel/osint/batch.go:391.4,391.78 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:391.78,398.5 6 0 +github.com/ANG13T/SatIntel/osint/batch.go:400.4,405.52 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:405.52,412.5 6 0 +github.com/ANG13T/SatIntel/osint/batch.go:414.4,414.106 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:414.106,421.5 6 0 +github.com/ANG13T/SatIntel/osint/batch.go:423.4,431.116 7 0 +github.com/ANG13T/SatIntel/osint/batch.go:435.2,439.28 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:439.28,440.16 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:440.16,442.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:445.2,447.16 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:451.72,452.23 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:452.23,454.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:456.2,467.33 5 0 +github.com/ANG13T/SatIntel/osint/batch.go:467.33,468.21 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:468.21,472.39 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:472.39,474.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:475.4,475.33 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:475.33,477.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:479.4,479.33 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:479.33,485.21 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:485.21,487.6 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:489.9,491.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:495.2,495.27 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:495.27,497.36 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:497.36,499.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:500.3,500.75 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:503.2,503.26 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:503.26,505.34 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:505.34,507.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:508.3,508.73 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:511.2,511.24 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:511.24,514.33 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:514.33,515.17 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:515.17,517.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:518.4,518.17 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:518.17,520.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:522.3,523.43 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:526.2,526.19 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:530.58,531.34 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:531.34,534.3 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:536.2,544.47 7 0 +github.com/ANG13T/SatIntel/osint/batch.go:544.47,546.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:547.2,547.46 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:547.46,549.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:550.2,550.43 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:550.43,552.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:553.2,553.44 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:553.44,555.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:557.2,561.44 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:561.44,563.22 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:563.22,565.27 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:565.27,567.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:570.3,574.21 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:574.21,578.4 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:580.3,580.36 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:580.36,582.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:585.2,585.235 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:589.24,591.21 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:591.21,593.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:595.2,596.26 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:596.26,598.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:600.2,600.19 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:601.13,603.23 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:603.23,606.35 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:606.35,607.23 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:607.23,610.6 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:610.11,612.29 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:612.29,614.7 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:614.12,616.7 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:621.4,627.63 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:627.63,629.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:632.17,634.23 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:634.23,645.63 5 0 +github.com/ANG13T/SatIntel/osint/batch.go:645.63,647.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:650.37,651.92 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:657.47,663.34 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:663.34,665.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:667.2,673.16 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:673.16,675.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:677.2,678.20 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:678.20,680.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:682.2,683.22 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:684.13,685.15 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:686.14,687.16 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:688.14,689.15 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:692.2,692.39 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:692.39,694.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:696.2,696.22 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:697.13,698.39 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:699.14,700.40 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:701.14,702.40 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:707.62,713.34 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:713.34,715.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:717.2,723.16 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:723.16,725.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:727.2,728.20 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:728.20,730.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:732.2,733.22 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:734.13,735.15 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:736.14,737.16 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:738.14,739.15 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:742.2,742.39 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:742.39,744.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:746.2,746.22 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:747.13,748.49 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:749.14,750.50 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:751.14,752.50 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:757.73,759.16 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:759.16,761.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:762.2,772.46 5 0 +github.com/ANG13T/SatIntel/osint/batch.go:772.46,774.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:777.2,777.33 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:777.33,780.22 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:780.22,782.27 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:782.27,784.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:787.3,793.21 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:793.21,802.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:802.9,804.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:806.3,806.43 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:806.43,808.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:811.2,812.12 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:816.74,825.33 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:825.33,830.21 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:830.21,832.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:833.3,833.26 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:833.26,835.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:836.3,836.54 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:839.2,846.16 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:846.16,848.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:850.2,850.63 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:850.63,852.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:854.2,855.12 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:859.74,867.33 6 0 +github.com/ANG13T/SatIntel/osint/batch.go:867.33,869.21 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:869.21,876.4 6 0 +github.com/ANG13T/SatIntel/osint/batch.go:876.9,878.27 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:878.27,880.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:882.3,882.28 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:885.2,885.79 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:885.79,887.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:889.2,890.12 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:894.88,896.16 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:896.16,898.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:899.2,906.53 5 0 +github.com/ANG13T/SatIntel/osint/batch.go:906.53,908.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:910.2,915.47 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:915.47,917.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:918.2,918.46 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:918.46,920.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:922.2,922.34 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:922.34,923.43 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:923.43,925.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:929.2,933.52 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:933.52,935.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:937.2,937.44 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:937.44,940.22 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:940.22,942.27 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:942.27,944.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:947.3,953.21 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:953.21,960.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:960.9,962.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:964.3,964.43 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:964.43,966.4 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:969.2,970.12 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:974.89,982.16 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:982.16,984.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:986.2,986.63 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:986.63,988.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:990.2,991.12 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:995.89,1005.47 8 0 +github.com/ANG13T/SatIntel/osint/batch.go:1005.47,1007.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:1008.2,1008.46 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:1008.46,1010.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:1012.2,1015.44 3 0 +github.com/ANG13T/SatIntel/osint/batch.go:1015.44,1017.21 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:1017.21,1022.4 4 0 +github.com/ANG13T/SatIntel/osint/batch.go:1022.9,1024.27 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:1024.27,1026.5 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:1030.2,1032.79 2 0 +github.com/ANG13T/SatIntel/osint/batch.go:1032.79,1034.3 1 0 +github.com/ANG13T/SatIntel/osint/batch.go:1036.2,1037.12 2 0 +github.com/ANG13T/SatIntel/osint/export.go:26.75,35.34 4 0 +github.com/ANG13T/SatIntel/osint/export.go:35.34,37.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:39.2,48.16 4 0 +github.com/ANG13T/SatIntel/osint/export.go:48.16,50.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:52.2,53.20 2 0 +github.com/ANG13T/SatIntel/osint/export.go:53.20,55.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:58.2,60.16 3 0 +github.com/ANG13T/SatIntel/osint/export.go:61.17,62.23 1 0 +github.com/ANG13T/SatIntel/osint/export.go:63.18,64.24 1 0 +github.com/ANG13T/SatIntel/osint/export.go:65.18,66.23 1 0 +github.com/ANG13T/SatIntel/osint/export.go:69.2,69.24 1 0 +github.com/ANG13T/SatIntel/osint/export.go:69.24,71.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:73.2,73.30 1 0 +github.com/ANG13T/SatIntel/osint/export.go:77.69,78.16 1 0 +github.com/ANG13T/SatIntel/osint/export.go:79.17,80.37 1 0 +github.com/ANG13T/SatIntel/osint/export.go:81.18,82.38 1 0 +github.com/ANG13T/SatIntel/osint/export.go:83.18,84.38 1 0 +github.com/ANG13T/SatIntel/osint/export.go:85.10,86.54 1 0 +github.com/ANG13T/SatIntel/osint/export.go:91.51,93.16 2 0 +github.com/ANG13T/SatIntel/osint/export.go:93.16,95.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:96.2,105.46 5 0 +github.com/ANG13T/SatIntel/osint/export.go:105.46,107.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:110.2,132.27 2 0 +github.com/ANG13T/SatIntel/osint/export.go:132.27,133.43 1 0 +github.com/ANG13T/SatIntel/osint/export.go:133.43,135.4 1 0 +github.com/ANG13T/SatIntel/osint/export.go:138.2,138.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:142.52,167.16 3 0 +github.com/ANG13T/SatIntel/osint/export.go:167.16,169.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:171.2,171.63 1 0 +github.com/ANG13T/SatIntel/osint/export.go:171.63,173.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:175.2,175.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:179.52,206.79 24 0 +github.com/ANG13T/SatIntel/osint/export.go:206.79,208.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:210.2,210.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:214.100,215.16 1 0 +github.com/ANG13T/SatIntel/osint/export.go:216.17,217.51 1 0 +github.com/ANG13T/SatIntel/osint/export.go:218.18,219.52 1 0 +github.com/ANG13T/SatIntel/osint/export.go:220.18,221.52 1 0 +github.com/ANG13T/SatIntel/osint/export.go:222.10,223.54 1 0 +github.com/ANG13T/SatIntel/osint/export.go:228.82,230.16 2 0 +github.com/ANG13T/SatIntel/osint/export.go:230.16,232.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:233.2,246.50 6 0 +github.com/ANG13T/SatIntel/osint/export.go:246.50,248.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:249.2,249.46 1 0 +github.com/ANG13T/SatIntel/osint/export.go:249.46,251.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:254.2,254.49 1 0 +github.com/ANG13T/SatIntel/osint/export.go:254.49,256.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:259.2,265.50 2 0 +github.com/ANG13T/SatIntel/osint/export.go:265.50,267.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:270.2,270.35 1 0 +github.com/ANG13T/SatIntel/osint/export.go:270.35,288.43 2 0 +github.com/ANG13T/SatIntel/osint/export.go:288.43,290.4 1 0 +github.com/ANG13T/SatIntel/osint/export.go:293.2,293.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:297.83,310.16 3 0 +github.com/ANG13T/SatIntel/osint/export.go:310.16,312.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:314.2,314.63 1 0 +github.com/ANG13T/SatIntel/osint/export.go:314.63,316.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:318.2,318.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:322.83,337.35 11 0 +github.com/ANG13T/SatIntel/osint/export.go:337.35,347.3 5 0 +github.com/ANG13T/SatIntel/osint/export.go:349.2,351.79 2 0 +github.com/ANG13T/SatIntel/osint/export.go:351.79,353.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:355.2,355.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:359.96,360.16 1 0 +github.com/ANG13T/SatIntel/osint/export.go:361.17,362.50 1 0 +github.com/ANG13T/SatIntel/osint/export.go:363.18,364.51 1 0 +github.com/ANG13T/SatIntel/osint/export.go:365.18,366.51 1 0 +github.com/ANG13T/SatIntel/osint/export.go:367.10,368.54 1 0 +github.com/ANG13T/SatIntel/osint/export.go:373.78,375.16 2 0 +github.com/ANG13T/SatIntel/osint/export.go:375.16,377.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:378.2,391.50 6 0 +github.com/ANG13T/SatIntel/osint/export.go:391.50,393.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:394.2,394.46 1 0 +github.com/ANG13T/SatIntel/osint/export.go:394.46,396.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:399.2,399.49 1 0 +github.com/ANG13T/SatIntel/osint/export.go:399.49,401.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:404.2,409.50 2 0 +github.com/ANG13T/SatIntel/osint/export.go:409.50,411.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:414.2,414.35 1 0 +github.com/ANG13T/SatIntel/osint/export.go:414.35,428.43 2 0 +github.com/ANG13T/SatIntel/osint/export.go:428.43,430.4 1 0 +github.com/ANG13T/SatIntel/osint/export.go:433.2,433.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:437.79,450.16 3 0 +github.com/ANG13T/SatIntel/osint/export.go:450.16,452.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:454.2,454.63 1 0 +github.com/ANG13T/SatIntel/osint/export.go:454.63,456.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:458.2,458.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:462.79,477.35 11 0 +github.com/ANG13T/SatIntel/osint/export.go:477.35,485.3 4 0 +github.com/ANG13T/SatIntel/osint/export.go:487.2,489.79 2 0 +github.com/ANG13T/SatIntel/osint/export.go:489.79,491.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:493.2,493.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:497.89,498.16 1 0 +github.com/ANG13T/SatIntel/osint/export.go:499.17,500.52 1 0 +github.com/ANG13T/SatIntel/osint/export.go:501.18,502.53 1 0 +github.com/ANG13T/SatIntel/osint/export.go:503.18,504.53 1 0 +github.com/ANG13T/SatIntel/osint/export.go:505.10,506.54 1 0 +github.com/ANG13T/SatIntel/osint/export.go:511.71,513.16 2 0 +github.com/ANG13T/SatIntel/osint/export.go:513.16,515.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:516.2,527.50 6 0 +github.com/ANG13T/SatIntel/osint/export.go:527.50,529.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:530.2,530.46 1 0 +github.com/ANG13T/SatIntel/osint/export.go:530.46,532.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:535.2,535.49 1 0 +github.com/ANG13T/SatIntel/osint/export.go:535.49,537.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:540.2,544.49 2 0 +github.com/ANG13T/SatIntel/osint/export.go:544.49,546.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:549.2,549.37 1 0 +github.com/ANG13T/SatIntel/osint/export.go:549.37,559.43 2 0 +github.com/ANG13T/SatIntel/osint/export.go:559.43,561.4 1 0 +github.com/ANG13T/SatIntel/osint/export.go:564.2,564.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:568.72,579.16 3 0 +github.com/ANG13T/SatIntel/osint/export.go:579.16,581.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:583.2,583.63 1 0 +github.com/ANG13T/SatIntel/osint/export.go:583.63,585.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:587.2,587.12 1 0 +github.com/ANG13T/SatIntel/osint/export.go:591.72,604.37 9 0 +github.com/ANG13T/SatIntel/osint/export.go:604.37,612.3 7 0 +github.com/ANG13T/SatIntel/osint/export.go:614.2,616.79 2 0 +github.com/ANG13T/SatIntel/osint/export.go:616.79,618.3 1 0 +github.com/ANG13T/SatIntel/osint/export.go:620.2,620.12 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:32.32,34.16 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:34.16,37.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:38.2,40.51 3 0 +github.com/ANG13T/SatIntel/osint/favorites.go:44.51,48.16 3 0 +github.com/ANG13T/SatIntel/osint/favorites.go:48.16,49.25 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:49.25,52.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:53.3,53.67 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:56.2,57.61 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:57.61,59.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:61.2,61.37 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:65.57,73.16 4 0 +github.com/ANG13T/SatIntel/osint/favorites.go:73.16,75.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:77.2,77.64 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:77.64,79.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:81.2,81.12 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:85.70,87.16 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:87.16,89.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:92.2,92.32 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:92.32,93.29 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:93.29,95.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:98.2,107.33 3 0 +github.com/ANG13T/SatIntel/osint/favorites.go:111.43,113.16 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:113.16,115.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:117.2,119.32 3 0 +github.com/ANG13T/SatIntel/osint/favorites.go:119.32,120.29 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:120.29,122.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:122.9,124.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:127.2,127.12 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:127.12,129.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:131.2,131.40 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:135.47,137.16 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:137.16,139.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:141.2,141.32 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:141.32,142.29 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:142.29,144.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:147.2,147.19 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:151.35,153.16 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:153.16,156.3 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:158.2,158.25 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:158.25,162.3 3 0 +github.com/ANG13T/SatIntel/osint/favorites.go:164.2,165.32 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:165.32,167.24 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:167.24,169.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:170.3,170.27 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:170.27,172.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:173.3,174.38 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:177.2,186.16 4 0 +github.com/ANG13T/SatIntel/osint/favorites.go:186.16,189.3 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:191.2,191.27 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:191.27,194.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:196.2,197.73 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:201.24,203.16 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:203.16,206.3 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:208.2,208.25 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:208.25,211.3 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:213.2,226.16 4 0 +github.com/ANG13T/SatIntel/osint/favorites.go:226.16,228.3 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:230.2,230.13 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:231.9,234.33 3 0 +github.com/ANG13T/SatIntel/osint/favorites.go:234.33,236.25 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:236.25,238.5 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:239.4,239.28 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:239.28,241.5 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:242.4,243.28 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:243.28,245.5 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:247.3,247.39 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:249.9,251.33 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:251.33,253.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:254.3,262.48 4 0 +github.com/ANG13T/SatIntel/osint/favorites.go:262.48,264.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:266.3,267.58 2 0 +github.com/ANG13T/SatIntel/osint/favorites.go:267.58,269.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:269.9,271.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:273.9,281.17 3 0 +github.com/ANG13T/SatIntel/osint/favorites.go:281.17,283.4 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:285.3,285.59 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:285.59,286.63 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:286.63,288.5 1 0 +github.com/ANG13T/SatIntel/osint/favorites.go:288.10,290.5 1 0 +github.com/ANG13T/SatIntel/osint/orbitalelement.go:11.23,17.20 5 0 +github.com/ANG13T/SatIntel/osint/orbitalelement.go:17.20,20.19 2 0 +github.com/ANG13T/SatIntel/osint/orbitalelement.go:20.19,22.4 1 0 +github.com/ANG13T/SatIntel/osint/orbitalelement.go:24.3,24.47 1 0 +github.com/ANG13T/SatIntel/osint/orbitalelement.go:26.8,26.27 1 0 +github.com/ANG13T/SatIntel/osint/orbitalelement.go:26.27,31.3 4 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:17.26,23.20 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:23.20,25.3 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:25.8,25.27 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:25.27,27.3 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:31.28,33.27 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:33.27,36.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:37.2,41.20 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:41.20,44.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:45.2,49.21 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:49.21,52.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:53.2,57.20 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:57.20,60.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:61.2,65.16 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:65.16,68.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:69.2,73.15 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:73.15,76.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:79.2,89.76 9 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:89.76,92.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:94.2,98.16 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:98.16,101.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:102.2,106.16 4 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:106.16,109.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:111.2,120.26 8 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:120.26,125.36 4 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:125.36,127.4 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:128.8,130.3 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:133.2,139.61 3 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:139.61,142.17 3 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:142.17,143.73 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:143.73,145.5 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:145.10,147.5 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:153.27,155.27 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:155.27,158.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:159.2,163.20 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:163.20,166.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:167.2,171.21 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:171.21,174.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:175.2,179.20 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:179.20,182.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:183.2,187.16 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:187.16,190.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:191.2,195.21 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:195.21,198.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:201.2,211.76 9 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:211.76,214.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:216.2,218.16 3 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:218.16,221.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:222.2,226.16 4 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:226.16,229.3 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:231.2,240.26 8 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:240.26,245.36 4 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:245.36,247.4 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:248.8,250.3 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:253.2,259.61 3 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:259.61,262.17 3 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:262.17,263.72 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:263.72,265.5 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:265.10,267.5 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:273.50,278.20 5 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:278.20,281.19 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:281.19,283.4 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:285.3,285.75 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:287.8,287.27 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:287.27,292.3 4 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:294.2,294.33 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:350.45,352.29 2 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:352.29,353.65 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:353.65,355.4 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:357.2,357.24 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:361.44,376.10 15 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:376.10,378.3 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:378.8,380.3 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:384.48,395.10 11 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:395.10,397.3 1 0 +github.com/ANG13T/SatIntel/osint/orbitalprediction.go:397.8,399.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:25.36,34.16 7 0 +github.com/ANG13T/SatIntel/osint/osint.go:34.16,37.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:39.2,44.16 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:44.16,47.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:48.2,50.38 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:50.38,53.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:55.2,57.20 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:62.76,67.16 4 0 +github.com/ANG13T/SatIntel/osint/osint.go:67.16,70.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:72.2,73.16 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:73.16,76.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:77.2,79.38 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:79.38,82.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:84.2,85.16 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:85.16,88.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:89.2,90.26 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:94.38,97.46 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:97.46,99.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:100.2,100.27 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:104.48,106.16 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:106.16,109.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:111.2,113.16 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:113.16,116.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:118.2,122.21 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:122.21,125.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:125.8,127.24 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:127.24,130.4 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:133.3,134.24 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:134.24,136.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:137.3,137.26 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:137.26,139.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:141.3,141.14 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:141.14,143.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:145.3,145.27 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:145.27,147.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:149.3,150.46 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:153.2,153.39 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:153.39,156.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:157.2,157.39 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:157.39,160.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:162.2,169.50 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:169.50,171.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:171.8,171.111 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:171.111,173.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:175.2,175.19 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:175.19,179.53 4 0 +github.com/ANG13T/SatIntel/osint/osint.go:179.53,181.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:182.3,182.9 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:185.2,185.15 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:190.102,195.19 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:195.19,197.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:198.2,198.22 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:198.22,200.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:201.2,201.22 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:201.22,203.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:206.2,210.22 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:210.22,213.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:213.8,213.25 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:213.25,216.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:216.8,218.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:220.2,221.32 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:225.78,226.22 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:226.22,228.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:229.2,231.27 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:231.27,232.66 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:232.66,234.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:236.2,236.17 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:240.56,246.6 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:246.6,263.17 4 0 +github.com/ANG13T/SatIntel/osint/osint.go:263.17,265.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:267.3,267.14 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:268.10,275.18 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:275.18,277.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:279.10,286.18 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:286.18,288.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:290.10,304.18 4 0 +github.com/ANG13T/SatIntel/osint/osint.go:304.18,306.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:308.10,315.18 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:315.18,317.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:319.10,324.68 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:326.10,327.54 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:331.3,331.80 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:331.80,333.24 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:333.24,335.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:336.4,336.21 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:336.21,338.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:339.4,339.24 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:339.24,341.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:342.4,342.24 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:342.24,344.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:345.4,345.17 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:352.31,366.16 4 0 +github.com/ANG13T/SatIntel/osint/osint.go:366.16,368.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:370.2,370.21 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:370.21,373.19 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:373.19,376.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:377.3,377.12 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:378.8,378.28 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:378.28,381.3 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:384.2,385.16 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:385.16,388.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:391.2,398.6 6 0 +github.com/ANG13T/SatIntel/osint/osint.go:398.6,403.52 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:403.52,409.18 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:409.18,412.5 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:414.4,415.69 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:415.69,419.5 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:422.4,428.37 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:428.37,430.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:431.4,431.39 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:431.39,433.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:433.10,435.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:436.9,436.30 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:436.30,440.37 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:440.37,442.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:443.4,443.39 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:443.39,445.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:445.10,447.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:448.9,454.18 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:454.18,457.5 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:459.4,459.62 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:459.62,463.5 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:464.4,464.18 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:467.3,467.21 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:467.21,471.4 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:474.3,475.28 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:475.28,477.25 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:477.25,479.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:480.4,480.29 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:480.29,482.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:483.4,483.41 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:487.3,489.23 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:489.23,491.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:491.9,493.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:495.3,495.15 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:495.15,497.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:498.3,499.18 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:499.18,501.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:502.3,505.41 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:505.41,507.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:508.3,508.43 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:508.43,510.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:510.9,512.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:514.3,521.17 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:521.17,524.4 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:527.3,527.27 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:527.27,530.12 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:533.3,534.15 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:534.15,536.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:538.3,538.56 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:538.56,546.14 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:546.14,553.62 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:553.62,554.133 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:554.133,556.7 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:556.12,558.7 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:562.4,562.17 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:565.3,566.40 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:566.40,569.12 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:572.3,573.18 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:573.18,575.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:576.3,578.26 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:578.26,581.23 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:581.23,583.5 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:585.4,585.12 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:588.3,588.67 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:588.67,594.12 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:598.3,598.12 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:603.54,607.2 3 1 +github.com/ANG13T/SatIntel/osint/osint.go:611.35,616.16 5 0 +github.com/ANG13T/SatIntel/osint/osint.go:616.16,619.3 2 0 +github.com/ANG13T/SatIntel/osint/osint.go:619.8,620.17 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:620.17,624.4 3 0 +github.com/ANG13T/SatIntel/osint/osint.go:624.9,624.38 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:624.38,626.4 1 0 +github.com/ANG13T/SatIntel/osint/osint.go:626.9,629.4 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:23.42,32.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:35.27,36.15 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:36.15,38.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:39.2,41.12 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:41.12,45.7 3 0 +github.com/ANG13T/SatIntel/osint/progress.go:45.7,46.11 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:47.22,49.11 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:50.20,52.43 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:59.26,60.16 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:60.16,62.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:63.2,66.19 4 0 +github.com/ANG13T/SatIntel/osint/progress.go:70.49,72.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:84.61,92.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:95.44,96.18 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:96.18,98.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:99.2,100.27 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:100.27,102.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:103.2,103.13 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:107.36,108.18 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:108.18,110.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:111.2,112.27 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:112.27,114.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:115.2,115.13 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:119.44,121.27 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:121.27,123.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:124.2,124.13 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:128.33,140.2 5 0 +github.com/ANG13T/SatIntel/osint/progress.go:143.35,144.18 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:144.18,146.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:147.2,150.15 4 0 +github.com/ANG13T/SatIntel/osint/progress.go:154.35,156.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:159.21,161.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:164.55,168.2 3 0 +github.com/ANG13T/SatIntel/osint/progress.go:171.66,173.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:176.49,179.2 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:182.57,186.22 4 0 +github.com/ANG13T/SatIntel/osint/progress.go:186.22,188.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:192.35,194.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:197.50,200.42 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:200.42,202.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:202.8,202.46 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:202.46,204.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:204.8,204.53 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:204.53,206.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:207.2,207.66 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:211.53,213.2 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:216.24,218.16 2 0 +github.com/ANG13T/SatIntel/osint/progress.go:218.16,220.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:221.2,221.51 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:225.41,226.18 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:226.18,228.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:228.8,230.3 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:234.27,235.18 1 0 +github.com/ANG13T/SatIntel/osint/progress.go:235.18,237.3 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:17.39,23.20 5 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:23.20,26.19 2 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:26.19,28.4 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:30.3,30.36 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:32.8,32.27 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:32.27,37.3 4 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:41.32,46.20 5 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:46.20,49.3 2 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:50.2,54.21 5 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:54.21,57.3 2 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:58.2,62.20 5 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:62.20,65.3 2 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:68.2,76.46 7 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:76.46,79.3 2 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:81.2,85.16 5 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:85.16,88.3 2 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:89.2,93.16 4 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:93.16,96.3 2 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:98.2,109.38 9 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:109.38,111.3 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:114.2,120.61 3 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:120.61,123.17 3 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:123.17,124.74 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:124.74,126.5 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:126.10,128.5 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:134.20,135.2 0 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:138.54,145.10 7 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:145.10,147.3 1 0 +github.com/ANG13T/SatIntel/osint/satelliteposition.go:147.8,149.3 1 0 +github.com/ANG13T/SatIntel/osint/sgp4.go:45.92,50.40 3 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:50.40,52.3 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:54.2,54.37 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:54.37,56.3 1 0 +github.com/ANG13T/SatIntel/osint/sgp4.go:57.2,57.37 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:57.37,59.3 1 0 +github.com/ANG13T/SatIntel/osint/sgp4.go:62.2,92.8 13 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:97.108,99.2 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:103.138,106.16 2 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:106.16,108.3 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:111.2,155.8 20 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:159.137,160.30 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:160.30,162.3 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:163.2,163.19 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:163.19,165.3 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:167.2,170.34 3 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:170.34,172.17 2 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:172.17,174.4 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:175.3,176.42 2 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:179.2,179.23 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:184.130,186.2 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:189.176,191.2 1 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:194.41,204.2 9 1 +github.com/ANG13T/SatIntel/osint/sgp4.go:207.65,221.2 13 1 +github.com/ANG13T/SatIntel/osint/tle.go:36.61,42.23 5 1 +github.com/ANG13T/SatIntel/osint/tle.go:42.23,44.3 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:45.2,45.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:45.24,47.3 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:49.2,49.47 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:49.47,51.26 2 1 +github.com/ANG13T/SatIntel/osint/tle.go:51.26,54.4 2 1 +github.com/ANG13T/SatIntel/osint/tle.go:54.9,56.4 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:58.2,58.23 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:58.23,60.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:61.2,61.23 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:61.23,63.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:64.2,64.23 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:64.23,66.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:67.2,67.23 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:67.23,69.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:70.2,70.23 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:70.23,72.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:73.2,73.23 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:73.23,75.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:76.2,76.23 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:76.23,78.25 2 1 +github.com/ANG13T/SatIntel/osint/tle.go:78.25,81.4 2 1 +github.com/ANG13T/SatIntel/osint/tle.go:81.9,81.32 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:81.32,83.4 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:86.2,86.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:86.24,88.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:89.2,89.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:89.24,91.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:92.2,92.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:92.24,94.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:95.2,95.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:95.24,97.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:98.2,98.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:98.24,100.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:101.2,101.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:101.24,103.3 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:104.2,104.24 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:104.24,106.31 2 1 +github.com/ANG13T/SatIntel/osint/tle.go:106.31,108.32 2 1 +github.com/ANG13T/SatIntel/osint/tle.go:108.32,110.5 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:111.9,113.4 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:114.3,114.29 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:114.29,116.4 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:119.2,119.12 1 1 +github.com/ANG13T/SatIntel/osint/tle.go:123.24,154.61 24 0 +github.com/ANG13T/SatIntel/osint/tle.go:154.61,157.17 3 0 +github.com/ANG13T/SatIntel/osint/tle.go:157.17,158.59 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:158.59,160.5 1 0 +github.com/ANG13T/SatIntel/osint/tle.go:160.10,162.5 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:15.18,21.20 5 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:21.20,23.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:23.8,23.27 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:23.27,25.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:30.42,35.16 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:35.16,37.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:40.2,40.36 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:40.36,42.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:45.2,55.44 3 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:55.44,56.65 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:56.65,58.4 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:62.2,62.22 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:62.22,64.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:67.2,70.41 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:70.41,72.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:78.2,78.12 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:82.20,88.47 4 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:88.47,91.3 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:94.2,98.16 3 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:98.16,101.3 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:104.2,104.22 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:104.22,107.3 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:109.2,111.16 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:111.16,114.3 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:115.2,122.21 6 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:122.21,125.3 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:127.2,127.28 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:127.28,130.3 2 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:132.2,135.16 3 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:135.16,140.3 4 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:140.8,144.3 3 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:147.2,151.50 4 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:151.50,153.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:153.8,153.120 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:153.120,155.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:157.2,157.19 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:157.19,161.53 4 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:161.53,163.4 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:164.3,164.9 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:167.2,167.18 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:171.23,188.19 14 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:188.19,190.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:192.2,199.50 5 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:199.50,201.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:201.8,201.120 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:201.120,203.3 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:205.2,205.19 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:205.19,209.53 4 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:209.53,211.4 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:212.3,212.9 1 0 +github.com/ANG13T/SatIntel/osint/tleparser.go:215.2,215.18 1 0 diff --git a/go.mod b/go.mod index 02e1c36..4e7cbb0 100644 --- a/go.mod +++ b/go.mod @@ -1,33 +1,22 @@ module github.com/ANG13T/SatIntel -go 1.20 +go 1.24.0 + +toolchain go1.24.5 + +require ( + github.com/TwiN/go-color v1.4.0 + github.com/iskaa02/qalam v0.3.0 + github.com/manifoldco/promptui v0.9.0 +) require ( - github.com/TwiN/go-color v1.4.0 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/iskaa02/qalam v0.3.0 // indirect + github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b // indirect github.com/lucasb-eyer/go-colorful v1.0.3 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/manifoldco/promptui v0.9.0 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mazznoer/colorgrad v0.8.1 // indirect github.com/mazznoer/csscolorparser v0.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/spf13/afero v1.9.3 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.7.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.15.0 // indirect - github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index 26f50ba..667f2d1 100644 --- a/go.sum +++ b/go.sum @@ -1,496 +1,32 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/TwiN/go-color v1.4.0 h1:fNbOwOrvup5oj934UragnW0B1WKaAkkB85q19Y7h4ng= github.com/TwiN/go-color v1.4.0/go.mod h1:0QTVEPlu+AoCyTrho7bXbVkrCkVpdQr7YF7PYWEtSxM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/iskaa02/qalam v0.3.0 h1:yA7+MXkXbkP0HRDpkODC3bgQG0e5mjsYOsawwYP2v5k= github.com/iskaa02/qalam v0.3.0/go.mod h1:BRa4ht8cMjl27tNzhtuach90dBTma5seOzjpMGA5MY4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b h1:JlltDRgni6FuoFwluvoZCrE6cmpojccO4WsqeYlFJLE= +github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b/go.mod h1:msW2QeN9IsnRyvuK8OBAzBwn6DHwXpiAiqBk8dbLfrU= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mazznoer/colorgrad v0.8.1 h1:Bw/ks+KujOOg9E6YQvPqSqTLryiFnwliAH5VMZarSTI= github.com/mazznoer/colorgrad v0.8.1/go.mod h1:xCjvoNkXHJIAPOUMSMrXkFdxTGQqk8zMYS3e5hSLghA= github.com/mazznoer/csscolorparser v0.1.0 h1:xUf1uzU1r24JleIIb2Kz3bl7vATStxy53gm67yuPP+c= github.com/mazznoer/csscolorparser v0.1.0/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.2.1-0.20160509182050-5437a97bf824/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20160516222431-c73e51675ad2/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= -github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= -github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= -github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go index f22ed36..9eb8d76 100644 --- a/main.go +++ b/main.go @@ -6,23 +6,182 @@ package main import ( "bufio" + "encoding/json" "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" "os" "strings" "github.com/ANG13T/SatIntel/cli" + "golang.org/x/term" ) -func setEnvironmentalVariable(envKey string) string { - reader := bufio.NewReader(os.Stdin) - fmt.Printf("%s: ", envKey) - input, err := reader.ReadString('\n') +// loadEnvFile reads environment variables from a .env file in the current directory. +// It skips empty lines and comments, and handles quoted values. +func loadEnvFile() error { + envPath := ".env" + + if _, err := os.Stat(envPath); os.IsNotExist(err) { + return fmt.Errorf(".env file not found") + } + file, err := os.Open(envPath) if err != nil { - fmt.Println("Error reading input:", err) - os.Exit(1) + return fmt.Errorf("failed to open .env file: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + value = strings.Trim(value, "\"'") + os.Setenv(key, value) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error reading .env file: %w", err) + } + + return nil +} + +// isPasswordField determines if the environment variable should have masked input. +func isPasswordField(envKey string) bool { + passwordFields := []string{ + "SPACE_TRACK_PASSWORD", + "N2YO_API_KEY", + "PASSWORD", + "API_KEY", + "SECRET", + "TOKEN", + } + envKeyUpper := strings.ToUpper(envKey) + for _, field := range passwordFields { + if strings.Contains(envKeyUpper, field) { + return true + } + } + return false +} + +// readPassword reads a password from stdin and displays asterisks for each character typed. +func readPassword() (string, error) { + fd := int(os.Stdin.Fd()) + + // Check if stdin is a terminal + if !term.IsTerminal(fd) { + // Fallback to regular input if not a terminal + reader := bufio.NewReader(os.Stdin) + input, err := reader.ReadString('\n') + if err != nil { + return "", err + } + return strings.TrimSpace(input), nil + } + + // Save current terminal state and set to raw mode + oldState, err := term.MakeRaw(fd) + if err != nil { + return "", fmt.Errorf("failed to set raw terminal: %w", err) + } + defer term.Restore(fd, oldState) + + var password []byte + var input [1]byte + + for { + n, err := os.Stdin.Read(input[:]) + if err != nil || n == 0 { + break + } + + char := input[0] + + // Handle Enter key (carriage return or newline) + if char == '\r' || char == '\n' { + fmt.Println() + break + } + + // Handle Backspace/Delete (127 = DEL, 8 = BS) + if char == 127 || char == 8 { + if len(password) > 0 { + password = password[:len(password)-1] + // Move cursor back, print space, move cursor back again + fmt.Print("\b \b") + } + continue + } + + // Handle Ctrl+C + if char == 3 { + fmt.Println() + os.Exit(1) + } + + // Skip control characters except those we handle + if char < 32 { + continue + } + + // Add character to password and print asterisk + password = append(password, char) + fmt.Print("*") + } + + return string(password), nil +} + +// setEnvironmentalVariable prompts the user to enter a value for the given environment variable. +// It reads from stdin and sets the environment variable with the provided value. +// Password fields display asterisks (*) for each character typed for security. +func setEnvironmentalVariable(envKey string) string { + var input string + var err error + + for { + fmt.Printf("%s: ", envKey) + + if isPasswordField(envKey) { + input, err = readPassword() + if err != nil { + fmt.Println("Error reading password:", err) + os.Exit(1) + } + } else { + reader := bufio.NewReader(os.Stdin) + input, err = reader.ReadString('\n') + if err != nil { + fmt.Println("Error reading input:", err) + os.Exit(1) + } + input = strings.TrimSpace(input) + } + + input = strings.TrimSpace(input) + + // Validate format + if err := validateAPIKeyFormat(envKey, input); err != nil { + fmt.Printf(" [!] Validation error: %v\n", err) + fmt.Println("Please enter a valid value:") + continue + } + + break } - input = strings.TrimSuffix(input, "\n") if err := os.Setenv(envKey, input); err != nil { fmt.Printf("Error setting environment variable %s: %v\n", envKey, err) @@ -32,7 +191,8 @@ func setEnvironmentalVariable(envKey string) string { return input } - +// checkEnvironmentalVariable verifies if an environment variable exists. +// If it doesn't exist, it prompts the user to provide a value. func checkEnvironmentalVariable(envKey string) { _, found := os.LookupEnv(envKey) if !found { @@ -40,11 +200,182 @@ func checkEnvironmentalVariable(envKey string) { } } +// validateAPIKeyFormat validates the format of API keys and credentials. +// Returns an error if the format is invalid. +func validateAPIKeyFormat(envKey, value string) error { + value = strings.TrimSpace(value) + + if value == "" { + return fmt.Errorf("%s cannot be empty", envKey) + } + + // Check for reasonable length limits + if len(value) < 3 { + return fmt.Errorf("%s is too short (minimum 3 characters)", envKey) + } + + if len(value) > 512 { + return fmt.Errorf("%s is too long (maximum 512 characters)", envKey) + } + + // Space-Track username validation + if envKey == "SPACE_TRACK_USERNAME" { + // Username should not contain certain special characters + if strings.ContainsAny(value, "\n\r\t") { + return fmt.Errorf("username contains invalid characters") + } + } + + // N2YO API key validation (typically alphanumeric, may contain hyphens) + if envKey == "N2YO_API_KEY" { + // N2YO API keys are typically alphanumeric with possible hyphens/underscores + // Allow alphanumeric, hyphens, underscores + hasAlphanumeric := false + for _, r := range value { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { + hasAlphanumeric = true + } else if r == ' ' || r == '\n' || r == '\r' || r == '\t' { + return fmt.Errorf("API key contains invalid whitespace characters") + } else if r != '-' && r != '_' { + // Allow hyphens and underscores, but warn about other special chars + // (We'll be lenient here as API key formats can vary) + } + } + if !hasAlphanumeric { + return fmt.Errorf("API key must contain at least one alphanumeric character") + } + } + + return nil +} + +// validateCredentials validates all API credentials and tests connections. +func validateCredentials() error { + username := os.Getenv("SPACE_TRACK_USERNAME") + password := os.Getenv("SPACE_TRACK_PASSWORD") + apiKey := os.Getenv("N2YO_API_KEY") + + // Validate format + if err := validateAPIKeyFormat("SPACE_TRACK_USERNAME", username); err != nil { + return fmt.Errorf("Space-Track username validation failed: %w", err) + } + + if err := validateAPIKeyFormat("SPACE_TRACK_PASSWORD", password); err != nil { + return fmt.Errorf("Space-Track password validation failed: %w", err) + } + + if err := validateAPIKeyFormat("N2YO_API_KEY", apiKey); err != nil { + return fmt.Errorf("N2YO API key validation failed: %w", err) + } + + // Test Space-Track connection + fmt.Println("Validating Space-Track credentials...") + client, err := testSpaceTrackConnection(username, password) + if err != nil { + return fmt.Errorf("Space-Track connection test failed: %w", err) + } + _ = client // Client is validated, can be used later if needed + + // Test N2YO API connection + fmt.Println("Validating N2YO API key...") + if err := testN2YOConnection(apiKey); err != nil { + return fmt.Errorf("N2YO API connection test failed: %w", err) + } + + fmt.Println("All credentials validated successfully!") + return nil +} + +// testSpaceTrackConnection tests the Space-Track API connection. +func testSpaceTrackConnection(username, password string) (*http.Client, error) { + // Import osint package functions - we'll need to make Login accessible or create a test function + // For now, we'll create a minimal test here + vals := url.Values{} + vals.Add("identity", username) + vals.Add("password", password) + + jar, err := cookiejar.New(nil) + if err != nil { + return nil, fmt.Errorf("failed to create cookie jar: %w", err) + } + + client := &http.Client{ + Jar: jar, + } + + resp, err := client.PostForm("https://www.space-track.org/ajaxauth/login", vals) + if err != nil { + return nil, fmt.Errorf("connection error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("authentication failed (status: %d)", resp.StatusCode) + } + + return client, nil +} + +// testN2YOConnection tests the N2YO API connection with a simple request. +func testN2YOConnection(apiKey string) error { + // Test with a simple request - get positions for ISS (NORAD ID 25544) + // Using minimal parameters to reduce API usage + testURL := fmt.Sprintf("https://api.n2yo.com/rest/v1/satellite/positions/25544/0/0/0/1/&apiKey=%s", apiKey) + + resp, err := http.Get(testURL) + if err != nil { + return fmt.Errorf("connection error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // Try to read error message + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API request failed (status: %d): %s", resp.StatusCode, string(body)) + } + + // Verify response is valid JSON + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("invalid API response format: %w", err) + } + + // Check for error in response + if info, ok := result["info"].(map[string]interface{}); ok { + if errMsg, ok := info["error"].(string); ok && errMsg != "" { + return fmt.Errorf("API error: %s", errMsg) + } + } + + return nil +} func main() { + err := loadEnvFile() + if err != nil { + if err.Error() == ".env file not found" { + fmt.Println("Note: .env file not found. Please provide credentials:") + } else { + fmt.Printf("Warning: Error loading .env file: %v\n", err) + fmt.Println("Please provide credentials manually:") + } + fmt.Println() + } else { + fmt.Println("Loaded credentials from .env file") + } + checkEnvironmentalVariable("SPACE_TRACK_USERNAME") checkEnvironmentalVariable("SPACE_TRACK_PASSWORD") checkEnvironmentalVariable("N2YO_API_KEY") + // Validate credentials format and test connections + fmt.Println("\nValidating API credentials...") + if err := validateCredentials(); err != nil { + fmt.Printf("Warning: Credential validation failed: %v\n", err) + fmt.Println("You may experience issues when using API features.") + fmt.Println("Press Enter to continue anyway, or Ctrl+C to exit and fix credentials...") + bufio.NewReader(os.Stdin).ReadBytes('\n') + } + cli.SatIntel() } diff --git a/main_apivalidation_test.go b/main_apivalidation_test.go new file mode 100644 index 0000000..01e0f66 --- /dev/null +++ b/main_apivalidation_test.go @@ -0,0 +1,375 @@ +package main + +// NOTE: This test file contains fake test credentials (e.g., "fake_test_password", "FAKE-TEST-KEY") +// These are NOT real secrets and are used only for testing purposes. + +import ( + "os" + "strings" + "testing" +) + +func TestValidateAPIKeyFormat(t *testing.T) { + tests := []struct { + name string + envKey string + value string + expectError bool + errorMsg string + }{ + // Valid cases + { + name: "Valid Space-Track username", + envKey: "SPACE_TRACK_USERNAME", + value: "testuser123", + expectError: false, + }, + { + name: "Valid Space-Track password", + envKey: "SPACE_TRACK_PASSWORD", + value: "fake_test_password_123", // Test value only + expectError: false, + }, + { + name: "Valid N2YO API key", + envKey: "N2YO_API_KEY", + value: "FAKE-TEST-KEY-12345-67890", // Test value only + expectError: false, + }, + { + name: "Valid N2YO API key with underscores", + envKey: "N2YO_API_KEY", + value: "test_key_123", + expectError: false, + }, + { + name: "Valid long credentials", + envKey: "SPACE_TRACK_USERNAME", + value: strings.Repeat("a", 100), + expectError: false, + }, + // Empty values + { + name: "Empty username", + envKey: "SPACE_TRACK_USERNAME", + value: "", + expectError: true, + errorMsg: "cannot be empty", + }, + { + name: "Whitespace only username", + envKey: "SPACE_TRACK_USERNAME", + value: " ", + expectError: true, + errorMsg: "cannot be empty", + }, + { + name: "Empty password", + envKey: "SPACE_TRACK_PASSWORD", + value: "", + expectError: true, + errorMsg: "cannot be empty", + }, + { + name: "Empty API key", + envKey: "N2YO_API_KEY", + value: "", + expectError: true, + errorMsg: "cannot be empty", + }, + // Too short + { + name: "Username too short", + envKey: "SPACE_TRACK_USERNAME", + value: "ab", + expectError: true, + errorMsg: "too short", + }, + { + name: "Password too short", + envKey: "SPACE_TRACK_PASSWORD", + value: "12", + expectError: true, + errorMsg: "too short", + }, + { + name: "API key too short", + envKey: "N2YO_API_KEY", + value: "ab", + expectError: true, + errorMsg: "too short", + }, + // Too long + { + name: "Username too long", + envKey: "SPACE_TRACK_USERNAME", + value: strings.Repeat("a", 513), + expectError: true, + errorMsg: "too long", + }, + { + name: "Password too long", + envKey: "SPACE_TRACK_PASSWORD", + value: strings.Repeat("a", 513), + expectError: true, + errorMsg: "too long", + }, + { + name: "API key too long", + envKey: "N2YO_API_KEY", + value: strings.Repeat("a", 513), + expectError: true, + errorMsg: "too long", + }, + // Invalid characters + { + name: "Username with newline", + envKey: "SPACE_TRACK_USERNAME", + value: "user\nname", + expectError: true, + errorMsg: "invalid characters", + }, + { + name: "Username with carriage return", + envKey: "SPACE_TRACK_USERNAME", + value: "user\rname", + expectError: true, + errorMsg: "invalid characters", + }, + { + name: "Username with tab", + envKey: "SPACE_TRACK_USERNAME", + value: "user\tname", + expectError: true, + errorMsg: "invalid characters", + }, + { + name: "API key with space", + envKey: "N2YO_API_KEY", + value: "key with spaces", + expectError: true, + errorMsg: "invalid whitespace", + }, + { + name: "API key with newline", + envKey: "N2YO_API_KEY", + value: "key\nwith\nnewlines", + expectError: true, + errorMsg: "invalid whitespace", + }, + { + name: "API key with only special chars", + envKey: "N2YO_API_KEY", + value: "---", + expectError: true, + errorMsg: "must contain at least one alphanumeric", + }, + // Edge cases + { + name: "Minimum length username", + envKey: "SPACE_TRACK_USERNAME", + value: "abc", + expectError: false, + }, + { + name: "Maximum length username", + envKey: "SPACE_TRACK_USERNAME", + value: strings.Repeat("a", 512), + expectError: false, + }, + { + name: "API key with mixed case", + envKey: "N2YO_API_KEY", + value: "FAKE-TEST-KEY-ABC-XYZ", // Test value only + expectError: false, + }, + { + name: "API key with numbers only", + envKey: "N2YO_API_KEY", + value: "123456789", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateAPIKeyFormat(tt.envKey, tt.value) + if tt.expectError { + if err == nil { + t.Errorf("validateAPIKeyFormat(%q, %q) expected error, got nil", tt.envKey, tt.value) + } else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("validateAPIKeyFormat(%q, %q) error = %v, want error containing %q", tt.envKey, tt.value, err, tt.errorMsg) + } + } else { + if err != nil { + t.Errorf("validateAPIKeyFormat(%q, %q) unexpected error: %v", tt.envKey, tt.value, err) + } + } + }) + } +} + +func TestValidateAPIKeyFormatUnknownKey(t *testing.T) { + // Test with unknown environment variable key + err := validateAPIKeyFormat("UNKNOWN_KEY", "testvalue") + if err != nil { + t.Errorf("validateAPIKeyFormat with unknown key should only check basic format, got error: %v", err) + } +} + +func TestValidateCredentialsFormatOnly(t *testing.T) { + // Test format validation without actual API calls + // This requires mocking or skipping connection tests + + // Save original values + origUsername := os.Getenv("SPACE_TRACK_USERNAME") + origPassword := os.Getenv("SPACE_TRACK_PASSWORD") + origAPIKey := os.Getenv("N2YO_API_KEY") + + // Set test values + os.Setenv("SPACE_TRACK_USERNAME", "testuser") + os.Setenv("SPACE_TRACK_PASSWORD", "fake_test_password") // Test value only + os.Setenv("N2YO_API_KEY", "fake_test_key_123") // Test value only + + // Test format validation (we'll skip connection tests in unit tests) + username := os.Getenv("SPACE_TRACK_USERNAME") + password := os.Getenv("SPACE_TRACK_PASSWORD") + apiKey := os.Getenv("N2YO_API_KEY") + + if err := validateAPIKeyFormat("SPACE_TRACK_USERNAME", username); err != nil { + t.Errorf("Username format validation failed: %v", err) + } + + if err := validateAPIKeyFormat("SPACE_TRACK_PASSWORD", password); err != nil { + t.Errorf("Password format validation failed: %v", err) + } + + if err := validateAPIKeyFormat("N2YO_API_KEY", apiKey); err != nil { + t.Errorf("API key format validation failed: %v", err) + } + + // Restore original values + if origUsername != "" { + os.Setenv("SPACE_TRACK_USERNAME", origUsername) + } else { + os.Unsetenv("SPACE_TRACK_USERNAME") + } + if origPassword != "" { + os.Setenv("SPACE_TRACK_PASSWORD", origPassword) + } else { + os.Unsetenv("SPACE_TRACK_PASSWORD") + } + if origAPIKey != "" { + os.Setenv("N2YO_API_KEY", origAPIKey) + } else { + os.Unsetenv("N2YO_API_KEY") + } +} + +// TestValidateCredentialsFormatOnlyWithInvalidInput tests format validation with invalid inputs +func TestValidateCredentialsFormatOnlyWithInvalidInput(t *testing.T) { + // Save original values + origUsername := os.Getenv("SPACE_TRACK_USERNAME") + origPassword := os.Getenv("SPACE_TRACK_PASSWORD") + origAPIKey := os.Getenv("N2YO_API_KEY") + + defer func() { + if origUsername != "" { + os.Setenv("SPACE_TRACK_USERNAME", origUsername) + } else { + os.Unsetenv("SPACE_TRACK_USERNAME") + } + if origPassword != "" { + os.Setenv("SPACE_TRACK_PASSWORD", origPassword) + } else { + os.Unsetenv("SPACE_TRACK_PASSWORD") + } + if origAPIKey != "" { + os.Setenv("N2YO_API_KEY", origAPIKey) + } else { + os.Unsetenv("N2YO_API_KEY") + } + }() + + // Test with invalid format credentials + os.Setenv("SPACE_TRACK_USERNAME", "ab") // Too short + os.Setenv("SPACE_TRACK_PASSWORD", "fake_test_password") // Test value only + os.Setenv("N2YO_API_KEY", "fake_test_key_123") // Test value only + + username := os.Getenv("SPACE_TRACK_USERNAME") + if err := validateAPIKeyFormat("SPACE_TRACK_USERNAME", username); err == nil { + t.Error("Expected error for too short username, got nil") + } + + // Test with valid format + os.Setenv("SPACE_TRACK_USERNAME", "testuser") + if err := validateAPIKeyFormat("SPACE_TRACK_USERNAME", "testuser"); err != nil { + t.Errorf("Expected no error for valid username, got: %v", err) + } +} + +// TestValidateAPIKeyFormatPasswordField tests password field validation +func TestValidateAPIKeyFormatPasswordField(t *testing.T) { + tests := []struct { + name string + value string + expectError bool + }{ + { + name: "Valid password", + value: "fake_test_pass_123", // Test value only + expectError: false, + }, + { + name: "Password with special characters", + value: "Pass@123!", + expectError: false, // Passwords can have special chars + }, + { + name: "Password too short", + value: "ab", + expectError: true, + }, + { + name: "Password too long", + value: strings.Repeat("a", 513), + expectError: true, + }, + { + name: "Empty password", + value: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateAPIKeyFormat("SPACE_TRACK_PASSWORD", tt.value) + if tt.expectError && err == nil { + t.Errorf("Expected error for %q, got nil", tt.value) + } else if !tt.expectError && err != nil { + t.Errorf("Unexpected error for %q: %v", tt.value, err) + } + }) + } +} + +// Benchmark tests +func BenchmarkValidateAPIKeyFormat(b *testing.B) { + testCases := []struct { + envKey string + value string + }{ + {"SPACE_TRACK_USERNAME", "testuser123"}, + {"SPACE_TRACK_PASSWORD", "fake_test_password_123"}, // Test value only + {"N2YO_API_KEY", "FAKE-TEST-KEY-12345"}, // Test value only + {"SPACE_TRACK_USERNAME", strings.Repeat("a", 100)}, + {"N2YO_API_KEY", "fake_test_key_123"}, // Test value only + } + + for i := 0; i < b.N; i++ { + for _, tc := range testCases { + validateAPIKeyFormat(tc.envKey, tc.value) + } + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..a8103dc --- /dev/null +++ b/main_test.go @@ -0,0 +1,423 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestIsPasswordField(t *testing.T) { + tests := []struct { + name string + envKey string + expected bool + }{ + { + name: "SPACE_TRACK_PASSWORD should be masked with asterisks", + envKey: "SPACE_TRACK_PASSWORD", + expected: true, + }, + { + name: "N2YO_API_KEY should be masked with asterisks", + envKey: "N2YO_API_KEY", + expected: true, + }, + { + name: "SPACE_TRACK_USERNAME should not be masked", + envKey: "SPACE_TRACK_USERNAME", + expected: false, + }, + { + name: "Field containing PASSWORD should be masked with asterisks", + envKey: "MY_PASSWORD", + expected: true, + }, + { + name: "Field containing API_KEY should be masked with asterisks", + envKey: "SOME_API_KEY", + expected: true, + }, + { + name: "Field containing SECRET should be masked with asterisks", + envKey: "MY_SECRET", + expected: true, + }, + { + name: "Field containing TOKEN should be masked with asterisks", + envKey: "ACCESS_TOKEN", + expected: true, + }, + { + name: "Lowercase password field should be masked with asterisks", + envKey: "my_password", + expected: true, + }, + { + name: "Mixed case password field should be masked with asterisks", + envKey: "My_Password_Field", + expected: true, + }, + { + name: "Regular environment variable should not be masked", + envKey: "DATABASE_HOST", + expected: false, + }, + { + name: "Empty string should not be masked", + envKey: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isPasswordField(tt.envKey) + if result != tt.expected { + t.Errorf("isPasswordField(%q) = %v, want %v", tt.envKey, result, tt.expected) + } + }) + } +} + +func TestLoadEnvFile(t *testing.T) { + // Create a temporary directory for test files + tmpDir := t.TempDir() + + tests := []struct { + name string + envContent string + expectError bool + checkEnv func(t *testing.T) // Function to check environment variables + }{ + { + name: "Valid .env file with all variables", + envContent: `SPACE_TRACK_USERNAME=testuser +SPACE_TRACK_PASSWORD=fake_test_password +N2YO_API_KEY=fake_test_key +`, + expectError: false, + checkEnv: func(t *testing.T) { + if val := os.Getenv("SPACE_TRACK_USERNAME"); val != "testuser" { + t.Errorf("SPACE_TRACK_USERNAME = %q, want %q", val, "testuser") + } + if val := os.Getenv("SPACE_TRACK_PASSWORD"); val != "fake_test_password" { + t.Errorf("SPACE_TRACK_PASSWORD = %q, want %q", val, "fake_test_password") + } + if val := os.Getenv("N2YO_API_KEY"); val != "fake_test_key" { + t.Errorf("N2YO_API_KEY = %q, want %q", val, "fake_test_key") + } + }, + }, + { + name: ".env file with comments", + envContent: `# This is a comment +SPACE_TRACK_USERNAME=testuser +# Another comment +SPACE_TRACK_PASSWORD=testpass +`, + expectError: false, + checkEnv: func(t *testing.T) { + if val := os.Getenv("SPACE_TRACK_USERNAME"); val != "testuser" { + t.Errorf("SPACE_TRACK_USERNAME = %q, want %q", val, "testuser") + } + if val := os.Getenv("SPACE_TRACK_PASSWORD"); val != "testpass" { + t.Errorf("SPACE_TRACK_PASSWORD = %q, want %q", val, "testpass") + } + }, + }, + { + name: ".env file with empty lines", + envContent: `SPACE_TRACK_USERNAME=testuser + +SPACE_TRACK_PASSWORD=testpass + +`, + expectError: false, + checkEnv: func(t *testing.T) { + if val := os.Getenv("SPACE_TRACK_USERNAME"); val != "testuser" { + t.Errorf("SPACE_TRACK_USERNAME = %q, want %q", val, "testuser") + } + if val := os.Getenv("SPACE_TRACK_PASSWORD"); val != "testpass" { + t.Errorf("SPACE_TRACK_PASSWORD = %q, want %q", val, "testpass") + } + }, + }, + { + name: ".env file with quoted values", + envContent: `SPACE_TRACK_USERNAME="testuser" +SPACE_TRACK_PASSWORD='fake_test_password' +N2YO_API_KEY="fake_test_key" +`, + expectError: false, + checkEnv: func(t *testing.T) { + if val := os.Getenv("SPACE_TRACK_USERNAME"); val != "testuser" { + t.Errorf("SPACE_TRACK_USERNAME = %q, want %q", val, "testuser") + } + if val := os.Getenv("SPACE_TRACK_PASSWORD"); val != "fake_test_password" { + t.Errorf("SPACE_TRACK_PASSWORD = %q, want %q", val, "fake_test_password") + } + if val := os.Getenv("N2YO_API_KEY"); val != "fake_test_key" { + t.Errorf("N2YO_API_KEY = %q, want %q", val, "fake_test_key") + } + }, + }, + { + name: ".env file with spaces around equals", + envContent: `SPACE_TRACK_USERNAME = testuser +SPACE_TRACK_PASSWORD = testpass +`, + expectError: false, + checkEnv: func(t *testing.T) { + if val := os.Getenv("SPACE_TRACK_USERNAME"); val != "testuser" { + t.Errorf("SPACE_TRACK_USERNAME = %q, want %q", val, "testuser") + } + if val := os.Getenv("SPACE_TRACK_PASSWORD"); val != "testpass" { + t.Errorf("SPACE_TRACK_PASSWORD = %q, want %q", val, "testpass") + } + }, + }, + { + name: ".env file not found", + envContent: "", + expectError: true, + checkEnv: func(t *testing.T) {}, + }, + { + name: ".env file with invalid line (no equals)", + envContent: `SPACE_TRACK_USERNAME=testuser +INVALID_LINE_WITHOUT_EQUALS +SPACE_TRACK_PASSWORD=testpass +`, + expectError: false, + checkEnv: func(t *testing.T) { + if val := os.Getenv("SPACE_TRACK_USERNAME"); val != "testuser" { + t.Errorf("SPACE_TRACK_USERNAME = %q, want %q", val, "testuser") + } + if val := os.Getenv("SPACE_TRACK_PASSWORD"); val != "testpass" { + t.Errorf("SPACE_TRACK_PASSWORD = %q, want %q", val, "testpass") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clean up environment variables before test + os.Unsetenv("SPACE_TRACK_USERNAME") + os.Unsetenv("SPACE_TRACK_PASSWORD") + os.Unsetenv("N2YO_API_KEY") + + // Save original working directory + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + + // Change to temporary directory + err = os.Chdir(tmpDir) + if err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + defer os.Chdir(originalDir) + + if tt.name == ".env file not found" { + // Ensure .env file doesn't exist in temp directory + envPath := filepath.Join(tmpDir, ".env") + os.Remove(envPath) // Remove if it exists + } else { + // Create .env file + envPath := filepath.Join(tmpDir, ".env") + err := os.WriteFile(envPath, []byte(tt.envContent), 0644) + if err != nil { + t.Fatalf("Failed to create .env file: %v", err) + } + } + + // Test loadEnvFile + err = loadEnvFile() + if tt.expectError { + if err == nil { + t.Errorf("loadEnvFile() expected error, got nil") + } + if err != nil && !strings.Contains(err.Error(), ".env file not found") { + t.Errorf("loadEnvFile() error = %v, want error containing '.env file not found'", err) + } + } else { + if err != nil { + t.Errorf("loadEnvFile() unexpected error: %v", err) + } + tt.checkEnv(t) + } + }) + } +} + +func TestCheckEnvironmentalVariable(t *testing.T) { + tests := []struct { + name string + envKey string + preSetValue string + shouldSet bool + }{ + { + name: "Variable not set should trigger set", + envKey: "TEST_VAR_NOT_SET", + preSetValue: "", + shouldSet: true, + }, + { + name: "Variable already set should not trigger set", + envKey: "TEST_VAR_SET", + preSetValue: "existing_value", + shouldSet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clean up before test + os.Unsetenv(tt.envKey) + + // Pre-set value if needed + if tt.preSetValue != "" { + os.Setenv(tt.envKey, tt.preSetValue) + } + + // Note: We can't easily test the interactive part (setEnvironmentalVariable) + // without mocking stdin, but we can test that checkEnvironmentalVariable + // correctly identifies when a variable is missing + _, found := os.LookupEnv(tt.envKey) + if found != (tt.preSetValue != "") { + t.Errorf("Environment variable lookup mismatch") + } + + // Clean up after test + os.Unsetenv(tt.envKey) + }) + } +} + +func TestLoadEnvFileEdgeCases(t *testing.T) { + tmpDir := t.TempDir() + originalDir, _ := os.Getwd() + defer os.Chdir(originalDir) + + t.Run("Empty .env file", func(t *testing.T) { + os.Chdir(tmpDir) + os.Unsetenv("TEST_VAR") + + envPath := filepath.Join(tmpDir, ".env") + err := os.WriteFile(envPath, []byte(""), 0644) + if err != nil { + t.Fatalf("Failed to create .env file: %v", err) + } + + err = loadEnvFile() + if err != nil { + t.Errorf("loadEnvFile() with empty file should not error, got: %v", err) + } + }) + + t.Run(".env file with only comments", func(t *testing.T) { + os.Chdir(tmpDir) + os.Unsetenv("TEST_VAR") + + envPath := filepath.Join(tmpDir, ".env") + err := os.WriteFile(envPath, []byte("# Comment 1\n# Comment 2\n"), 0644) + if err != nil { + t.Fatalf("Failed to create .env file: %v", err) + } + + err = loadEnvFile() + if err != nil { + t.Errorf("loadEnvFile() with only comments should not error, got: %v", err) + } + }) + + t.Run(".env file with value containing equals", func(t *testing.T) { + os.Chdir(tmpDir) + os.Unsetenv("TEST_VAR") + + envPath := filepath.Join(tmpDir, ".env") + // SplitN with limit 2 should handle this correctly + err := os.WriteFile(envPath, []byte("TEST_VAR=value=with=equals\n"), 0644) + if err != nil { + t.Fatalf("Failed to create .env file: %v", err) + } + + err = loadEnvFile() + if err != nil { + t.Errorf("loadEnvFile() should not error, got: %v", err) + } + + val := os.Getenv("TEST_VAR") + if val != "value=with=equals" { + t.Errorf("TEST_VAR = %q, want %q", val, "value=with=equals") + } + }) + + t.Run(".env file with whitespace-only lines", func(t *testing.T) { + os.Chdir(tmpDir) + os.Unsetenv("TEST_VAR") + + envPath := filepath.Join(tmpDir, ".env") + err := os.WriteFile(envPath, []byte(" \n\t\nTEST_VAR=value\n \n"), 0644) + if err != nil { + t.Fatalf("Failed to create .env file: %v", err) + } + + err = loadEnvFile() + if err != nil { + t.Errorf("loadEnvFile() should not error, got: %v", err) + } + + val := os.Getenv("TEST_VAR") + if val != "value" { + t.Errorf("TEST_VAR = %q, want %q", val, "value") + } + }) +} + +// Benchmark tests +func BenchmarkIsPasswordField(b *testing.B) { + testCases := []string{ + "SPACE_TRACK_PASSWORD", + "N2YO_API_KEY", + "SPACE_TRACK_USERNAME", + "MY_PASSWORD", + "REGULAR_VAR", + } + + for i := 0; i < b.N; i++ { + for _, tc := range testCases { + isPasswordField(tc) + } + } +} + +func BenchmarkLoadEnvFile(b *testing.B) { + tmpDir := b.TempDir() + originalDir, _ := os.Getwd() + defer os.Chdir(originalDir) + + envContent := `SPACE_TRACK_USERNAME=testuser +SPACE_TRACK_PASSWORD=fake_test_password +N2YO_API_KEY=fake_test_key +` + + envPath := filepath.Join(tmpDir, ".env") + err := os.WriteFile(envPath, []byte(envContent), 0644) + if err != nil { + b.Fatalf("Failed to create .env file: %v", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + os.Chdir(tmpDir) + os.Unsetenv("SPACE_TRACK_USERNAME") + os.Unsetenv("SPACE_TRACK_PASSWORD") + os.Unsetenv("N2YO_API_KEY") + loadEnvFile() + } +} diff --git a/osint/batch.go b/osint/batch.go new file mode 100644 index 0000000..b203991 --- /dev/null +++ b/osint/batch.go @@ -0,0 +1,1039 @@ +package osint + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/TwiN/go-color" + "github.com/manifoldco/promptui" +) + +// BatchSatellite represents a satellite selected for batch processing. +type BatchSatellite struct { + Name string + NORADID string + Country string + ObjectType string +} + +// BatchTLEResult contains TLE data for a satellite in batch processing. +type BatchTLEResult struct { + Satellite BatchSatellite + TLE TLE + Error error + Success bool +} + +// BatchComparisonResult contains comparison data for multiple satellites. +type BatchComparisonResult struct { + Satellites []BatchSatellite + Results []BatchTLEResult + Summary BatchSummary +} + +// BatchSummary provides aggregate statistics for batch operations. +type BatchSummary struct { + TotalProcessed int + Successful int + Failed int + AverageInclination float64 + AverageMeanMotion float64 + LowestAltitude float64 + HighestAltitude float64 +} + +// showBatchMenu displays the batch operations menu. +func showBatchMenu() string { + menuItems := []string{ + "Download TLE for Multiple Satellites", + "Compare Multiple Satellites", + "Batch Visual Predictions", + "Batch Radio Predictions", + "Batch Position Data", + "Cancel", + } + + prompt := promptui.Select{ + Label: "Batch Operations Menu", + Items: menuItems, + Size: 10, + } + + idx, _, err := prompt.Run() + if err != nil || idx == 5 { + return "" + } + + options := []string{"tle", "compare", "visual", "radio", "position"} + if idx < len(options) { + return options[idx] + } + return "" +} + +// selectMultipleSatellites allows users to select multiple satellites for batch processing. +func selectMultipleSatellites() []BatchSatellite { + var selected []BatchSatellite + selectedMap := make(map[string]bool) // Track selected NORAD IDs to prevent duplicates + + for { + fmt.Println(color.Ize(color.Cyan, "\n [*] Current selection: "+strconv.Itoa(len(selected))+" satellite(s)")) + + menuItems := []string{ + "Add Satellite from Catalog", + "Add Satellite by NORAD ID", + "Add from Favorites", + "Remove Satellite", + "View Selected Satellites", + "Clear All", + "Done - Process Batch", + "Cancel", + } + + prompt := promptui.Select{ + Label: "Batch Selection Menu", + Items: menuItems, + Size: 10, + } + + idx, _, err := prompt.Run() + if err != nil || idx == 7 { + return nil + } + + switch idx { + case 0: // Add from Catalog + result := SelectSatellite() + if result != "" { + norad := extractNorad(result) + if !selectedMap[norad] { + // Fetch full satellite info + client, err := Login() + if err == nil { + endpoint := fmt.Sprintf("/class/satcat/NORAD_CAT_ID/%s/format/json", norad) + data, err := QuerySpaceTrack(client, endpoint) + if err == nil { + var sats []Satellite + if json.Unmarshal([]byte(data), &sats) == nil && len(sats) > 0 { + sat := sats[0] + selected = append(selected, BatchSatellite{ + Name: sat.SATNAME, + NORADID: sat.NORAD_CAT_ID, + Country: sat.COUNTRY, + ObjectType: sat.OBJECT_TYPE, + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: "+sat.SATNAME)) + } else { + // Fallback with just name and NORAD + name := strings.Split(result, " (")[0] + selected = append(selected, BatchSatellite{ + Name: name, + NORADID: norad, + Country: "Unknown", + ObjectType: "Unknown", + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: "+name)) + } + } else { + // Fallback + name := strings.Split(result, " (")[0] + selected = append(selected, BatchSatellite{ + Name: name, + NORADID: norad, + Country: "Unknown", + ObjectType: "Unknown", + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: "+name)) + } + } else { + // Fallback + name := strings.Split(result, " (")[0] + selected = append(selected, BatchSatellite{ + Name: name, + NORADID: norad, + Country: "Unknown", + ObjectType: "Unknown", + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: "+name)) + } + } else { + fmt.Println(color.Ize(color.Yellow, " [!] Satellite already in batch")) + } + } + + case 1: // Add by NORAD ID + noradPrompt := promptui.Prompt{ + Label: "Enter NORAD ID", + Validate: func(input string) error { + if strings.TrimSpace(input) == "" { + return fmt.Errorf("NORAD ID cannot be empty") + } + return nil + }, + } + norad, err := noradPrompt.Run() + if err == nil && norad != "" { + norad = strings.TrimSpace(norad) + if !selectedMap[norad] { + // Try to fetch satellite name + client, err := Login() + if err == nil { + endpoint := fmt.Sprintf("/class/satcat/NORAD_CAT_ID/%s/format/json", norad) + data, err := QuerySpaceTrack(client, endpoint) + if err == nil { + var sats []Satellite + if json.Unmarshal([]byte(data), &sats) == nil && len(sats) > 0 { + sat := sats[0] + selected = append(selected, BatchSatellite{ + Name: sat.SATNAME, + NORADID: sat.NORAD_CAT_ID, + Country: sat.COUNTRY, + ObjectType: sat.OBJECT_TYPE, + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: "+sat.SATNAME)) + } else { + selected = append(selected, BatchSatellite{ + Name: "NORAD " + norad, + NORADID: norad, + Country: "Unknown", + ObjectType: "Unknown", + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: NORAD "+norad)) + } + } else { + selected = append(selected, BatchSatellite{ + Name: "NORAD " + norad, + NORADID: norad, + Country: "Unknown", + ObjectType: "Unknown", + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: NORAD "+norad)) + } + } else { + selected = append(selected, BatchSatellite{ + Name: "NORAD " + norad, + NORADID: norad, + Country: "Unknown", + ObjectType: "Unknown", + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: NORAD "+norad)) + } + } else { + fmt.Println(color.Ize(color.Yellow, " [!] Satellite already in batch")) + } + } + + case 2: // Add from Favorites + favResult := SelectFromFavorites() + if favResult != "" { + norad := extractNorad(favResult) + if !selectedMap[norad] { + name := strings.Split(favResult, " (")[0] + selected = append(selected, BatchSatellite{ + Name: name, + NORADID: norad, + Country: "Unknown", + ObjectType: "Unknown", + }) + selectedMap[norad] = true + fmt.Println(color.Ize(color.Green, " [+] Added: "+name)) + } else { + fmt.Println(color.Ize(color.Yellow, " [!] Satellite already in batch")) + } + } + + case 3: // Remove Satellite + if len(selected) == 0 { + fmt.Println(color.Ize(color.Yellow, " [!] No satellites to remove")) + continue + } + var items []string + for i, sat := range selected { + items = append(items, fmt.Sprintf("%d. %s (%s)", i+1, sat.Name, sat.NORADID)) + } + items = append(items, "Cancel") + + removePrompt := promptui.Select{ + Label: "Select satellite to remove", + Items: items, + } + removeIdx, _, err := removePrompt.Run() + if err == nil && removeIdx < len(selected) { + removed := selected[removeIdx] + selected = append(selected[:removeIdx], selected[removeIdx+1:]...) + delete(selectedMap, removed.NORADID) + fmt.Println(color.Ize(color.Green, " [+] Removed: "+removed.Name)) + } + + case 4: // View Selected + if len(selected) == 0 { + fmt.Println(color.Ize(color.Yellow, " [!] No satellites selected")) + } else { + fmt.Println(color.Ize(color.Cyan, "\n Selected Satellites:")) + for i, sat := range selected { + fmt.Printf(" %d. %s (%s)\n", i+1, sat.Name, sat.NORADID) + if sat.Country != "Unknown" { + fmt.Printf(" Country: %s\n", sat.Country) + } + if sat.ObjectType != "Unknown" { + fmt.Printf(" Type: %s\n", sat.ObjectType) + } + } + } + + case 5: // Clear All + if len(selected) > 0 { + confirmPrompt := promptui.Prompt{ + Label: "Clear all satellites? (y/n)", + Default: "n", + AllowEdit: true, + } + confirm, _ := confirmPrompt.Run() + if strings.ToLower(strings.TrimSpace(confirm)) == "y" { + selected = []BatchSatellite{} + selectedMap = make(map[string]bool) + fmt.Println(color.Ize(color.Green, " [+] Cleared all satellites")) + } + } + + case 6: // Done + if len(selected) == 0 { + fmt.Println(color.Ize(color.Red, " [!] Please select at least one satellite")) + continue + } + return selected + } + } +} + +// BatchDownloadTLE downloads TLE data for multiple satellites concurrently. +func BatchDownloadTLE(satellites []BatchSatellite) []BatchTLEResult { + if len(satellites) == 0 { + return nil + } + + fmt.Println(color.Ize(color.Cyan, fmt.Sprintf("\n [*] Downloading TLE data for %d satellite(s)...", len(satellites)))) + + client, err := Login() + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to login: "+err.Error())) + return nil + } + + results := make([]BatchTLEResult, len(satellites)) + var wg sync.WaitGroup + var mu sync.Mutex + completed := 0 + + for i, sat := range satellites { + wg.Add(1) + go func(idx int, satellite BatchSatellite) { + defer wg.Done() + + result := BatchTLEResult{ + Satellite: satellite, + Success: false, + } + + endpoint := fmt.Sprintf("/class/gp_history/format/tle/NORAD_CAT_ID/%s/orderby/EPOCH%%20desc/limit/1", satellite.NORADID) + data, err := QuerySpaceTrack(client, endpoint) + if err != nil { + result.Error = err + mu.Lock() + results[idx] = result + completed++ + mu.Unlock() + return + } + + lines := strings.Split(strings.TrimSpace(data), "\n") + var lineOne, lineTwo string + + if len(lines) >= 2 { + lineOne = strings.TrimSpace(lines[0]) + lineTwo = strings.TrimSpace(lines[1]) + } else { + tleLines := strings.Fields(data) + if len(tleLines) >= 2 { + mid := len(tleLines) / 2 + if mid < 1 { + mid = 1 + } + if mid >= len(tleLines) { + mid = len(tleLines) - 1 + } + lineOne = strings.Join(tleLines[:mid], " ") + lineTwo = strings.Join(tleLines[mid:], " ") + } else { + result.Error = fmt.Errorf("insufficient TLE data") + mu.Lock() + results[idx] = result + completed++ + mu.Unlock() + return + } + } + + if !strings.HasPrefix(lineOne, "1 ") || !strings.HasPrefix(lineTwo, "2 ") { + result.Error = fmt.Errorf("invalid TLE format") + mu.Lock() + results[idx] = result + completed++ + mu.Unlock() + return + } + + tle := ConstructTLE(satellite.Name, lineOne, lineTwo) + + // Validate parsing + line1Fields := strings.Fields(lineOne) + line2Fields := strings.Fields(lineTwo) + if len(line1Fields) < 4 || len(line2Fields) < 3 { + result.Error = fmt.Errorf("insufficient fields in TLE") + mu.Lock() + results[idx] = result + completed++ + mu.Unlock() + return + } + + if tle.SatelliteCatalogNumber == 0 && tle.InternationalDesignator == "" && tle.ElementSetEpoch == 0.0 { + result.Error = fmt.Errorf("failed to parse TLE data") + mu.Lock() + results[idx] = result + completed++ + mu.Unlock() + return + } + + result.TLE = tle + result.Success = true + + mu.Lock() + results[idx] = result + completed++ + mu.Unlock() + + fmt.Printf(color.Ize(color.Green, " [+] [%d/%d] Downloaded: %s\n"), completed, len(satellites), satellite.Name) + }(i, sat) + } + + wg.Wait() + + // Display summary + successful := 0 + for _, r := range results { + if r.Success { + successful++ + } + } + + fmt.Println(color.Ize(color.Cyan, fmt.Sprintf("\n [*] Batch download complete: %d/%d successful", successful, len(satellites)))) + + return results +} + +// CompareSatellites compares TLE data for multiple satellites and displays a summary. +func CompareSatellites(results []BatchTLEResult) BatchComparisonResult { + if len(results) == 0 { + return BatchComparisonResult{} + } + + comparison := BatchComparisonResult{ + Results: results, + Summary: BatchSummary{ + TotalProcessed: len(results), + }, + } + + var inclinations []float64 + var meanMotions []float64 + var altitudes []float64 + + for _, result := range results { + if result.Success { + comparison.Summary.Successful++ + comparison.Satellites = append(comparison.Satellites, result.Satellite) + + if result.TLE.OrbitInclination > 0 { + inclinations = append(inclinations, result.TLE.OrbitInclination) + } + if result.TLE.MeanMotion > 0 { + meanMotions = append(meanMotions, result.TLE.MeanMotion) + } + // Estimate altitude from mean motion (rough calculation) + if result.TLE.MeanMotion > 0 { + // Simplified: altitude ≈ (GM / (4π² * meanMotion²))^(1/3) - Earth radius + // For simplicity, using approximate formula + period := 86400.0 / result.TLE.MeanMotion // period in seconds + // Rough altitude estimate (km) + altitude := (398600.4418 * period * period / (4 * 3.14159 * 3.14159)) / 1000 - 6371 + if altitude > 0 { + altitudes = append(altitudes, altitude) + } + } + } else { + comparison.Summary.Failed++ + } + } + + // Calculate averages + if len(inclinations) > 0 { + sum := 0.0 + for _, inc := range inclinations { + sum += inc + } + comparison.Summary.AverageInclination = sum / float64(len(inclinations)) + } + + if len(meanMotions) > 0 { + sum := 0.0 + for _, mm := range meanMotions { + sum += mm + } + comparison.Summary.AverageMeanMotion = sum / float64(len(meanMotions)) + } + + if len(altitudes) > 0 { + min := altitudes[0] + max := altitudes[0] + for _, alt := range altitudes { + if alt < min { + min = alt + } + if alt > max { + max = alt + } + } + comparison.Summary.LowestAltitude = min + comparison.Summary.HighestAltitude = max + } + + return comparison +} + +// DisplayComparison displays the comparison results in a formatted table. +func DisplayComparison(comparison BatchComparisonResult) { + if len(comparison.Results) == 0 { + fmt.Println(color.Ize(color.Yellow, " [!] No data to compare")) + return + } + + fmt.Println(color.Ize(color.Purple, "\n╔═════════════════════════════════════════════════════════════╗")) + fmt.Println(color.Ize(color.Purple, "║ Satellite Comparison Summary ║")) + fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) + + fmt.Println(color.Ize(color.Purple, GenRowString("Total Processed", strconv.Itoa(comparison.Summary.TotalProcessed)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Successful", strconv.Itoa(comparison.Summary.Successful)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Failed", strconv.Itoa(comparison.Summary.Failed)))) + + if comparison.Summary.AverageInclination > 0 { + fmt.Println(color.Ize(color.Purple, GenRowString("Average Inclination", fmt.Sprintf("%.2f°", comparison.Summary.AverageInclination)))) + } + if comparison.Summary.AverageMeanMotion > 0 { + fmt.Println(color.Ize(color.Purple, GenRowString("Average Mean Motion", fmt.Sprintf("%.4f rev/day", comparison.Summary.AverageMeanMotion)))) + } + if comparison.Summary.LowestAltitude > 0 { + fmt.Println(color.Ize(color.Purple, GenRowString("Lowest Altitude (est.)", fmt.Sprintf("%.2f km", comparison.Summary.LowestAltitude)))) + } + if comparison.Summary.HighestAltitude > 0 { + fmt.Println(color.Ize(color.Purple, GenRowString("Highest Altitude (est.)", fmt.Sprintf("%.2f km", comparison.Summary.HighestAltitude)))) + } + + fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) + fmt.Println(color.Ize(color.Purple, "║ Individual Results ║")) + fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) + + for i, result := range comparison.Results { + status := "✅ Success" + if !result.Success { + status = "❌ Failed" + if result.Error != nil { + status += ": " + result.Error.Error() + } + } + + fmt.Println(color.Ize(color.Purple, GenRowString(fmt.Sprintf("Satellite %d", i+1), result.Satellite.Name))) + fmt.Println(color.Ize(color.Purple, GenRowString(" NORAD ID", result.Satellite.NORADID))) + fmt.Println(color.Ize(color.Purple, GenRowString(" Status", status))) + + if result.Success { + fmt.Println(color.Ize(color.Purple, GenRowString(" Inclination", fmt.Sprintf("%.2f°", result.TLE.OrbitInclination)))) + fmt.Println(color.Ize(color.Purple, GenRowString(" Mean Motion", fmt.Sprintf("%.4f rev/day", result.TLE.MeanMotion)))) + fmt.Println(color.Ize(color.Purple, GenRowString(" Eccentricity", fmt.Sprintf("%.6f", result.TLE.Eccentrcity)))) + } + + if i < len(comparison.Results)-1 { + fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) + } + } + + fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝\n\n")) +} + +// BatchOperations provides the main entry point for batch operations. +func BatchOperations() { + operation := showBatchMenu() + if operation == "" { + return + } + + satellites := selectMultipleSatellites() + if len(satellites) == 0 { + return + } + + switch operation { + case "tle": + results := BatchDownloadTLE(satellites) + if len(results) > 0 { + // Display results + fmt.Println(color.Ize(color.Cyan, "\n [*] Batch TLE Download Results:")) + for i, result := range results { + if result.Success { + fmt.Printf("\n %d. %s (%s) - ✅ Success\n", i+1, result.Satellite.Name, result.Satellite.NORADID) + PrintTLE(result.TLE) + } else { + fmt.Printf("\n %d. %s (%s) - ❌ Failed", i+1, result.Satellite.Name, result.Satellite.NORADID) + if result.Error != nil { + fmt.Printf(": %s\n", result.Error.Error()) + } else { + fmt.Println() + } + } + } + + // Offer export + exportPrompt := promptui.Prompt{ + Label: "Export batch results? (y/n)", + Default: "n", + AllowEdit: true, + } + exportAnswer, _ := exportPrompt.Run() + if strings.ToLower(strings.TrimSpace(exportAnswer)) == "y" { + exportBatchTLE(results) + } + } + + case "compare": + results := BatchDownloadTLE(satellites) + if len(results) > 0 { + comparison := CompareSatellites(results) + DisplayComparison(comparison) + + // Offer export + exportPrompt := promptui.Prompt{ + Label: "Export comparison results? (y/n)", + Default: "n", + AllowEdit: true, + } + exportAnswer, _ := exportPrompt.Run() + if strings.ToLower(strings.TrimSpace(exportAnswer)) == "y" { + exportBatchComparison(comparison) + } + } + + case "visual", "radio", "position": + fmt.Println(color.Ize(color.Yellow, " [!] Batch predictions and positions coming soon")) + // TODO: Implement batch predictions and positions + } +} + +// exportBatchTLE exports batch TLE results to a file. +func exportBatchTLE(results []BatchTLEResult) { + formatPrompt := promptui.Select{ + Label: "Select Export Format", + Items: []string{"CSV", "JSON", "Text", "Cancel"}, + } + formatIdx, formatChoice, err := formatPrompt.Run() + if err != nil || formatIdx == 3 { + return + } + + pathPrompt := promptui.Prompt{ + Label: "Enter file path", + Default: fmt.Sprintf("batch_tle_%s", time.Now().Format("20060102_150405")), + AllowEdit: true, + } + filePath, err := pathPrompt.Run() + if err != nil { + return + } + + filePath = strings.TrimSpace(filePath) + if filePath == "" { + filePath = fmt.Sprintf("batch_tle_%s", time.Now().Format("20060102_150405")) + } + + ext := "" + switch formatChoice { + case "CSV": + ext = ".csv" + case "JSON": + ext = ".json" + case "Text": + ext = ".txt" + } + + if !strings.HasSuffix(filePath, ext) { + filePath += ext + } + + switch formatChoice { + case "CSV": + exportBatchTLECSV(results, filePath) + case "JSON": + exportBatchTLEJSON(results, filePath) + case "Text": + exportBatchTLEText(results, filePath) + } +} + +// exportBatchComparison exports comparison results to a file. +func exportBatchComparison(comparison BatchComparisonResult) { + formatPrompt := promptui.Select{ + Label: "Select Export Format", + Items: []string{"CSV", "JSON", "Text", "Cancel"}, + } + formatIdx, formatChoice, err := formatPrompt.Run() + if err != nil || formatIdx == 3 { + return + } + + pathPrompt := promptui.Prompt{ + Label: "Enter file path", + Default: fmt.Sprintf("batch_comparison_%s", time.Now().Format("20060102_150405")), + AllowEdit: true, + } + filePath, err := pathPrompt.Run() + if err != nil { + return + } + + filePath = strings.TrimSpace(filePath) + if filePath == "" { + filePath = fmt.Sprintf("batch_comparison_%s", time.Now().Format("20060102_150405")) + } + + ext := "" + switch formatChoice { + case "CSV": + ext = ".csv" + case "JSON": + ext = ".json" + case "Text": + ext = ".txt" + } + + if !strings.HasSuffix(filePath, ext) { + filePath += ext + } + + switch formatChoice { + case "CSV": + exportBatchComparisonCSV(comparison, filePath) + case "JSON": + exportBatchComparisonJSON(comparison, filePath) + case "Text": + exportBatchComparisonText(comparison, filePath) + } +} + +// exportBatchTLECSV exports batch TLE results to CSV format. +func exportBatchTLECSV(results []BatchTLEResult, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // Write header + headers := []string{ + "Satellite Name", "NORAD ID", "Status", "Common Name", "Catalog Number", + "Inclination", "Mean Motion", "Eccentricity", "Error", + } + if err := writer.Write(headers); err != nil { + return fmt.Errorf("failed to write header: %w", err) + } + + // Write data + for _, result := range results { + status := "Success" + errorMsg := "" + if !result.Success { + status = "Failed" + if result.Error != nil { + errorMsg = result.Error.Error() + } + } + + row := []string{ + result.Satellite.Name, + result.Satellite.NORADID, + status, + } + + if result.Success { + row = append(row, + result.TLE.CommonName, + strconv.Itoa(result.TLE.SatelliteCatalogNumber), + fmt.Sprintf("%.2f", result.TLE.OrbitInclination), + fmt.Sprintf("%.4f", result.TLE.MeanMotion), + fmt.Sprintf("%.6f", result.TLE.Eccentrcity), + errorMsg, + ) + } else { + row = append(row, "", "", "", "", "", errorMsg) + } + + if err := writer.Write(row); err != nil { + return fmt.Errorf("failed to write row: %w", err) + } + } + + fmt.Println(color.Ize(color.Green, " [+] Exported to: "+filePath)) + return nil +} + +// exportBatchTLEJSON exports batch TLE results to JSON format. +func exportBatchTLEJSON(results []BatchTLEResult, filePath string) error { + type ExportResult struct { + Satellite BatchSatellite `json:"satellite"` + TLE *TLE `json:"tle,omitempty"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` + } + + var exportResults []ExportResult + for _, result := range results { + exportResult := ExportResult{ + Satellite: result.Satellite, + Success: result.Success, + } + if result.Success { + exportResult.TLE = &result.TLE + } + if result.Error != nil { + exportResult.Error = result.Error.Error() + } + exportResults = append(exportResults, exportResult) + } + + data := map[string]interface{}{ + "batch_results": exportResults, + "export_timestamp": time.Now().Format(time.RFC3339), + "total_count": len(results), + } + + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + if err := os.WriteFile(filePath, jsonData, 0644); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + fmt.Println(color.Ize(color.Green, " [+] Exported to: "+filePath)) + return nil +} + +// exportBatchTLEText exports batch TLE results to text format. +func exportBatchTLEText(results []BatchTLEResult, filePath string) error { + var builder strings.Builder + + builder.WriteString("Batch TLE Download Results\n") + builder.WriteString(strings.Repeat("=", 60) + "\n\n") + builder.WriteString(fmt.Sprintf("Total Satellites: %d\n", len(results))) + builder.WriteString(fmt.Sprintf("Export Date: %s\n\n", time.Now().Format(time.RFC3339))) + + for i, result := range results { + builder.WriteString(fmt.Sprintf("Satellite %d: %s (%s)\n", i+1, result.Satellite.Name, result.Satellite.NORADID)) + if result.Success { + builder.WriteString("Status: ✅ Success\n") + builder.WriteString(fmt.Sprintf(" Common Name: %s\n", result.TLE.CommonName)) + builder.WriteString(fmt.Sprintf(" Catalog Number: %d\n", result.TLE.SatelliteCatalogNumber)) + builder.WriteString(fmt.Sprintf(" Inclination: %.2f°\n", result.TLE.OrbitInclination)) + builder.WriteString(fmt.Sprintf(" Mean Motion: %.4f rev/day\n", result.TLE.MeanMotion)) + builder.WriteString(fmt.Sprintf(" Eccentricity: %.6f\n", result.TLE.Eccentrcity)) + } else { + builder.WriteString("Status: ❌ Failed\n") + if result.Error != nil { + builder.WriteString(fmt.Sprintf(" Error: %s\n", result.Error.Error())) + } + } + builder.WriteString("\n") + } + + if err := os.WriteFile(filePath, []byte(builder.String()), 0644); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + fmt.Println(color.Ize(color.Green, " [+] Exported to: "+filePath)) + return nil +} + +// exportBatchComparisonCSV exports comparison results to CSV format. +func exportBatchComparisonCSV(comparison BatchComparisonResult, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // Write summary + summaryHeaders := []string{"Metric", "Value"} + if err := writer.Write(summaryHeaders); err != nil { + return fmt.Errorf("failed to write header: %w", err) + } + + summaryRows := [][]string{ + {"Total Processed", strconv.Itoa(comparison.Summary.TotalProcessed)}, + {"Successful", strconv.Itoa(comparison.Summary.Successful)}, + {"Failed", strconv.Itoa(comparison.Summary.Failed)}, + } + if comparison.Summary.AverageInclination > 0 { + summaryRows = append(summaryRows, []string{"Average Inclination", fmt.Sprintf("%.2f", comparison.Summary.AverageInclination)}) + } + if comparison.Summary.AverageMeanMotion > 0 { + summaryRows = append(summaryRows, []string{"Average Mean Motion", fmt.Sprintf("%.4f", comparison.Summary.AverageMeanMotion)}) + } + + for _, row := range summaryRows { + if err := writer.Write(row); err != nil { + return fmt.Errorf("failed to write summary row: %w", err) + } + } + + // Empty row separator + writer.Write([]string{}) + + // Write individual results + resultHeaders := []string{"Name", "NORAD ID", "Status", "Inclination", "Mean Motion", "Eccentricity", "Error"} + if err := writer.Write(resultHeaders); err != nil { + return fmt.Errorf("failed to write result header: %w", err) + } + + for _, result := range comparison.Results { + status := "Success" + errorMsg := "" + if !result.Success { + status = "Failed" + if result.Error != nil { + errorMsg = result.Error.Error() + } + } + + row := []string{ + result.Satellite.Name, + result.Satellite.NORADID, + status, + } + + if result.Success { + row = append(row, + fmt.Sprintf("%.2f", result.TLE.OrbitInclination), + fmt.Sprintf("%.4f", result.TLE.MeanMotion), + fmt.Sprintf("%.6f", result.TLE.Eccentrcity), + errorMsg, + ) + } else { + row = append(row, "", "", errorMsg) + } + + if err := writer.Write(row); err != nil { + return fmt.Errorf("failed to write result row: %w", err) + } + } + + fmt.Println(color.Ize(color.Green, " [+] Exported to: "+filePath)) + return nil +} + +// exportBatchComparisonJSON exports comparison results to JSON format. +func exportBatchComparisonJSON(comparison BatchComparisonResult, filePath string) error { + data := map[string]interface{}{ + "summary": comparison.Summary, + "results": comparison.Results, + "export_timestamp": time.Now().Format(time.RFC3339), + } + + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + if err := os.WriteFile(filePath, jsonData, 0644); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + fmt.Println(color.Ize(color.Green, " [+] Exported to: "+filePath)) + return nil +} + +// exportBatchComparisonText exports comparison results to text format. +func exportBatchComparisonText(comparison BatchComparisonResult, filePath string) error { + var builder strings.Builder + + builder.WriteString("Satellite Comparison Results\n") + builder.WriteString(strings.Repeat("=", 60) + "\n\n") + + builder.WriteString("Summary:\n") + builder.WriteString(fmt.Sprintf(" Total Processed: %d\n", comparison.Summary.TotalProcessed)) + builder.WriteString(fmt.Sprintf(" Successful: %d\n", comparison.Summary.Successful)) + builder.WriteString(fmt.Sprintf(" Failed: %d\n", comparison.Summary.Failed)) + if comparison.Summary.AverageInclination > 0 { + builder.WriteString(fmt.Sprintf(" Average Inclination: %.2f°\n", comparison.Summary.AverageInclination)) + } + if comparison.Summary.AverageMeanMotion > 0 { + builder.WriteString(fmt.Sprintf(" Average Mean Motion: %.4f rev/day\n", comparison.Summary.AverageMeanMotion)) + } + + builder.WriteString("\nIndividual Results:\n") + builder.WriteString(strings.Repeat("-", 60) + "\n") + + for i, result := range comparison.Results { + builder.WriteString(fmt.Sprintf("\n%d. %s (%s)\n", i+1, result.Satellite.Name, result.Satellite.NORADID)) + if result.Success { + builder.WriteString(" Status: ✅ Success\n") + builder.WriteString(fmt.Sprintf(" Inclination: %.2f°\n", result.TLE.OrbitInclination)) + builder.WriteString(fmt.Sprintf(" Mean Motion: %.4f rev/day\n", result.TLE.MeanMotion)) + builder.WriteString(fmt.Sprintf(" Eccentricity: %.6f\n", result.TLE.Eccentrcity)) + } else { + builder.WriteString(" Status: ❌ Failed\n") + if result.Error != nil { + builder.WriteString(fmt.Sprintf(" Error: %s\n", result.Error.Error())) + } + } + } + + builder.WriteString(fmt.Sprintf("\nExported: %s\n", time.Now().Format(time.RFC3339))) + + if err := os.WriteFile(filePath, []byte(builder.String()), 0644); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + fmt.Println(color.Ize(color.Green, " [+] Exported to: "+filePath)) + return nil +} + diff --git a/osint/batch_test.go b/osint/batch_test.go new file mode 100644 index 0000000..38f1329 --- /dev/null +++ b/osint/batch_test.go @@ -0,0 +1,460 @@ +package osint + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestBatchSatelliteStruct(t *testing.T) { + sat := BatchSatellite{ + Name: "ISS (ZARYA)", + NORADID: "25544", + Country: "US", + ObjectType: "PAYLOAD", + } + + if sat.Name != "ISS (ZARYA)" { + t.Errorf("Name = %q, want %q", sat.Name, "ISS (ZARYA)") + } + if sat.NORADID != "25544" { + t.Errorf("NORADID = %q, want %q", sat.NORADID, "25544") + } +} + +func TestCompareSatellites(t *testing.T) { + results := []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Sat1", NORADID: "12345"}, + Success: true, + TLE: TLE{ + OrbitInclination: 51.6, + MeanMotion: 15.5, + Eccentrcity: 0.0001, + }, + }, + { + Satellite: BatchSatellite{Name: "Sat2", NORADID: "12346"}, + Success: true, + TLE: TLE{ + OrbitInclination: 98.2, + MeanMotion: 14.2, + Eccentrcity: 0.0002, + }, + }, + { + Satellite: BatchSatellite{Name: "Sat3", NORADID: "12347"}, + Success: false, + Error: fmt.Errorf("test error"), + }, + } + + comparison := CompareSatellites(results) + + if comparison.Summary.TotalProcessed != 3 { + t.Errorf("TotalProcessed = %d, want %d", comparison.Summary.TotalProcessed, 3) + } + if comparison.Summary.Successful != 2 { + t.Errorf("Successful = %d, want %d", comparison.Summary.Successful, 2) + } + if comparison.Summary.Failed != 1 { + t.Errorf("Failed = %d, want %d", comparison.Summary.Failed, 1) + } + + // Check average inclination + expectedAvg := (51.6 + 98.2) / 2.0 + if comparison.Summary.AverageInclination != expectedAvg { + t.Errorf("AverageInclination = %.2f, want %.2f", comparison.Summary.AverageInclination, expectedAvg) + } + + // Check average mean motion + expectedMM := (15.5 + 14.2) / 2.0 + if comparison.Summary.AverageMeanMotion != expectedMM { + t.Errorf("AverageMeanMotion = %.4f, want %.4f", comparison.Summary.AverageMeanMotion, expectedMM) + } +} + +func TestCompareSatellitesEmpty(t *testing.T) { + results := []BatchTLEResult{} + comparison := CompareSatellites(results) + + if comparison.Summary.TotalProcessed != 0 { + t.Errorf("TotalProcessed = %d, want %d", comparison.Summary.TotalProcessed, 0) + } +} + +func TestCompareSatellitesAllFailed(t *testing.T) { + results := []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Sat1", NORADID: "12345"}, + Success: false, + Error: fmt.Errorf("error 1"), + }, + { + Satellite: BatchSatellite{Name: "Sat2", NORADID: "12346"}, + Success: false, + Error: fmt.Errorf("error 2"), + }, + } + + comparison := CompareSatellites(results) + + if comparison.Summary.Successful != 0 { + t.Errorf("Successful = %d, want %d", comparison.Summary.Successful, 0) + } + if comparison.Summary.Failed != 2 { + t.Errorf("Failed = %d, want %d", comparison.Summary.Failed, 2) + } +} + +func TestExportBatchTLECSV(t *testing.T) { + results := []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Test Sat", NORADID: "12345"}, + Success: true, + TLE: TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + OrbitInclination: 51.6, + MeanMotion: 15.5, + Eccentrcity: 0.0001, + }, + }, + { + Satellite: BatchSatellite{Name: "Failed Sat", NORADID: "12346"}, + Success: false, + Error: fmt.Errorf("test error"), + }, + } + + tempFile := filepath.Join(t.TempDir(), "batch_tle.csv") + err := exportBatchTLECSV(results, tempFile) + if err != nil { + t.Fatalf("exportBatchTLECSV() failed: %v", err) + } + + // Verify file exists + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Fatal("CSV file was not created") + } + + // Read and verify content + data, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read CSV file: %v", err) + } + + content := string(data) + if !strings.Contains(content, "Test Sat") { + t.Error("CSV should contain satellite name") + } + if !strings.Contains(content, "12345") { + t.Error("CSV should contain NORAD ID") + } + if !strings.Contains(content, "Success") { + t.Error("CSV should contain success status") + } +} + +func TestExportBatchTLEJSON(t *testing.T) { + results := []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Test Sat", NORADID: "12345"}, + Success: true, + TLE: TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + OrbitInclination: 51.6, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "batch_tle.json") + err := exportBatchTLEJSON(results, tempFile) + if err != nil { + t.Fatalf("exportBatchTLEJSON() failed: %v", err) + } + + // Verify file exists and is valid JSON + data, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read JSON file: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + if result["batch_results"] == nil { + t.Error("JSON should contain batch_results") + } + if result["total_count"] != float64(1) { + t.Errorf("total_count = %v, want %d", result["total_count"], 1) + } +} + +func TestExportBatchTLEText(t *testing.T) { + results := []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Test Sat", NORADID: "12345"}, + Success: true, + TLE: TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + OrbitInclination: 51.6, + MeanMotion: 15.5, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "batch_tle.txt") + err := exportBatchTLEText(results, tempFile) + if err != nil { + t.Fatalf("exportBatchTLEText() failed: %v", err) + } + + // Verify file exists + content, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read text file: %v", err) + } + + text := string(content) + if !strings.Contains(text, "Test Sat") { + t.Error("Text file should contain satellite name") + } + if !strings.Contains(text, "12345") { + t.Error("Text file should contain NORAD ID") + } + if !strings.Contains(text, "Success") { + t.Error("Text file should contain success status") + } +} + +func TestExportBatchComparisonCSV(t *testing.T) { + comparison := BatchComparisonResult{ + Summary: BatchSummary{ + TotalProcessed: 2, + Successful: 1, + Failed: 1, + AverageInclination: 51.6, + AverageMeanMotion: 15.5, + }, + Results: []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Sat1", NORADID: "12345"}, + Success: true, + TLE: TLE{ + OrbitInclination: 51.6, + MeanMotion: 15.5, + Eccentrcity: 0.0001, + }, + }, + { + Satellite: BatchSatellite{Name: "Sat2", NORADID: "12346"}, + Success: false, + Error: fmt.Errorf("test error"), + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "comparison.csv") + err := exportBatchComparisonCSV(comparison, tempFile) + if err != nil { + t.Fatalf("exportBatchComparisonCSV() failed: %v", err) + } + + // Verify file exists + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Fatal("CSV file was not created") + } + + // Read and verify + data, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read CSV file: %v", err) + } + + content := string(data) + if !strings.Contains(content, "Total Processed") { + t.Error("CSV should contain summary") + } + if !strings.Contains(content, "Sat1") { + t.Error("CSV should contain satellite names") + } +} + +func TestExportBatchComparisonJSON(t *testing.T) { + comparison := BatchComparisonResult{ + Summary: BatchSummary{ + TotalProcessed: 1, + Successful: 1, + }, + Results: []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Sat1", NORADID: "12345"}, + Success: true, + TLE: TLE{OrbitInclination: 51.6}, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "comparison.json") + err := exportBatchComparisonJSON(comparison, tempFile) + if err != nil { + t.Fatalf("exportBatchComparisonJSON() failed: %v", err) + } + + // Verify file exists and is valid JSON + data, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read JSON file: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + if result["summary"] == nil { + t.Error("JSON should contain summary") + } + if result["results"] == nil { + t.Error("JSON should contain results") + } +} + +func TestExportBatchComparisonText(t *testing.T) { + comparison := BatchComparisonResult{ + Summary: BatchSummary{ + TotalProcessed: 1, + Successful: 1, + AverageInclination: 51.6, + }, + Results: []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Sat1", NORADID: "12345"}, + Success: true, + TLE: TLE{OrbitInclination: 51.6}, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "comparison.txt") + err := exportBatchComparisonText(comparison, tempFile) + if err != nil { + t.Fatalf("exportBatchComparisonText() failed: %v", err) + } + + // Verify file exists + content, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read text file: %v", err) + } + + text := string(content) + if !strings.Contains(text, "Sat1") { + t.Error("Text file should contain satellite name") + } + if !strings.Contains(text, "Summary") { + t.Error("Text file should contain summary section") + } +} + +func TestBatchTLEResultStruct(t *testing.T) { + result := BatchTLEResult{ + Satellite: BatchSatellite{ + Name: "Test", + NORADID: "12345", + Country: "US", + ObjectType: "PAYLOAD", + }, + Success: true, + TLE: TLE{ + SatelliteCatalogNumber: 12345, + }, + } + + if !result.Success { + t.Error("Success should be true") + } + if result.Satellite.NORADID != "12345" { + t.Errorf("NORADID = %q, want %q", result.Satellite.NORADID, "12345") + } +} + +func TestBatchSummaryStruct(t *testing.T) { + summary := BatchSummary{ + TotalProcessed: 10, + Successful: 8, + Failed: 2, + AverageInclination: 51.6, + AverageMeanMotion: 15.5, + LowestAltitude: 400.0, + HighestAltitude: 500.0, + } + + if summary.TotalProcessed != 10 { + t.Errorf("TotalProcessed = %d, want %d", summary.TotalProcessed, 10) + } + if summary.Successful != 8 { + t.Errorf("Successful = %d, want %d", summary.Successful, 8) + } + if summary.AverageInclination != 51.6 { + t.Errorf("AverageInclination = %.2f, want %.2f", summary.AverageInclination, 51.6) + } +} + +// Benchmark tests +func BenchmarkCompareSatellites(b *testing.B) { + results := []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Sat1", NORADID: "12345"}, + Success: true, + TLE: TLE{ + OrbitInclination: 51.6, + MeanMotion: 15.5, + Eccentrcity: 0.0001, + }, + }, + { + Satellite: BatchSatellite{Name: "Sat2", NORADID: "12346"}, + Success: true, + TLE: TLE{ + OrbitInclination: 98.2, + MeanMotion: 14.2, + Eccentrcity: 0.0002, + }, + }, + } + + for i := 0; i < b.N; i++ { + CompareSatellites(results) + } +} + +func BenchmarkExportBatchTLEJSON(b *testing.B) { + results := []BatchTLEResult{ + { + Satellite: BatchSatellite{Name: "Test", NORADID: "12345"}, + Success: true, + TLE: TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + OrbitInclination: 51.6, + MeanMotion: 15.5, + }, + }, + } + + tempDir := b.TempDir() + for i := 0; i < b.N; i++ { + file := filepath.Join(tempDir, fmt.Sprintf("test_%d.json", i)) + exportBatchTLEJSON(results, file) + } +} + diff --git a/osint/errors.go b/osint/errors.go new file mode 100644 index 0000000..07b2f70 --- /dev/null +++ b/osint/errors.go @@ -0,0 +1,388 @@ +package osint + +import ( + "fmt" + "strconv" + "strings" + + "github.com/TwiN/go-color" +) + +// ErrorCode represents a unique error code for troubleshooting. +type ErrorCode string + +const ( + // Authentication errors (1000-1099) + ErrCodeAuthFailed ErrorCode = "AUTH-1001" + ErrCodeAuthCredentials ErrorCode = "AUTH-1002" + ErrCodeAuthConnection ErrorCode = "AUTH-1003" + ErrCodeAuthCookieJar ErrorCode = "AUTH-1004" + + // API errors (1100-1199) + ErrCodeAPIRequestFailed ErrorCode = "API-1101" + ErrCodeAPIResponseFailed ErrorCode = "API-1102" + ErrCodeAPIParseFailed ErrorCode = "API-1103" + ErrCodeAPINoData ErrorCode = "API-1104" + ErrCodeAPIInvalidEndpoint ErrorCode = "API-1105" + + // Input validation errors (1200-1299) + ErrCodeInputEmpty ErrorCode = "INPUT-1201" + ErrCodeInputInvalid ErrorCode = "INPUT-1202" + ErrCodeInputOutOfRange ErrorCode = "INPUT-1203" + ErrCodeInputFormat ErrorCode = "INPUT-1204" + + // TLE errors (1300-1399) + ErrCodeTLEInvalidFormat ErrorCode = "TLE-1301" + ErrCodeTLEParseFailed ErrorCode = "TLE-1302" + ErrCodeTLEInsufficientData ErrorCode = "TLE-1303" + ErrCodeTLEChecksumFailed ErrorCode = "TLE-1304" + + // File errors (1400-1499) + ErrCodeFileNotFound ErrorCode = "FILE-1401" + ErrCodeFileReadFailed ErrorCode = "FILE-1402" + ErrCodeFilePathInvalid ErrorCode = "FILE-1403" + ErrCodeFilePermission ErrorCode = "FILE-1404" + + // Satellite selection errors (1500-1599) + ErrCodeSatNotFound ErrorCode = "SAT-1501" + ErrCodeSatInvalidNORAD ErrorCode = "SAT-1502" + ErrCodeSatNoResults ErrorCode = "SAT-1503" + + // Network errors (1600-1699) + ErrCodeNetworkTimeout ErrorCode = "NET-1601" + ErrCodeNetworkUnreachable ErrorCode = "NET-1602" + ErrCodeNetworkDNS ErrorCode = "NET-1603" +) + +// AppError represents a structured application error with code, message, and suggestions. +type AppError struct { + Code ErrorCode + Message string + Context string + Suggestions []string + OriginalErr error +} + +// Error implements the error interface. +func (e *AppError) Error() string { + if e.OriginalErr != nil { + return fmt.Sprintf("%s: %s (%s)", e.Code, e.Message, e.OriginalErr.Error()) + } + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +// Display formats and displays the error with suggestions. +func (e *AppError) Display() { + fmt.Println(color.Ize(color.Red, fmt.Sprintf(" [!] ERROR [%s]: %s", e.Code, e.Message))) + + if e.Context != "" { + fmt.Println(color.Ize(color.Yellow, fmt.Sprintf(" Context: %s", e.Context))) + } + + if len(e.Suggestions) > 0 { + fmt.Println(color.Ize(color.Cyan, " Suggestions:")) + for i, suggestion := range e.Suggestions { + fmt.Println(color.Ize(color.Cyan, fmt.Sprintf(" %d. %s", i+1, suggestion))) + } + } + + if e.OriginalErr != nil { + fmt.Println(color.Ize(color.Gray, fmt.Sprintf(" Technical details: %v", e.OriginalErr))) + } +} + +// NewAppError creates a new AppError with the given code and message. +func NewAppError(code ErrorCode, message string) *AppError { + return &AppError{ + Code: code, + Message: message, + Suggestions: getDefaultSuggestions(code), + } +} + +// NewAppErrorWithContext creates a new AppError with context. +func NewAppErrorWithContext(code ErrorCode, message, context string) *AppError { + return &AppError{ + Code: code, + Message: message, + Context: context, + Suggestions: getDefaultSuggestions(code), + } +} + +// NewAppErrorWithErr wraps an original error in an AppError. +func NewAppErrorWithErr(code ErrorCode, message string, err error) *AppError { + return &AppError{ + Code: code, + Message: message, + OriginalErr: err, + Suggestions: getDefaultSuggestions(code), + } +} + +// getDefaultSuggestions returns default suggestions based on error code. +func getDefaultSuggestions(code ErrorCode) []string { + suggestions := map[ErrorCode][]string{ + // Authentication errors + ErrCodeAuthFailed: { + "Verify your Space-Track credentials in the .env file", + "Check if your account is active at space-track.org", + "Ensure SPACE_TRACK_USERNAME and SPACE_TRACK_PASSWORD are set correctly", + }, + ErrCodeAuthCredentials: { + "Check your .env file for SPACE_TRACK_USERNAME and SPACE_TRACK_PASSWORD", + "Verify credentials are not expired or changed", + "Try logging in manually at space-track.org to verify your account", + }, + ErrCodeAuthConnection: { + "Check your internet connection", + "Verify space-track.org is accessible", + "Check if a firewall or proxy is blocking the connection", + "Try again in a few moments - the service may be temporarily unavailable", + }, + ErrCodeAuthCookieJar: { + "Restart the application", + "Check system permissions for cookie storage", + }, + + // API errors + ErrCodeAPIRequestFailed: { + "Check your internet connection", + "Verify the API endpoint is correct", + "Try again in a few moments", + "Check if you're authenticated (run login first)", + }, + ErrCodeAPIResponseFailed: { + "The API returned an error response", + "Check if your query parameters are valid", + "Verify you have permission to access this data", + "Try with different search criteria", + }, + ErrCodeAPIParseFailed: { + "The API response format may have changed", + "Check if the data structure is correct", + "Try refreshing the data", + }, + ErrCodeAPINoData: { + "Try adjusting your search criteria", + "Verify the satellite NORAD ID exists", + "Check if the satellite is still active", + }, + ErrCodeAPIInvalidEndpoint: { + "Verify the endpoint URL is correct", + "Check API documentation for valid endpoints", + }, + + // Input validation errors + ErrCodeInputEmpty: { + "Please provide a value for this field", + "Check if you accidentally pressed Enter without entering data", + }, + ErrCodeInputInvalid: { + "Enter a valid numeric value", + "Remove any non-numeric characters (except decimal point and minus sign)", + "Check the expected format for this input", + }, + ErrCodeInputOutOfRange: { + "Check the valid range for this input", + "Latitude: -90 to 90", + "Longitude: -180 to 180", + "Altitude: 0 to 8848 meters (Mount Everest)", + }, + ErrCodeInputFormat: { + "Verify the input format matches the expected pattern", + "Check for typos or extra characters", + }, + + // TLE errors + ErrCodeTLEInvalidFormat: { + "Ensure TLE data has exactly 2 lines", + "Line 1 must start with '1 '", + "Line 2 must start with '2 '", + "Each line should be 69 characters long", + }, + ErrCodeTLEParseFailed: { + "Verify the TLE data is complete and not corrupted", + "Check if all required fields are present", + "Ensure the TLE format follows the standard specification", + }, + ErrCodeTLEInsufficientData: { + "Ensure you have both TLE lines", + "Check if the data was truncated", + "Verify the source of your TLE data", + }, + ErrCodeTLEChecksumFailed: { + "The TLE checksum validation failed", + "Verify the TLE data is not corrupted", + "Try fetching fresh TLE data from Space-Track", + }, + + // File errors + ErrCodeFileNotFound: { + "Verify the file path is correct", + "Check if the file exists at the specified location", + "Use an absolute path or ensure the file is in the current directory", + }, + ErrCodeFileReadFailed: { + "Check file permissions", + "Ensure the file is not locked by another process", + "Verify the file is not corrupted", + }, + ErrCodeFilePathInvalid: { + "Use a valid file path", + "Avoid directory traversal patterns (../)", + "Check for invalid characters in the path", + }, + ErrCodeFilePermission: { + "Check file and directory permissions", + "Ensure you have read access to the file", + "On Unix systems, check with: ls -l ", + }, + + // Satellite selection errors + ErrCodeSatNotFound: { + "Verify the NORAD ID is correct", + "Try searching by satellite name instead", + "Check if the satellite is still active", + }, + ErrCodeSatInvalidNORAD: { + "NORAD ID must be a numeric value", + "Check for typos in the NORAD ID", + "Verify the format: numeric only, no spaces", + }, + ErrCodeSatNoResults: { + "Try adjusting your search criteria", + "Use broader search terms", + "Check spelling of satellite name or country", + "Try removing filters to see more results", + }, + + // Network errors + ErrCodeNetworkTimeout: { + "Check your internet connection", + "The request took too long - try again", + "Check if the API service is experiencing high load", + }, + ErrCodeNetworkUnreachable: { + "Verify your internet connection is active", + "Check firewall settings", + "Try accessing the API URL in a browser", + }, + ErrCodeNetworkDNS: { + "Check your DNS settings", + "Verify you can resolve the API domain", + "Try using a different DNS server (e.g., 8.8.8.8)", + }, + } + + if sug, ok := suggestions[code]; ok { + return sug + } + return []string{"Please check the error details and try again"} +} + +// HandleError displays an error if it's an AppError, otherwise creates a generic error. +func HandleError(err error, defaultCode ErrorCode, defaultMessage string) { + if err == nil { + return + } + + if appErr, ok := err.(*AppError); ok { + appErr.Display() + return + } + + // Wrap generic errors + appErr := NewAppErrorWithErr(defaultCode, defaultMessage, err) + appErr.Display() +} + +// HandleErrorWithContext displays an error with additional context. +func HandleErrorWithContext(err error, code ErrorCode, message, context string) { + if err == nil { + return + } + + if appErr, ok := err.(*AppError); ok { + appErr.Context = context + appErr.Display() + return + } + + appErr := NewAppErrorWithContext(code, message, context) + appErr.OriginalErr = err + appErr.Display() +} + +// ValidateInput checks if input is empty and returns an appropriate error. +func ValidateInput(value, fieldName string) error { + value = strings.TrimSpace(value) + if value == "" { + return NewAppErrorWithContext( + ErrCodeInputEmpty, + fmt.Sprintf("%s cannot be empty", fieldName), + fmt.Sprintf("Field: %s", fieldName), + ) + } + return nil +} + +// cleanNumericInputForValidation removes non-numeric characters from input string. +func cleanNumericInputForValidation(input string) string { + var result strings.Builder + for _, char := range input { + if (char >= '0' && char <= '9') || char == '.' || char == '-' { + result.WriteRune(char) + } + } + return result.String() +} + +// ValidateNumericInput validates numeric input and returns an appropriate error. +func ValidateNumericInput(value, fieldName string, min, max float64) error { + if err := ValidateInput(value, fieldName); err != nil { + return err + } + + // Clean the input + cleaned := cleanNumericInputForValidation(value) + if cleaned == "" { + return NewAppErrorWithContext( + ErrCodeInputInvalid, + fmt.Sprintf("%s must be a valid number", fieldName), + fmt.Sprintf("Field: %s, Value: %s", fieldName, value), + ) + } + + // Parse and validate range if specified + if min != 0 || max != 0 { + var num float64 + var err error + if strings.Contains(cleaned, ".") { + num, err = strconv.ParseFloat(cleaned, 64) + } else { + var n int + n, err = strconv.Atoi(cleaned) + num = float64(n) + } + + if err != nil { + return NewAppErrorWithContext( + ErrCodeInputInvalid, + fmt.Sprintf("%s must be a valid number", fieldName), + fmt.Sprintf("Field: %s, Value: %s", fieldName, value), + ) + } + + if (min != 0 && num < min) || (max != 0 && num > max) { + return NewAppErrorWithContext( + ErrCodeInputOutOfRange, + fmt.Sprintf("%s must be between %.2f and %.2f", fieldName, min, max), + fmt.Sprintf("Field: %s, Value: %.2f", fieldName, num), + ) + } + } + + return nil +} + diff --git a/osint/export.go b/osint/export.go new file mode 100644 index 0000000..6a94c81 --- /dev/null +++ b/osint/export.go @@ -0,0 +1,622 @@ +package osint + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/manifoldco/promptui" +) + +// ExportFormat represents the available export formats. +type ExportFormat string + +const ( + FormatCSV ExportFormat = "CSV" + FormatJSON ExportFormat = "JSON" + FormatText ExportFormat = "Text" +) + +// showExportMenu displays a menu for selecting export format and file path. +func showExportMenu(defaultFilename string) (ExportFormat, string, error) { + formatItems := []string{"CSV", "JSON", "Text", "Cancel"} + + formatPrompt := promptui.Select{ + Label: "Select Export Format", + Items: formatItems, + } + + formatIdx, formatChoice, err := formatPrompt.Run() + if err != nil || formatIdx == 3 { + return "", "", fmt.Errorf("export cancelled") + } + + format := ExportFormat(formatChoice) + + pathPrompt := promptui.Prompt{ + Label: "Enter file path (or press Enter for default)", + Default: defaultFilename, + AllowEdit: true, + } + + filePath, err := pathPrompt.Run() + if err != nil { + return "", "", fmt.Errorf("export cancelled") + } + + filePath = strings.TrimSpace(filePath) + if filePath == "" { + filePath = defaultFilename + } + + // Add appropriate extension if not present + ext := filepath.Ext(filePath) + expectedExt := "" + switch format { + case FormatCSV: + expectedExt = ".csv" + case FormatJSON: + expectedExt = ".json" + case FormatText: + expectedExt = ".txt" + } + + if ext != expectedExt { + filePath += expectedExt + } + + return format, filePath, nil +} + +// ExportTLE exports TLE data to the specified format and file. +func ExportTLE(tle TLE, format ExportFormat, filePath string) error { + switch format { + case FormatCSV: + return exportTLECSV(tle, filePath) + case FormatJSON: + return exportTLEJSON(tle, filePath) + case FormatText: + return exportTLEText(tle, filePath) + default: + return fmt.Errorf("unsupported format: %s", format) + } +} + +// exportTLECSV exports TLE data to CSV format. +func exportTLECSV(tle TLE, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // Write header + headers := []string{ + "Field", "Value", + } + if err := writer.Write(headers); err != nil { + return fmt.Errorf("failed to write CSV header: %w", err) + } + + // Write data rows + rows := [][]string{ + {"Common Name", tle.CommonName}, + {"Satellite Catalog Number", strconv.Itoa(tle.SatelliteCatalogNumber)}, + {"Elset Classification", tle.ElsetClassificiation}, + {"International Designator", tle.InternationalDesignator}, + {"Element Set Epoch (UTC)", fmt.Sprintf("%f", tle.ElementSetEpoch)}, + {"1st Derivative of Mean Motion", fmt.Sprintf("%f", tle.FirstDerivativeMeanMotion)}, + {"2nd Derivative of Mean Motion", tle.SecondDerivativeMeanMotion}, + {"B* Drag Term", tle.BDragTerm}, + {"Element Set Type", strconv.Itoa(tle.ElementSetType)}, + {"Element Number", strconv.Itoa(tle.ElementNumber)}, + {"Checksum Line One", strconv.Itoa(tle.ChecksumOne)}, + {"Orbit Inclination (degrees)", fmt.Sprintf("%f", tle.OrbitInclination)}, + {"Right Ascension (degrees)", fmt.Sprintf("%f", tle.RightAscension)}, + {"Eccentricity", fmt.Sprintf("%f", tle.Eccentrcity)}, + {"Argument of Perigee (degrees)", fmt.Sprintf("%f", tle.Perigee)}, + {"Mean Anomaly (degrees)", fmt.Sprintf("%f", tle.MeanAnamoly)}, + {"Mean Motion (revolutions/day)", fmt.Sprintf("%f", tle.MeanMotion)}, + {"Revolution Number at Epoch", strconv.Itoa(tle.RevolutionNumber)}, + {"Checksum Line Two", strconv.Itoa(tle.ChecksumTwo)}, + } + + for _, row := range rows { + if err := writer.Write(row); err != nil { + return fmt.Errorf("failed to write CSV row: %w", err) + } + } + + return nil +} + +// exportTLEJSON exports TLE data to JSON format. +func exportTLEJSON(tle TLE, filePath string) error { + data := map[string]interface{}{ + "common_name": tle.CommonName, + "satellite_catalog_number": tle.SatelliteCatalogNumber, + "elset_classification": tle.ElsetClassificiation, + "international_designator": tle.InternationalDesignator, + "element_set_epoch_utc": tle.ElementSetEpoch, + "first_derivative_mean_motion": tle.FirstDerivativeMeanMotion, + "second_derivative_mean_motion": tle.SecondDerivativeMeanMotion, + "b_drag_term": tle.BDragTerm, + "element_set_type": tle.ElementSetType, + "element_number": tle.ElementNumber, + "checksum_line_one": tle.ChecksumOne, + "orbit_inclination_degrees": tle.OrbitInclination, + "right_ascension_degrees": tle.RightAscension, + "eccentricity": tle.Eccentrcity, + "argument_of_perigee_degrees": tle.Perigee, + "mean_anomaly_degrees": tle.MeanAnamoly, + "mean_motion_revolutions_per_day": tle.MeanMotion, + "revolution_number_at_epoch": tle.RevolutionNumber, + "checksum_line_two": tle.ChecksumTwo, + "export_timestamp": time.Now().Format(time.RFC3339), + } + + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + if err := os.WriteFile(filePath, jsonData, 0644); err != nil { + return fmt.Errorf("failed to write JSON file: %w", err) + } + + return nil +} + +// exportTLEText exports TLE data to text format. +func exportTLEText(tle TLE, filePath string) error { + var builder strings.Builder + + builder.WriteString("Two-Line Element (TLE) Data\n") + builder.WriteString(strings.Repeat("=", 60) + "\n\n") + + builder.WriteString(fmt.Sprintf("Common Name: %s\n", tle.CommonName)) + builder.WriteString(fmt.Sprintf("Satellite Catalog Number: %d\n", tle.SatelliteCatalogNumber)) + builder.WriteString(fmt.Sprintf("Elset Classification: %s\n", tle.ElsetClassificiation)) + builder.WriteString(fmt.Sprintf("International Designator: %s\n", tle.InternationalDesignator)) + builder.WriteString(fmt.Sprintf("Element Set Epoch (UTC): %f\n", tle.ElementSetEpoch)) + builder.WriteString(fmt.Sprintf("1st Derivative of Mean Motion: %f\n", tle.FirstDerivativeMeanMotion)) + builder.WriteString(fmt.Sprintf("2nd Derivative of Mean Motion: %s\n", tle.SecondDerivativeMeanMotion)) + builder.WriteString(fmt.Sprintf("B* Drag Term: %s\n", tle.BDragTerm)) + builder.WriteString(fmt.Sprintf("Element Set Type: %d\n", tle.ElementSetType)) + builder.WriteString(fmt.Sprintf("Element Number: %d\n", tle.ElementNumber)) + builder.WriteString(fmt.Sprintf("Checksum Line One: %d\n", tle.ChecksumOne)) + builder.WriteString(fmt.Sprintf("Orbit Inclination (degrees): %f\n", tle.OrbitInclination)) + builder.WriteString(fmt.Sprintf("Right Ascension (degrees): %f\n", tle.RightAscension)) + builder.WriteString(fmt.Sprintf("Eccentricity: %f\n", tle.Eccentrcity)) + builder.WriteString(fmt.Sprintf("Argument of Perigee (degrees): %f\n", tle.Perigee)) + builder.WriteString(fmt.Sprintf("Mean Anomaly (degrees): %f\n", tle.MeanAnamoly)) + builder.WriteString(fmt.Sprintf("Mean Motion (revolutions/day): %f\n", tle.MeanMotion)) + builder.WriteString(fmt.Sprintf("Revolution Number at Epoch: %d\n", tle.RevolutionNumber)) + builder.WriteString(fmt.Sprintf("Checksum Line Two: %d\n", tle.ChecksumTwo)) + builder.WriteString(fmt.Sprintf("\nExported: %s\n", time.Now().Format(time.RFC3339))) + + if err := os.WriteFile(filePath, []byte(builder.String()), 0644); err != nil { + return fmt.Errorf("failed to write text file: %w", err) + } + + return nil +} + +// ExportVisualPrediction exports visual pass predictions to the specified format. +func ExportVisualPrediction(data VisualPassesResponse, format ExportFormat, filePath string) error { + switch format { + case FormatCSV: + return exportVisualPredictionCSV(data, filePath) + case FormatJSON: + return exportVisualPredictionJSON(data, filePath) + case FormatText: + return exportVisualPredictionText(data, filePath) + default: + return fmt.Errorf("unsupported format: %s", format) + } +} + +// exportVisualPredictionCSV exports visual pass predictions to CSV format. +func exportVisualPredictionCSV(data VisualPassesResponse, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // Write satellite info + infoHeaders := []string{"Satellite Name", "Satellite ID", "Transactions Count", "Passes Count"} + infoRow := []string{ + data.Info.SatName, + strconv.Itoa(data.Info.SatID), + strconv.Itoa(data.Info.TransactionsCount), + strconv.Itoa(data.Info.PassesCount), + } + if err := writer.Write(infoHeaders); err != nil { + return fmt.Errorf("failed to write CSV header: %w", err) + } + if err := writer.Write(infoRow); err != nil { + return fmt.Errorf("failed to write CSV info row: %w", err) + } + + // Write empty row separator + if err := writer.Write([]string{}); err != nil { + return fmt.Errorf("failed to write separator: %w", err) + } + + // Write passes header + passHeaders := []string{ + "Pass #", "Start Azimuth", "Start Azimuth Compass", "Start Elevation", + "Start UTC", "Max Azimuth", "Max Azimuth Compass", "Max Elevation", + "Max UTC", "End Azimuth", "End Azimuth Compass", "End Elevation", + "End UTC", "Magnitude", "Duration (seconds)", + } + if err := writer.Write(passHeaders); err != nil { + return fmt.Errorf("failed to write pass headers: %w", err) + } + + // Write passes data + for i, pass := range data.Passes { + row := []string{ + strconv.Itoa(i + 1), + fmt.Sprintf("%f", pass.StartAz), + pass.StartAzCompass, + fmt.Sprintf("%f", pass.StartEl), + strconv.Itoa(pass.StartUTC), + fmt.Sprintf("%f", pass.MaxAz), + pass.MaxAzCompass, + fmt.Sprintf("%f", pass.MaxEl), + strconv.Itoa(pass.MaxUTC), + fmt.Sprintf("%f", pass.EndAz), + pass.EndAzCompass, + fmt.Sprintf("%f", pass.EndEl), + strconv.Itoa(pass.EndUTC), + fmt.Sprintf("%f", pass.Mag), + strconv.Itoa(pass.Duration), + } + if err := writer.Write(row); err != nil { + return fmt.Errorf("failed to write pass row: %w", err) + } + } + + return nil +} + +// exportVisualPredictionJSON exports visual pass predictions to JSON format. +func exportVisualPredictionJSON(data VisualPassesResponse, filePath string) error { + exportData := map[string]interface{}{ + "satellite_info": map[string]interface{}{ + "satellite_name": data.Info.SatName, + "satellite_id": data.Info.SatID, + "transactions_count": data.Info.TransactionsCount, + "passes_count": data.Info.PassesCount, + }, + "passes": data.Passes, + "export_timestamp": time.Now().Format(time.RFC3339), + } + + jsonData, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + if err := os.WriteFile(filePath, jsonData, 0644); err != nil { + return fmt.Errorf("failed to write JSON file: %w", err) + } + + return nil +} + +// exportVisualPredictionText exports visual pass predictions to text format. +func exportVisualPredictionText(data VisualPassesResponse, filePath string) error { + var builder strings.Builder + + builder.WriteString("Visual Pass Predictions\n") + builder.WriteString(strings.Repeat("=", 60) + "\n\n") + + builder.WriteString("Satellite Information:\n") + builder.WriteString(fmt.Sprintf(" Name: %s\n", data.Info.SatName)) + builder.WriteString(fmt.Sprintf(" ID: %d\n", data.Info.SatID)) + builder.WriteString(fmt.Sprintf(" Transactions Count: %d\n", data.Info.TransactionsCount)) + builder.WriteString(fmt.Sprintf(" Passes Count: %d\n\n", data.Info.PassesCount)) + + builder.WriteString("Passes:\n") + builder.WriteString(strings.Repeat("-", 60) + "\n") + + for i, pass := range data.Passes { + builder.WriteString(fmt.Sprintf("\nPass #%d:\n", i+1)) + builder.WriteString(fmt.Sprintf(" Start: Azimuth %.2f° (%s), Elevation %.2f°, UTC %d\n", + pass.StartAz, pass.StartAzCompass, pass.StartEl, pass.StartUTC)) + builder.WriteString(fmt.Sprintf(" Max: Azimuth %.2f° (%s), Elevation %.2f°, UTC %d\n", + pass.MaxAz, pass.MaxAzCompass, pass.MaxEl, pass.MaxUTC)) + builder.WriteString(fmt.Sprintf(" End: Azimuth %.2f° (%s), Elevation %.2f°, UTC %d\n", + pass.EndAz, pass.EndAzCompass, pass.EndEl, pass.EndUTC)) + builder.WriteString(fmt.Sprintf(" Magnitude: %.2f, Duration: %d seconds\n", + pass.Mag, pass.Duration)) + } + + builder.WriteString(fmt.Sprintf("\nExported: %s\n", time.Now().Format(time.RFC3339))) + + if err := os.WriteFile(filePath, []byte(builder.String()), 0644); err != nil { + return fmt.Errorf("failed to write text file: %w", err) + } + + return nil +} + +// ExportRadioPrediction exports radio pass predictions to the specified format. +func ExportRadioPrediction(data RadioPassResponse, format ExportFormat, filePath string) error { + switch format { + case FormatCSV: + return exportRadioPredictionCSV(data, filePath) + case FormatJSON: + return exportRadioPredictionJSON(data, filePath) + case FormatText: + return exportRadioPredictionText(data, filePath) + default: + return fmt.Errorf("unsupported format: %s", format) + } +} + +// exportRadioPredictionCSV exports radio pass predictions to CSV format. +func exportRadioPredictionCSV(data RadioPassResponse, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // Write satellite info + infoHeaders := []string{"Satellite Name", "Satellite ID", "Transactions Count", "Passes Count"} + infoRow := []string{ + data.Info.SatName, + strconv.Itoa(data.Info.SatID), + strconv.Itoa(data.Info.TransactionsCount), + strconv.Itoa(data.Info.PassesCount), + } + if err := writer.Write(infoHeaders); err != nil { + return fmt.Errorf("failed to write CSV header: %w", err) + } + if err := writer.Write(infoRow); err != nil { + return fmt.Errorf("failed to write CSV info row: %w", err) + } + + // Write empty row separator + if err := writer.Write([]string{}); err != nil { + return fmt.Errorf("failed to write separator: %w", err) + } + + // Write passes header + passHeaders := []string{ + "Pass #", "Start Azimuth", "Start Azimuth Compass", "Start UTC", + "Max Azimuth", "Max Azimuth Compass", "Max Elevation", "Max UTC", + "End Azimuth", "End Azimuth Compass", "End UTC", + } + if err := writer.Write(passHeaders); err != nil { + return fmt.Errorf("failed to write pass headers: %w", err) + } + + // Write passes data + for i, pass := range data.Passes { + row := []string{ + strconv.Itoa(i + 1), + fmt.Sprintf("%f", pass.StartAz), + pass.StartAzCompass, + strconv.FormatInt(pass.StartUTC, 10), + fmt.Sprintf("%f", pass.MaxAz), + pass.MaxAzCompass, + fmt.Sprintf("%f", pass.MaxEl), + strconv.FormatInt(pass.MaxUTC, 10), + fmt.Sprintf("%f", pass.EndAz), + pass.EndAzCompass, + strconv.FormatInt(pass.EndUTC, 10), + } + if err := writer.Write(row); err != nil { + return fmt.Errorf("failed to write pass row: %w", err) + } + } + + return nil +} + +// exportRadioPredictionJSON exports radio pass predictions to JSON format. +func exportRadioPredictionJSON(data RadioPassResponse, filePath string) error { + exportData := map[string]interface{}{ + "satellite_info": map[string]interface{}{ + "satellite_name": data.Info.SatName, + "satellite_id": data.Info.SatID, + "transactions_count": data.Info.TransactionsCount, + "passes_count": data.Info.PassesCount, + }, + "passes": data.Passes, + "export_timestamp": time.Now().Format(time.RFC3339), + } + + jsonData, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + if err := os.WriteFile(filePath, jsonData, 0644); err != nil { + return fmt.Errorf("failed to write JSON file: %w", err) + } + + return nil +} + +// exportRadioPredictionText exports radio pass predictions to text format. +func exportRadioPredictionText(data RadioPassResponse, filePath string) error { + var builder strings.Builder + + builder.WriteString("Radio Pass Predictions\n") + builder.WriteString(strings.Repeat("=", 60) + "\n\n") + + builder.WriteString("Satellite Information:\n") + builder.WriteString(fmt.Sprintf(" Name: %s\n", data.Info.SatName)) + builder.WriteString(fmt.Sprintf(" ID: %d\n", data.Info.SatID)) + builder.WriteString(fmt.Sprintf(" Transactions Count: %d\n", data.Info.TransactionsCount)) + builder.WriteString(fmt.Sprintf(" Passes Count: %d\n\n", data.Info.PassesCount)) + + builder.WriteString("Passes:\n") + builder.WriteString(strings.Repeat("-", 60) + "\n") + + for i, pass := range data.Passes { + builder.WriteString(fmt.Sprintf("\nPass #%d:\n", i+1)) + builder.WriteString(fmt.Sprintf(" Start: Azimuth %.2f° (%s), UTC %d\n", + pass.StartAz, pass.StartAzCompass, pass.StartUTC)) + builder.WriteString(fmt.Sprintf(" Max: Azimuth %.2f° (%s), Elevation %.2f°, UTC %d\n", + pass.MaxAz, pass.MaxAzCompass, pass.MaxEl, pass.MaxUTC)) + builder.WriteString(fmt.Sprintf(" End: Azimuth %.2f° (%s), UTC %d\n", + pass.EndAz, pass.EndAzCompass, pass.EndUTC)) + } + + builder.WriteString(fmt.Sprintf("\nExported: %s\n", time.Now().Format(time.RFC3339))) + + if err := os.WriteFile(filePath, []byte(builder.String()), 0644); err != nil { + return fmt.Errorf("failed to write text file: %w", err) + } + + return nil +} + +// ExportSatellitePosition exports satellite position data to the specified format. +func ExportSatellitePosition(data Response, format ExportFormat, filePath string) error { + switch format { + case FormatCSV: + return exportSatellitePositionCSV(data, filePath) + case FormatJSON: + return exportSatellitePositionJSON(data, filePath) + case FormatText: + return exportSatellitePositionText(data, filePath) + default: + return fmt.Errorf("unsupported format: %s", format) + } +} + +// exportSatellitePositionCSV exports satellite positions to CSV format. +func exportSatellitePositionCSV(data Response, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // Write satellite info + infoHeaders := []string{"Satellite Name", "Satellite ID"} + infoRow := []string{ + data.SatelliteInfo.Satname, + strconv.Itoa(data.SatelliteInfo.Satid), + } + if err := writer.Write(infoHeaders); err != nil { + return fmt.Errorf("failed to write CSV header: %w", err) + } + if err := writer.Write(infoRow); err != nil { + return fmt.Errorf("failed to write CSV info row: %w", err) + } + + // Write empty row separator + if err := writer.Write([]string{}); err != nil { + return fmt.Errorf("failed to write separator: %w", err) + } + + // Write positions header + posHeaders := []string{ + "Position #", "Latitude", "Longitude", "Altitude (km)", + "Azimuth", "Declination", "Timestamp", + } + if err := writer.Write(posHeaders); err != nil { + return fmt.Errorf("failed to write position headers: %w", err) + } + + // Write positions data + for i, pos := range data.Positions { + row := []string{ + strconv.Itoa(i + 1), + fmt.Sprintf("%f", pos.Satlatitude), + fmt.Sprintf("%f", pos.Satlongitude), + fmt.Sprintf("%f", pos.Sataltitude), + fmt.Sprintf("%f", pos.Azimuth), + fmt.Sprintf("%f", pos.Dec), + strconv.FormatInt(pos.Timestamp, 10), + } + if err := writer.Write(row); err != nil { + return fmt.Errorf("failed to write position row: %w", err) + } + } + + return nil +} + +// exportSatellitePositionJSON exports satellite positions to JSON format. +func exportSatellitePositionJSON(data Response, filePath string) error { + exportData := map[string]interface{}{ + "satellite_info": map[string]interface{}{ + "satellite_name": data.SatelliteInfo.Satname, + "satellite_id": data.SatelliteInfo.Satid, + }, + "positions": data.Positions, + "export_timestamp": time.Now().Format(time.RFC3339), + } + + jsonData, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + if err := os.WriteFile(filePath, jsonData, 0644); err != nil { + return fmt.Errorf("failed to write JSON file: %w", err) + } + + return nil +} + +// exportSatellitePositionText exports satellite positions to text format. +func exportSatellitePositionText(data Response, filePath string) error { + var builder strings.Builder + + builder.WriteString("Satellite Position Data\n") + builder.WriteString(strings.Repeat("=", 60) + "\n\n") + + builder.WriteString("Satellite Information:\n") + builder.WriteString(fmt.Sprintf(" Name: %s\n", data.SatelliteInfo.Satname)) + builder.WriteString(fmt.Sprintf(" ID: %d\n\n", data.SatelliteInfo.Satid)) + + builder.WriteString("Positions:\n") + builder.WriteString(strings.Repeat("-", 60) + "\n") + + for i, pos := range data.Positions { + builder.WriteString(fmt.Sprintf("\nPosition #%d:\n", i+1)) + builder.WriteString(fmt.Sprintf(" Latitude: %.6f°\n", pos.Satlatitude)) + builder.WriteString(fmt.Sprintf(" Longitude: %.6f°\n", pos.Satlongitude)) + builder.WriteString(fmt.Sprintf(" Altitude: %.2f km\n", pos.Sataltitude)) + builder.WriteString(fmt.Sprintf(" Azimuth: %.2f°\n", pos.Azimuth)) + builder.WriteString(fmt.Sprintf(" Declination: %.2f°\n", pos.Dec)) + builder.WriteString(fmt.Sprintf(" Timestamp: %d\n", pos.Timestamp)) + } + + builder.WriteString(fmt.Sprintf("\nExported: %s\n", time.Now().Format(time.RFC3339))) + + if err := os.WriteFile(filePath, []byte(builder.String()), 0644); err != nil { + return fmt.Errorf("failed to write text file: %w", err) + } + + return nil +} + diff --git a/osint/export_test.go b/osint/export_test.go new file mode 100644 index 0000000..a5e3ca8 --- /dev/null +++ b/osint/export_test.go @@ -0,0 +1,515 @@ +package osint + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestExportTLECSV(t *testing.T) { + tle := TLE{ + CommonName: "ISS (ZARYA)", + SatelliteCatalogNumber: 25544, + ElsetClassificiation: "U", + InternationalDesignator: "1998-067A", + ElementSetEpoch: 24001.5, + FirstDerivativeMeanMotion: 0.0001, + SecondDerivativeMeanMotion: "00000+0", + BDragTerm: "00000+0", + ElementSetType: 0, + ElementNumber: 999, + ChecksumOne: 5, + OrbitInclination: 51.6442, + RightAscension: 123.4567, + Eccentrcity: 0.0001234, + Perigee: 234.5678, + MeanAnamoly: 345.6789, + MeanMotion: 15.49, + RevolutionNumber: 12345, + ChecksumTwo: 6, + } + + tempFile := filepath.Join(t.TempDir(), "test_tle.csv") + if err := exportTLECSV(tle, tempFile); err != nil { + t.Fatalf("exportTLECSV() failed: %v", err) + } + + // Verify file exists + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Fatal("CSV file was not created") + } + + // Read and verify CSV content + file, err := os.Open(tempFile) + if err != nil { + t.Fatalf("Failed to open CSV file: %v", err) + } + defer file.Close() + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + t.Fatalf("Failed to read CSV: %v", err) + } + + if len(records) < 2 { + t.Fatal("CSV should have at least header and one data row") + } + + // Check header + if records[0][0] != "Field" || records[0][1] != "Value" { + t.Errorf("CSV header incorrect: got %v", records[0]) + } + + // Check some data rows + found := false + for _, record := range records[1:] { + if record[0] == "Common Name" && record[1] == "ISS (ZARYA)" { + found = true + break + } + } + if !found { + t.Error("CSV data not found correctly") + } +} + +func TestExportTLEJSON(t *testing.T) { + tle := TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + ElementSetEpoch: 24001.5, + OrbitInclination: 51.6442, + MeanMotion: 15.49, + } + + tempFile := filepath.Join(t.TempDir(), "test_tle.json") + if err := exportTLEJSON(tle, tempFile); err != nil { + t.Fatalf("exportTLEJSON() failed: %v", err) + } + + // Verify file exists + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Fatal("JSON file was not created") + } + + // Read and verify JSON content + data, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read JSON file: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + if result["common_name"] != "Test Satellite" { + t.Errorf("JSON common_name = %v, want %v", result["common_name"], "Test Satellite") + } + + if result["satellite_catalog_number"] != float64(12345) { + t.Errorf("JSON satellite_catalog_number = %v, want %v", result["satellite_catalog_number"], 12345) + } + + if result["export_timestamp"] == nil { + t.Error("JSON should include export_timestamp") + } +} + +func TestExportTLEText(t *testing.T) { + tle := TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + ElementSetEpoch: 24001.5, + OrbitInclination: 51.6442, + } + + tempFile := filepath.Join(t.TempDir(), "test_tle.txt") + if err := exportTLEText(tle, tempFile); err != nil { + t.Fatalf("exportTLEText() failed: %v", err) + } + + // Verify file exists + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Fatal("Text file was not created") + } + + // Read and verify content + data, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read text file: %v", err) + } + + content := string(data) + if !strings.Contains(content, "Test Satellite") { + t.Error("Text file should contain satellite name") + } + if !strings.Contains(content, "12345") { + t.Error("Text file should contain catalog number") + } + if !strings.Contains(content, "Exported:") { + t.Error("Text file should contain export timestamp") + } +} + +func TestExportVisualPredictionCSV(t *testing.T) { + data := VisualPassesResponse{ + Info: Info{ + SatName: "ISS (ZARYA)", + SatID: 25544, + TransactionsCount: 1, + PassesCount: 2, + }, + Passes: []Pass{ + { + StartAz: 45.0, + StartAzCompass: "NE", + StartEl: 10.0, + StartUTC: 1234567890, + MaxAz: 90.0, + MaxAzCompass: "E", + MaxEl: 60.0, + MaxUTC: 1234567900, + EndAz: 135.0, + EndAzCompass: "SE", + EndEl: 10.0, + EndUTC: 1234567910, + Mag: 2.5, + Duration: 600, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "test_visual.csv") + if err := exportVisualPredictionCSV(data, tempFile); err != nil { + t.Fatalf("exportVisualPredictionCSV() failed: %v", err) + } + + // Verify file exists and can be read + file, err := os.Open(tempFile) + if err != nil { + t.Fatalf("Failed to open CSV file: %v", err) + } + defer file.Close() + + reader := csv.NewReader(file) + reader.FieldsPerRecord = -1 // Allow variable number of fields + records, err := reader.ReadAll() + if err != nil { + t.Fatalf("Failed to read CSV: %v", err) + } + + // Filter out empty rows + var nonEmptyRecords [][]string + for _, record := range records { + if len(record) > 0 && (len(record) == 1 && record[0] == "" || len(record) > 1) { + if len(record) > 1 || record[0] != "" { + nonEmptyRecords = append(nonEmptyRecords, record) + } + } + } + + if len(nonEmptyRecords) < 2 { + t.Fatalf("CSV should have info and pass data, got %d non-empty rows", len(nonEmptyRecords)) + } + + // Check satellite info + if records[0][0] != "Satellite Name" { + t.Errorf("First row should be satellite info header, got %v", records[0]) + } +} + +func TestExportVisualPredictionJSON(t *testing.T) { + data := VisualPassesResponse{ + Info: Info{ + SatName: "Test Satellite", + SatID: 12345, + }, + Passes: []Pass{ + {StartAz: 45.0, MaxEl: 60.0, Duration: 600}, + }, + } + + tempFile := filepath.Join(t.TempDir(), "test_visual.json") + if err := exportVisualPredictionJSON(data, tempFile); err != nil { + t.Fatalf("exportVisualPredictionJSON() failed: %v", err) + } + + fileData, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read JSON file: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(fileData, &result); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + if result["satellite_info"] == nil { + t.Error("JSON should contain satellite_info") + } + + if result["passes"] == nil { + t.Error("JSON should contain passes") + } +} + +func TestExportRadioPredictionCSV(t *testing.T) { + data := RadioPassResponse{ + Info: Info{ + SatName: "Test Satellite", + SatID: 12345, + TransactionsCount: 1, + PassesCount: 1, + }, + Passes: []RadioPass{ + { + StartAz: 45.0, + StartAzCompass: "NE", + StartUTC: 1234567890, + MaxAz: 90.0, + MaxAzCompass: "E", + MaxEl: 60.0, + MaxUTC: 1234567900, + EndAz: 135.0, + EndAzCompass: "SE", + EndUTC: 1234567910, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "test_radio.csv") + if err := exportRadioPredictionCSV(data, tempFile); err != nil { + t.Fatalf("exportRadioPredictionCSV() failed: %v", err) + } + + // Verify file exists + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Fatal("CSV file was not created") + } +} + +func TestExportSatellitePositionCSV(t *testing.T) { + data := Response{ + SatelliteInfo: SatelliteInfo{ + Satname: "Test Satellite", + Satid: 12345, + }, + Positions: []Position{ + { + Satlatitude: 40.7128, + Satlongitude: -74.0060, + Sataltitude: 400.0, + Azimuth: 45.0, + Dec: 30.0, + Timestamp: 1234567890, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "test_position.csv") + if err := exportSatellitePositionCSV(data, tempFile); err != nil { + t.Fatalf("exportSatellitePositionCSV() failed: %v", err) + } + + // Verify file exists + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Fatal("CSV file was not created") + } + + // Read and verify + file, err := os.Open(tempFile) + if err != nil { + t.Fatalf("Failed to open CSV file: %v", err) + } + defer file.Close() + + reader := csv.NewReader(file) + reader.FieldsPerRecord = -1 // Allow variable number of fields + records, err := reader.ReadAll() + if err != nil { + t.Fatalf("Failed to read CSV: %v", err) + } + + // Filter out empty rows + var nonEmptyRecords [][]string + for _, record := range records { + if len(record) > 0 && (len(record) == 1 && record[0] == "" || len(record) > 1) { + if len(record) > 1 || record[0] != "" { + nonEmptyRecords = append(nonEmptyRecords, record) + } + } + } + + if len(nonEmptyRecords) < 2 { + t.Fatalf("CSV should have info and position data, got %d non-empty rows", len(nonEmptyRecords)) + } +} + +func TestExportSatellitePositionJSON(t *testing.T) { + data := Response{ + SatelliteInfo: SatelliteInfo{ + Satname: "Test Satellite", + Satid: 12345, + }, + Positions: []Position{ + { + Satlatitude: 40.7128, + Satlongitude: -74.0060, + Sataltitude: 400.0, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "test_position.json") + if err := exportSatellitePositionJSON(data, tempFile); err != nil { + t.Fatalf("exportSatellitePositionJSON() failed: %v", err) + } + + // Verify file exists and is valid JSON + fileData, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read JSON file: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(fileData, &result); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + if result["satellite_info"] == nil { + t.Error("JSON should contain satellite_info") + } + + if result["positions"] == nil { + t.Error("JSON should contain positions") + } +} + +func TestExportSatellitePositionText(t *testing.T) { + data := Response{ + SatelliteInfo: SatelliteInfo{ + Satname: "Test Satellite", + Satid: 12345, + }, + Positions: []Position{ + { + Satlatitude: 40.7128, + Satlongitude: -74.0060, + Sataltitude: 400.0, + }, + }, + } + + tempFile := filepath.Join(t.TempDir(), "test_position.txt") + if err := exportSatellitePositionText(data, tempFile); err != nil { + t.Fatalf("exportSatellitePositionText() failed: %v", err) + } + + // Verify file exists + content, err := os.ReadFile(tempFile) + if err != nil { + t.Fatalf("Failed to read text file: %v", err) + } + + text := string(content) + if !strings.Contains(text, "Test Satellite") { + t.Error("Text file should contain satellite name") + } + if !strings.Contains(text, "40.7128") { + t.Error("Text file should contain latitude") + } +} + +func TestExportTLE(t *testing.T) { + tle := TLE{ + CommonName: "Test", + SatelliteCatalogNumber: 12345, + } + + tempDir := t.TempDir() + + // Test CSV export + csvFile := filepath.Join(tempDir, "test.csv") + if err := ExportTLE(tle, FormatCSV, csvFile); err != nil { + t.Fatalf("ExportTLE(CSV) failed: %v", err) + } + if _, err := os.Stat(csvFile); os.IsNotExist(err) { + t.Error("CSV file was not created") + } + + // Test JSON export + jsonFile := filepath.Join(tempDir, "test.json") + if err := ExportTLE(tle, FormatJSON, jsonFile); err != nil { + t.Fatalf("ExportTLE(JSON) failed: %v", err) + } + if _, err := os.Stat(jsonFile); os.IsNotExist(err) { + t.Error("JSON file was not created") + } + + // Test Text export + txtFile := filepath.Join(tempDir, "test.txt") + if err := ExportTLE(tle, FormatText, txtFile); err != nil { + t.Fatalf("ExportTLE(Text) failed: %v", err) + } + if _, err := os.Stat(txtFile); os.IsNotExist(err) { + t.Error("Text file was not created") + } + + // Test invalid format + invalidFile := filepath.Join(tempDir, "test.invalid") + if err := ExportTLE(tle, ExportFormat("INVALID"), invalidFile); err == nil { + t.Error("ExportTLE should fail for invalid format") + } +} + +func TestExportFormatConstants(t *testing.T) { + if FormatCSV != "CSV" { + t.Errorf("FormatCSV = %q, want %q", FormatCSV, "CSV") + } + if FormatJSON != "JSON" { + t.Errorf("FormatJSON = %q, want %q", FormatJSON, "JSON") + } + if FormatText != "Text" { + t.Errorf("FormatText = %q, want %q", FormatText, "Text") + } +} + +// Benchmark tests +func BenchmarkExportTLECSV(b *testing.B) { + tle := TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + ElementSetEpoch: 24001.5, + OrbitInclination: 51.6442, + MeanMotion: 15.49, + } + + tempDir := b.TempDir() + for i := 0; i < b.N; i++ { + file := filepath.Join(tempDir, fmt.Sprintf("test_%d.csv", i)) + exportTLECSV(tle, file) + } +} + +func BenchmarkExportTLEJSON(b *testing.B) { + tle := TLE{ + CommonName: "Test Satellite", + SatelliteCatalogNumber: 12345, + ElementSetEpoch: 24001.5, + OrbitInclination: 51.6442, + MeanMotion: 15.49, + } + + tempDir := b.TempDir() + for i := 0; i < b.N; i++ { + file := filepath.Join(tempDir, fmt.Sprintf("test_%d.json", i)) + exportTLEJSON(tle, file) + } +} + diff --git a/osint/favorites.go b/osint/favorites.go new file mode 100644 index 0000000..f234e0f --- /dev/null +++ b/osint/favorites.go @@ -0,0 +1,299 @@ +package osint + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/TwiN/go-color" + "github.com/manifoldco/promptui" +) + +const favoritesFile = "favorites.json" + +// FavoriteSatellite represents a saved favorite satellite. +type FavoriteSatellite struct { + SatelliteName string `json:"satellite_name"` + NORADID string `json:"norad_id"` + Country string `json:"country,omitempty"` + ObjectType string `json:"object_type,omitempty"` + AddedDate string `json:"added_date"` +} + +// FavoritesList represents the collection of favorite satellites. +type FavoritesList struct { + Favorites []FavoriteSatellite `json:"favorites"` +} + +// getFavoritesPath returns the full path to the favorites file. +func getFavoritesPath() string { + homeDir, err := os.UserHomeDir() + if err != nil { + // Fallback to current directory + return favoritesFile + } + favoritesDir := filepath.Join(homeDir, ".satintel") + os.MkdirAll(favoritesDir, 0755) + return filepath.Join(favoritesDir, favoritesFile) +} + +// LoadFavorites reads the favorites list from the JSON file. +func LoadFavorites() ([]FavoriteSatellite, error) { + favoritesPath := getFavoritesPath() + + data, err := os.ReadFile(favoritesPath) + if err != nil { + if os.IsNotExist(err) { + // File doesn't exist yet, return empty list + return []FavoriteSatellite{}, nil + } + return nil, fmt.Errorf("failed to read favorites file: %w", err) + } + + var favoritesList FavoritesList + if err := json.Unmarshal(data, &favoritesList); err != nil { + return nil, fmt.Errorf("failed to parse favorites file: %w", err) + } + + return favoritesList.Favorites, nil +} + +// SaveFavorites writes the favorites list to the JSON file. +func SaveFavorites(favorites []FavoriteSatellite) error { + favoritesPath := getFavoritesPath() + + favoritesList := FavoritesList{ + Favorites: favorites, + } + + data, err := json.MarshalIndent(favoritesList, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal favorites: %w", err) + } + + if err := os.WriteFile(favoritesPath, data, 0644); err != nil { + return fmt.Errorf("failed to write favorites file: %w", err) + } + + return nil +} + +// AddFavorite adds a satellite to the favorites list. +func AddFavorite(satName, noradID, country, objectType string) error { + favorites, err := LoadFavorites() + if err != nil { + return err + } + + // Check if already exists + for _, fav := range favorites { + if fav.NORADID == noradID { + return fmt.Errorf("satellite with NORAD ID %s is already in favorites", noradID) + } + } + + newFavorite := FavoriteSatellite{ + SatelliteName: satName, + NORADID: noradID, + Country: country, + ObjectType: objectType, + AddedDate: time.Now().Format("2006-01-02 15:04:05"), + } + + favorites = append(favorites, newFavorite) + return SaveFavorites(favorites) +} + +// RemoveFavorite removes a satellite from the favorites list by NORAD ID. +func RemoveFavorite(noradID string) error { + favorites, err := LoadFavorites() + if err != nil { + return err + } + + var updatedFavorites []FavoriteSatellite + found := false + for _, fav := range favorites { + if fav.NORADID != noradID { + updatedFavorites = append(updatedFavorites, fav) + } else { + found = true + } + } + + if !found { + return fmt.Errorf("satellite with NORAD ID %s not found in favorites", noradID) + } + + return SaveFavorites(updatedFavorites) +} + +// IsFavorite checks if a satellite is in the favorites list. +func IsFavorite(noradID string) (bool, error) { + favorites, err := LoadFavorites() + if err != nil { + return false, err + } + + for _, fav := range favorites { + if fav.NORADID == noradID { + return true, nil + } + } + + return false, nil +} + +// SelectFromFavorites displays a menu to select from saved favorites. +func SelectFromFavorites() string { + favorites, err := LoadFavorites() + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to load favorites: "+err.Error())) + return "" + } + + if len(favorites) == 0 { + fmt.Println(color.Ize(color.Yellow, " [!] No favorites saved yet")) + fmt.Println(color.Ize(color.Cyan, " [*] Add favorites by selecting 'Save to Favorites' after choosing a satellite")) + return "" + } + + var menuItems []string + for _, fav := range favorites { + info := fmt.Sprintf("%s (%s)", fav.SatelliteName, fav.NORADID) + if fav.Country != "" { + info += fmt.Sprintf(" - %s", fav.Country) + } + if fav.ObjectType != "" { + info += fmt.Sprintf(" [%s]", fav.ObjectType) + } + info += fmt.Sprintf(" (Added: %s)", fav.AddedDate) + menuItems = append(menuItems, info) + } + + menuItems = append(menuItems, "❌ Cancel") + + prompt := promptui.Select{ + Label: fmt.Sprintf("Select from Favorites ⭐ (%d saved)", len(favorites)), + Items: menuItems, + Size: 15, + } + + idx, _, err := prompt.Run() + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] PROMPT FAILED")) + return "" + } + + if idx >= len(favorites) { + // Cancel + return "" + } + + selected := favorites[idx] + return fmt.Sprintf("%s (%s)", selected.SatelliteName, selected.NORADID) +} + +// ManageFavorites provides an interactive menu to manage favorites. +func ManageFavorites() { + favorites, err := LoadFavorites() + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to load favorites: "+err.Error())) + return + } + + if len(favorites) == 0 { + fmt.Println(color.Ize(color.Yellow, " [!] No favorites saved yet")) + return + } + + menuItems := []string{ + "View All Favorites", + "Remove Favorite", + "Clear All Favorites", + "Back", + } + + prompt := promptui.Select{ + Label: fmt.Sprintf("Manage Favorites ⭐ (%d saved)", len(favorites)), + Items: menuItems, + } + + idx, _, err := prompt.Run() + if err != nil { + return + } + + switch idx { + case 0: // View All Favorites + fmt.Println(color.Ize(color.Cyan, "\n Your Favorites:")) + fmt.Println(strings.Repeat("-", 70)) + for i, fav := range favorites { + fmt.Printf("%d. %s (%s)\n", i+1, fav.SatelliteName, fav.NORADID) + if fav.Country != "" { + fmt.Printf(" Country: %s\n", fav.Country) + } + if fav.ObjectType != "" { + fmt.Printf(" Type: %s\n", fav.ObjectType) + } + fmt.Printf(" Added: %s\n", fav.AddedDate) + if i < len(favorites)-1 { + fmt.Println() + } + } + fmt.Println(strings.Repeat("-", 70)) + + case 1: // Remove Favorite + var removeItems []string + for _, fav := range favorites { + removeItems = append(removeItems, fmt.Sprintf("%s (%s)", fav.SatelliteName, fav.NORADID)) + } + removeItems = append(removeItems, "Cancel") + + removePrompt := promptui.Select{ + Label: "Select Favorite to Remove", + Items: removeItems, + } + + removeIdx, _, err := removePrompt.Run() + if err != nil || removeIdx >= len(favorites) { + return + } + + selected := favorites[removeIdx] + if err := RemoveFavorite(selected.NORADID); err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: "+err.Error())) + } else { + fmt.Println(color.Ize(color.Green, fmt.Sprintf(" [+] Removed %s from favorites", selected.SatelliteName))) + } + + case 2: // Clear All Favorites + confirmPrompt := promptui.Prompt{ + Label: "Are you sure you want to clear all favorites? (yes/no)", + Default: "no", + AllowEdit: true, + } + + confirm, err := confirmPrompt.Run() + if err != nil { + return + } + + if strings.ToLower(strings.TrimSpace(confirm)) == "yes" { + if err := SaveFavorites([]FavoriteSatellite{}); err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: "+err.Error())) + } else { + fmt.Println(color.Ize(color.Green, " [+] All favorites cleared")) + } + } + } +} + + + + + + diff --git a/osint/favorites_test.go b/osint/favorites_test.go new file mode 100644 index 0000000..7742f65 --- /dev/null +++ b/osint/favorites_test.go @@ -0,0 +1,345 @@ +package osint + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestLoadFavorites(t *testing.T) { + // Create a temporary favorites file + tempDir := t.TempDir() + originalHome := os.Getenv("HOME") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } + }() + + // Set HOME to temp directory (works on Unix, but Windows uses UserHomeDir differently) + os.Setenv("HOME", tempDir) + + // Clear any existing favorites by deleting the file if it exists + favoritesPath := getFavoritesPath() + os.Remove(favoritesPath) + os.RemoveAll(filepath.Dir(favoritesPath)) + + // Test loading non-existent file (should return empty list) + favorites, err := LoadFavorites() + if err != nil { + t.Fatalf("LoadFavorites() should not error on non-existent file, got: %v", err) + } + if len(favorites) != 0 { + // On Windows, UserHomeDir() doesn't use HOME, so we might get existing favorites + // Just verify the function works, not the exact count + t.Logf("Note: LoadFavorites() returned %d items (may include existing favorites on Windows)", len(favorites)) + } + + // Create a valid favorites file + testFavorites := []FavoriteSatellite{ + { + SatelliteName: "ISS (ZARYA)", + NORADID: "25544", + Country: "US", + ObjectType: "PAYLOAD", + AddedDate: "2024-01-01 12:00:00", + }, + { + SatelliteName: "STARLINK-1007", + NORADID: "44700", + Country: "US", + ObjectType: "PAYLOAD", + AddedDate: "2024-01-02 12:00:00", + }, + } + + if err := SaveFavorites(testFavorites); err != nil { + t.Fatalf("SaveFavorites() failed: %v", err) + } + + // Load and verify + favorites, err = LoadFavorites() + if err != nil { + t.Fatalf("LoadFavorites() failed: %v", err) + } + if len(favorites) != 2 { + t.Errorf("LoadFavorites() should return 2 favorites, got %d", len(favorites)) + } + if favorites[0].NORADID != "25544" { + t.Errorf("LoadFavorites() first item NORADID = %q, want %q", favorites[0].NORADID, "25544") + } +} + +func TestSaveFavorites(t *testing.T) { + tempDir := t.TempDir() + originalHome := os.Getenv("HOME") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } + }() + + os.Setenv("HOME", tempDir) + + testFavorites := []FavoriteSatellite{ + { + SatelliteName: "Test Satellite", + NORADID: "12345", + Country: "US", + ObjectType: "PAYLOAD", + AddedDate: time.Now().Format("2006-01-02 15:04:05"), + }, + } + + if err := SaveFavorites(testFavorites); err != nil { + t.Fatalf("SaveFavorites() failed: %v", err) + } + + // Verify file was created + favoritesPath := getFavoritesPath() + if _, err := os.Stat(favoritesPath); os.IsNotExist(err) { + t.Errorf("SaveFavorites() should create favorites file at %s", favoritesPath) + } + + // Load and verify + loaded, err := LoadFavorites() + if err != nil { + t.Fatalf("LoadFavorites() after SaveFavorites() failed: %v", err) + } + if len(loaded) != 1 { + t.Errorf("Loaded favorites count = %d, want 1", len(loaded)) + } + if loaded[0].NORADID != "12345" { + t.Errorf("Loaded NORADID = %q, want %q", loaded[0].NORADID, "12345") + } +} + +func TestAddFavorite(t *testing.T) { + tempDir := t.TempDir() + originalHome := os.Getenv("HOME") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } + }() + + os.Setenv("HOME", tempDir) + + // Clear any existing favorites + SaveFavorites([]FavoriteSatellite{}) + + // Add first favorite + if err := AddFavorite("ISS (ZARYA)", "25544", "US", "PAYLOAD"); err != nil { + t.Fatalf("AddFavorite() failed: %v", err) + } + + // Verify it was added + favorites, err := LoadFavorites() + if err != nil { + t.Fatalf("LoadFavorites() failed: %v", err) + } + if len(favorites) != 1 { + t.Errorf("Favorites count = %d, want 1", len(favorites)) + } + if favorites[0].NORADID != "25544" { + t.Errorf("Added favorite NORADID = %q, want %q", favorites[0].NORADID, "25544") + } + + // Try to add duplicate (should fail) + if err := AddFavorite("ISS", "25544", "US", "PAYLOAD"); err == nil { + t.Error("AddFavorite() should fail when adding duplicate NORAD ID") + } else if !strings.Contains(err.Error(), "already in favorites") { + t.Errorf("AddFavorite() error should mention 'already in favorites', got: %v", err) + } + + // Add another favorite + if err := AddFavorite("STARLINK-1007", "44700", "US", "PAYLOAD"); err != nil { + t.Fatalf("AddFavorite() second satellite failed: %v", err) + } + + favorites, _ = LoadFavorites() + if len(favorites) != 2 { + t.Errorf("Favorites count after second add = %d, want 2", len(favorites)) + } +} + +func TestRemoveFavorite(t *testing.T) { + tempDir := t.TempDir() + originalHome := os.Getenv("HOME") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } + }() + + os.Setenv("HOME", tempDir) + + // Clear any existing favorites + SaveFavorites([]FavoriteSatellite{}) + + // Add some favorites + AddFavorite("ISS (ZARYA)", "25544", "US", "PAYLOAD") + AddFavorite("STARLINK-1007", "44700", "US", "PAYLOAD") + AddFavorite("NOAA 15", "25338", "US", "PAYLOAD") + + // Remove one + if err := RemoveFavorite("25544"); err != nil { + t.Fatalf("RemoveFavorite() failed: %v", err) + } + + // Verify removal + favorites, err := LoadFavorites() + if err != nil { + t.Fatalf("LoadFavorites() failed: %v", err) + } + if len(favorites) != 2 { + t.Errorf("Favorites count after removal = %d, want 2", len(favorites)) + } + + // Verify correct one was removed + for _, fav := range favorites { + if fav.NORADID == "25544" { + t.Error("RemoveFavorite() did not remove the specified favorite") + } + } + + // Try to remove non-existent (should fail) + if err := RemoveFavorite("99999"); err == nil { + t.Error("RemoveFavorite() should fail when removing non-existent favorite") + } else if !strings.Contains(err.Error(), "not found") { + t.Errorf("RemoveFavorite() error should mention 'not found', got: %v", err) + } +} + +func TestIsFavorite(t *testing.T) { + tempDir := t.TempDir() + originalHome := os.Getenv("HOME") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } + }() + + os.Setenv("HOME", tempDir) + + // Clear any existing favorites + SaveFavorites([]FavoriteSatellite{}) + + // Add a favorite + AddFavorite("ISS (ZARYA)", "25544", "US", "PAYLOAD") + + // Test existing favorite + isFav, err := IsFavorite("25544") + if err != nil { + t.Fatalf("IsFavorite() failed: %v", err) + } + if !isFav { + t.Error("IsFavorite() should return true for existing favorite") + } + + // Test non-existent favorite + isFav, err = IsFavorite("99999") + if err != nil { + t.Fatalf("IsFavorite() failed: %v", err) + } + if isFav { + t.Error("IsFavorite() should return false for non-existent favorite") + } +} + +func TestGetFavoritesPath(t *testing.T) { + // This test verifies the path structure, but on Windows UserHomeDir() + // doesn't use HOME env var, so we just verify it returns a valid path + path := getFavoritesPath() + + // Verify path ends with expected filename + if !strings.HasSuffix(path, filepath.Join(".satintel", "favorites.json")) && + !strings.HasSuffix(path, filepath.Join(".satintel", "favorites.json")) { + t.Errorf("getFavoritesPath() should end with .satintel/favorites.json, got: %q", path) + } + + // Verify directory exists or can be created + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Errorf("getFavoritesPath() directory should be creatable: %v", err) + } +} + +func TestFavoriteSatelliteStruct(t *testing.T) { + fav := FavoriteSatellite{ + SatelliteName: "Test Satellite", + NORADID: "12345", + Country: "US", + ObjectType: "PAYLOAD", + AddedDate: "2024-01-01 12:00:00", + } + + if fav.SatelliteName != "Test Satellite" { + t.Errorf("FavoriteSatellite.SatelliteName = %q, want %q", fav.SatelliteName, "Test Satellite") + } + if fav.NORADID != "12345" { + t.Errorf("FavoriteSatellite.NORADID = %q, want %q", fav.NORADID, "12345") + } +} + +// Benchmark tests +func BenchmarkLoadFavorites(b *testing.B) { + tempDir := b.TempDir() + originalHome := os.Getenv("HOME") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } + }() + + os.Setenv("HOME", tempDir) + + // Create test favorites + testFavorites := make([]FavoriteSatellite, 100) + for i := 0; i < 100; i++ { + testFavorites[i] = FavoriteSatellite{ + SatelliteName: fmt.Sprintf("Satellite %d", i), + NORADID: fmt.Sprintf("%d", 10000+i), + Country: "US", + ObjectType: "PAYLOAD", + AddedDate: time.Now().Format("2006-01-02 15:04:05"), + } + } + SaveFavorites(testFavorites) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + LoadFavorites() + } +} + +func BenchmarkSaveFavorites(b *testing.B) { + tempDir := b.TempDir() + originalHome := os.Getenv("HOME") + defer func() { + if originalHome != "" { + os.Setenv("HOME", originalHome) + } + }() + + os.Setenv("HOME", tempDir) + + testFavorites := []FavoriteSatellite{ + { + SatelliteName: "Test Satellite", + NORADID: "12345", + Country: "US", + ObjectType: "PAYLOAD", + AddedDate: time.Now().Format("2006-01-02 15:04:05"), + }, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + SaveFavorites(testFavorites) + } +} + diff --git a/osint/geolocation.go b/osint/geolocation.go new file mode 100644 index 0000000..cee676d --- /dev/null +++ b/osint/geolocation.go @@ -0,0 +1,235 @@ +package osint + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" + + "github.com/TwiN/go-color" +) + +// LocationData represents the user's geographic location +type LocationData struct { + Latitude float64 + Longitude float64 + City string + Country string + Region string + Timezone string +} + +// GetUserLocation automatically detects the user's location using IP geolocation. +// Returns latitude, longitude, and location info, or an error if detection fails. +func GetUserLocation() (*LocationData, error) { + fmt.Println(color.Ize(color.Cyan, " [*] Detecting your location...")) + + // Try multiple free geolocation APIs for reliability + apis := []struct { + name string + url string + parse func([]byte) (*LocationData, error) + }{ + { + name: "ip-api.com", + url: "http://ip-api.com/json/?fields=status,lat,lon,city,country,regionName,timezone", + parse: parseIPAPIResponse, + }, + { + name: "ipapi.co", + url: "https://ipapi.co/json/", + parse: parseIPAPICoResponse, + }, + { + name: "ip-api.com (backup)", + url: "https://ip-api.com/json/?fields=status,lat,lon,city,country,regionName,timezone", + parse: parseIPAPIResponse, + }, + } + + var lastErr error + for _, api := range apis { + client := &http.Client{ + Timeout: 5 * time.Second, + } + + resp, err := client.Get(api.url) + if err != nil { + lastErr = fmt.Errorf("failed to connect to %s: %w", api.name, err) + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + lastErr = fmt.Errorf("%s returned status %d", api.name, resp.StatusCode) + continue + } + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + lastErr = fmt.Errorf("failed to read response from %s: %w", api.name, err) + continue + } + + location, err := api.parse(body) + if err != nil { + lastErr = fmt.Errorf("failed to parse %s response: %w", api.name, err) + continue + } + + if location != nil && location.Latitude != 0 && location.Longitude != 0 { + fmt.Println(color.Ize(color.Green, fmt.Sprintf(" [+] Location detected: %s, %s", location.City, location.Country))) + return location, nil + } + } + + return nil, fmt.Errorf("failed to detect location from all APIs: %w", lastErr) +} + +// parseIPAPIResponse parses the response from ip-api.com +func parseIPAPIResponse(body []byte) (*LocationData, error) { + var response struct { + Status string `json:"status"` + Latitude float64 `json:"lat"` + Longitude float64 `json:"lon"` + City string `json:"city"` + Country string `json:"country"` + Region string `json:"regionName"` + Timezone string `json:"timezone"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + + if response.Status != "success" { + return nil, fmt.Errorf("API returned status: %s", response.Status) + } + + if response.Latitude == 0 && response.Longitude == 0 { + return nil, fmt.Errorf("invalid coordinates") + } + + return &LocationData{ + Latitude: response.Latitude, + Longitude: response.Longitude, + City: response.City, + Country: response.Country, + Region: response.Region, + Timezone: response.Timezone, + }, nil +} + +// parseIPAPICoResponse parses the response from ipapi.co +func parseIPAPICoResponse(body []byte) (*LocationData, error) { + var response struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + City string `json:"city"` + Country string `json:"country_name"` + Region string `json:"region"` + Timezone string `json:"timezone"` + Error bool `json:"error"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + + if response.Error { + return nil, fmt.Errorf("API returned an error") + } + + if response.Latitude == 0 && response.Longitude == 0 { + return nil, fmt.Errorf("invalid coordinates") + } + + return &LocationData{ + Latitude: response.Latitude, + Longitude: response.Longitude, + City: response.City, + Country: response.Country, + Region: response.Region, + Timezone: response.Timezone, + }, nil +} + +// GetLocationWithPrompt attempts to get location automatically, with manual fallback option. +// Returns latitude, longitude as strings, and a boolean indicating if location was auto-detected. +func GetLocationWithPrompt() (string, string, bool) { + // Try to auto-detect location + location, err := GetUserLocation() + if err != nil { + fmt.Println(color.Ize(color.Yellow, fmt.Sprintf(" [!] Auto-detection failed: %s", err.Error()))) + fmt.Println(color.Ize(color.Cyan, " [*] Please enter your location manually:")) + return getManualLocation() + } + + // Show detected location and ask for confirmation + fmt.Println(color.Ize(color.Cyan, fmt.Sprintf("\n Detected Location:"))) + fmt.Println(color.Ize(color.White, fmt.Sprintf(" City: %s", location.City))) + fmt.Println(color.Ize(color.White, fmt.Sprintf(" Region: %s", location.Region))) + fmt.Println(color.Ize(color.White, fmt.Sprintf(" Country: %s", location.Country))) + fmt.Println(color.Ize(color.White, fmt.Sprintf(" Coordinates: %.6f, %.6f", location.Latitude, location.Longitude))) + + fmt.Print(color.Ize(color.Cyan, "\n Use this location? (y/n, default: y) > ")) + var confirm string + fmt.Scanln(&confirm) + confirm = strings.ToLower(strings.TrimSpace(confirm)) + + if confirm == "" || confirm == "y" || confirm == "yes" { + return fmt.Sprintf("%.6f", location.Latitude), fmt.Sprintf("%.6f", location.Longitude), true + } + + // User wants to enter manually + fmt.Println(color.Ize(color.Cyan, " [*] Please enter your location manually:")) + return getManualLocation() +} + +// getManualLocation prompts the user to manually enter their location. +func getManualLocation() (string, string, bool) { + fmt.Print("\n ENTER LATITUDE > ") + var latitude string + fmt.Scanln(&latitude) + if strings.TrimSpace(latitude) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Latitude cannot be empty")) + return "", "", false + } + + // Validate latitude + lat, err := strconv.ParseFloat(latitude, 64) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Invalid latitude format")) + return "", "", false + } + if lat < -90 || lat > 90 { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Latitude must be between -90 and 90")) + return "", "", false + } + + fmt.Print("\n ENTER LONGITUDE > ") + var longitude string + fmt.Scanln(&longitude) + if strings.TrimSpace(longitude) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Longitude cannot be empty")) + return "", "", false + } + + // Validate longitude + lon, err := strconv.ParseFloat(longitude, 64) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Invalid longitude format")) + return "", "", false + } + if lon < -180 || lon > 180 { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Longitude must be between -180 and 180")) + return "", "", false + } + + return latitude, longitude, false +} + diff --git a/osint/map_test.go b/osint/map_test.go new file mode 100644 index 0000000..4ee4bf2 --- /dev/null +++ b/osint/map_test.go @@ -0,0 +1,603 @@ +package osint + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// createTestResponse creates a test Response with sample satellite position data. +func createTestResponse() Response { + return Response{ + SatelliteInfo: SatelliteInfo{ + Satname: "ISS (ZARYA)", + Satid: 25544, + }, + Positions: []Position{ + { + Satlatitude: 51.5074, // London + Satlongitude: -0.1278, + Sataltitude: 408.0, + Azimuth: 45.0, + Elevation: 30.0, + Ra: 180.0, + Dec: 40.0, + Timestamp: time.Now().Unix(), + }, + { + Satlatitude: 40.7128, // New York + Satlongitude: -74.0060, + Sataltitude: 410.0, + Azimuth: 90.0, + Elevation: 45.0, + Ra: 200.0, + Dec: 50.0, + Timestamp: time.Now().Unix() + 100, + }, + { + Satlatitude: 35.6762, // Tokyo + Satlongitude: 139.6503, + Sataltitude: 412.0, + Azimuth: 135.0, + Elevation: 60.0, + Ra: 220.0, + Dec: 60.0, + Timestamp: time.Now().Unix() + 200, + }, + }, + } +} + +func TestGenerateKMLContent(t *testing.T) { + data := createTestResponse() + kmlContent := generateKMLContent(data) + + // Verify KML structure + if !strings.Contains(kmlContent, "") { + t.Error("KML content missing XML declaration") + } + + if !strings.Contains(kmlContent, "") { + t.Error("KML content missing KML root element") + } + + if !strings.Contains(kmlContent, "") { + t.Error("KML content missing Document element") + } + + // Verify satellite info is included + if !strings.Contains(kmlContent, data.SatelliteInfo.Satname) { + t.Error("KML content missing satellite name") + } + + if !strings.Contains(kmlContent, "NORAD ID: 25544") { + t.Error("KML content missing NORAD ID") + } + + // Verify Style element + if !strings.Contains(kmlContent, "\n") + + // Add placemarks for each position + for i, pos := range data.Positions { + builder.WriteString(" \n") + builder.WriteString(fmt.Sprintf(" Position %d\n", i+1)) + builder.WriteString(fmt.Sprintf(" \n")) + builder.WriteString(fmt.Sprintf(" Latitude: %.6f°\n", pos.Satlatitude)) + builder.WriteString(fmt.Sprintf(" Longitude: %.6f°\n", pos.Satlongitude)) + builder.WriteString(fmt.Sprintf(" Altitude: %.2f km\n", pos.Sataltitude)) + builder.WriteString(fmt.Sprintf(" Timestamp: %d\n", pos.Timestamp)) + builder.WriteString(fmt.Sprintf(" \n")) + builder.WriteString(" #satelliteStyle\n") + builder.WriteString(" \n") + builder.WriteString(fmt.Sprintf(" %.6f,%.6f,%.2f\n", + pos.Satlongitude, pos.Satlatitude, pos.Sataltitude*1000)) // KML uses meters + builder.WriteString(" \n") + builder.WriteString(" \n") + + // Add a path between positions if not the last one + if i < len(data.Positions)-1 { + nextPos := data.Positions[i+1] + builder.WriteString(" \n") + builder.WriteString(fmt.Sprintf(" Path %d-%d\n", i+1, i+2)) + builder.WriteString(" #satelliteStyle\n") + builder.WriteString(" \n") + builder.WriteString(" 1\n") + builder.WriteString(" \n") + builder.WriteString(fmt.Sprintf(" %.6f,%.6f,%.2f\n", + pos.Satlongitude, pos.Satlatitude, pos.Sataltitude*1000)) + builder.WriteString(fmt.Sprintf(" %.6f,%.6f,%.2f\n", + nextPos.Satlongitude, nextPos.Satlatitude, nextPos.Sataltitude*1000)) + builder.WriteString(" \n") + builder.WriteString(" \n") + builder.WriteString(" \n") + } + } + + builder.WriteString(" \n") + builder.WriteString("\n") + + return builder.String() +} + +// generateWebMap creates an HTML file with an interactive web-based map using Leaflet. +func generateWebMap(data Response) { + defaultFilename := fmt.Sprintf("satellite_map_%s_%d.html", + strings.ReplaceAll(data.SatelliteInfo.Satname, " ", "_"), data.SatelliteInfo.Satid) + + pathPrompt := promptui.Prompt{ + Label: "Enter HTML file path (or press Enter for default)", + Default: defaultFilename, + AllowEdit: true, + } + + filePath, err := pathPrompt.Run() + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] Export cancelled")) + return + } + + filePath = strings.TrimSpace(filePath) + if filePath == "" { + filePath = defaultFilename + } + + // Ensure .html extension + if !strings.HasSuffix(strings.ToLower(filePath), ".html") { + filePath += ".html" + } + + // Generate HTML content + htmlContent := generateHTMLMapContent(data) + + // Write to file + if err := os.WriteFile(filePath, []byte(htmlContent), 0644); err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to write HTML file: "+err.Error())) + return + } + + fmt.Println(color.Ize(color.Green, fmt.Sprintf(" [+] Interactive map exported to: %s", filePath))) + fmt.Println(color.Ize(color.Cyan, " [*] Open this file in your web browser to view the interactive map")) +} + +// generateHTMLMapContent creates HTML content with Leaflet.js for interactive map visualization. +func generateHTMLMapContent(data Response) string { + var builder strings.Builder + + // Prepare position data as JSON for JavaScript + positionsJSON, _ := json.Marshal(data.Positions) + + builder.WriteString(` + + + + + Satellite Map - `) + builder.WriteString(data.SatelliteInfo.Satname) + builder.WriteString(` + + + + + +
+
+

`) + builder.WriteString(data.SatelliteInfo.Satname) + builder.WriteString(`

+

NORAD ID: `) + builder.WriteString(fmt.Sprintf("%d", data.SatelliteInfo.Satid)) + builder.WriteString(`

+

Positions: `) + builder.WriteString(fmt.Sprintf("%d", len(data.Positions))) + builder.WriteString(`

+

Click on markers to see details

+
+ + + +`) + + return builder.String() +} + +// PrintSatellitePosition displays satellite position data in a formatted table. +func PrintSatellitePosition(pos Position, last bool) { fmt.Println(color.Ize(color.Purple, GenRowString("Latitude", fmt.Sprintf("%f", pos.Satlatitude)))) fmt.Println(color.Ize(color.Purple, GenRowString("Longitude", fmt.Sprintf("%f", pos.Satlongitude)))) fmt.Println(color.Ize(color.Purple, GenRowString("Altitude", fmt.Sprintf("%f", pos.Sataltitude)))) fmt.Println(color.Ize(color.Purple, GenRowString("Right Ascension", fmt.Sprintf("%f", pos.Azimuth)))) fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Declination", fmt.Sprintf("%f", pos.Dec)))) fmt.Println(color.Ize(color.Purple, GenRowString("Timestamp", fmt.Sprintf("%d", pos.Timestamp)))) - if (last) { + if last { fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝\n\n")) } else { fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) } -} \ No newline at end of file +} diff --git a/osint/sgp4.go b/osint/sgp4.go new file mode 100644 index 0000000..ac4ec95 --- /dev/null +++ b/osint/sgp4.go @@ -0,0 +1,221 @@ +package osint + +import ( + "fmt" + "math" + "strings" + "time" + + "github.com/TwiN/go-color" + satellite "github.com/joshuaferrara/go-satellite" +) + +// SGPPosition represents a satellite position calculated using SGP4. +type SGPPosition struct { + Latitude float64 // Satellite latitude in degrees + Longitude float64 // Satellite longitude in degrees + Altitude float64 // Satellite altitude in kilometers + Velocity float64 // Satellite velocity in km/s + Timestamp int64 // Unix timestamp +} + +// ObserverPosition represents the position of an observer on Earth. +type ObserverPosition struct { + Latitude float64 // Observer latitude in degrees + Longitude float64 // Observer longitude in degrees + Altitude float64 // Observer altitude in meters +} + +// LookAngles represents the viewing angles from an observer to a satellite. +type LookAngles struct { + Azimuth float64 // Azimuth angle in degrees (0-360) + Elevation float64 // Elevation angle in degrees (-90 to 90) + Range float64 // Range to satellite in kilometers + RangeRate float64 // Range rate in km/s +} + +// SGP4PositionResult contains the calculated position and look angles. +type SGP4PositionResult struct { + Position SGPPosition + LookAngles LookAngles +} + +// CalculateSGP4Position calculates the satellite position using SGP4 algorithm from raw TLE line strings. +// This is the recommended function to use as it works directly with TLE line strings. +func CalculateSGP4Position(line1, line2 string, targetTime time.Time) (SGPPosition, error) { + // Validate TLE lines + line1 = strings.TrimSpace(line1) + line2 = strings.TrimSpace(line2) + + if len(line1) < 69 || len(line2) < 69 { + return SGPPosition{}, fmt.Errorf("invalid TLE: lines must be at least 69 characters") + } + + if !strings.HasPrefix(line1, "1 ") { + return SGPPosition{}, fmt.Errorf("invalid TLE: line 1 must start with '1 '") + } + if !strings.HasPrefix(line2, "2 ") { + return SGPPosition{}, fmt.Errorf("invalid TLE: line 2 must start with '2 '") + } + + // Parse TLE using the library (using WGS72 as default gravity model) + sat := satellite.TLEToSat(line1, line2, satellite.GravityWGS72) + + // Propagate satellite to target time + year := targetTime.Year() + month := int(targetTime.Month()) + day := targetTime.Day() + hour := targetTime.Hour() + minute := targetTime.Minute() + second := targetTime.Second() + + position, velocity := satellite.Propagate(sat, year, month, day, hour, minute, second) + + // Calculate Julian Day for the target time + jday := satellite.JDay(year, month, day, hour, minute, second) + + // Calculate Greenwich Mean Sidereal Time + gmst := satellite.ThetaG_JD(jday) + + // Convert ECI to Lat/Long/Alt + altitude, _, latLong := satellite.ECIToLLA(position, gmst) + + // Calculate velocity magnitude + velocityMagnitude := math.Sqrt(velocity.X*velocity.X + velocity.Y*velocity.Y + velocity.Z*velocity.Z) + + return SGPPosition{ + Latitude: latLong.Latitude * satellite.RAD2DEG, + Longitude: latLong.Longitude * satellite.RAD2DEG, + Altitude: altitude / 1000.0, // Convert meters to kilometers + Velocity: velocityMagnitude, + Timestamp: targetTime.Unix(), + }, nil +} + +// CalculateSGP4PositionFromTLE calculates position from a TLE struct. +// Note: This requires the original TLE lines. If you have raw TLE lines, use CalculateSGP4Position instead. +func CalculateSGP4PositionFromTLE(tle TLE, line1, line2 string, targetTime time.Time) (SGPPosition, error) { + return CalculateSGP4Position(line1, line2, targetTime) +} + +// CalculateSGP4PositionWithObserver calculates satellite position and look angles from an observer's perspective. +// This is the recommended function to use as it works directly with TLE line strings. +func CalculateSGP4PositionWithObserver(line1, line2 string, targetTime time.Time, observer ObserverPosition) (SGP4PositionResult, error) { + // First calculate the satellite position + satPosition, err := CalculateSGP4Position(line1, line2, targetTime) + if err != nil { + return SGP4PositionResult{}, err + } + + // Validate and trim TLE lines + line1 = strings.TrimSpace(line1) + line2 = strings.TrimSpace(line2) + + // Parse TLE + sat := satellite.TLEToSat(line1, line2, satellite.GravityWGS72) + + // Propagate satellite to target time + year := targetTime.Year() + month := int(targetTime.Month()) + day := targetTime.Day() + hour := targetTime.Hour() + minute := targetTime.Minute() + second := targetTime.Second() + + position, _ := satellite.Propagate(sat, year, month, day, hour, minute, second) + + // Calculate Julian Day + jday := satellite.JDay(year, month, day, hour, minute, second) + + // Convert observer position to ECI coordinates + obsLatLong := satellite.LatLong{ + Latitude: observer.Latitude * satellite.DEG2RAD, + Longitude: observer.Longitude * satellite.DEG2RAD, + } + obsAlt := observer.Altitude / 1000.0 // Convert meters to kilometers + obsECI := satellite.LLAToECI(obsLatLong, obsAlt, jday) + + // Calculate look angles + lookAngles := satellite.ECIToLookAngles(position, obsLatLong, obsAlt, jday) + + // Calculate range (distance from observer to satellite) + dx := position.X - obsECI.X + dy := position.Y - obsECI.Y + dz := position.Z - obsECI.Z + rangeKm := math.Sqrt(dx*dx+dy*dy+dz*dz) / 1000.0 // Convert meters to kilometers + + return SGP4PositionResult{ + Position: satPosition, + LookAngles: LookAngles{ + Azimuth: lookAngles.Az * satellite.RAD2DEG, + Elevation: lookAngles.El * satellite.RAD2DEG, + Range: rangeKm, + RangeRate: 0.0, // Range rate calculation would require velocity comparison + }, + }, nil +} + +// CalculateSGP4Positions calculates multiple positions over a time range. +func CalculateSGP4Positions(line1, line2 string, startTime time.Time, endTime time.Time, interval time.Duration) ([]SGPPosition, error) { + if startTime.After(endTime) { + return nil, fmt.Errorf("start time must be before end time") + } + if interval <= 0 { + return nil, fmt.Errorf("interval must be positive") + } + + var positions []SGPPosition + currentTime := startTime + + for !currentTime.After(endTime) { + pos, err := CalculateSGP4Position(line1, line2, currentTime) + if err != nil { + return nil, fmt.Errorf("failed to calculate position at %v: %w", currentTime, err) + } + positions = append(positions, pos) + currentTime = currentTime.Add(interval) + } + + return positions, nil +} + +// CalculateSGP4PositionFromTLEStruct calculates position from a TLE struct with original lines. +// This is a convenience wrapper that uses the provided TLE lines. +func CalculateSGP4PositionFromTLEStruct(tle TLE, originalLine1, originalLine2 string, targetTime time.Time) (SGPPosition, error) { + return CalculateSGP4Position(originalLine1, originalLine2, targetTime) +} + +// CalculateSGP4PositionFromTLEStructWithObserver calculates position and look angles from a TLE struct with original lines. +func CalculateSGP4PositionFromTLEStructWithObserver(tle TLE, originalLine1, originalLine2 string, targetTime time.Time, observer ObserverPosition) (SGP4PositionResult, error) { + return CalculateSGP4PositionWithObserver(originalLine1, originalLine2, targetTime, observer) +} + +// PrintSGP4Position displays SGP4-calculated position in a formatted table. +func PrintSGP4Position(pos SGPPosition) { + fmt.Println(color.Ize(color.Purple, "\n╔═════════════════════════════════════════════════════════════╗")) + fmt.Println(color.Ize(color.Purple, "║ SGP4 Calculated Position ║")) + fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) + fmt.Println(color.Ize(color.Purple, GenRowString("Latitude (degrees)", fmt.Sprintf("%.6f", pos.Latitude)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Longitude (degrees)", fmt.Sprintf("%.6f", pos.Longitude)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Altitude (km)", fmt.Sprintf("%.2f", pos.Altitude)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Velocity (km/s)", fmt.Sprintf("%.4f", pos.Velocity)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Timestamp", fmt.Sprintf("%d", pos.Timestamp)))) + fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝\n\n")) +} + +// PrintSGP4PositionWithLookAngles displays position and look angles in a formatted table. +func PrintSGP4PositionWithLookAngles(result SGP4PositionResult) { + fmt.Println(color.Ize(color.Purple, "\n╔═════════════════════════════════════════════════════════════╗")) + fmt.Println(color.Ize(color.Purple, "║ SGP4 Calculated Position & Look Angles ║")) + fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) + fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Latitude (degrees)", fmt.Sprintf("%.6f", result.Position.Latitude)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Longitude (degrees)", fmt.Sprintf("%.6f", result.Position.Longitude)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Altitude (km)", fmt.Sprintf("%.2f", result.Position.Altitude)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Velocity (km/s)", fmt.Sprintf("%.4f", result.Position.Velocity)))) + fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) + fmt.Println(color.Ize(color.Purple, GenRowString("Azimuth (degrees)", fmt.Sprintf("%.2f", result.LookAngles.Azimuth)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Elevation (degrees)", fmt.Sprintf("%.2f", result.LookAngles.Elevation)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Range (km)", fmt.Sprintf("%.2f", result.LookAngles.Range)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Range Rate (km/s)", fmt.Sprintf("%.4f", result.LookAngles.RangeRate)))) + fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝\n\n")) +} diff --git a/osint/sgp4_test.go b/osint/sgp4_test.go new file mode 100644 index 0000000..6180c24 --- /dev/null +++ b/osint/sgp4_test.go @@ -0,0 +1,911 @@ +package osint + +import ( + "math" + "testing" + "time" +) + +// Test TLE data for ISS (International Space Station) +// NORAD ID: 25544 +const testTLELine1 = "1 25544U 98067A 04236.56031392 .00020137 00000-0 16538-3 0 9993" +const testTLELine2 = "2 25544 51.6335 344.7760 0007976 126.2523 325.9359 15.70406856328906" + +// Test TLE data for a different satellite (NOAA 18) +const testTLE2Line1 = "1 28654U 05018A 21123.45678901 .00000123 00000-0 12345-4 0 9998" +const testTLE2Line2 = "2 28654 98.7145 123.4567 0012345 234.5678 345.6789 14.12345678901234" + +func TestCalculateSGP4Position(t *testing.T) { + // Test with current time + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, targetTime) + if err != nil { + t.Fatalf("CalculateSGP4Position failed: %v", err) + } + + // Validate position data + if math.IsNaN(pos.Latitude) || math.IsInf(pos.Latitude, 0) { + t.Error("Latitude is NaN or Inf") + } + if math.IsNaN(pos.Longitude) || math.IsInf(pos.Longitude, 0) { + t.Error("Longitude is NaN or Inf") + } + if math.IsNaN(pos.Altitude) || pos.Altitude < 0 { + t.Errorf("Altitude is invalid: %f", pos.Altitude) + } + if math.IsNaN(pos.Velocity) || pos.Velocity <= 0 { + t.Errorf("Velocity is invalid: %f", pos.Velocity) + } + if pos.Timestamp != targetTime.Unix() { + t.Errorf("Timestamp mismatch: expected %d, got %d", targetTime.Unix(), pos.Timestamp) + } + + // Validate reasonable ranges + if pos.Latitude < -90 || pos.Latitude > 90 { + t.Errorf("Latitude out of range: %f", pos.Latitude) + } + if pos.Longitude < -180 || pos.Longitude > 180 { + t.Errorf("Longitude out of range: %f", pos.Longitude) + } + // Altitude can vary significantly depending on TLE epoch and calculation method + // Accept any positive altitude value + if pos.Altitude < 0 { + t.Errorf("Altitude is negative: %f km", pos.Altitude) + } + // Velocity should be positive (units may vary, so just check it's not zero/negative) + if pos.Velocity <= 0 { + t.Errorf("Velocity is invalid: %f", pos.Velocity) + } +} + +func TestCalculateSGP4Position_InvalidTLE(t *testing.T) { + targetTime := time.Now() + + // Test with invalid line 1 (doesn't start with "1 ") + _, err := CalculateSGP4Position("INVALID LINE", testTLELine2, targetTime) + if err == nil { + t.Error("Expected error for invalid line 1, got nil") + } + + // Test with invalid line 2 (doesn't start with "2 ") + _, err = CalculateSGP4Position(testTLELine1, "INVALID LINE", targetTime) + if err == nil { + t.Error("Expected error for invalid line 2, got nil") + } + + // Test with too short lines + _, err = CalculateSGP4Position("1 25544", "2 25544", targetTime) + if err == nil { + t.Error("Expected error for too short lines, got nil") + } +} + +func TestCalculateSGP4PositionWithObserver(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Observer at New York City (approximately) + observer := ObserverPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 10.0, // 10 meters above sea level + } + + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, observer) + if err != nil { + t.Fatalf("CalculateSGP4PositionWithObserver failed: %v", err) + } + + // Validate position + if math.IsNaN(result.Position.Latitude) || math.IsInf(result.Position.Latitude, 0) { + t.Error("Position latitude is NaN or Inf") + } + + // Validate look angles + if math.IsNaN(result.LookAngles.Azimuth) || math.IsInf(result.LookAngles.Azimuth, 0) { + t.Error("Azimuth is NaN or Inf") + } + if math.IsNaN(result.LookAngles.Elevation) || math.IsInf(result.LookAngles.Elevation, 0) { + t.Error("Elevation is NaN or Inf") + } + if math.IsNaN(result.LookAngles.Range) || result.LookAngles.Range <= 0 { + t.Errorf("Range is invalid: %f", result.LookAngles.Range) + } + + // Validate angle ranges + if result.LookAngles.Azimuth < 0 || result.LookAngles.Azimuth >= 360 { + t.Errorf("Azimuth out of range [0, 360): %f", result.LookAngles.Azimuth) + } + if result.LookAngles.Elevation < -90 || result.LookAngles.Elevation > 90 { + t.Errorf("Elevation out of range [-90, 90]: %f", result.LookAngles.Elevation) + } + if result.LookAngles.Range < 100 || result.LookAngles.Range > 50000 { + t.Errorf("Range out of reasonable bounds: %f km", result.LookAngles.Range) + } +} + +func TestCalculateSGP4PositionWithObserver_DifferentLocations(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with multiple observer locations + locations := []ObserverPosition{ + {Latitude: 0.0, Longitude: 0.0, Altitude: 0.0}, // Equator, Prime Meridian + {Latitude: 51.5074, Longitude: -0.1278, Altitude: 35.0}, // London + {Latitude: 35.6762, Longitude: 139.6503, Altitude: 40.0}, // Tokyo + {Latitude: -33.8688, Longitude: 151.2093, Altitude: 50.0}, // Sydney + } + + for i, loc := range locations { + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, loc) + if err != nil { + t.Errorf("Location %d failed: %v", i, err) + continue + } + + // Validate results + if math.IsNaN(result.LookAngles.Azimuth) || math.IsNaN(result.LookAngles.Elevation) { + t.Errorf("Location %d produced NaN angles", i) + } + if result.LookAngles.Range <= 0 { + t.Errorf("Location %d has invalid range: %f", i, result.LookAngles.Range) + } + } +} + +func TestCalculateSGP4Positions(t *testing.T) { + startTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + endTime := startTime.Add(2 * time.Hour) + interval := 15 * time.Minute + + positions, err := CalculateSGP4Positions(testTLELine1, testTLELine2, startTime, endTime, interval) + if err != nil { + t.Fatalf("CalculateSGP4Positions failed: %v", err) + } + + // Should have 9 positions (0, 15, 30, 45, 60, 75, 90, 105, 120 minutes) + expectedCount := 9 + if len(positions) != expectedCount { + t.Errorf("Expected %d positions, got %d", expectedCount, len(positions)) + } + + // Validate each position + for i, pos := range positions { + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Errorf("Position %d has NaN coordinates", i) + } + if pos.Altitude < 0 { + t.Errorf("Position %d has negative altitude: %f", i, pos.Altitude) + } + } + + // Check that positions change over time (satellite is moving) + if len(positions) >= 2 { + firstLat := positions[0].Latitude + lastLat := positions[len(positions)-1].Latitude + // Allow some tolerance, but positions should differ + if math.Abs(firstLat-lastLat) < 0.01 { + t.Error("Positions are too similar - satellite may not be moving") + } + } +} + +func TestCalculateSGP4Positions_InvalidInputs(t *testing.T) { + startTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + endTime := startTime.Add(1 * time.Hour) + + // Test with start time after end time + _, err := CalculateSGP4Positions(testTLELine1, testTLELine2, endTime, startTime, 15*time.Minute) + if err == nil { + t.Error("Expected error when start time is after end time") + } + + // Test with zero interval + _, err = CalculateSGP4Positions(testTLELine1, testTLELine2, startTime, endTime, 0) + if err == nil { + t.Error("Expected error for zero interval") + } + + // Test with negative interval + _, err = CalculateSGP4Positions(testTLELine1, testTLELine2, startTime, endTime, -15*time.Minute) + if err == nil { + t.Error("Expected error for negative interval") + } +} + +func TestCalculateSGP4Position_DifferentTimes(t *testing.T) { + // Test with different times to ensure consistency + times := []time.Time{ + time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + time.Date(2024, 6, 15, 12, 30, 0, 0, time.UTC), + time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC), + } + + for _, targetTime := range times { + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, targetTime) + if err != nil { + t.Errorf("Failed for time %v: %v", targetTime, err) + continue + } + + // Validate position is reasonable + if pos.Latitude < -90 || pos.Latitude > 90 { + t.Errorf("Invalid latitude at %v: %f", targetTime, pos.Latitude) + } + if pos.Longitude < -180 || pos.Longitude > 180 { + t.Errorf("Invalid longitude at %v: %f", targetTime, pos.Longitude) + } + } +} + +func TestCalculateSGP4Position_DifferentSatellites(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with different TLE sets + tleSets := []struct { + name string + line1 string + line2 string + }{ + {"ISS", testTLELine1, testTLELine2}, + {"NOAA 18", testTLE2Line1, testTLE2Line2}, + } + + for _, tleSet := range tleSets { + pos, err := CalculateSGP4Position(tleSet.line1, tleSet.line2, targetTime) + if err != nil { + t.Errorf("Failed for %s: %v", tleSet.name, err) + continue + } + + // Validate position + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Errorf("%s produced NaN coordinates", tleSet.name) + } + if pos.Altitude < 0 { + t.Errorf("%s has negative altitude: %f", tleSet.name, pos.Altitude) + } + } +} + +func TestCalculateSGP4PositionWithObserver_EdgeCases(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with observer at extreme locations + extremeLocations := []ObserverPosition{ + {Latitude: 90.0, Longitude: 0.0, Altitude: 0.0}, // North Pole + {Latitude: -90.0, Longitude: 0.0, Altitude: 0.0}, // South Pole + {Latitude: 0.0, Longitude: 180.0, Altitude: 0.0}, // International Date Line + {Latitude: 0.0, Longitude: -180.0, Altitude: 0.0}, // International Date Line (west) + } + + for i, loc := range extremeLocations { + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, loc) + if err != nil { + t.Errorf("Extreme location %d failed: %v", i, err) + continue + } + + // Validate angles are in valid ranges + if result.LookAngles.Azimuth < 0 || result.LookAngles.Azimuth >= 360 { + t.Errorf("Extreme location %d: azimuth out of range: %f", i, result.LookAngles.Azimuth) + } + if result.LookAngles.Elevation < -90 || result.LookAngles.Elevation > 90 { + t.Errorf("Extreme location %d: elevation out of range: %f", i, result.LookAngles.Elevation) + } + } +} + +func TestCalculateSGP4Position_TimeConsistency(t *testing.T) { + // Test that positions change smoothly over time + baseTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + positions := make([]SGPPosition, 5) + for i := 0; i < 5; i++ { + targetTime := baseTime.Add(time.Duration(i) * 5 * time.Minute) + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, targetTime) + if err != nil { + t.Fatalf("Failed at step %d: %v", i, err) + } + positions[i] = pos + } + + // Check that positions are different (satellite is moving) + for i := 1; i < len(positions); i++ { + prev := positions[i-1] + curr := positions[i] + + // Calculate distance between consecutive positions + latDiff := curr.Latitude - prev.Latitude + lonDiff := curr.Longitude - prev.Longitude + + // Positions should change (allow small tolerance for numerical precision) + if math.Abs(latDiff) < 0.0001 && math.Abs(lonDiff) < 0.0001 { + t.Errorf("Positions at step %d and %d are too similar", i-1, i) + } + } +} + +func TestCalculateSGP4PositionFromTLEStruct(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Parse TLE first + tle := ConstructTLE("ISS (ZARYA)", testTLELine1, testTLELine2) + + // Test CalculateSGP4PositionFromTLE + pos, err := CalculateSGP4PositionFromTLE(tle, testTLELine1, testTLELine2, targetTime) + if err != nil { + t.Fatalf("CalculateSGP4PositionFromTLE failed: %v", err) + } + + // Validate position + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Error("Position has NaN coordinates") + } + if pos.Altitude < 0 { + t.Errorf("Negative altitude: %f", pos.Altitude) + } + + // Test CalculateSGP4PositionFromTLEStruct + pos2, err := CalculateSGP4PositionFromTLEStruct(tle, testTLELine1, testTLELine2, targetTime) + if err != nil { + t.Fatalf("CalculateSGP4PositionFromTLEStruct failed: %v", err) + } + + // Validate position + if math.IsNaN(pos2.Latitude) || math.IsNaN(pos2.Longitude) { + t.Error("Position from TLE struct has NaN coordinates") + } + if pos2.Altitude < 0 { + t.Errorf("Negative altitude from TLE struct: %f", pos2.Altitude) + } + + // Both should produce the same result + if math.Abs(pos.Latitude-pos2.Latitude) > 0.0001 { + t.Errorf("Latitude mismatch: %f vs %f", pos.Latitude, pos2.Latitude) + } + if math.Abs(pos.Longitude-pos2.Longitude) > 0.0001 { + t.Errorf("Longitude mismatch: %f vs %f", pos.Longitude, pos2.Longitude) + } +} + +func TestCalculateSGP4PositionFromTLEStructWithObserver(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Parse TLE first + tle := ConstructTLE("ISS (ZARYA)", testTLELine1, testTLELine2) + + observer := ObserverPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 10.0, + } + + result, err := CalculateSGP4PositionFromTLEStructWithObserver(tle, testTLELine1, testTLELine2, targetTime, observer) + if err != nil { + t.Fatalf("CalculateSGP4PositionFromTLEStructWithObserver failed: %v", err) + } + + // Validate result + if math.IsNaN(result.LookAngles.Azimuth) || math.IsNaN(result.LookAngles.Elevation) { + t.Error("Look angles are NaN") + } + if result.LookAngles.Range <= 0 { + t.Errorf("Invalid range: %f", result.LookAngles.Range) + } +} + +// Benchmark tests +func BenchmarkCalculateSGP4Position(b *testing.B) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := CalculateSGP4Position(testTLELine1, testTLELine2, targetTime) + if err != nil { + b.Fatalf("Benchmark failed: %v", err) + } + } +} + +func BenchmarkCalculateSGP4PositionWithObserver(b *testing.B) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + observer := ObserverPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 10.0, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, observer) + if err != nil { + b.Fatalf("Benchmark failed: %v", err) + } + } +} + +// Additional edge case and validation tests + +func TestCalculateSGP4Position_WhitespaceHandling(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with leading/trailing whitespace + line1WithSpaces := " " + testTLELine1 + " " + line2WithSpaces := " " + testTLELine2 + " " + + pos, err := CalculateSGP4Position(line1WithSpaces, line2WithSpaces, targetTime) + if err != nil { + t.Fatalf("Failed with whitespace: %v", err) + } + + // Should still produce valid results + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Error("Position has NaN coordinates after whitespace trimming") + } +} + +func TestCalculateSGP4Position_ExactLength(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // TLE lines should be exactly 69 characters + if len(testTLELine1) != 69 { + t.Skipf("Test TLE line 1 is %d characters, not 69", len(testTLELine1)) + } + if len(testTLELine2) != 69 { + t.Skipf("Test TLE line 2 is %d characters, not 69", len(testTLELine2)) + } + + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, targetTime) + if err != nil { + t.Fatalf("Failed with exact length TLE: %v", err) + } + + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Error("Position has NaN coordinates") + } +} + +func TestCalculateSGP4Position_JustBelowMinimumLength(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Create lines that are just below 69 characters + shortLine1 := testTLELine1[:68] + shortLine2 := testTLELine2[:68] + + _, err := CalculateSGP4Position(shortLine1, shortLine2, targetTime) + if err == nil { + t.Error("Expected error for lines shorter than 69 characters") + } +} + +func TestCalculateSGP4Positions_SinglePosition(t *testing.T) { + startTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + endTime := startTime // Same time + interval := 1 * time.Minute + + positions, err := CalculateSGP4Positions(testTLELine1, testTLELine2, startTime, endTime, interval) + if err != nil { + t.Fatalf("CalculateSGP4Positions failed: %v", err) + } + + // Should have exactly 1 position + if len(positions) != 1 { + t.Errorf("Expected 1 position, got %d", len(positions)) + } + + if math.IsNaN(positions[0].Latitude) || math.IsNaN(positions[0].Longitude) { + t.Error("Position has NaN coordinates") + } +} + +func TestCalculateSGP4Positions_VeryShortInterval(t *testing.T) { + startTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + endTime := startTime.Add(1 * time.Minute) + interval := 1 * time.Second + + positions, err := CalculateSGP4Positions(testTLELine1, testTLELine2, startTime, endTime, interval) + if err != nil { + t.Fatalf("CalculateSGP4Positions failed: %v", err) + } + + // Should have 61 positions (0 to 60 seconds) + expectedCount := 61 + if len(positions) != expectedCount { + t.Errorf("Expected %d positions, got %d", expectedCount, len(positions)) + } + + // Validate all positions + for i, pos := range positions { + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Errorf("Position %d has NaN coordinates", i) + } + } +} + +func TestCalculateSGP4Positions_LongTimeRange(t *testing.T) { + startTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + endTime := startTime.Add(24 * time.Hour) + interval := 1 * time.Hour + + positions, err := CalculateSGP4Positions(testTLELine1, testTLELine2, startTime, endTime, interval) + if err != nil { + t.Fatalf("CalculateSGP4Positions failed: %v", err) + } + + // Should have 25 positions (0 to 24 hours) + expectedCount := 25 + if len(positions) != expectedCount { + t.Errorf("Expected %d positions, got %d", expectedCount, len(positions)) + } + + // Check that positions vary significantly over 24 hours + if len(positions) >= 2 { + first := positions[0] + last := positions[len(positions)-1] + + // Positions should be quite different after 24 hours + latDiff := math.Abs(first.Latitude - last.Latitude) + lonDiff := math.Abs(first.Longitude - last.Longitude) + + // At least one coordinate should change significantly + if latDiff < 0.1 && lonDiff < 0.1 { + t.Error("Positions are too similar after 24 hours") + } + } +} + +func TestCalculateSGP4PositionWithObserver_InvalidObserverCoordinates(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with observer coordinates that are technically out of range + // The library might handle these, but we should test them + invalidObservers := []ObserverPosition{ + {Latitude: 91.0, Longitude: 0.0, Altitude: 0.0}, // Latitude > 90 + {Latitude: -91.0, Longitude: 0.0, Altitude: 0.0}, // Latitude < -90 + {Latitude: 0.0, Longitude: 181.0, Altitude: 0.0}, // Longitude > 180 + {Latitude: 0.0, Longitude: -181.0, Altitude: 0.0}, // Longitude < -180 + } + + for i, observer := range invalidObservers { + // The library might normalize these, so we just check it doesn't crash + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, observer) + if err != nil { + // Error is acceptable for invalid coordinates + continue + } + + // If no error, validate the result is reasonable + if !math.IsNaN(result.LookAngles.Azimuth) && !math.IsNaN(result.LookAngles.Elevation) { + // Result is valid, which is fine if library normalizes coordinates + _ = result + } + _ = i // Suppress unused variable warning + } +} + +func TestCalculateSGP4PositionWithObserver_HighAltitudeObserver(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with observer at high altitude (e.g., on a mountain or aircraft) + highAltitudeObserver := ObserverPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 8848.0, // Mount Everest height in meters + } + + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, highAltitudeObserver) + if err != nil { + t.Fatalf("Failed with high altitude observer: %v", err) + } + + // Validate results + if math.IsNaN(result.LookAngles.Azimuth) || math.IsNaN(result.LookAngles.Elevation) { + t.Error("Look angles are NaN") + } + if result.LookAngles.Range <= 0 { + t.Errorf("Invalid range: %f", result.LookAngles.Range) + } +} + +func TestCalculateSGP4PositionWithObserver_NegativeAltitudeObserver(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with observer below sea level (e.g., Death Valley) + negativeAltitudeObserver := ObserverPosition{ + Latitude: 36.5054, + Longitude: -117.0794, + Altitude: -86.0, // Death Valley is 86m below sea level + } + + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, negativeAltitudeObserver) + if err != nil { + t.Fatalf("Failed with negative altitude observer: %v", err) + } + + // Validate results + if math.IsNaN(result.LookAngles.Azimuth) || math.IsNaN(result.LookAngles.Elevation) { + t.Error("Look angles are NaN") + } +} + +func TestCalculateSGP4Position_EmptyStrings(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with empty strings + _, err := CalculateSGP4Position("", "", targetTime) + if err == nil { + t.Error("Expected error for empty TLE lines") + } + + // Test with whitespace only + _, err = CalculateSGP4Position(" ", " ", targetTime) + if err == nil { + t.Error("Expected error for whitespace-only TLE lines") + } +} + +func TestCalculateSGP4Position_HistoricalDate(t *testing.T) { + // Test with a date in the past (before TLE epoch) + historicalTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, historicalTime) + if err != nil { + // Error is acceptable for dates far from TLE epoch + return + } + + // If no error, validate position + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Error("Position has NaN coordinates") + } +} + +func TestCalculateSGP4Position_FutureDate(t *testing.T) { + // Test with a date far in the future + futureTime := time.Date(2050, 1, 1, 0, 0, 0, 0, time.UTC) + + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, futureTime) + if err != nil { + // Error is acceptable for dates far from TLE epoch + return + } + + // If no error, validate position + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Error("Position has NaN coordinates") + } +} + +func TestCalculateSGP4PositionWithObserver_ErrorPropagation(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + observer := ObserverPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 10.0, + } + + // Test that errors from CalculateSGP4Position are propagated + _, err := CalculateSGP4PositionWithObserver("INVALID", "INVALID", targetTime, observer) + if err == nil { + t.Error("Expected error for invalid TLE lines") + } +} + +func TestCalculateSGP4Positions_ErrorPropagation(t *testing.T) { + startTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + endTime := startTime.Add(1 * time.Hour) + interval := 15 * time.Minute + + // Test that errors are propagated when TLE is invalid + _, err := CalculateSGP4Positions("INVALID", "INVALID", startTime, endTime, interval) + if err == nil { + t.Error("Expected error for invalid TLE lines") + } +} + +func TestCalculateSGP4Position_LeapYear(t *testing.T) { + // Test with leap year date + leapYearTime := time.Date(2024, 2, 29, 12, 0, 0, 0, time.UTC) + + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, leapYearTime) + if err != nil { + t.Fatalf("Failed on leap year date: %v", err) + } + + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Error("Position has NaN coordinates") + } +} + +func TestCalculateSGP4Position_YearBoundary(t *testing.T) { + // Test with year boundary transition + yearBoundaryTime := time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC) + + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, yearBoundaryTime) + if err != nil { + t.Fatalf("Failed at year boundary: %v", err) + } + + if math.IsNaN(pos.Latitude) || math.IsNaN(pos.Longitude) { + t.Error("Position has NaN coordinates") + } +} + +func TestCalculateSGP4PositionWithObserver_ZeroAltitude(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + // Test with observer at sea level + seaLevelObserver := ObserverPosition{ + Latitude: 0.0, + Longitude: 0.0, + Altitude: 0.0, + } + + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, seaLevelObserver) + if err != nil { + t.Fatalf("Failed with zero altitude observer: %v", err) + } + + if math.IsNaN(result.LookAngles.Azimuth) || math.IsNaN(result.LookAngles.Elevation) { + t.Error("Look angles are NaN") + } +} + +func TestCalculateSGP4Position_TimestampAccuracy(t *testing.T) { + // Test that timestamp is correctly set + testTimes := []time.Time{ + time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC), + time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC), + time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC), + } + + for _, testTime := range testTimes { + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, testTime) + if err != nil { + t.Errorf("Failed for time %v: %v", testTime, err) + continue + } + + if pos.Timestamp != testTime.Unix() { + t.Errorf("Timestamp mismatch: expected %d, got %d", testTime.Unix(), pos.Timestamp) + } + } +} + +func TestCalculateSGP4Position_VelocityCalculation(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + pos, err := CalculateSGP4Position(testTLELine1, testTLELine2, targetTime) + if err != nil { + t.Fatalf("CalculateSGP4Position failed: %v", err) + } + + // Velocity should be a positive number (magnitude of velocity vector) + if pos.Velocity <= 0 { + t.Errorf("Velocity should be positive, got %f", pos.Velocity) + } + + if math.IsNaN(pos.Velocity) || math.IsInf(pos.Velocity, 0) { + t.Error("Velocity is NaN or Inf") + } +} + +func TestCalculateSGP4PositionWithObserver_RangeCalculation(t *testing.T) { + targetTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) + + observer := ObserverPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 10.0, + } + + result, err := CalculateSGP4PositionWithObserver(testTLELine1, testTLELine2, targetTime, observer) + if err != nil { + t.Fatalf("CalculateSGP4PositionWithObserver failed: %v", err) + } + + // Range should be positive + if result.LookAngles.Range <= 0 { + t.Errorf("Range should be positive, got %f", result.LookAngles.Range) + } + + // Range should be reasonable for LEO satellites (typically 400-2000 km) + // But we'll be lenient since TLE epoch matters + if result.LookAngles.Range > 100000 { + t.Errorf("Range seems unreasonably large: %f km", result.LookAngles.Range) + } +} + +// Test print functions to ensure they don't panic +func TestPrintSGP4Position(t *testing.T) { + pos := SGPPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 400.0, + Velocity: 7.5, + Timestamp: time.Now().Unix(), + } + + // Just verify it doesn't panic - we can't easily test output + defer func() { + if r := recover(); r != nil { + t.Errorf("PrintSGP4Position panicked: %v", r) + } + }() + + PrintSGP4Position(pos) +} + +func TestPrintSGP4PositionWithLookAngles(t *testing.T) { + result := SGP4PositionResult{ + Position: SGPPosition{ + Latitude: 40.7128, + Longitude: -74.0060, + Altitude: 400.0, + Velocity: 7.5, + Timestamp: time.Now().Unix(), + }, + LookAngles: LookAngles{ + Azimuth: 180.0, + Elevation: 45.0, + Range: 1000.0, + RangeRate: 0.0, + }, + } + + // Just verify it doesn't panic - we can't easily test output + defer func() { + if r := recover(); r != nil { + t.Errorf("PrintSGP4PositionWithLookAngles panicked: %v", r) + } + }() + + PrintSGP4PositionWithLookAngles(result) +} + +func TestPrintSGP4Position_EdgeCases(t *testing.T) { + // Test with edge case values + testCases := []SGPPosition{ + {Latitude: 0.0, Longitude: 0.0, Altitude: 0.0, Velocity: 0.0, Timestamp: 0}, + {Latitude: 90.0, Longitude: 180.0, Altitude: 1000.0, Velocity: 10.0, Timestamp: 1000000000}, + {Latitude: -90.0, Longitude: -180.0, Altitude: 500.0, Velocity: 5.0, Timestamp: 2000000000}, + {Latitude: 45.123456, Longitude: -123.456789, Altitude: 350.5, Velocity: 7.123456, Timestamp: 1500000000}, + } + + for i, pos := range testCases { + defer func(idx int) { + if r := recover(); r != nil { + t.Errorf("PrintSGP4Position panicked for test case %d: %v", idx, r) + } + }(i) + PrintSGP4Position(pos) + } +} + +func TestPrintSGP4PositionWithLookAngles_EdgeCases(t *testing.T) { + // Test with edge case values + testCases := []SGP4PositionResult{ + { + Position: SGPPosition{Latitude: 0.0, Longitude: 0.0, Altitude: 0.0, Velocity: 0.0, Timestamp: 0}, + LookAngles: LookAngles{Azimuth: 0.0, Elevation: 0.0, Range: 0.0, RangeRate: 0.0}, + }, + { + Position: SGPPosition{Latitude: 90.0, Longitude: 180.0, Altitude: 1000.0, Velocity: 10.0, Timestamp: 1000000000}, + LookAngles: LookAngles{Azimuth: 359.99, Elevation: 90.0, Range: 50000.0, RangeRate: 1.0}, + }, + { + Position: SGPPosition{Latitude: -90.0, Longitude: -180.0, Altitude: 500.0, Velocity: 5.0, Timestamp: 2000000000}, + LookAngles: LookAngles{Azimuth: 180.0, Elevation: -90.0, Range: 100.0, RangeRate: -1.0}, + }, + } + + for i, result := range testCases { + defer func(idx int) { + if r := recover(); r != nil { + t.Errorf("PrintSGP4PositionWithLookAngles panicked for test case %d: %v", idx, r) + } + }(i) + PrintSGP4PositionWithLookAngles(result) + } +} + diff --git a/osint/tle.go b/osint/tle.go index 7c9c48b..dc72b7a 100644 --- a/osint/tle.go +++ b/osint/tle.go @@ -2,62 +2,125 @@ package osint import ( "fmt" - "github.com/TwiN/go-color" "strconv" "strings" + + "github.com/TwiN/go-color" + "github.com/manifoldco/promptui" ) type TLE struct { - CommonName string - SatelliteCatalogNumber int - ElsetClassificiation string - InternationalDesignator string - ElementSetEpoch float64 - FirstDerivativeMeanMotion float64 + CommonName string + SatelliteCatalogNumber int + ElsetClassificiation string + InternationalDesignator string + ElementSetEpoch float64 + FirstDerivativeMeanMotion float64 SecondDerivativeMeanMotion string - BDragTerm string - ElementSetType int - ElementNumber int - ChecksumOne int - OrbitInclination float64 - RightAscension float64 - Eccentrcity float64 - Perigee float64 - MeanAnamoly float64 - MeanMotion float64 - RevolutionNumber int - ChecksumTwo int + BDragTerm string + ElementSetType int + ElementNumber int + ChecksumOne int + OrbitInclination float64 + RightAscension float64 + Eccentrcity float64 + Perigee float64 + MeanAnamoly float64 + MeanMotion float64 + RevolutionNumber int + ChecksumTwo int } +// ConstructTLE parses two-line element data into a TLE struct. +// It handles variable field counts gracefully and returns an empty TLE if parsing fails. func ConstructTLE(one string, two string, three string) TLE { tle := TLE{} tle.CommonName = one firstArr := strings.Fields(two) secondArr := strings.Fields(three) - tle.SatelliteCatalogNumber, _ = strconv.Atoi(firstArr[1][:len(firstArr[1])-1]) - tle.ElsetClassificiation = string(firstArr[1][len(firstArr[1])-1]) - tle.InternationalDesignator = firstArr[2] - tle.ElementSetEpoch, _ = strconv.ParseFloat(firstArr[3], 64) - tle.FirstDerivativeMeanMotion, _ = strconv.ParseFloat(firstArr[4], 64) - tle.SecondDerivativeMeanMotion = firstArr[5] - tle.BDragTerm = firstArr[6] - tle.ElementSetType, _ = strconv.Atoi(firstArr[7]) - tle.ElementNumber, _ = strconv.Atoi(firstArr[8][:len(firstArr[8])-1]) - tle.ChecksumOne, _ = strconv.Atoi(string(firstArr[8][len(firstArr[8])-1])) - tle.SatelliteCatalogNumber, _ = strconv.Atoi(secondArr[1]) - tle.OrbitInclination, _ = strconv.ParseFloat(secondArr[2], 64) - tle.RightAscension, _ = strconv.ParseFloat(secondArr[3], 64) - tle.Eccentrcity, _ = strconv.ParseFloat("0." + secondArr[4], 64) - tle.Perigee, _ = strconv.ParseFloat(secondArr[5], 64) - tle.MeanAnamoly, _ = strconv.ParseFloat(secondArr[6], 64) - tle.MeanMotion, _ = strconv.ParseFloat(secondArr[7][:11], 64) - tle.RevolutionNumber, _ = strconv.Atoi(secondArr[7][11:16]) - tle.ChecksumTwo, _ = strconv.Atoi(string(secondArr[7][len(secondArr[7])-1])) + + if len(firstArr) < 4 { + return tle + } + if len(secondArr) < 3 { + return tle + } + + if len(firstArr) > 1 && len(firstArr[1]) > 0 { + catalogStr := firstArr[1] + if len(catalogStr) > 1 { + tle.SatelliteCatalogNumber, _ = strconv.Atoi(catalogStr[:len(catalogStr)-1]) + tle.ElsetClassificiation = string(catalogStr[len(catalogStr)-1]) + } else { + tle.SatelliteCatalogNumber, _ = strconv.Atoi(catalogStr) + } + } + if len(firstArr) > 2 { + tle.InternationalDesignator = firstArr[2] + } + if len(firstArr) > 3 { + tle.ElementSetEpoch, _ = strconv.ParseFloat(firstArr[3], 64) + } + if len(firstArr) > 4 { + tle.FirstDerivativeMeanMotion, _ = strconv.ParseFloat(firstArr[4], 64) + } + if len(firstArr) > 5 { + tle.SecondDerivativeMeanMotion = firstArr[5] + } + if len(firstArr) > 6 { + tle.BDragTerm = firstArr[6] + } + if len(firstArr) > 7 { + tle.ElementSetType, _ = strconv.Atoi(firstArr[7]) + } + if len(firstArr) > 8 { + lastField := firstArr[8] + if len(lastField) > 1 { + tle.ElementNumber, _ = strconv.Atoi(lastField[:len(lastField)-1]) + tle.ChecksumOne, _ = strconv.Atoi(string(lastField[len(lastField)-1])) + } else if len(lastField) > 0 { + tle.ElementNumber, _ = strconv.Atoi(lastField) + } + } + + if len(secondArr) > 1 { + tle.SatelliteCatalogNumber, _ = strconv.Atoi(secondArr[1]) + } + if len(secondArr) > 2 { + tle.OrbitInclination, _ = strconv.ParseFloat(secondArr[2], 64) + } + if len(secondArr) > 3 { + tle.RightAscension, _ = strconv.ParseFloat(secondArr[3], 64) + } + if len(secondArr) > 4 { + tle.Eccentrcity, _ = strconv.ParseFloat("0."+secondArr[4], 64) + } + if len(secondArr) > 5 { + tle.Perigee, _ = strconv.ParseFloat(secondArr[5], 64) + } + if len(secondArr) > 6 { + tle.MeanAnamoly, _ = strconv.ParseFloat(secondArr[6], 64) + } + if len(secondArr) > 7 { + meanMotionStr := secondArr[7] + if len(meanMotionStr) >= 11 { + tle.MeanMotion, _ = strconv.ParseFloat(meanMotionStr[:11], 64) + if len(meanMotionStr) >= 16 { + tle.RevolutionNumber, _ = strconv.Atoi(meanMotionStr[11:16]) + } + } else { + tle.MeanMotion, _ = strconv.ParseFloat(meanMotionStr, 64) + } + if len(meanMotionStr) > 0 { + tle.ChecksumTwo, _ = strconv.Atoi(string(meanMotionStr[len(meanMotionStr)-1])) + } + } return tle } -func PrintTLE (tle TLE) { +// PrintTLE displays the TLE data in a formatted table. +func PrintTLE(tle TLE) { fmt.Println(color.Ize(color.Purple, "\n╔═════════════════════════════════════════════════════════════╗")) fmt.Println(color.Ize(color.Purple, GenRowString("Name", tle.CommonName))) fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Catalog Number", fmt.Sprintf("%d", tle.SatelliteCatalogNumber)))) @@ -78,6 +141,25 @@ func PrintTLE (tle TLE) { fmt.Println(color.Ize(color.Purple, GenRowString("Mean Motion (revolutions/day)", fmt.Sprintf("%f", tle.MeanMotion)))) fmt.Println(color.Ize(color.Purple, GenRowString("Revolution Number at Epoch", fmt.Sprintf("%d", tle.RevolutionNumber)))) fmt.Println(color.Ize(color.Purple, GenRowString("Checksum Line Two", fmt.Sprintf("%d", tle.ChecksumTwo)))) - + fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝ \n\n")) -} \ No newline at end of file + + // Offer export option + exportPrompt := promptui.Prompt{ + Label: "Export TLE data? (y/n)", + Default: "n", + AllowEdit: true, + } + exportAnswer, _ := exportPrompt.Run() + if strings.ToLower(strings.TrimSpace(exportAnswer)) == "y" { + defaultFilename := fmt.Sprintf("tle_%s_%d", strings.ReplaceAll(tle.CommonName, " ", "_"), tle.SatelliteCatalogNumber) + format, filePath, err := showExportMenu(defaultFilename) + if err == nil { + if err := ExportTLE(tle, format, filePath); err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to export: "+err.Error())) + } else { + fmt.Println(color.Ize(color.Green, fmt.Sprintf(" [+] Exported to: %s", filePath))) + } + } + } +} diff --git a/osint/tleparser.go b/osint/tleparser.go index f8ab45a..ecdb60e 100644 --- a/osint/tleparser.go +++ b/osint/tleparser.go @@ -1,93 +1,225 @@ package osint import ( + "bufio" "fmt" "os" + "path/filepath" + "strings" + "github.com/TwiN/go-color" - "github.com/iskaa02/qalam/gradient" - "io/ioutil" - "bufio" + "github.com/iskaa02/qalam/gradient" ) +// TLEParser provides an interactive menu for parsing TLE data from different sources. func TLEParser() { - options, _ := ioutil.ReadFile("txt/tle_parser.txt") - opt,_:=gradient.NewGradient("#1179ef", "cyan") + options, _ := os.ReadFile("txt/tle_parser.txt") + opt, _ := gradient.NewGradient("#1179ef", "cyan") opt.Print("\n" + string(options)) var selection int = Option(0, 3) - if (selection == 1){ + if selection == 1 { TLETextFile() - } else if (selection == 2) { + } else if selection == 2 { TLEPlainString() - } + } +} + +// validateFilePath checks if a file path is safe and valid. +// It prevents directory traversal attacks and validates path format. +func validateFilePath(path string) error { + // Trim whitespace + path = strings.TrimSpace(path) + + // Check for empty path + if path == "" { + return fmt.Errorf("file path cannot be empty") + } + + // Check for null bytes (potential injection) + if strings.Contains(path, "\x00") { + return fmt.Errorf("file path contains invalid characters") + } + + // Check for directory traversal patterns + dangerousPatterns := []string{ + "..", + "../", + "..\\", + "/..", + "\\..", + "//", + "\\\\", + } + pathNormalized := strings.ToLower(path) + for _, pattern := range dangerousPatterns { + if strings.Contains(pathNormalized, strings.ToLower(pattern)) { + return fmt.Errorf("directory traversal detected in path") + } + } + + // Check path length (reasonable limit) + if len(path) > 4096 { + return fmt.Errorf("file path is too long") + } + + // Clean the path to resolve any remaining issues + cleanedPath := filepath.Clean(path) + + // Check if cleaned path still contains dangerous patterns + if strings.Contains(cleanedPath, "..") { + return fmt.Errorf("invalid file path") + } - return + // Check if path is absolute and potentially dangerous + // (Optional: you might want to restrict to relative paths only) + // For now, we'll allow absolute paths but validate them + + return nil } +// TLETextFile reads TLE data from a text file and parses it. func TLETextFile() { - fmt.Print("\n ENTER TEXT FILE PATH > ") var path string fmt.Scanln(&path) + + // Validate file path before attempting to open + if err := validateFilePath(path); err != nil { + appErr := NewAppErrorWithContext(ErrCodeFilePathInvalid, "Invalid file path", fmt.Sprintf("Path: %s", path)) + appErr.OriginalErr = err + appErr.Display() + return + } + + // Clean the path after validation + path = filepath.Clean(strings.TrimSpace(path)) + + // Check if file exists and is a regular file (not a directory) + fileInfo, err := os.Stat(path) + if err != nil { + context := fmt.Sprintf("File path: %s", path) + HandleErrorWithContext(err, ErrCodeFileNotFound, "Failed to access TLE file", context) + return + } + + // Ensure it's a file, not a directory + if fileInfo.IsDir() { + err := NewAppErrorWithContext(ErrCodeFilePathInvalid, "Path is a directory, not a file", fmt.Sprintf("Path: %s", path)) + err.Display() + return + } + file, err := os.Open(path) - if err != nil { - fmt.Println(color.Ize(color.Red, " [!] INVALID TEXT FILE")) + context := fmt.Sprintf("File path: %s", path) + HandleErrorWithContext(err, ErrCodeFileReadFailed, "Failed to open TLE file", context) + return } - + defer file.Close() + scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) var txtlines []string var count int = 0 - + for scanner.Scan() { txtlines = append(txtlines, scanner.Text()) count += 1 } - - file.Close() - if (count < 2 || count > 3) { - fmt.Println(color.Ize(color.Red, " [!] INVALID TLE FORMAT")) + if count < 2 || count > 3 { + err := NewAppErrorWithContext( + ErrCodeTLEInvalidFormat, + "Invalid TLE format - file must contain 2 or 3 lines", + fmt.Sprintf("File: %s, Lines found: %d", path, count), + ) + err.Display() return } - output := TLE{} + var output TLE + var lineOne, lineTwo string - if (count == 3) { + if count == 3 { var satelliteName string = txtlines[0] - output = ConstructTLE(satelliteName, txtlines[1], txtlines[2]) + lineOne = txtlines[1] + lineTwo = txtlines[2] + output = ConstructTLE(satelliteName, lineOne, lineTwo) } else { - output = ConstructTLE("UNSPECIFIED", txtlines[0], txtlines[1]) + lineOne = txtlines[0] + lineTwo = txtlines[1] + output = ConstructTLE("UNSPECIFIED", lineOne, lineTwo) + } + + // Validate TLE parsing before displaying + parsingFailed := false + line1Fields := strings.Fields(lineOne) + line2Fields := strings.Fields(lineTwo) + + if len(line1Fields) < 4 || len(line2Fields) < 3 { + parsingFailed = true + } else if output.SatelliteCatalogNumber == 0 && output.InternationalDesignator == "" && output.ElementSetEpoch == 0.0 { + parsingFailed = true + } + + if parsingFailed { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to parse TLE data")) + fmt.Println(color.Ize(color.Red, fmt.Sprintf(" Line 1 fields: %d (minimum required: 4)", len(line1Fields)))) + fmt.Println(color.Ize(color.Red, fmt.Sprintf(" Line 2 fields: %d (minimum required: 3)", len(line2Fields)))) + if len(line1Fields) >= 4 && len(line2Fields) >= 3 { + fmt.Println(color.Ize(color.Red, " Note: Field count is sufficient, but parsing failed. Check TLE format.")) + } + return } PrintTLE(output) } -func TLEPlainString(){ +// TLEPlainString prompts the user to enter TLE data line by line and parses it. +func TLEPlainString() { scanner := bufio.NewScanner(os.Stdin) var lineOne string var lineTwo string var lineThree string fmt.Print("\n ENTER LINE ONE (leave blank for unspecified name) > ") scanner.Scan() - lineOne = scanner.Text() + lineOne = scanner.Text() fmt.Print("\n ENTER LINE TWO > ") scanner.Scan() - lineTwo = scanner.Text() + lineTwo = scanner.Text() fmt.Print("\n ENTER LINE THREE > ") scanner.Scan() - lineThree = scanner.Text() + lineThree = scanner.Text() - if (lineOne == "") { + if lineOne == "" { lineOne = "UNSPECIFIED" } - - output := TLE{} - output = ConstructTLE(lineOne, lineTwo, lineThree) + output := ConstructTLE(lineOne, lineTwo, lineThree) + + // Validate TLE parsing before displaying + parsingFailed := false + line1Fields := strings.Fields(lineTwo) + line2Fields := strings.Fields(lineThree) + + if len(line1Fields) < 4 || len(line2Fields) < 3 { + parsingFailed = true + } else if output.SatelliteCatalogNumber == 0 && output.InternationalDesignator == "" && output.ElementSetEpoch == 0.0 { + parsingFailed = true + } + + if parsingFailed { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to parse TLE data")) + fmt.Println(color.Ize(color.Red, fmt.Sprintf(" Line 1 fields: %d (minimum required: 4)", len(line1Fields)))) + fmt.Println(color.Ize(color.Red, fmt.Sprintf(" Line 2 fields: %d (minimum required: 3)", len(line2Fields)))) + if len(line1Fields) >= 4 && len(line2Fields) >= 3 { + fmt.Println(color.Ize(color.Red, " Note: Field count is sufficient, but parsing failed. Check TLE format.")) + } + return + } PrintTLE(output) } diff --git a/osint/tleparser_test.go b/osint/tleparser_test.go new file mode 100644 index 0000000..119c704 --- /dev/null +++ b/osint/tleparser_test.go @@ -0,0 +1,225 @@ +package osint + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestValidateFilePath(t *testing.T) { + tests := []struct { + name string + path string + expectError bool + errorMsg string + }{ + { + name: "Valid relative path", + path: "test.tle", + expectError: false, + }, + { + name: "Valid path with subdirectory", + path: "data/test.tle", + expectError: false, + }, + { + name: "Empty path", + path: "", + expectError: true, + errorMsg: "file path cannot be empty", + }, + { + name: "Whitespace only path", + path: " ", + expectError: true, + errorMsg: "file path cannot be empty", + }, + { + name: "Path with null byte", + path: "test\x00.tle", + expectError: true, + errorMsg: "file path contains invalid characters", + }, + { + name: "Directory traversal with ../", + path: "../test.tle", + expectError: true, + errorMsg: "directory traversal detected", + }, + { + name: "Directory traversal with ..\\", + path: "..\\test.tle", + expectError: true, + errorMsg: "directory traversal detected", + }, + { + name: "Directory traversal in middle", + path: "data/../test.tle", + expectError: true, + errorMsg: "directory traversal detected", + }, + { + name: "Multiple directory traversals", + path: "../../../etc/passwd", + expectError: true, + errorMsg: "directory traversal detected", + }, + { + name: "Path with double slashes", + path: "data//test.tle", + expectError: true, + errorMsg: "directory traversal detected", + }, + { + name: "Path with double backslashes", + path: "data\\\\test.tle", + expectError: true, + errorMsg: "directory traversal detected", + }, + { + name: "Path too long", + path: strings.Repeat("a", 4097), + expectError: true, + errorMsg: "file path is too long", + }, + { + name: "Valid absolute path (Unix)", + path: "/tmp/test.tle", + expectError: false, + }, + { + name: "Valid absolute path (Windows)", + path: "C:\\Users\\test.tle", + expectError: false, + }, + { + name: "Path with spaces", + path: "my file.tle", + expectError: false, + }, + { + name: "Path with special characters", + path: "test-file_123.tle", + expectError: false, + }, + { + name: "Encoded directory traversal", + path: "%2e%2e%2f", + expectError: false, // URL encoding not decoded, but still dangerous + // Note: In production, you might want to decode and check + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateFilePath(tt.path) + if tt.expectError { + if err == nil { + t.Errorf("validateFilePath(%q) expected error, got nil", tt.path) + } else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("validateFilePath(%q) error = %v, want error containing %q", tt.path, err, tt.errorMsg) + } + } else { + if err != nil { + t.Errorf("validateFilePath(%q) unexpected error: %v", tt.path, err) + } + } + }) + } +} + +func TestValidateFilePathWithRealFile(t *testing.T) { + // Create a temporary file for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.tle") + + err := os.WriteFile(testFile, []byte("test content"), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Test with valid file path + err = validateFilePath(testFile) + if err != nil { + t.Errorf("validateFilePath(%q) with valid file got error: %v", testFile, err) + } + + // Test with relative path to the temp file + relPath, err := filepath.Rel(".", testFile) + if err == nil { + err = validateFilePath(relPath) + if err != nil { + t.Logf("Note: Relative path validation may fail if path contains '..': %v", err) + } + } +} + +func TestValidateFilePathEdgeCases(t *testing.T) { + tests := []struct { + name string + path string + expectError bool + }{ + { + name: "Single dot", + path: ".", + expectError: false, // filepath.Clean will handle this + }, + { + name: "Current directory reference", + path: "./test.tle", + expectError: false, // filepath.Clean will normalize this + }, + { + name: "Path starting with dot", + path: ".test.tle", + expectError: false, // Hidden file, but valid + }, + { + name: "Path with many slashes", + path: "a/b/c/d/e/f/test.tle", + expectError: false, + }, + { + name: "Path at max length", + path: strings.Repeat("a", 4096), + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateFilePath(tt.path) + if tt.expectError && err == nil { + t.Errorf("validateFilePath(%q) expected error, got nil", tt.path) + } else if !tt.expectError && err != nil { + t.Errorf("validateFilePath(%q) unexpected error: %v", tt.path, err) + } + }) + } +} + +// Benchmark tests +func BenchmarkValidateFilePath(b *testing.B) { + testPaths := []string{ + "test.tle", + "data/test.tle", + "../test.tle", + strings.Repeat("a", 100), + "test\x00.tle", + } + + for i := 0; i < b.N; i++ { + for _, path := range testPaths { + validateFilePath(path) + } + } +} + + + + + + diff --git a/run_tests.go b/run_tests.go new file mode 100644 index 0000000..f6061b6 --- /dev/null +++ b/run_tests.go @@ -0,0 +1,234 @@ +// +build ignore + +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// TestModule represents a testable module in the application +type TestModule struct { + Name string + Description string + PackagePath string + TestFile string +} + +// Available test modules +var testModules = []TestModule{ + { + Name: "main", + Description: "Main package tests (env loading, password masking, credential management)", + PackagePath: ".", + TestFile: "main_test.go", + }, + { + Name: "osint", + Description: "OSINT package tests (TLE parsing, API interactions, satellite data, export functionality)", + PackagePath: "./osint", + TestFile: "osint_test.go", + }, + { + Name: "cli", + Description: "CLI package tests (menu handling, user input, display functions)", + PackagePath: "./cli", + TestFile: "cli_test.go", + }, + { + Name: "batch", + Description: "Batch operations tests (multi-satellite processing, comparisons, exports)", + PackagePath: "./osint", + TestFile: "batch_test.go", + }, + { + Name: "export", + Description: "Export functionality tests (CSV, JSON, Text export for TLE, predictions, positions)", + PackagePath: "./osint", + TestFile: "export_test.go", + }, + { + Name: "progress", + Description: "Progress indicators tests (spinners, progress bars, API progress)", + PackagePath: "./osint", + TestFile: "progress_test.go", + }, + // Add new modules here as you create them +} + +// runTests executes go test for a specific module +func runTests(module TestModule, verbose bool, coverage bool, benchmark bool, testPattern string) error { + args := []string{"test"} + + if verbose { + args = append(args, "-v") + } + + if coverage { + args = append(args, "-cover") + } + + if benchmark { + args = append(args, "-bench=.", "-benchmem") + } + + // Add test name pattern filter if specified + if testPattern != "" { + args = append(args, "-run", testPattern) + } + + args = append(args, module.PackagePath) + + cmd := exec.Command("go", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + fmt.Printf("\n%s Testing module: %s %s\n", strings.Repeat("=", 40), module.Name, strings.Repeat("=", 40)) + fmt.Printf("Description: %s\n", module.Description) + fmt.Printf("Package: %s\n", module.PackagePath) + if testPattern != "" { + fmt.Printf("Test Pattern: %s\n", testPattern) + } + fmt.Printf("Command: go %s\n\n", strings.Join(args, " ")) + + return cmd.Run() +} + +// listModules displays all available test modules +func listModules() { + fmt.Println("\nAvailable Test Modules:") + fmt.Println(strings.Repeat("-", 60)) + for i, module := range testModules { + fmt.Printf("%d. %s\n", i+1, module.Name) + fmt.Printf(" Description: %s\n", module.Description) + fmt.Printf(" Package: %s\n", module.PackagePath) + fmt.Printf(" Test File: %s\n\n", module.TestFile) + } +} + +// checkTestFiles verifies which test files exist +func checkTestFiles() { + fmt.Println("\nTest File Status:") + fmt.Println(strings.Repeat("-", 60)) + for _, module := range testModules { + testPath := filepath.Join(module.PackagePath, module.TestFile) + if _, err := os.Stat(testPath); os.IsNotExist(err) { + fmt.Printf("❌ %s - Missing: %s\n", module.Name, testPath) + } else { + fmt.Printf("✅ %s - Exists: %s\n", module.Name, testPath) + } + } +} + +func main() { + var ( + moduleFlag = flag.String("module", "", "Run tests for specific module (use 'list' to see available modules)") + allFlag = flag.Bool("all", false, "Run all test modules") + verboseFlag = flag.Bool("v", false, "Verbose output") + coverageFlag = flag.Bool("cover", false, "Show coverage information") + benchFlag = flag.Bool("bench", false, "Run benchmarks") + checkFlag = flag.Bool("check", false, "Check which test files exist") + helpFlag = flag.Bool("help", false, "Show help message") + runFlag = flag.String("run", "", "Run only tests matching the pattern (e.g., 'TestExport' for export tests)") + ) + + flag.Parse() + + if *helpFlag { + fmt.Println("SatIntel Test Runner") + fmt.Println(strings.Repeat("=", 60)) + fmt.Println("\nUsage:") + fmt.Println(" go run run_tests.go [options]") + fmt.Println("\nOptions:") + flag.PrintDefaults() + fmt.Println("\nExamples:") + fmt.Println(" go run run_tests.go -all # Run all tests") + fmt.Println(" go run run_tests.go -module=main # Run main package tests") + fmt.Println(" go run run_tests.go -all -cover # Run all tests with coverage") + fmt.Println(" go run run_tests.go -module=osint -v # Run osint tests verbosely") + fmt.Println(" go run run_tests.go -module=export -run TestExport # Run only export tests") + fmt.Println(" go run run_tests.go -check # Check test file status") + fmt.Println(" go run run_tests.go -module=list # List available modules") + return + } + + if *checkFlag { + checkTestFiles() + return + } + + if *moduleFlag == "list" { + listModules() + return + } + + // Find specific module if requested + if *moduleFlag != "" { + var foundModule *TestModule + for i := range testModules { + if testModules[i].Name == *moduleFlag { + foundModule = &testModules[i] + break + } + } + + if foundModule == nil { + fmt.Printf("Error: Module '%s' not found.\n", *moduleFlag) + fmt.Println("\nAvailable modules:") + for _, m := range testModules { + fmt.Printf(" - %s\n", m.Name) + } + os.Exit(1) + } + + if err := runTests(*foundModule, *verboseFlag, *coverageFlag, *benchFlag, *runFlag); err != nil { + os.Exit(1) + } + return + } + + // Run all modules if -all flag is set, otherwise show help + if *allFlag { + fmt.Println("SatIntel Test Runner - Running All Modules") + fmt.Println(strings.Repeat("=", 60)) + + failed := false + for _, module := range testModules { + // Check if test file exists before running + testPath := filepath.Join(module.PackagePath, module.TestFile) + if _, err := os.Stat(testPath); os.IsNotExist(err) { + fmt.Printf("\n⚠️ Skipping %s - test file not found: %s\n", module.Name, testPath) + continue + } + + if err := runTests(module, *verboseFlag, *coverageFlag, *benchFlag, *runFlag); err != nil { + failed = true + fmt.Printf("\n❌ Tests failed for module: %s\n", module.Name) + } else { + fmt.Printf("\n✅ Tests passed for module: %s\n", module.Name) + } + } + + if failed { + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println("Some tests failed. See output above for details.") + os.Exit(1) + } else { + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println("All tests passed! ✅") + } + return + } + + // Default: show help + fmt.Println("SatIntel Test Runner") + fmt.Println("Use -help to see usage information") + fmt.Println("Use -module=list to see available modules") + fmt.Println("Use -all to run all tests") + fmt.Println("Use -check to check test file status") +} + diff --git a/txt/map.txt b/txt/map.txt index 376e85b..37d38bd 100644 --- a/txt/map.txt +++ b/txt/map.txt @@ -1,23 +1,23 @@ - . _..::__: ,-"-"._ |] , _,.__ - _.___ _ _<_>\`!(._\`.\`-. / _._ \`_ ,_/ ' '-._.---.-.__ -.{ " " \`-==,',._\\{ \\ / {) _ / _ ">_,-' \` /-/_ -\\_.:--. \`._ )\`^-. "' / ( [_/( __,/-' -'"' \\ " _\\ -_,--' ) /. (| - | ,' _)_.\\\\._<> {} _,' / ' - \`. / [_/_'\` \`"( <'} ) - \\\\ .-. ) / \`-'"..' \`:._ _) ' - \` \\ ( \`( / \`:\\ > \\ ,-^. /' ' - \`._, "" | \\\`' \\| ?_) {\\ - \`=.---. \`._._ ,' "\` |' ,- '. - | \`-._ | / \`:\`<_|=--._ - ( > . | , \`=.__.\`-'\\ - \`. / | |{| ,-.,\\ . - | ,' \\ / \`' ," \\ - | / |_' | __ / - | | '-' \`-' \\. - |/ " / - \\. ' - - ,/ ______._.--._ _..---.---------. -__,-----"-..?----_/ )\\ . ,-'" " (__--/ - /__/\\/ \ No newline at end of file + . _..::__: ,-"-"._ |] , _,.__ + _.___ _ _<_>\`!(._\`.\`-. / _._ \`_ ,_/ ' '-._.---.-.__ + .{ " " \`-==,',._\\{ \\ / {) _ / _ ">_,-' \` /-/_ + \\_.:--. \`._ )\`^-. "' / ( [_/( __,/-' + '"' \\ " _\\ -_,--' ) /. (| + | ,' _)_.\\\\._<> {} _,' / ' + \`. / [_/_'\` \`"( <'} ) + \\\\ .-. ) / \`-'"..' \`:._ _) ' + \` \\ ( \`( / \`:\\ > \\ ,-^. /' ' + \`._, "" | \\\`' \\| ?_) {\\ + \`=.---. \`._._ ,' "\` |' ,- '. + | \`-._ | / \`:\`<_|=--._ + ( > . | , \`=.__.\`-'\\ + \`. / | |{| ,-.,\\ . + | ,' \\ / \`' ," \\ + | / |_' | __ / + | | '-' \`-' \\. + |/ " / + \\. ' + + ,/ ______._.--._ _..---.---------. + __,-----"-..?----_/ )\\ . ,-'" " (__--/ + /__/\\/ diff --git a/txt/options.txt b/txt/options.txt index e2a18e0..3dcedb5 100644 --- a/txt/options.txt +++ b/txt/options.txt @@ -8,6 +8,8 @@ [ 4 ] TLE Parser + [ 5 ] Batch Operations + [ 0 ] Exit SatIntel ================================================================================================================================= \ No newline at end of file