mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 10:58:25 +02:00
@@ -0,0 +1 @@
|
|||||||
|
result/
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
+74
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+27
@@ -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
|
||||||
|
}
|
||||||
@@ -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});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user