diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abffe9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Environment variables +.env + +# Build artifacts +SatIntel +SatIntel.exe +*.exe + +# Go workspace file +go.work + +# Documentation files +IMPROVEMENTS.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/QUICK_TEST.md b/QUICK_TEST.md new file mode 100644 index 0000000..5185bd5 --- /dev/null +++ b/QUICK_TEST.md @@ -0,0 +1,47 @@ +# 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..039a225 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,68 +1,103 @@ 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 < 5 { 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() } } +// 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..3792079 --- /dev/null +++ b/cli/cli_test.go @@ -0,0 +1,64 @@ +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 + }) + } +} + +// Placeholder for future CLI tests +// Add more tests as you implement testable CLI functions + diff --git a/go.mod b/go.mod index 02e1c36..a2fb32f 100644 --- a/go.mod +++ b/go.mod @@ -1,33 +1,20 @@ 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/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..c68ad8f 100644 --- a/go.sum +++ b/go.sum @@ -1,496 +1,25 @@ -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/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= -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= +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= 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/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..85fc36e --- /dev/null +++ b/osint/favorites.go @@ -0,0 +1,294 @@ +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/orbitalelement.go b/osint/orbitalelement.go index 27a5a1b..a19e99a 100644 --- a/osint/orbitalelement.go +++ b/osint/orbitalelement.go @@ -1,32 +1,32 @@ package osint import ( - "io/ioutil" "fmt" - "github.com/iskaa02/qalam/gradient" + "os" + + "github.com/iskaa02/qalam/gradient" ) +// OrbitalElement displays orbital element data for a selected satellite. func OrbitalElement() { - options, _ := ioutil.ReadFile("txt/orbital_element.txt") - opt,_:=gradient.NewGradient("#1179ef", "cyan") + options, _ := os.ReadFile("txt/orbital_element.txt") + opt, _ := gradient.NewGradient("#1179ef", "cyan") opt.Print("\n" + string(options)) var selection int = Option(0, 3) - if (selection == 1) { + if selection == 1 { result := SelectSatellite() - if (result == "") { + if result == "" { return } PrintNORADInfo(extractNorad(result), result) - } else if (selection == 2) { + } else if selection == 2 { fmt.Print("\n ENTER NORAD ID > ") var norad string fmt.Scanln(&norad) PrintNORADInfo(norad, "UNSPECIFIED") - } - - return -} \ No newline at end of file + } +} diff --git a/osint/orbitalprediction.go b/osint/orbitalprediction.go index 10c2172..4c4c1e1 100644 --- a/osint/orbitalprediction.go +++ b/osint/orbitalprediction.go @@ -1,31 +1,33 @@ package osint import ( + "encoding/json" "fmt" - "github.com/iskaa02/qalam/gradient" - "io/ioutil" - "strconv" + "net/http" "os" + "strconv" + "strings" + "github.com/TwiN/go-color" - "net/http" - "encoding/json" + "github.com/iskaa02/qalam/gradient" + "github.com/manifoldco/promptui" ) +// OrbitalPrediction provides an interactive menu for visual and radio pass predictions. func OrbitalPrediction() { - options, _ := ioutil.ReadFile("txt/orbital_prediction.txt") - opt,_:=gradient.NewGradient("#1179ef", "cyan") + options, _ := os.ReadFile("txt/orbital_prediction.txt") + opt, _ := gradient.NewGradient("#1179ef", "cyan") opt.Print("\n" + string(options)) var selection int = Option(0, 3) - - if (selection == 1) { + + if selection == 1 { GetVisualPrediction() - } else if (selection == 2) { + } else if selection == 2 { GetRadioPrediction() - } - - return + } } +// GetVisualPrediction fetches and displays visual pass predictions for a satellite. func GetVisualPrediction() { selection := SatelliteSelection() if selection.norad == "" { @@ -35,67 +37,107 @@ func GetVisualPrediction() { 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 + } 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 + } fmt.Print("\n ENTER ALTITUDE > ") var altitude string fmt.Scanln(&altitude) + if strings.TrimSpace(altitude) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Altitude cannot be empty")) + return + } fmt.Print("\n ENTER DAYS OF PREDICTION > ") var days string fmt.Scanln(&days) + if strings.TrimSpace(days) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Days cannot be empty")) + return + } fmt.Print("\n ENTER MIN VISIBILITY > ") var vis string fmt.Scanln(&vis) + if strings.TrimSpace(vis) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Minimum visibility cannot be empty")) + return + } _, err := strconv.ParseFloat(latitude, 64) _, err2 := strconv.ParseFloat(longitude, 64) _, err3 := strconv.ParseFloat(altitude, 64) _, err4 := strconv.Atoi(days) - _, err5:= strconv.Atoi(vis) + _, err5 := strconv.Atoi(vis) - if err != nil || err2 != nil || err3 != nil || err4 != nil || err5 != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: INVALID INPUT")) + if err != nil || err2 != nil || err3 != nil || err4 != nil || err5 != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: INVALID INPUT - Please enter valid numbers")) return } url := "https://api.n2yo.com/rest/v1/satellite/visualpasses/" + selection.norad + "/" + latitude + "/" + longitude + "/" + altitude + "/" + days + "/" + vis + "/&apiKey=" + os.Getenv("N2YO_API_KEY") - resp, err := http.Get(url) - if err != nil { - fmt.Println(err) - } - defer resp.Body.Close() - - var data VisualPassesResponse - err = json.NewDecoder(resp.Body).Decode(&data) - if err != nil { - fmt.Println(err) - } + resp, err := http.Get(url) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to fetch visual pass data: "+err.Error())) + return + } + defer resp.Body.Close() + + var data VisualPassesResponse + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to parse response: "+err.Error())) + return + } fmt.Println(color.Ize(color.Purple, "\n╔═════════════════════════════════════════════════════════════╗")) fmt.Println(color.Ize(color.Purple, "║ Satellite Information ║")) fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Name", data.Info.SatName))) - fmt.Println(color.Ize(color.Purple, GenRowString("Satellite ID", fmt.Sprintf("%d", data.Info.SatID)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Satellite ID", fmt.Sprintf("%d", data.Info.SatID)))) fmt.Println(color.Ize(color.Purple, GenRowString("Transactions Count", fmt.Sprintf("%d", data.Info.TransactionsCount)))) fmt.Println(color.Ize(color.Purple, GenRowString("Passes Count", fmt.Sprintf("%d", data.Info.PassesCount)))) - if (len(data.Passes) > 0) { + if len(data.Passes) > 0 { fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) fmt.Println(color.Ize(color.Purple, "║ Satellite Passes ║")) fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) - + for in, pos := range data.Passes { - PrintVisualPass(pos, in == len(data.Passes) - 1) + PrintVisualPass(pos, in == len(data.Passes)-1) } } else { fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝\n\n")) } - return + // Offer export option + exportPrompt := promptui.Prompt{ + Label: "Export visual pass predictions? (y/n)", + Default: "n", + AllowEdit: true, + } + exportAnswer, _ := exportPrompt.Run() + if strings.ToLower(strings.TrimSpace(exportAnswer)) == "y" { + defaultFilename := fmt.Sprintf("visual_passes_%s_%d", strings.ReplaceAll(data.Info.SatName, " ", "_"), data.Info.SatID) + format, filePath, err := showExportMenu(defaultFilename) + if err == nil { + if err := ExportVisualPrediction(data, 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))) + } + } + } } +// GetRadioPrediction fetches and displays radio pass predictions for a satellite. func GetRadioPrediction() { selection := SatelliteSelection() if selection.norad == "" { @@ -105,82 +147,122 @@ func GetRadioPrediction() { 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 + } 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 + } fmt.Print("\n ENTER ALTITUDE > ") var altitude string fmt.Scanln(&altitude) + if strings.TrimSpace(altitude) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Altitude cannot be empty")) + return + } fmt.Print("\n ENTER DAYS OF PREDICTION > ") var days string fmt.Scanln(&days) + if strings.TrimSpace(days) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Days cannot be empty")) + return + } fmt.Print("\n ENTER MIN ELEVATION > ") var elevation string fmt.Scanln(&elevation) + if strings.TrimSpace(elevation) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Minimum elevation cannot be empty")) + return + } _, err := strconv.ParseFloat(latitude, 64) _, err2 := strconv.ParseFloat(longitude, 64) _, err3 := strconv.ParseFloat(altitude, 64) _, err4 := strconv.Atoi(days) - _, err5:= strconv.Atoi(elevation) + _, err5 := strconv.Atoi(elevation) - if err != nil || err2 != nil || err3 != nil || err4 != nil || err5 != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: INVALID INPUT")) + if err != nil || err2 != nil || err3 != nil || err4 != nil || err5 != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: INVALID INPUT - Please enter valid numbers")) return } url := "https://api.n2yo.com/rest/v1/satellite/radiopasses/" + selection.norad + "/" + latitude + "/" + longitude + "/" + altitude + "/" + days + "/" + elevation + "/&apiKey=" + os.Getenv("N2YO_API_KEY") - resp, err := http.Get(url) - if err != nil { - fmt.Println(err) - } - defer resp.Body.Close() - - var data RadioPassResponse - err = json.NewDecoder(resp.Body).Decode(&data) - if err != nil { - fmt.Println(err) - } + resp, err := http.Get(url) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to fetch radio pass data: "+err.Error())) + return + } + defer resp.Body.Close() + + var data RadioPassResponse + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to parse response: "+err.Error())) + return + } fmt.Println(color.Ize(color.Purple, "\n╔═════════════════════════════════════════════════════════════╗")) fmt.Println(color.Ize(color.Purple, "║ Satellite Information ║")) fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Name", data.Info.SatName))) - fmt.Println(color.Ize(color.Purple, GenRowString("Satellite ID", fmt.Sprintf("%d", data.Info.SatID)))) + fmt.Println(color.Ize(color.Purple, GenRowString("Satellite ID", fmt.Sprintf("%d", data.Info.SatID)))) fmt.Println(color.Ize(color.Purple, GenRowString("Transactions Count", fmt.Sprintf("%d", data.Info.TransactionsCount)))) fmt.Println(color.Ize(color.Purple, GenRowString("Passes Count", fmt.Sprintf("%d", data.Info.PassesCount)))) - if (len(data.Passes) > 0) { + if len(data.Passes) > 0 { fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) fmt.Println(color.Ize(color.Purple, "║ Satellite Passes ║")) fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) - + for in, pos := range data.Passes { - PrintRadioPass(pos, in == len(data.Passes) - 1) + PrintRadioPass(pos, in == len(data.Passes)-1) } } else { fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝\n\n")) } - return + // Offer export option + exportPrompt := promptui.Prompt{ + Label: "Export radio pass predictions? (y/n)", + Default: "n", + AllowEdit: true, + } + exportAnswer, _ := exportPrompt.Run() + if strings.ToLower(strings.TrimSpace(exportAnswer)) == "y" { + defaultFilename := fmt.Sprintf("radio_passes_%s_%d", strings.ReplaceAll(data.Info.SatName, " ", "_"), data.Info.SatID) + format, filePath, err := showExportMenu(defaultFilename) + if err == nil { + if err := ExportRadioPrediction(data, 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))) + } + } + } } +// SatelliteSelection provides an interactive menu for selecting a satellite by catalog or NORAD ID. func SatelliteSelection() SatelliteSelectionType { - options, _ := ioutil.ReadFile("txt/orbital_element.txt") - opt,_:=gradient.NewGradient("#1179ef", "cyan") + options, _ := os.ReadFile("txt/orbital_element.txt") + opt, _ := gradient.NewGradient("#1179ef", "cyan") opt.Print("\n" + string(options)) var selection int = Option(0, 3) - if (selection == 1) { + if selection == 1 { result := SelectSatellite() - if (result == "") { + if result == "" { return SatelliteSelectionType{} } return SatelliteSelectionType{norad: extractNorad(result), name: result} - } else if (selection == 2) { + } else if selection == 2 { fmt.Print("\n ENTER NORAD ID > ") var norad string fmt.Scanln(&norad) @@ -192,58 +274,58 @@ func SatelliteSelection() SatelliteSelectionType { type SatelliteSelectionType struct { norad string - name string + name string } type VisualPassesResponse struct { - Info Info `json:"info"` - Passes []Pass `json:"passes"` + Info Info `json:"info"` + Passes []Pass `json:"passes"` } type Info struct { - SatID int `json:"satid"` - SatName string `json:"satname"` - TransactionsCount int `json:"transactionscount"` - PassesCount int `json:"passescount"` + SatID int `json:"satid"` + SatName string `json:"satname"` + TransactionsCount int `json:"transactionscount"` + PassesCount int `json:"passescount"` } type Pass struct { - StartAz float64 `json:"startAz"` - StartAzCompass string `json:"startAzCompass"` - StartEl float64 `json:"startEl"` - StartUTC int `json:"startUTC"` - MaxAz float64 `json:"maxAz"` - MaxAzCompass string `json:"maxAzCompass"` - MaxEl float64 `json:"maxEl"` - MaxUTC int `json:"maxUTC"` - EndAz float64 `json:"endAz"` - EndAzCompass string `json:"endAzCompass"` - EndEl float64 `json:"endEl"` - EndUTC int `json:"endUTC"` - Mag float64 `json:"mag"` - Duration int `json:"duration"` + StartAz float64 `json:"startAz"` + StartAzCompass string `json:"startAzCompass"` + StartEl float64 `json:"startEl"` + StartUTC int `json:"startUTC"` + MaxAz float64 `json:"maxAz"` + MaxAzCompass string `json:"maxAzCompass"` + MaxEl float64 `json:"maxEl"` + MaxUTC int `json:"maxUTC"` + EndAz float64 `json:"endAz"` + EndAzCompass string `json:"endAzCompass"` + EndEl float64 `json:"endEl"` + EndUTC int `json:"endUTC"` + Mag float64 `json:"mag"` + Duration int `json:"duration"` } type RadioPass struct { - StartAz float64 `json:"startAz"` - StartAzCompass string `json:"startAzCompass"` - StartUTC int64 `json:"startUTC"` - MaxAz float64 `json:"maxAz"` - MaxAzCompass string `json:"maxAzCompass"` - MaxEl float64 `json:"maxEl"` - MaxUTC int64 `json:"maxUTC"` - EndAz float64 `json:"endAz"` - EndAzCompass string `json:"endAzCompass"` - EndUTC int64 `json:"endUTC"` + StartAz float64 `json:"startAz"` + StartAzCompass string `json:"startAzCompass"` + StartUTC int64 `json:"startUTC"` + MaxAz float64 `json:"maxAz"` + MaxAzCompass string `json:"maxAzCompass"` + MaxEl float64 `json:"maxEl"` + MaxUTC int64 `json:"maxUTC"` + EndAz float64 `json:"endAz"` + EndAzCompass string `json:"endAzCompass"` + EndUTC int64 `json:"endUTC"` } type RadioPassResponse struct { - Info Info `json:"info"` - Passes []RadioPass `json:"passes"` + Info Info `json:"info"` + Passes []RadioPass `json:"passes"` } - -func PrintVisualPass (pass Pass, last bool) { +// PrintVisualPass displays visual pass information in a formatted table. +func PrintVisualPass(pass Pass, last bool) { fmt.Println(color.Ize(color.Purple, GenRowString("Start Azimuth", fmt.Sprintf("%f", pass.StartAz)))) fmt.Println(color.Ize(color.Purple, GenRowString("Start Azimuth Compass", pass.StartAzCompass))) fmt.Println(color.Ize(color.Purple, GenRowString("Start Elevation", fmt.Sprintf("%f", pass.StartEl)))) @@ -258,15 +340,15 @@ func PrintVisualPass (pass Pass, last bool) { fmt.Println(color.Ize(color.Purple, GenRowString("End UTC", fmt.Sprintf("%d", pass.EndUTC)))) fmt.Println(color.Ize(color.Purple, GenRowString("Max Visual Magnitude", fmt.Sprintf("%f", pass.Mag)))) fmt.Println(color.Ize(color.Purple, GenRowString("Visible Duration", fmt.Sprintf("%d", pass.Duration)))) - if (last) { + if last { fmt.Println(color.Ize(color.Purple, "╚═════════════════════════════════════════════════════════════╝\n\n")) } else { fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) } } - -func PrintRadioPass (pass RadioPass, last bool) { +// PrintRadioPass displays radio pass information in a formatted table. +func PrintRadioPass(pass RadioPass, last bool) { fmt.Println(color.Ize(color.Purple, GenRowString("Start Azimuth", fmt.Sprintf("%f", pass.StartAz)))) fmt.Println(color.Ize(color.Purple, GenRowString("Start Azimuth Compass", pass.StartAzCompass))) fmt.Println(color.Ize(color.Purple, GenRowString("Start UTC", fmt.Sprintf("%d", pass.StartUTC)))) @@ -277,9 +359,9 @@ func PrintRadioPass (pass RadioPass, last bool) { fmt.Println(color.Ize(color.Purple, GenRowString("End Azimuth", fmt.Sprintf("%f", pass.EndAz)))) fmt.Println(color.Ize(color.Purple, GenRowString("End Azimuth Compass", pass.EndAzCompass))) fmt.Println(color.Ize(color.Purple, GenRowString("End UTC", fmt.Sprintf("%d", pass.EndUTC)))) - 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/osint.go b/osint/osint.go index 1beefd6..618317f 100644 --- a/osint/osint.go +++ b/osint/osint.go @@ -1,132 +1,612 @@ package osint import ( + "encoding/json" "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" "os" - "github.com/TwiN/go-color" - "io/ioutil" "strconv" "strings" - "encoding/json" - "net/http" + + "github.com/TwiN/go-color" "github.com/manifoldco/promptui" - "net/url" ) -const authURL = "https://www.space-track.org/ajaxauth/login" - -func extractNorad(str string) string { - start := strings.Index(str, "(") - end := strings.Index(str, ")") - if start == -1 || end == -1 || start >= end { - return "" - } - return str[start+1:end] -} +const ( + authURL = "https://www.space-track.org/ajaxauth/login" + queryBaseURL = "https://www.space-track.org/basicspacedata/query" +) -func PrintNORADInfo(norad string, name string) { +// Login authenticates with Space-Track API using credentials from environment variables. +// Returns an HTTP client with a cookie jar to maintain the session. +func Login() (*http.Client, error) { vals := url.Values{} vals.Add("identity", os.Getenv("SPACE_TRACK_USERNAME")) vals.Add("password", os.Getenv("SPACE_TRACK_PASSWORD")) - vals.Add("query", "https://www.space-track.org/basicspacedata/query/class/gp_history/format/tle/NORAD_CAT_ID/" + norad + "/orderby/EPOCH%20desc/limit/1") - client := &http.Client{} + 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(authURL, vals) if err != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: API REQUEST TO SPACE TRACK")) + return nil, fmt.Errorf("unable to authenticate with Space-Track: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("authentication failed with status code: %d", resp.StatusCode) + } + + fmt.Println(color.Ize(color.Green, " [+] Logged in successfully")) + return client, nil +} + +// QuerySpaceTrack sends a GET request to the Space-Track API using the authenticated client. +// Returns the response body as a string. +func QuerySpaceTrack(client *http.Client, endpoint string) (string, error) { + req, err := http.NewRequest("GET", queryBaseURL+endpoint, nil) + if err != nil { + return "", fmt.Errorf("failed to create query request: %w", err) } + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to fetch data from Space-Track: %w", err) + } defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - fmt.Println(color.Ize(color.Red, " [!] ERROR: API REQUEST TO SPACE TRACK")) + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("query returned non-success status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + return string(body), nil +} + +// extractNorad extracts the NORAD ID from a string in the format "Name (NORAD_ID)". +func extractNorad(str string) string { + start := strings.Index(str, "(") + end := strings.Index(str, ")") + if start == -1 || end == -1 || start >= end { + return "" + } + return str[start+1 : end] +} + +// PrintNORADInfo fetches and displays TLE data for a satellite identified by its NORAD ID. +func PrintNORADInfo(norad string, name string) { + client, err := Login() + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: "+err.Error())) + return } - respData, err := ioutil.ReadAll(resp.Body) + endpoint := fmt.Sprintf("/class/gp_history/format/tle/NORAD_CAT_ID/%s/orderby/EPOCH%%20desc/limit/1", norad) + data, err := QuerySpaceTrack(client, endpoint) if err != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: API REQUEST TO SPACE TRACK")) + fmt.Println(color.Ize(color.Red, " [!] ERROR: "+err.Error())) + 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 { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Invalid TLE data - insufficient fields")) + return + } + + // Calculate split point, ensuring we can create two meaningful lines + mid := 9 + if len(tleLines) < 9 { + mid = len(tleLines) / 2 + } + if mid > len(tleLines) { + mid = len(tleLines) / 2 + } + // Ensure mid is at least 1 to prevent empty lineOne + if mid < 1 { + mid = 1 + } + // Ensure mid is less than len(tleLines) to ensure lineTwo is non-empty + if mid >= len(tleLines) { + mid = len(tleLines) - 1 + } + + lineOne = strings.Join(tleLines[:mid], " ") + lineTwo = strings.Join(tleLines[mid:], " ") + } + + if !strings.HasPrefix(lineOne, "1 ") { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Invalid TLE format - line 1 should start with '1 '")) + return + } + if !strings.HasPrefix(lineTwo, "2 ") { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Invalid TLE format - line 2 should start with '2 '")) + return } - tleLines := strings.Fields(string(respData)) - mid := (len(tleLines)/2) + 1 - lineOne := strings.Join(tleLines[:mid], " ") - lineTwo := strings.Join(tleLines[mid:], " ") tle := ConstructTLE(name, lineOne, lineTwo) + + parsingFailed := false + + line1Fields := strings.Fields(lineOne) + line2Fields := strings.Fields(lineTwo) + + if len(line1Fields) < 4 || len(line2Fields) < 3 { + parsingFailed = true + } else if tle.SatelliteCatalogNumber == 0 && tle.InternationalDesignator == "" && tle.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(tle) } +// buildSatcatQuery constructs a Space-Track API query string with optional filters and pagination. +// Note: Space-Track API uses path segments for filtering. For name search, we'll filter client-side. +func buildSatcatQuery(searchName, country, objectType, launchYear string, page, pageSize int) string { + var parts []string + parts = append(parts, "/class/satcat") + + // Add filters (name search is handled client-side for partial matching) + if country != "" { + parts = append(parts, fmt.Sprintf("/COUNTRY/%s", url.QueryEscape(country))) + } + if objectType != "" { + parts = append(parts, fmt.Sprintf("/OBJECT_TYPE/%s", url.QueryEscape(objectType))) + } + if launchYear != "" { + parts = append(parts, fmt.Sprintf("/LAUNCH_YEAR/%s", url.QueryEscape(launchYear))) + } + + // Add ordering + parts = append(parts, "/orderby/SATNAME%20asc") + + // For name search, fetch more results to filter client-side + // Otherwise use normal pagination + if searchName != "" { + // Fetch more results for client-side filtering + parts = append(parts, "/limit/500") + } else if pageSize > 0 { + offset := (page - 1) * pageSize + parts = append(parts, fmt.Sprintf("/limit/%d,%d", pageSize, offset)) + } else { + parts = append(parts, "/limit/50") + } + + parts = append(parts, "/emptyresult/show") + return strings.Join(parts, "") +} + +// filterSatellitesByName filters satellites by name (case-insensitive partial match). +func filterSatellitesByName(sats []Satellite, searchName string) []Satellite { + if searchName == "" { + return sats + } + searchLower := strings.ToLower(searchName) + var filtered []Satellite + for _, sat := range sats { + if strings.Contains(strings.ToLower(sat.SATNAME), searchLower) { + filtered = append(filtered, sat) + } + } + return filtered +} + +// showSearchMenu displays an interactive menu for searching satellites. +func showSearchMenu() (string, string, string, string) { + searchName := "" + country := "" + objectType := "" + launchYear := "" + + for { + menuItems := []string{ + "Search by Name", + "Filter by Country", + "Filter by Object Type", + "Filter by Launch Year", + "Clear All Filters", + "Search & Continue", + } + + prompt := promptui.Select{ + Label: "Satellite Search & Filter Options", + Items: menuItems, + Size: 10, + } + + idx, _, err := prompt.Run() + if err != nil { + return "", "", "", "" + } + + switch idx { + case 0: // Search by Name + namePrompt := promptui.Prompt{ + Label: "Enter satellite name (or part of name)", + Default: searchName, + AllowEdit: true, + } + result, err := namePrompt.Run() + if err == nil { + searchName = strings.TrimSpace(result) + } + + case 1: // Filter by Country + countryPrompt := promptui.Prompt{ + Label: "Enter country code (e.g., US, RU, CN)", + Default: country, + AllowEdit: true, + } + result, err := countryPrompt.Run() + if err == nil { + country = strings.TrimSpace(result) + } + + case 2: // Filter by Object Type + typeItems := []string{ + "PAYLOAD", + "ROCKET BODY", + "DEBRIS", + "UNKNOWN", + "TBA", + "", + } + typePrompt := promptui.Select{ + Label: "Select Object Type", + Items: typeItems, + } + _, result, err := typePrompt.Run() + if err == nil { + objectType = result + } + + case 3: // Filter by Launch Year + yearPrompt := promptui.Prompt{ + Label: "Enter launch year (e.g., 2020)", + Default: launchYear, + AllowEdit: true, + } + result, err := yearPrompt.Run() + if err == nil { + launchYear = strings.TrimSpace(result) + } + + case 4: // Clear All Filters + searchName = "" + country = "" + objectType = "" + launchYear = "" + fmt.Println(color.Ize(color.Green, " [+] All filters cleared")) + + case 5: // Search & Continue + return searchName, country, objectType, launchYear + } + + // Show current filters + if searchName != "" || country != "" || objectType != "" || launchYear != "" { + fmt.Println(color.Ize(color.Cyan, "\n Current Filters:")) + if searchName != "" { + fmt.Printf(" Name: %s\n", searchName) + } + if country != "" { + fmt.Printf(" Country: %s\n", country) + } + if objectType != "" { + fmt.Printf(" Object Type: %s\n", objectType) + } + if launchYear != "" { + fmt.Printf(" Launch Year: %s\n", launchYear) + } + fmt.Println() + } + } +} + +// SelectSatellite fetches a list of satellites from Space-Track with search, filter, and pagination support. +// Returns the selected satellite name with its NORAD ID in parentheses. func SelectSatellite() string { - vals := url.Values{} - vals.Add("identity", os.Getenv("SPACE_TRACK_USERNAME")) - vals.Add("password", os.Getenv("SPACE_TRACK_PASSWORD")) - vals.Add("query", "https://www.space-track.org/basicspacedata/query/class/satcat/orderby/SATNAME asc/limit/10/emptyresult/show") + // First, show option to select from favorites or search + initialMenu := []string{ + "⭐ Select from Favorites", + "🔍 Search Satellites", + "❌ Cancel", + } - client := &http.Client{} + initialPrompt := promptui.Select{ + Label: "Satellite Selection", + Items: initialMenu, + } - resp, err := client.PostForm(authURL, vals) + initialIdx, _, err := initialPrompt.Run() if err != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: API REQUEST TO SPACE TRACK")) return "" } - defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - fmt.Println(color.Ize(color.Red, " [!] ERROR: API REQUEST TO SPACE TRACK")) + if initialIdx == 0 { + // Select from favorites + result := SelectFromFavorites() + if result != "" { + // Offer to remove from favorites or continue + return result + } + return "" + } else if initialIdx == 2 { + // Cancel return "" } - respData, err := ioutil.ReadAll(resp.Body) + // Continue with search + client, err := Login() if err != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: API REQUEST TO SPACE TRACK")) + fmt.Println(color.Ize(color.Red, " [!] ERROR: "+err.Error())) return "" } - var sats []Satellite - if err := json.Unmarshal(respData, &sats); err != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: API REQUEST TO SPACE TRACK")) - return "" - } + // Show search/filter menu + searchName, country, objectType, launchYear := showSearchMenu() - var satStrings []string - for _, sat := range sats { - satStrings = append(satStrings, sat.SATNAME + " (" + sat.NORAD_CAT_ID + ")") - } - prompt := promptui.Select{ - Label: "Select a Satellite 🛰", - Items: satStrings, - } - _, result, err := prompt.Run() - if err != nil { - fmt.Println(color.Ize(color.Red, " [!] PROMPT FAILED")) + page := 1 + pageSize := 20 + var allFilteredSats []Satellite + var totalPages int + + for { + var sats []Satellite + + // If we have name search, we need to fetch all and filter client-side + // Cache the filtered results to avoid refetching + if searchName != "" && len(allFilteredSats) == 0 { + // Fetch a larger batch for client-side filtering + endpoint := buildSatcatQuery(searchName, country, objectType, launchYear, 1, 0) + data, err := QuerySpaceTrack(client, endpoint) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: "+err.Error())) + return "" + } + + var fetchedSats []Satellite + if err := json.Unmarshal([]byte(data), &fetchedSats); err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to parse satellite data")) + fmt.Printf("Error details: %v\n", err) + return "" + } + + // Apply client-side name filtering + allFilteredSats = filterSatellitesByName(fetchedSats, searchName) + totalPages = (len(allFilteredSats) + pageSize - 1) / pageSize + + // Apply pagination + startIdx := (page - 1) * pageSize + endIdx := startIdx + pageSize + if endIdx > len(allFilteredSats) { + endIdx = len(allFilteredSats) + } + if startIdx < len(allFilteredSats) { + sats = allFilteredSats[startIdx:endIdx] + } else { + sats = []Satellite{} + } + } else if searchName != "" { + // Use cached filtered results with pagination + startIdx := (page - 1) * pageSize + endIdx := startIdx + pageSize + if endIdx > len(allFilteredSats) { + endIdx = len(allFilteredSats) + } + if startIdx < len(allFilteredSats) { + sats = allFilteredSats[startIdx:endIdx] + } else { + sats = []Satellite{} + } + } else { + // No name search - use server-side pagination + endpoint := buildSatcatQuery(searchName, country, objectType, launchYear, page, pageSize) + data, err := QuerySpaceTrack(client, endpoint) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: "+err.Error())) + return "" + } + + if err := json.Unmarshal([]byte(data), &sats); err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to parse satellite data")) + fmt.Printf("Error details: %v\n", err) + return "" + } + totalPages = 0 // Unknown for server-side pagination + } + + if len(sats) == 0 { + fmt.Println(color.Ize(color.Yellow, " [!] No satellites found with current filters")) + fmt.Println(color.Ize(color.Cyan, " [*] Try adjusting your search criteria")) + return "" + } + + // Build display strings with additional info + var satStrings []string + for _, sat := range sats { + info := fmt.Sprintf("%s (%s)", sat.SATNAME, sat.NORAD_CAT_ID) + if sat.COUNTRY != "" { + info += fmt.Sprintf(" - %s", sat.COUNTRY) + } + if sat.OBJECT_TYPE != "" { + info += fmt.Sprintf(" [%s]", sat.OBJECT_TYPE) + } + satStrings = append(satStrings, info) + } + + // Add navigation options + var menuItems []string + hasNextPage := false + if searchName != "" { + hasNextPage = page < totalPages + } else { + hasNextPage = len(sats) == pageSize + } + + if page > 1 { + menuItems = append(menuItems, "◄ Previous Page") + } + menuItems = append(menuItems, satStrings...) + if hasNextPage { + menuItems = append(menuItems, "Next Page ►") + } + menuItems = append(menuItems, "⭐ View Favorites", "🔍 New Search", "❌ Cancel") + + pageInfo := fmt.Sprintf("Page %d", page) + if searchName != "" && totalPages > 0 { + pageInfo += fmt.Sprintf(" of %d", totalPages) + } + if len(sats) == pageSize && hasNextPage { + pageInfo += " (showing 20 results)" + } else { + pageInfo += fmt.Sprintf(" (%d results)", len(sats)) + } + + prompt := promptui.Select{ + Label: fmt.Sprintf("Select a Satellite 🛰 - %s", pageInfo), + Items: menuItems, + Size: 15, + } + + idx, _, err := prompt.Run() + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] PROMPT FAILED")) + return "" + } + + // Handle navigation + if page > 1 && idx == 0 { + // Previous Page + page-- + continue + } + + startIdx := 0 + if page > 1 { + startIdx = 1 + } + + if idx >= startIdx && idx < startIdx+len(satStrings) { + // Selected a satellite - extract just the name and NORAD ID for compatibility + selectedIdx := idx - startIdx + selectedSat := sats[selectedIdx] + result := fmt.Sprintf("%s (%s)", selectedSat.SATNAME, selectedSat.NORAD_CAT_ID) + + // Check if already in favorites and offer to save/remove + isFav, _ := IsFavorite(selectedSat.NORAD_CAT_ID) + if !isFav { + savePrompt := promptui.Prompt{ + Label: fmt.Sprintf("Save %s to favorites? (y/n)", selectedSat.SATNAME), + Default: "n", + AllowEdit: true, + } + saveAnswer, _ := savePrompt.Run() + if strings.ToLower(strings.TrimSpace(saveAnswer)) == "y" { + if err := AddFavorite(selectedSat.SATNAME, selectedSat.NORAD_CAT_ID, selectedSat.COUNTRY, selectedSat.OBJECT_TYPE); err != nil { + fmt.Println(color.Ize(color.Yellow, " [!] "+err.Error())) + } else { + fmt.Println(color.Ize(color.Green, fmt.Sprintf(" [+] Saved %s to favorites", selectedSat.SATNAME))) + } + } + } + + return result + } + + nextPageIdx := startIdx + len(satStrings) + if idx == nextPageIdx && hasNextPage { + // Next Page + page++ + continue + } + + favoritesIdx := nextPageIdx + if hasNextPage { + favoritesIdx++ + } + newSearchIdx := favoritesIdx + 1 + + if idx == favoritesIdx { + // View Favorites + favResult := SelectFromFavorites() + if favResult != "" { + return favResult + } + // Continue showing current page + continue + } + + if idx == newSearchIdx || (idx == favoritesIdx && !hasNextPage) { + // New Search - reset cache + allFilteredSats = []Satellite{} + searchName, country, objectType, launchYear = showSearchMenu() + page = 1 + totalPages = 0 + continue + } + + // Cancel return "" } - return result } -func GenRowString(intro string, input string) string{ +// GenRowString formats a key-value pair into a table row with proper spacing. +func GenRowString(intro string, input string) string { var totalCount int = 4 + len(intro) + len(input) + 2 var useCount = 63 - totalCount return "║ " + intro + ": " + input + strings.Repeat(" ", useCount) + " ║" } +// Option prompts the user for a numeric input within a specified range. +// Returns the selected number, or exits the program if the minimum value is chosen. func Option(min int, max int) int { 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")) return Option(min, max) - } else { - if (num == min) { + } else { + if num == min { fmt.Println(color.Ize(color.Blue, " Escaping Orbit...")) os.Exit(1) return 0 - } else if (num > min && num < max + 1) { + } else if num > min && num < max+1 { return num } else { fmt.Println(color.Ize(color.Red, " [!] INVALID INPUT")) return Option(min, max) } - } -} \ No newline at end of file + } +} diff --git a/osint/osint_test.go b/osint/osint_test.go new file mode 100644 index 0000000..357f87f --- /dev/null +++ b/osint/osint_test.go @@ -0,0 +1,334 @@ +package osint + +import ( + "strings" + "testing" +) + +func TestExtractNorad(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Valid format with parentheses", + input: "ISS (ZARYA) (25544)", + expected: "ZARYA", + }, + { + name: "Simple format", + input: "Satellite Name (12345)", + expected: "12345", + }, + { + name: "Multiple parentheses - takes first", + input: "Name (NORAD_ID) (extra)", + expected: "NORAD_ID", + }, + { + name: "No parentheses", + input: "Satellite Name", + expected: "", + }, + { + name: "Empty string", + input: "", + expected: "", + }, + { + name: "Only opening parenthesis", + input: "Name (NORAD", + expected: "", + }, + { + name: "Only closing parenthesis", + input: "Name NORAD)", + expected: "", + }, + { + name: "Reversed parentheses", + input: "Name )NORAD(", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractNorad(tt.input) + if result != tt.expected { + t.Errorf("extractNorad(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestGenRowString(t *testing.T) { + tests := []struct { + name string + intro string + input string + checkPrefix bool + checkSuffix bool + checkLength bool + }{ + { + name: "Normal case", + intro: "Name", + input: "Test", + checkPrefix: true, + checkSuffix: true, + checkLength: true, + }, + { + name: "Empty input", + intro: "Field", + input: "", + checkPrefix: true, + checkSuffix: true, + checkLength: true, + }, + { + name: "Short intro and input", + intro: "ID", + input: "123", + checkPrefix: true, + checkSuffix: true, + checkLength: true, + }, + { + name: "Long intro", + intro: "Very Long Field Name That Takes Up Space", + input: "Value", + checkPrefix: true, + checkSuffix: true, + checkLength: false, // May exceed 67 chars if too long + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GenRowString(tt.intro, tt.input) + // Check that result has correct structure + if tt.checkPrefix && !strings.HasPrefix(result, "║ ") { + t.Errorf("GenRowString() should start with '║ '") + } + if tt.checkSuffix && !strings.HasSuffix(result, " ║") { + t.Errorf("GenRowString() should end with ' ║'") + } + if !strings.Contains(result, tt.intro+": "+tt.input) { + t.Errorf("GenRowString() should contain intro and input") + } + // Check total length only if not too long + if tt.checkLength && len(result) != 67 { + t.Errorf("GenRowString() length = %d, want 67", len(result)) + } + }) + } +} + +// Benchmark tests +func BenchmarkExtractNorad(b *testing.B) { + testCases := []string{ + "ISS (ZARYA) (25544)", + "Satellite Name (12345)", + "Name (NORAD_ID)", + "Invalid Format", + } + + for i := 0; i < b.N; i++ { + for _, tc := range testCases { + extractNorad(tc) + } + } +} + +func BenchmarkGenRowString(b *testing.B) { + for i := 0; i < b.N; i++ { + GenRowString("Field Name", "Field Value") + } +} + +func TestBuildSatcatQuery(t *testing.T) { + tests := []struct { + name string + searchName string + country string + objectType string + launchYear string + page int + pageSize int + wantContain []string + }{ + { + name: "Basic query with no filters", + searchName: "", + country: "", + objectType: "", + launchYear: "", + page: 1, + pageSize: 20, + wantContain: []string{"/class/satcat", "/orderby/SATNAME%20asc", "/limit/20,0", "/emptyresult/show"}, + }, + { + name: "Query with country filter", + searchName: "", + country: "US", + objectType: "", + launchYear: "", + page: 1, + pageSize: 20, + wantContain: []string{"/class/satcat", "/COUNTRY/US", "/orderby/SATNAME%20asc"}, + }, + { + name: "Query with object type filter", + searchName: "", + country: "", + objectType: "PAYLOAD", + launchYear: "", + page: 1, + pageSize: 20, + wantContain: []string{"/class/satcat", "/OBJECT_TYPE/PAYLOAD", "/orderby/SATNAME%20asc"}, + }, + { + name: "Query with launch year filter", + searchName: "", + country: "", + objectType: "", + launchYear: "2020", + page: 1, + pageSize: 20, + wantContain: []string{"/class/satcat", "/LAUNCH_YEAR/2020", "/orderby/SATNAME%20asc"}, + }, + { + name: "Query with name search (should fetch more)", + searchName: "ISS", + country: "", + objectType: "", + launchYear: "", + page: 1, + pageSize: 20, + wantContain: []string{"/class/satcat", "/limit/500"}, + }, + { + name: "Query with pagination", + searchName: "", + country: "", + objectType: "", + launchYear: "", + page: 3, + pageSize: 20, + wantContain: []string{"/limit/20,40"}, + }, + { + name: "Query with multiple filters", + searchName: "", + country: "US", + objectType: "PAYLOAD", + launchYear: "2020", + page: 1, + pageSize: 20, + wantContain: []string{"/COUNTRY/US", "/OBJECT_TYPE/PAYLOAD", "/LAUNCH_YEAR/2020"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := buildSatcatQuery(tt.searchName, tt.country, tt.objectType, tt.launchYear, tt.page, tt.pageSize) + for _, want := range tt.wantContain { + if !strings.Contains(result, want) { + t.Errorf("buildSatcatQuery() = %q, should contain %q", result, want) + } + } + }) + } +} + +func TestFilterSatellitesByName(t *testing.T) { + sats := []Satellite{ + {SATNAME: "ISS (ZARYA)", NORAD_CAT_ID: "25544"}, + {SATNAME: "STARLINK-1007", NORAD_CAT_ID: "44700"}, + {SATNAME: "NOAA 15", NORAD_CAT_ID: "25338"}, + {SATNAME: "STARLINK-1008", NORAD_CAT_ID: "44701"}, + {SATNAME: "Hubble Space Telescope", NORAD_CAT_ID: "20580"}, + } + + tests := []struct { + name string + searchName string + wantCount int + wantNames []string + }{ + { + name: "Empty search returns all", + searchName: "", + wantCount: 5, + wantNames: []string{"ISS (ZARYA)", "STARLINK-1007", "NOAA 15", "STARLINK-1008", "Hubble Space Telescope"}, + }, + { + name: "Search for STARLINK", + searchName: "STARLINK", + wantCount: 2, + wantNames: []string{"STARLINK-1007", "STARLINK-1008"}, + }, + { + name: "Search case insensitive", + searchName: "iss", + wantCount: 1, + wantNames: []string{"ISS (ZARYA)"}, + }, + { + name: "Search partial match", + searchName: "100", + wantCount: 2, + wantNames: []string{"STARLINK-1007", "STARLINK-1008"}, + }, + { + name: "Search with no matches", + searchName: "XYZ", + wantCount: 0, + wantNames: []string{}, + }, + { + name: "Search for Hubble", + searchName: "Hubble", + wantCount: 1, + wantNames: []string{"Hubble Space Telescope"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filterSatellitesByName(sats, tt.searchName) + if len(result) != tt.wantCount { + t.Errorf("filterSatellitesByName() returned %d results, want %d", len(result), tt.wantCount) + } + for i, wantName := range tt.wantNames { + if i < len(result) && result[i].SATNAME != wantName { + t.Errorf("filterSatellitesByName() result[%d].SATNAME = %q, want %q", i, result[i].SATNAME, wantName) + } + } + }) + } +} + +func BenchmarkBuildSatcatQuery(b *testing.B) { + for i := 0; i < b.N; i++ { + buildSatcatQuery("ISS", "US", "PAYLOAD", "2020", 1, 20) + } +} + +func BenchmarkFilterSatellitesByName(b *testing.B) { + sats := []Satellite{ + {SATNAME: "ISS (ZARYA)", NORAD_CAT_ID: "25544"}, + {SATNAME: "STARLINK-1007", NORAD_CAT_ID: "44700"}, + {SATNAME: "NOAA 15", NORAD_CAT_ID: "25338"}, + {SATNAME: "STARLINK-1008", NORAD_CAT_ID: "44701"}, + {SATNAME: "Hubble Space Telescope", NORAD_CAT_ID: "20580"}, + } + + for i := 0; i < b.N; i++ { + filterSatellitesByName(sats, "STARLINK") + } +} + diff --git a/osint/satelliteposition.go b/osint/satelliteposition.go index 4fff648..de8192a 100644 --- a/osint/satelliteposition.go +++ b/osint/satelliteposition.go @@ -1,106 +1,140 @@ package osint import ( - "io/ioutil" - "fmt" - "github.com/iskaa02/qalam/gradient" "encoding/json" - "github.com/TwiN/go-color" - "net/http" - "strconv" + "fmt" + "net/http" "os" + "strconv" + "strings" + + "github.com/TwiN/go-color" + "github.com/iskaa02/qalam/gradient" + "github.com/manifoldco/promptui" ) +// SatellitePositionVisualization provides an interactive menu for viewing satellite positions. func SatellitePositionVisualization() { - options, _ := ioutil.ReadFile("txt/orbital_element.txt") - opt,_:=gradient.NewGradient("#1179ef", "cyan") + options, _ := os.ReadFile("txt/orbital_element.txt") + opt, _ := gradient.NewGradient("#1179ef", "cyan") opt.Print("\n" + string(options)) var selection int = Option(0, 3) - if (selection == 1) { + if selection == 1 { result := SelectSatellite() - if (result == "") { + if result == "" { return } GetLocation(extractNorad(result)) - } else if (selection == 2) { + } else if selection == 2 { fmt.Print("\n ENTER NORAD ID > ") var norad string fmt.Scanln(&norad) GetLocation(norad) } - - return } +// GetLocation fetches and displays the current position of a satellite for a given observer location. func GetLocation(norad string) { 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 + } 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 + } fmt.Print("\n ENTER ALTITUDE > ") var altitude string fmt.Scanln(&altitude) + if strings.TrimSpace(altitude) == "" { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Altitude cannot be empty")) + return + } _, err := strconv.ParseFloat(latitude, 64) _, err2 := strconv.ParseFloat(longitude, 64) _, err3 := strconv.Atoi(altitude) if err != nil || err2 != nil || err3 != nil { - fmt.Println(color.Ize(color.Red, " [!] ERROR: INVALID INPUT")) + fmt.Println(color.Ize(color.Red, " [!] ERROR: INVALID INPUT - Please enter valid numbers")) return } url := "https://api.n2yo.com/rest/v1/satellite/positions/" + norad + "/" + latitude + "/" + longitude + "/" + altitude + "/2/&apiKey=" + os.Getenv("N2YO_API_KEY") - resp, err := http.Get(url) - if err != nil { - fmt.Println(err) - } - defer resp.Body.Close() - - var data Response - err = json.NewDecoder(resp.Body).Decode(&data) - if err != nil { - fmt.Println(err) - } + resp, err := http.Get(url) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to fetch satellite position data: "+err.Error())) + return + } + defer resp.Body.Close() + + var data Response + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Failed to parse response: "+err.Error())) + return + } fmt.Println(color.Ize(color.Purple, "\n╔═════════════════════════════════════════════════════════════╗")) fmt.Println(color.Ize(color.Purple, "║ Satellite Information ║")) fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) fmt.Println(color.Ize(color.Purple, GenRowString("Satellite Name", data.SatelliteInfo.Satname))) - fmt.Println(color.Ize(color.Purple, GenRowString("Satellite ID", fmt.Sprintf("%d", data.SatelliteInfo.Satid)))) - // fmt.Println(color.Ize(color.Purple, "║ ║")) + fmt.Println(color.Ize(color.Purple, GenRowString("Satellite ID", fmt.Sprintf("%d", data.SatelliteInfo.Satid)))) fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) fmt.Println(color.Ize(color.Purple, "║ Satellite Positions ║")) fmt.Println(color.Ize(color.Purple, "╠═════════════════════════════════════════════════════════════╣")) - for in, pos := range data.Positions { - PrintSatellitePosition(pos, in == len(data.Positions) - 1) - } + for in, pos := range data.Positions { + PrintSatellitePosition(pos, in == len(data.Positions)-1) + } + // Offer export option + exportPrompt := promptui.Prompt{ + Label: "Export satellite positions? (y/n)", + Default: "n", + AllowEdit: true, + } + exportAnswer, _ := exportPrompt.Run() + if strings.ToLower(strings.TrimSpace(exportAnswer)) == "y" { + defaultFilename := fmt.Sprintf("positions_%s_%d", strings.ReplaceAll(data.SatelliteInfo.Satname, " ", "_"), data.SatelliteInfo.Satid) + format, filePath, err := showExportMenu(defaultFilename) + if err == nil { + if err := ExportSatellitePosition(data, 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))) + } + } + } } +// DisplayMap is a placeholder for future map visualization functionality. func DisplayMap() { - // TODO } -func PrintSatellitePosition (pos Position, last bool) { +// 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/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..e0b5b69 100644 --- a/osint/tleparser.go +++ b/osint/tleparser.go @@ -1,93 +1,216 @@ 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") + } + } - return + // 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") + } + + // 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 { + fmt.Println(color.Ize(color.Red, fmt.Sprintf(" [!] ERROR: %s", err.Error()))) + 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 { + fmt.Println(color.Ize(color.Red, " [!] INVALID TEXT FILE")) + return + } + + // Ensure it's a file, not a directory + if fileInfo.IsDir() { + fmt.Println(color.Ize(color.Red, " [!] ERROR: Path is a directory, not a file")) + return + } + file, err := os.Open(path) - + if err != nil { fmt.Println(color.Ize(color.Red, " [!] INVALID TEXT FILE")) + 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) { + if count < 2 || count > 3 { fmt.Println(color.Ize(color.Red, " [!] INVALID TLE FORMAT")) 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..a1053da --- /dev/null +++ b/osint/tleparser_test.go @@ -0,0 +1,220 @@ +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..6c54dc6 --- /dev/null +++ b/run_tests.go @@ -0,0 +1,222 @@ +// +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: "export", + Description: "Export functionality tests (CSV, JSON, Text export for TLE, predictions, positions)", + PackagePath: "./osint", + TestFile: "export_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") +} +