commit 47a45d72a95226b9dc65bf322923e32bdf6bb250 Author: Hadi <112569860+anotherhadi@users.noreply.github.com> Date: Fri May 9 18:49:36 2025 +0200 Init Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com> diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1cd791b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5615ec7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Hadi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b91ad6d --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# GH-Recon + +## Project Overview + +Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API. + +## Features + +- Retrieve basic user profile information (username, ID, avatar, bio, creation dates) +- List organizations and roles +- Fetch SSH and GPG keys +- Enumerate social accounts +- Extract unique commit authors (name + email) in both chronological orders + +## Disclaimer + +This tool is intended for educational purposes only. Use responsibly and ensure you have permission to access the data you are querying. + +## Prerequisites + +- Go 1.18+ +- GitHub Personal Access Token (recommended for higher rate limits) + +## Installation + +```bash +go get github.com/anotherhadi/gh-recon +``` + +## Usage + +```bash +gh-recon --username TARGET_USER [--token YOUR_TOKEN] +``` + +### Flags + +- `--username`: GitHub username to inspect (required) +- `--token`: Personal Access Token (optional but recommended) + +## Example + +```bash +gh-recon --username anotherhadi --token ghp_ABC123... +``` + +## Todo + +Feel free to contribute! Here are some ideas: + +- Fetch names in License files +- Fetch emails in README files/comments diff --git a/commits.go b/commits.go new file mode 100644 index 0000000..3635fb8 --- /dev/null +++ b/commits.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "fmt" + + "github.com/charmbracelet/log" + "github.com/google/go-github/v72/github" +) + +func commits(client *github.Client, ctx context.Context, username string) { + // Commits + fmt.Println( + GreyStyle.Render("[")+GreenStyle.Render("+")+GreyStyle.Render("]"), + GreyStyle.Render("Commits:\n"), + ) + + seenNames := make(map[string]struct{}) + seenEmails := make(map[string]struct{}) + var names, emails []string + + collect := func(order string) error { + opts := &github.SearchOptions{ + Sort: "author-date", + Order: order, // "desc" ou "asc" + ListOptions: github.ListOptions{PerPage: 100}, + } + for page := 1; page <= 10; page++ { + opts.ListOptions.Page = page + result, resp, err := client.Search.Commits( + ctx, + fmt.Sprintf("author:%s", username), + opts, + ) + if err != nil { + return fmt.Errorf("fetch page %d (%s): %w", page, order, err) + } + WaitForRateLimit(resp) + if len(result.Commits) == 0 { + break + } + for _, item := range result.Commits { + a := item.Commit.GetAuthor() + name := a.GetName() + email := a.GetEmail() + if _, seen := seenNames[name]; !seen { + seenNames[name] = struct{}{} + names = append(names, name) + } + if _, seen := seenEmails[email]; !seen { + seenEmails[email] = struct{}{} + emails = append(emails, email) + } + } + } + return nil + } + + if err := collect("desc"); err != nil { + log.Error("Failed to fetch commits", "err", err) + return + } + if err := collect("asc"); err != nil { + log.Error("Failed to fetch commits", "err", err) + return + } + + for i, name := range names { + PrintInfo("Name "+fmt.Sprint(i+1), name) + } + for i, email := range emails { + PrintInfo("Email "+fmt.Sprint(i+1), email) + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3ed609e --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1746663147, + "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f4848c7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + description = + "GH-Recon: Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API."; + + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; + + outputs = { self, nixpkgs }: + let + supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; + + forAllSystems = f: + nixpkgs.lib.genAttrs supportedSystems + (system: f system (import nixpkgs { inherit system; })); + + pname = "gh-recon"; + version = "0.1.0"; + + ldflags = [ "-s" "-w" ]; + + in { + packages = forAllSystems (system: pkgs: { + "${pname}" = pkgs.buildGoModule { + inherit pname version ldflags; + + src = ./.; + + vendorHash = "sha256-CPk8B8FKEoN8qff6WV/iBf0eVjTBMVfJQvlVcti6dfM="; + + meta = with pkgs.lib; { + description = + "Fetches and aggregates public OSINT data for a GitHub user."; + homepage = "https://github.com/anotherhadi/gh-recon"; + platforms = platforms.unix; + }; + }; + }); + + defaultPackage = + forAllSystems (system: pkgs: self.packages.${system}.${pname}); + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3dfc0b8 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/anotherhadi/gh-recon + +go 1.24.2 + +require ( + github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/log v0.4.1 + github.com/google/go-github/v72 v72.0.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d31c4bc --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= +github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM= +github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..a75c367 --- /dev/null +++ b/keys.go @@ -0,0 +1,140 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/charmbracelet/log" + "github.com/google/go-github/v72/github" +) + +func keys(client *github.Client, ctx context.Context, username string) { + sshKeys, resp, err := client.Users.ListKeys(ctx, username, nil) + if err != nil { + log.Error("Failed to fetch user's keys", "err", err) + os.Exit(1) + } + if len(sshKeys) == 0 { + fmt.Println( + GreyStyle.Render("[")+ + RedStyle.Render("x")+ + GreyStyle.Render("]"), + GreyStyle.Render("No keys found for user "+username), + ) + } else { + fmt.Println( + GreyStyle.Render("[")+ + GreenStyle.Render("+")+ + GreyStyle.Render("]"), + GreyStyle.Render("Keys:\n"), + ) + } + for _, key := range sshKeys { + PrintInfo("ID", fmt.Sprintf("%d", key.GetID())) + PrintInfo("URL", key.GetURL()) + PrintInfo("Title", key.GetTitle()) + PrintInfo("Created At", key.GetCreatedAt().String()) + PrintInfo("Key", key.GetKey()) + PrintInfo("Read Only", fmt.Sprintf("%t", key.GetReadOnly())) + PrintInfo("Verified", fmt.Sprintf("%t", key.GetVerified())) + PrintInfo("Last Used", key.GetLastUsed().String()) + PrintInfo("Added By", key.GetAddedBy()) + fmt.Println() + } + WaitForRateLimit(resp) + + gpgKeys, resp, err := client.Users.ListGPGKeys(ctx, username, nil) + if err != nil { + log.Error("Failed to fetch user's GPG keys", "err", err) + } + + if len(gpgKeys) == 0 { + fmt.Println( + GreyStyle.Render("[")+ + RedStyle.Render("x")+ + GreyStyle.Render("]"), + GreyStyle.Render("No GPG keys found\n"), + ) + } else { + fmt.Println( + GreyStyle.Render("[")+ + GreenStyle.Render("+")+ + GreyStyle.Render("]"), + GreyStyle.Render("GPG Keys:\n"), + ) + } + for _, key := range gpgKeys { + PrintInfo("ID", fmt.Sprintf("%d", key.GetID())) + PrintInfo("Key ID", key.GetKeyID()) + PrintInfo("Public Key", key.GetPublicKey()) + PrintInfo("Created At", key.GetCreatedAt().String()) + PrintInfo("Expires At", key.GetExpiresAt().String()) + PrintInfo("Can Sign", fmt.Sprintf("%t", key.GetCanSign())) + PrintInfo("Can Encrypt Comms", fmt.Sprintf("%t", key.GetCanEncryptComms())) + PrintInfo("Can Encrypt Storage", fmt.Sprintf("%t", key.GetCanEncryptStorage())) + PrintInfo("Can Certify", fmt.Sprintf("%t", key.GetCanCertify())) + PrintInfo("Primary Key ID", fmt.Sprintf("%d", key.GetPrimaryKeyID())) + PrintInfo("Raw Key", key.GetRawKey()) + PrintInfo("Emails", fmt.Sprintf("%d", len(key.Emails))) + for _, email := range key.Emails { + PrintInfo("\tEmail", email.GetEmail()) + PrintInfo("\tVerified", fmt.Sprintf("%t", email.GetVerified())) + } + PrintInfo("Subkeys", fmt.Sprintf("%d", len(key.Subkeys))) + for _, subkey := range key.Subkeys { + PrintInfo("\tSubkey ID", fmt.Sprintf("%d", subkey.GetID())) + PrintInfo("\tSubkey Key ID", subkey.GetKeyID()) + PrintInfo("\tSubkey Created At", subkey.GetCreatedAt().String()) + PrintInfo("\tSubkey Expires At", subkey.GetExpiresAt().String()) + PrintInfo("\tSubkey Can Sign", fmt.Sprintf("%t", subkey.GetCanSign())) + PrintInfo( + "\tSubkey Can Encrypt Comms", + fmt.Sprintf("%t", subkey.GetCanEncryptComms()), + ) + PrintInfo( + "\tSubkey Can Encrypt Storage", + fmt.Sprintf("%t", subkey.GetCanEncryptStorage()), + ) + PrintInfo("\tSubkey Can Certify", fmt.Sprintf("%t", subkey.GetCanCertify())) + PrintInfo("\tSubkey Primary Key ID", fmt.Sprintf("%d", subkey.GetPrimaryKeyID())) + PrintInfo("\tSubkey Raw Key", subkey.GetRawKey()) + PrintInfo("\tSubkey Public Key", subkey.GetPublicKey()) + } + fmt.Println() + } + WaitForRateLimit(resp) + + signingKeys, resp, err := client.Users.ListSSHSigningKeys( + ctx, + username, + nil, + ) + if err != nil { + log.Error("Failed to fetch user's signing keys", "err", err) + } + if len(signingKeys) == 0 { + fmt.Println( + GreyStyle.Render("[")+ + RedStyle.Render("x")+ + GreyStyle.Render("]"), + GreyStyle.Render("No signing keys found\n"), + ) + } else { + fmt.Println( + GreyStyle.Render("[")+ + GreenStyle.Render("+")+ + GreyStyle.Render("]"), + GreyStyle.Render("Signing Keys:\n"), + ) + } + for _, key := range signingKeys { + PrintInfo("ID", fmt.Sprintf("%d", key.GetID())) + PrintInfo("Key", key.GetKey()) + PrintInfo("Title", key.GetTitle()) + PrintInfo("Created At", key.GetCreatedAt().String()) + fmt.Println() + } + + WaitForRateLimit(resp) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3534a3a --- /dev/null +++ b/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/charmbracelet/log" + "github.com/google/go-github/v72/github" +) + +func main() { + var username string + var token string + flag.StringVar(&username, "username", "", "Target username") + flag.StringVar(&token, "token", "", "Github token") + flag.Parse() + + if username == "" { + fmt.Println("Please provide a username with the --username flag") + os.Exit(1) + } + err := parseUsername(username) + if err != nil { + log.Error("Invalid username", "err", err) + os.Exit(1) + } + + client := github.NewClient(nil) + if token == "" { + log.Info( + "It's recommended to set a Github token for better rate limits. You can set it using the --token flag.", + ) + } else { + client = client.WithAuthToken(token) + } + + ctx := context.Background() + + header() + userInfo(client, ctx, username) + orgs(client, ctx, username) + keys(client, ctx, username) + socials(client, username) + commits(client, ctx, username) +} diff --git a/orgs.go b/orgs.go new file mode 100644 index 0000000..9dfa743 --- /dev/null +++ b/orgs.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "fmt" + + "github.com/charmbracelet/log" + "github.com/google/go-github/v72/github" +) + +func orgs(client *github.Client, ctx context.Context, username string) { + orgs, resp, err := client.Organizations.List(ctx, username, nil) + if err != nil { + log.Error("Failed to fetch user's organizations", "err", err) + } + if len(orgs) == 0 { + fmt.Println( + GreyStyle.Render("[")+ + RedStyle.Render("x")+ + GreyStyle.Render("]"), + GreyStyle.Render("No organizations found for user "+username), + ) + } else { + fmt.Println( + GreyStyle.Render("[")+ + GreenStyle.Render("+")+ + GreyStyle.Render("]"), + GreyStyle.Render("Organizations:\n"), + ) + } + for _, org := range orgs { + PrintInfo("Login", org.GetLogin()) + PrintInfo("ID", fmt.Sprintf("%d", org.GetID())) + PrintInfo("Node ID", org.GetNodeID()) + PrintInfo("URL", org.GetURL()) + PrintInfo("Repos URL", org.GetReposURL()) + PrintInfo("Events URL", org.GetEventsURL()) + PrintInfo("Hooks URL", org.GetHooksURL()) + PrintInfo("Issues URL", org.GetIssuesURL()) + PrintInfo("Members URL", org.GetMembersURL()) + PrintInfo("Public Members URL", org.GetPublicMembersURL()) + PrintInfo("Avatar URL", org.GetAvatarURL()) + PrintInfo("Description", org.GetDescription()) + fmt.Println() + } + WaitForRateLimit(resp) +} diff --git a/social-media.go b/social-media.go new file mode 100644 index 0000000..902f835 --- /dev/null +++ b/social-media.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/log" + "github.com/google/go-github/v72/github" +) + +func socials(client *github.Client, username string) { + resp, err := FetchGitHubAPI(client, "", "/users/"+username+"/social_accounts") + if err != nil { + log.Error("Failed to fetch user's social media accounts", "err", err) + } + + type SocialAccount struct { + Provider string `json:"provider"` + URL string `json:"url"` + } + type SocialAccounts []SocialAccount + + var socialAccounts SocialAccounts + err = json.Unmarshal(resp, &socialAccounts) + if err != nil { + log.Error("Failed to unmarshal social media accounts", "err", err) + } + if len(socialAccounts) == 0 { + fmt.Println( + GreyStyle.Render("[")+ + RedStyle.Render("x")+ + GreyStyle.Render("]"), + GreyStyle.Render("No social media accounts found\n"), + ) + } else { + fmt.Println( + GreyStyle.Render("[")+ + GreenStyle.Render("+")+ + GreyStyle.Render("]"), + GreyStyle.Render("Social Media Accounts:\n"), + ) + } + for _, account := range socialAccounts { + PrintInfo("Provider", account.Provider) + PrintInfo("URL", account.URL) + fmt.Println() + } +} diff --git a/user.go b/user.go new file mode 100644 index 0000000..8f4e228 --- /dev/null +++ b/user.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/charmbracelet/log" + "github.com/google/go-github/v72/github" +) + +func userInfo(client *github.Client, ctx context.Context, username string) { + fmt.Println( + GreyStyle.Render("[")+ + GreenStyle.Render("-")+ + GreyStyle.Render("]"), + GreyStyle.Render("Fetching user info...\n"), + ) + + user, resp, err := client.Users.Get(ctx, username) + if resp.StatusCode == 404 { + fmt.Println( + GreyStyle.Render("[")+ + RedStyle.Render("x")+ + GreyStyle.Render("]"), + GreyStyle.Render("Error:"), + RedStyle.Render("User not found"), + ) + os.Exit(1) + } + if err != nil { + log.Error("Failed to fetch user's information", "err", err) + os.Exit(1) + } + + PrintInfo("Username", user.GetLogin()) + PrintInfo("ID", fmt.Sprintf("%d", user.GetID())) + PrintInfo("Avatar URL", user.GetAvatarURL()) + PrintInfo("Gravatar ID", user.GetGravatarID()) + PrintInfo("Name", user.GetName()) + PrintInfo("Company", user.GetCompany()) + PrintInfo("Location", user.GetLocation()) + PrintInfo("Email", user.GetEmail()) + PrintInfo("Hireable", fmt.Sprintf("%t", user.GetHireable())) + PrintInfo("Bio", user.GetBio()) + PrintInfo("Public Repos", fmt.Sprintf("%d", user.GetPublicRepos())) + PrintInfo("Public Gists", fmt.Sprintf("%d", user.GetPublicGists())) + PrintInfo("Followers", fmt.Sprintf("%d", user.GetFollowers())) + PrintInfo("Following", fmt.Sprintf("%d", user.GetFollowing())) + PrintInfo("Created At", user.GetCreatedAt().String()) + PrintInfo("Updated At", user.GetUpdatedAt().String()) + PrintInfo("Suspended At", user.GetSuspendedAt().String()) + PrintInfo("Type", user.GetType()) + PrintInfo("Site Admin", fmt.Sprintf("%t", user.GetSiteAdmin())) + PrintInfo("Total Private Repos", fmt.Sprintf("%d", user.GetTotalPrivateRepos())) + PrintInfo("Owned Private Repos", fmt.Sprintf("%d", user.GetOwnedPrivateRepos())) + PrintInfo("Private Gists", fmt.Sprintf("%d", user.GetPrivateGists())) + PrintInfo("Disk Usage", fmt.Sprintf("%d", user.GetDiskUsage())) + PrintInfo("Collaborators", fmt.Sprintf("%d", user.GetCollaborators())) + PrintInfo("Plan", user.GetPlan().GetName()) + fmt.Println() + + WaitForRateLimit(resp) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..989c5e3 --- /dev/null +++ b/utils.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" + "github.com/google/go-github/v72/github" +) + +var ( + Grey = lipgloss.Color("#7d7d7d") + Green = lipgloss.Color("#a6e3a1") + Red = lipgloss.Color("#f38ba8") + + GreyStyle = lipgloss.NewStyle().Foreground(Grey) + GreenStyle = lipgloss.NewStyle().Foreground(Green) + RedStyle = lipgloss.NewStyle().Foreground(Red) +) + +func header() { + asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ " + fmt.Println(GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n"))) +} + +func parseUsername(username string) error { + if username == "" { + return fmt.Errorf("username is required") + } + if strings.Contains(username, " ") { + return fmt.Errorf("username cannot contain spaces") + } + if strings.Contains(username, "@") { + return fmt.Errorf("username cannot contain @") + } + return nil +} + +func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) { + url := "https://api.github.com" + path + userAgent := "GHRecon/1.0" + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request for %s: %w", url, err) + } + + if token != "" { + req.Header.Set("Authorization", "token "+token) + } + req.Header.Set("Accept", "application/vnd.github.v3+json") + req.Header.Set("User-Agent", userAgent) + + resp, err := github.Client().Do(req) + if err != nil { + return nil, fmt.Errorf("error executing request for %s: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf( + "request for %s failed with status %d: %s", + url, + resp.StatusCode, + string(bodyBytes), + ) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf( + "error reading response body for %s: %w", + url, + err, + ) + } + + return bodyBytes, nil +} + +func PrintInfo(key, value string) { + if value == "" || value == "0" || value == "0001-01-01 00:00:00 +0000 UTC" { + return + } + fmt.Printf("%s %s\n", GreyStyle.Render(key+":"), GreenStyle.Render(value)) +} + +func WaitForRateLimit(resp *github.Response) { + if resp.Rate.Remaining == 0 { + log.Info( + "Rate limit reached, waiting for reset... (time:" + resp.Rate.Reset.Time.String() + ")", + ) + time.Sleep(time.Until(resp.Rate.Reset.Time) + time.Second) + } +}