diff --git a/README.md b/README.md new file mode 100644 index 0000000..898fa22 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# GithubID + +GithubID is an OSINT tool that can be used to retrieve the identities (name/email) that a given Github user has used in their public commits. + +Under the hood, this uses Github's GraphQL API to query for the commits a user has pushed and then extracts their contact information from them. + +![Screenshot highlighting basic usage of this tool](./img/screenshot.png) + +## Usage + +You'll need a Github Token to use this tool. You can generate a Personal Access Token [here](https://github.com/settings/tokens). + +You can set your token as the `GH_TOKEN` environment variable or pass it through a flag: +```bash +$ githubid -user torvalds -token +``` + +## TODO + +- [ ] Add pagination support for the GraphQL call; +- [ ] Add [gharchive.org](https://www.gharchive.org/)/bigquery support to allow finding deleted commits (accidentally got a $100 bill from google cloud and am too depressed to do this now); \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..43e2fdf --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/beescuit/githubid + +go 1.22.4 + +require ( + github.com/machinebox/graphql v0.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1b8d8a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= +github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/img/screenshot.png b/img/screenshot.png new file mode 100644 index 0000000..6d2293c Binary files /dev/null and b/img/screenshot.png differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..d4c3b9f --- /dev/null +++ b/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + + "github.com/machinebox/graphql" +) + +var query = ` + query($userName:String!, $id:ID) { + user(login: $userName){ + repositoriesContributedTo(includeUserRepositories: true, contributionTypes: COMMIT, first: 100) { + pageInfo { + hasNextPage + endCursor + } + nodes { + defaultBranchRef { + target { + ... on Commit { + history(author: {id: $id}) { + pageInfo { + hasNextPage + endCursor + } + nodes { + commitUrl + author { + email + name + } + } + } + } + } + } + } + } + } + }` + +func main() { + username := flag.String("user", "", "(REQUIRED) Username of the target github account") + printsource := flag.Bool("source", false, "Print commit URLs alongside discovered identities") + showall := flag.Bool("all", false, "Print all commits (will repeat duplicate identities)") + flagtoken := flag.String("token", "", "Github API Bearer token (can also be set from the GH_TOKEN env variable)") + + flag.Parse() + + if *username == "" { + flag.PrintDefaults() + os.Exit(0) + } + + var token = "" + + if *flagtoken == "" { + token = os.Getenv("GH_TOKEN") + } else { + token = *flagtoken + } + + if token == "" { + fmt.Println("Github token missing. Please generate one and set it through the -token flag or the GH_TOKEN environment variable") + os.Exit(0) + } + + userreq, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/users/%s", *username), nil) + if err != nil { + panic(err) + } + + userreq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + httpclient := http.Client{} + res, err := httpclient.Do(userreq) + if err != nil { + fmt.Printf("Error fetching user ID: %s\n", err) + os.Exit(1) + } + + if res.StatusCode == 401 { + fmt.Println("Your Github token seems to be invalid.") + os.Exit(1) + } + + var userres struct { + NodeID string `json:"node_id"` + } + + err = json.NewDecoder(res.Body).Decode(&userres) + if err != nil { + fmt.Printf("Error parsing github api response: %s\n", err) + os.Exit(1) + } + + userid := userres.NodeID + + client := graphql.NewClient("https://api.github.com/graphql") + + req := graphql.NewRequest(query) + + req.Var("userName", username) + req.Var("id", userid) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + var respData struct { + User struct { + RepositoriesContributedTo struct { + PageInfo struct { + HasNextPage bool + EndCursor string + } + Nodes []struct { + DefaultBranchRef struct { + Target struct { + History struct { + PageInfo struct { + HasNextPage bool + EndCursor string + } + Nodes []struct { + CommitURL string + Author struct { + Email string + Name string + } + } + } + } + } + } + } + } + } + + err = client.Run(context.Background(), req, &respData) + if err != nil { + log.Fatalf("Failed to execute request: %v", err) + } + + unique := make(map[string]bool) + + for _, repo := range respData.User.RepositoriesContributedTo.Nodes { + for _, commit := range repo.DefaultBranchRef.Target.History.Nodes { + identity := fmt.Sprintf("%s <%s>", commit.Author.Name, commit.Author.Email) + if _, exists := unique[identity]; *showall || !exists { + unique[identity] = true + if *printsource { + fmt.Printf("%s - %s\n", identity, commit.CommitURL) + } else { + fmt.Println(identity) + } + } + } + } +}