gitrecon
Version:
This CLI tool is designed to retrieve exposed email addresses from a GitHub user’s public activity and to identify a GitHub username associated with a given email address. It is intended strictly for educational use and ethical security research. Please u
1,161 lines (981 loc) • 78.4 kB
JavaScript
#!/usr/bin/env node
// Import required modules
const axios = require('axios');
const { ArgumentParser } = require('argparse');
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const colors = {
GREEN: '\x1b[32m',
YELLOW: '\x1b[33m',
NC: '\x1b[0m',
CYAN: '\x1b[36m',
RED: '\x1b[31m',
BLUE: '\x1b[34m',
MAGENTA: '\x1b[35m',
BRIGHT: '\x1b[1m',
DIM: '\x1b[2m',
}
// Constants
const API_URL = 'https://api.github.com';
const GITLAB_API_URL = 'https://gitlab.com/api/v4';
const HEADER = {
Accept: 'application/vnd.github.v3+json',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
};
let found = []
let rateLimitInfo = {
remaining: null,
limit: null,
resetTime: null
};
let DELAY = 1000; // Default delay of one second between requests
// Factory function to create Repository objects
const Repository = (name, isFork) => ({
name,
isFork,
});
// Function to update HTTP headers
const updateHeader = (updateObj) => {
Object.assign(HEADER, updateObj);
};
// Helper function to check email validity
const isValidEmail = (email) => {
const re = /\S+@\S+\.\S+/;
return re.test(email);
};
// Function to mask email addresses for display
const maskEmail = (email) => {
const parts = email.split('@');
if (parts.length !== 2) return email;
const [name, domain] = parts;
// For very short usernames, just show first character
if (name.length <= 3) {
return `${name.charAt(0)}${'*'.repeat(name.length - 1)}@${domain}`;
}
// For normal usernames, show first and last character
const maskedName = `${name.charAt(0)}${'*'.repeat(name.length - 2)}${name.charAt(name.length - 1)}`;
return `${maskedName}@${domain}`;
};
// Function to convert data to HTML format
const generateHtml = (data) => {
// Simple HTML template for the report
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitRecon Report</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 1000px; margin: 0 auto; padding: 20px; }
h1, h2, h3 { color: #333; }
.container { border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 20px; }
.profile { display: flex; align-items: center; gap: 20px; }
.avatar { width: 100px; height: 100px; border-radius: 50%; }
.info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 20px; }
.info-item { padding: 8px; border-bottom: 1px solid #eee; }
.label { font-weight: bold; color: #555; }
.keys, .emails { margin-top: 20px; }
pre { background-color: #f5f5f5; padding: 10px; border-radius: 3px; overflow-x: auto; }
.email-item, .key-item { background-color: #f9f9f9; padding: 10px; margin-bottom: 8px; border-radius: 3px; }
.footer { margin-top: 30px; text-align: center; font-size: 0.8em; color: #777; }
</style>
</head>
<body>
<h1>GitRecon Report</h1>
<div class="container">
<div class="profile">
${data.avatar_url ? `<img src="${data.avatar_url}" alt="${data.login || data.username} avatar" class="avatar">` : ''}
<div>
<h2>${data.login || data.username || data.name || 'Unknown User'}</h2>
<p>${data.bio || data.description || ''}</p>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<span class="label">Name:</span> ${data.name || 'N/A'}
</div>
<div class="info-item">
<span class="label">ID:</span> ${data.id || 'N/A'}
</div>
<div class="info-item">
<span class="label">Location:</span> ${data.location || 'N/A'}
</div>
<div class="info-item">
<span class="label">Email:</span> ${data.email || data.public_email || 'N/A'}
</div>
<div class="info-item">
<span class="label">Company/Organization:</span> ${data.company || data.organization || 'N/A'}
</div>
<div class="info-item">
<span class="label">Blog/Website:</span> ${data.blog || data.web_url || 'N/A'}
</div>
<div class="info-item">
<span class="label">Twitter:</span> ${data.twitter_username || data.twitter || 'N/A'}
</div>
<div class="info-item">
<span class="label">Created:</span> ${data.created_at ? new Date(data.created_at).toLocaleDateString() : 'N/A'}
</div>
<div class="info-item">
<span class="label">Followers:</span> ${data.followers || 'N/A'}
</div>
<div class="info-item">
<span class="label">Following:</span> ${data.following || 'N/A'}
</div>
</div>
</div>
${data.organizations && data.organizations.length > 0 ? `
<div class="container">
<h3>Organizations (${data.organizations.length})</h3>
<div class="info-grid">
${data.organizations.map(org => `
<div class="info-item">${org}</div>
`).join('')}
</div>
</div>
` : ''}
${data.keys && data.keys.length > 0 ? `
<div class="container keys">
<h3>Public Keys (${data.keys.length})</h3>
${data.keys.map(key => `
<div class="key-item">
${key.id ? `<div><span class="label">ID:</span> ${key.id}</div>` : ''}
${key.title ? `<div><span class="label">Title:</span> ${key.title}</div>` : ''}
${key.created_at ? `<div><span class="label">Created:</span> ${key.created_at}</div>` : ''}
${key.expires_at ? `<div><span class="label">Expires:</span> ${key.expires_at}</div>` : ''}
<div><span class="label">Key:</span></div>
<pre>${key.key}</pre>
</div>
`).join('')}
</div>
` : ''}
${data.leaked_emails && data.leaked_emails.length > 0 ? `
<div class="container emails">
<h3>Leaked Emails (${data.leaked_emails.length})</h3>
${data.leaked_emails.map(email => `
<div class="email-item">
<div>${email}</div>
${data.email_details && data.email_details.find(d => d.email === email) ? `
<div style="font-size: 0.9em; color: #666; margin-top: 5px;">
Associated names: ${data.email_details.find(d => d.email === email).names.join(', ')}
</div>
${data.email_details.find(d => d.email === email).sources && data.email_details.find(d => d.email === email).sources.length > 0 ? `
<div style="font-size: 0.9em; color: #666;">
Found in: ${data.email_details.find(d => d.email === email).sources.join(', ')}
</div>
` : ''}
` : ''}
</div>
`).join('')}
</div>
` : ''}
<div class="footer">
<p>Generated with GitRecon on ${new Date().toLocaleString()}</p>
</div>
</body>
</html>`;
};
// Function to save output to files
const saveOutput = (data, format, username, site) => {
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
const baseFilename = `${username}_${site}_${timestamp}`;
// Create output directory if it doesn't exist
const outputDir = path.join(process.cwd(), 'gitrecon-results');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Save JSON output
if (format === 'json' || format === 'all') {
const jsonFilePath = path.join(outputDir, `${baseFilename}.json`);
fs.writeFileSync(jsonFilePath, JSON.stringify(data, null, 2));
console.log(`${colors.GREEN}JSON report saved to: ${colors.YELLOW}${jsonFilePath}${colors.GREEN}`);
}
// Save HTML output
if (format === 'html' || format === 'all') {
const htmlFilePath = path.join(outputDir, `${baseFilename}.html`);
fs.writeFileSync(htmlFilePath, generateHtml(data));
console.log(`${colors.GREEN}HTML report saved to: ${colors.YELLOW}${htmlFilePath}${colors.GREEN}`);
}
return { outputDir, baseFilename };
};
// Function to download avatar
const downloadAvatar = async (url, username, site) => {
if (!url) return null;
try {
const response = await axios.get(url, {
responseType: 'arraybuffer',
headers: { 'User-Agent': HEADER['User-Agent'] }
});
// Create output directory if it doesn't exist
const outputDir = path.join(process.cwd(), 'gitrecon-results');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const avatarPath = path.join(outputDir, `${username}_${site}_avatar.jpg`);
fs.writeFileSync(avatarPath, Buffer.from(response.data));
console.log(`${colors.GREEN}Avatar downloaded to: ${colors.YELLOW}${avatarPath}${colors.GREEN}`);
return avatarPath;
} catch (error) {
console.error(`${colors.RED}Error downloading avatar: ${error.message}`);
return null;
}
};
// Function to retrieve user's repositories
const getRepositories = async (username) => {
const repositoriesSeen = new Set();
const repositories = [];
let pageCounter = 1;
while (true) {
let continueLoop = true;
// Construct the URL for fetching repositories
const url = `${API_URL}/users/${username}/repos?per_page=100&page=${pageCounter}`;
const result = await apiCall(url);
if ('message' in result) {
if (result.message.includes('API rate limit exceeded for ')) {
console.warn('API rate limit exceeded - not all repos were fetched');
break;
}
if (result.message === 'Not Found') {
console.warn(`There is no user with the username "${username}"`);
break;
}
}
// Process each repository in the result
for (const repository of result) {
const repoName = repository.name;
if (repositoriesSeen.has(repoName)) {
continueLoop = false;
break;
} else {
repositories.push(Repository(repoName, repository.fork));
repositoriesSeen.add(repoName);
}
}
if (continueLoop && result.length === 100) {
pageCounter += 1;
} else {
break;
}
}
return repositories;
};
// Function to retrieve email addresses from a repository's commits
const getEmails = async (username, repoName) => {
const emailsToName = new Map();
const seenCommits = new Set();
let pageCounter = 1;
let commitCounter = 1;
while (true) {
let continueLoop = true;
const url = `${API_URL}/repos/${username}/${repoName}/commits?per_page=100&page=${pageCounter}`;
const result = await apiCall(url);
if ('message' in result) {
if (result.message === 'Git Repository is empty.' || result.message === 'No commit found') {
console.info(`${colors.YELLOW}Repository ${repoName} is empty - skipping${colors.NC}`);
return emailsToName;
}
if (result.message.includes('API rate limit exceeded for ')) {
console.warn('API rate limit exceeded');
return emailsToName;
}
if (result.message === 'Not Found') {
console.warn(`Repository Not Found: "${repoName}"`);
return emailsToName;
}
}
// Process each commit in the result
for (const commit of result) {
const sha = commit.sha;
if (seenCommits.has(sha)) {
continueLoop = false;
break;
}
seenCommits.add(sha);
// console.info(`Scanning commit -> ${commitCounter}`);
commitCounter += 1;
if (!commit.author) {
continue;
}
const user = commit.author.login;
if (user.toLowerCase() === username.toLowerCase()) {
const { author, committer } = commit.commit;
const authorName = author.name;
const authorEmail = author.email;
const committerName = committer.name;
const committerEmail = committer.email;
if (authorEmail) {
if (!emailsToName.has(authorEmail)) {
emailsToName.set(authorEmail, new Set());
}
emailsToName.get(authorEmail).add(authorName);
}
if (committerEmail) {
if (!emailsToName.has(committerEmail)) {
emailsToName.set(committerEmail, new Set());
}
emailsToName.get(committerEmail).add(committerName);
}
}
}
if (continueLoop && result.length === 100) {
pageCounter += 1;
} else {
break;
}
}
return emailsToName;
};
// Function to make API calls with a delay
const apiCall = async (url, options = {}) => {
await new Promise((resolve) => setTimeout(resolve, DELAY));
try {
const response = await axios.get(url, {
headers: options.headers || HEADER,
timeout: options.timeout || 10000
});
// Update rate limit info if GitHub API
if (url.includes('api.github.com') && response.headers) {
const remaining = response.headers['x-ratelimit-remaining'];
const limit = response.headers['x-ratelimit-limit'];
const resetHeader = response.headers['x-ratelimit-reset'];
if (remaining && limit && resetHeader) {
const resetTime = new Date(parseInt(resetHeader) * 1000);
rateLimitInfo = {
remaining: parseInt(remaining),
limit: parseInt(limit),
resetTime
};
// Display rate limit info
console.log(`${colors.DIM}Rate limit: ${remaining}/${limit} (Resets: ${resetTime.toLocaleTimeString()})${colors.NC}`);
// Warn if rate limit is getting low
if (parseInt(remaining) < 10) {
console.warn(`${colors.YELLOW}Warning: GitHub API rate limit is getting low (${remaining} remaining)${colors.NC}`);
}
}
}
return response.data;
} catch (error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (error.response.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') {
const resetTime = new Date(parseInt(error.response.headers['x-ratelimit-reset']) * 1000);
console.error(`${colors.RED}Error: GitHub API rate limit exceeded. Resets at ${resetTime.toLocaleTimeString()}${colors.NC}`);
} else {
console.error(`${colors.RED}API Error: ${error.response.status} - ${error.response.data.message || JSON.stringify(error.response.data)}${colors.NC}`);
}
return { error: true, message: error.response.data.message || 'API request failed', status: error.response.status };
} else if (error.request) {
// The request was made but no response was received
console.error(`${colors.RED}Network Error: No response received from server${colors.NC}`);
return { error: true, message: 'Network error - no response received' };
} else {
// Something happened in setting up the request that triggered an Error
console.error(`${colors.RED}Request Error: ${error.message}${colors.NC}`);
return { error: true, message: error.message };
}
}
};
// Function to find GitHub username by email
const findUsernameByEmail = async (email) => {
console.info(`${colors.GREEN}Searching for GitHub username with email "${colors.YELLOW}${email}${colors.GREEN}"${colors.NC}`);
const url = `${API_URL}/search/users?q=${email}`;
const result = await apiCall(url);
if (result && result.total_count > 0) {
const user = result.items[0];
console.log(`${colors.GREEN}Found GitHub username ${colors.YELLOW}${user.login}${colors.GREEN} for email ${colors.YELLOW}${email}${colors.GREEN}${colors.NC}`);
return user.login;
} else {
console.log(`${colors.YELLOW}No GitHub username found for email ${colors.CYAN}${email}${colors.YELLOW}${colors.NC}`);
return null;
}
};
// Add a utility function for real-time saving
const saveRealTime = (data, username, site, outputDir) => {
const baseFilename = `${username}_${site}_realtime`;
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const jsonFilePath = path.join(outputDir, `${baseFilename}.json`);
// Add timestamp to the data
data.last_updated = new Date().toISOString();
// Write the data to the file, overwriting any previous content
fs.writeFileSync(jsonFilePath, JSON.stringify(data, null, 2));
console.log(`${colors.GREEN}Real-time data saved to: ${colors.YELLOW}${jsonFilePath}${colors.GREEN}`);
return jsonFilePath;
};
// Function to run GitHub reconnaissance
const runGithubRecon = async (username, options = {}) => {
const { downloadAvatarFlag = false, saveFork = false, outputFormat = null } = options;
console.info(`${colors.GREEN}Running GitHub reconnaissance on user "${colors.YELLOW}${username}${colors.GREEN}"${colors.NC}`);
// Create output directory if it doesn't exist
const outputDir = path.join(process.cwd(), 'gitrecon-results');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Prepare result object with basic structure
let result = {
username: username,
scan_started_at: new Date().toISOString(),
organizations: [],
leaked_emails: [],
email_details: [],
keys: []
};
// Check rate limit before starting
try {
const rateLimitData = await apiCall(`${API_URL}/rate_limit`);
if (rateLimitData && rateLimitData.resources && rateLimitData.resources.core && rateLimitData.resources.core.remaining < 50) {
console.warn(`${colors.YELLOW}Warning: You have only ${rateLimitData.resources.core.remaining} GitHub API requests remaining.${colors.NC}`);
console.warn(`${colors.YELLOW}Consider using a GitHub token with --token option.${colors.NC}`);
}
} catch (error) {
// Continue anyway if rate limit check fails
}
// Fetch profile info
const userInfo = await apiCall(`${API_URL}/users/${username}`);
if (userInfo.error || (userInfo.message && userInfo.message.includes('Not Found'))) {
console.error(`${colors.RED}Error: GitHub user "${username}" not found${colors.NC}`);
return null;
}
// Update result with user info
Object.assign(result, {
username: userInfo.login,
name: userInfo.name,
id: userInfo.id,
avatar_url: userInfo.avatar_url,
email: userInfo.email,
location: userInfo.location,
bio: userInfo.bio,
company: userInfo.company,
blog: userInfo.blog,
twitter_username: userInfo.twitter_username,
followers: userInfo.followers,
following: userInfo.following,
created_at: userInfo.created_at,
updated_at: userInfo.updated_at
});
// Save initial data
saveRealTime(result, username, 'github', outputDir);
console.log(`${colors.GREEN}Found GitHub user: ${colors.YELLOW}${userInfo.login || username}${colors.GREEN} (${colors.YELLOW}${userInfo.name || 'No name'}${colors.GREEN})${colors.NC}`);
// Fetch organizations
const orgsData = await apiCall(`${API_URL}/users/${username}/orgs`);
let orgs = [];
if (Array.isArray(orgsData)) {
orgs = orgsData.map(org => org.login);
if (orgs.length > 0) {
console.log(`${colors.GREEN}Found ${colors.YELLOW}${orgs.length}${colors.GREEN} organizations: ${colors.YELLOW}${orgs.join(', ')}${colors.GREEN}${colors.NC}`);
} else {
console.log(`${colors.GREEN}No organizations found${colors.NC}`);
}
// Update result with organizations
result.organizations = orgs;
saveRealTime(result, username, 'github', outputDir);
} else {
console.warn(`${colors.YELLOW}Error fetching organizations: ${orgsData.message || 'Unknown error'}${colors.NC}`);
}
// Fetch public SSH keys
const keysData = await apiCall(`${API_URL}/users/${username}/keys`);
let keys = [];
if (Array.isArray(keysData)) {
keys = keysData;
if (keys.length > 0) {
console.log(`${colors.GREEN}Found ${colors.YELLOW}${keys.length}${colors.GREEN} public SSH keys${colors.NC}`);
} else {
console.log(`${colors.GREEN}No public SSH keys found${colors.NC}`);
}
// Update result with keys
result.keys = keys.map(key => ({
id: key.id,
key: key.key
}));
saveRealTime(result, username, 'github', outputDir);
} else {
console.warn(`${colors.YELLOW}Error fetching public keys: ${keysData.message || 'Unknown error'}${colors.NC}`);
}
// Gather repositories
const repositories = await getRepositories(username);
console.log(`${colors.GREEN}Found ${colors.YELLOW}${repositories.length}${colors.GREEN} public repositories${colors.NC}`);
// Filter out forks if needed (using non-forked repos first)
const reposToScan = repositories
.sort((a, b) => (a.isFork ? 1 : -1))
.filter(repo => !repo.isFork || saveFork)
.map(repo => repo.name);
console.log(`${colors.GREEN}Scanning ${colors.YELLOW}${reposToScan.length}${colors.GREEN} repositories for leaked emails${colors.NC}`);
// Initialize email tracking
const emailsToName = new Map();
const emailsToRepo = new Map();
let allLeakedEmails = [];
// Scan each repository for commits
const totalRepos = reposToScan.length;
for (let i = 0; i < totalRepos; i++) {
const repo = reposToScan[i];
process.stdout.write(`${colors.GREEN}Scanning repository ${colors.YELLOW}${i + 1}/${totalRepos}${colors.GREEN}: ${colors.CYAN}${repo}${colors.GREEN}...${colors.NC}`);
try {
const newEmails = await getEmails(username, repo);
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
let newEmailsCount = 0;
for (const [email, names] of newEmails.entries()) {
// Track the repository where this email was found
if (!emailsToRepo.has(email)) {
emailsToRepo.set(email, new Set());
}
emailsToRepo.get(email).add(repo);
// Track all names associated with this email
if (!emailsToName.has(email)) {
emailsToName.set(email, new Set());
allLeakedEmails.push(email);
newEmailsCount++;
}
// Add all names from this repository
names.forEach(name => emailsToName.get(email).add(name));
}
if (newEmailsCount > 0) {
console.log(`${colors.GREEN}Found ${colors.YELLOW}${newEmailsCount}${colors.GREEN} new emails in ${colors.CYAN}${repo}${colors.GREEN}${colors.NC}`);
// Update result with email details
const emailDetails = Array.from(emailsToName.entries()).map(([email, namesSet]) => ({
email,
names: Array.from(namesSet),
sources: Array.from(emailsToRepo.get(email) || [])
}));
result.leaked_emails = allLeakedEmails;
result.email_details = emailDetails;
result.scan_progress = `${i + 1}/${totalRepos} repositories scanned`;
// Save after each repository that yields new emails
saveRealTime(result, username, 'github', outputDir);
} else {
// Periodically save progress, every 5 repositories
if (i % 5 === 0 && i > 0) {
result.scan_progress = `${i + 1}/${totalRepos} repositories scanned`;
saveRealTime(result, username, 'github', outputDir);
}
}
} catch (error) {
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
console.error(`${colors.RED}Error scanning ${repo}: ${error.message}${colors.NC}`);
// Save progress on error
const emailDetails = Array.from(emailsToName.entries()).map(([email, namesSet]) => ({
email,
names: Array.from(namesSet),
sources: Array.from(emailsToRepo.get(email) || [])
}));
result.leaked_emails = allLeakedEmails;
result.email_details = emailDetails;
result.scan_progress = `${i + 1}/${totalRepos} repositories scanned`;
result.last_error = {
repository: repo,
message: error.message,
timestamp: new Date().toISOString()
};
saveRealTime(result, username, 'github', outputDir);
}
}
// Prepare email details for display and output
const emailDetails = Array.from(emailsToName.entries()).map(([email, namesSet]) => ({
email,
names: Array.from(namesSet),
sources: Array.from(emailsToRepo.get(email) || [])
}));
// Display results
console.log(`\n${colors.GREEN}Reconnaissance completed:${colors.NC}`);
console.log(`${colors.GREEN}User: ${colors.YELLOW}${userInfo.login} (${userInfo.name || 'No name'})${colors.NC}`);
console.log(`${colors.GREEN}URL: ${colors.CYAN}https://github.com/${username}${colors.NC}`);
console.log(`${colors.GREEN}Organizations: ${colors.YELLOW}${orgs.length > 0 ? orgs.join(', ') : 'None'}${colors.NC}`);
console.log(`${colors.GREEN}Public Keys: ${colors.YELLOW}${keys.length}${colors.NC}`);
console.log(`${colors.GREEN}Leaked Emails: ${colors.YELLOW}${allLeakedEmails.length}${colors.NC}`);
if (keys.length > 0) {
console.log(`\n${colors.YELLOW}Public SSH Keys:${colors.NC}`);
keys.forEach((key, index) => {
console.log(`${colors.CYAN}Key #${index + 1}:${colors.NC}`);
console.log(`${key.key.substring(0, 40)}...`);
});
}
if (allLeakedEmails.length > 0) {
console.log(`\n${colors.YELLOW}Leaked Emails:${colors.NC}`);
// Create a more organized table format for output
const emailTable = emailDetails.map(detail => ({
email: detail.email,
names: Array.from(detail.names).join(', ').substring(0, 30) + (Array.from(detail.names).join(', ').length > 30 ? '...' : ''),
sources: Array.from(detail.sources).length
}));
console.table(emailTable);
}
// Build full result object with final data
result.scan_completed_at = new Date().toISOString();
result.scan_progress = "completed";
// Download avatar if requested
if (downloadAvatarFlag && userInfo.avatar_url) {
await downloadAvatar(userInfo.avatar_url, username, 'github');
}
// Save final output if requested
if (outputFormat) {
saveOutput(result, outputFormat, username, 'github');
}
return result;
};
// Function to run GitHub organization reconnaissance
const runGithubOrganizationRecon = async (orgName, options = {}) => {
const { downloadAvatarFlag = false, outputFormat = null, verbose = false } = options;
console.info(`${colors.GREEN}Running GitHub reconnaissance on organization "${colors.YELLOW}${orgName}${colors.GREEN}"${colors.NC}`);
// Create output directory if it doesn't exist
const outputDir = path.join(process.cwd(), 'gitrecon-results');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Prepare result object with basic structure
let result = {
organization: orgName,
scan_started_at: new Date().toISOString(),
members: [],
repositories: [],
leaked_emails: [],
email_details: []
};
// Fetch organization info
const orgInfo = await apiCall(`${API_URL}/orgs/${orgName}`);
if (orgInfo.error || (orgInfo.message && orgInfo.message.includes('Not Found'))) {
console.error(`${colors.RED}Error: GitHub organization "${orgName}" not found${colors.NC}`);
return null;
}
// Update result with organization info
Object.assign(result, {
organization: orgInfo.login,
name: orgInfo.name,
id: orgInfo.id,
description: orgInfo.description,
location: orgInfo.location,
blog: orgInfo.blog,
email: orgInfo.email,
twitter_username: orgInfo.twitter_username,
created_at: orgInfo.created_at,
updated_at: orgInfo.updated_at,
avatar_url: orgInfo.avatar_url
});
// Save initial data
saveRealTime(result, orgName, 'github_org', outputDir);
console.log(`${colors.GREEN}Found GitHub organization: ${colors.YELLOW}${orgInfo.login || orgName}${colors.GREEN} (${colors.YELLOW}${orgInfo.name || 'No name'}${colors.GREEN})${colors.NC}`);
// Fetch organization members
const membersData = await apiCall(`${API_URL}/orgs/${orgName}/members?per_page=100`);
let members = [];
if (Array.isArray(membersData)) {
members = membersData;
console.log(`${colors.GREEN}Found ${colors.YELLOW}${members.length}${colors.GREEN} organization members${colors.NC}`);
// Update result with members
result.members = members.map(member => ({
login: member.login,
id: member.id,
type: member.type,
avatar_url: member.avatar_url
}));
saveRealTime(result, orgName, 'github_org', outputDir);
if (verbose && members.length > 0) {
console.log(`${colors.YELLOW}Organization Members:${colors.NC}`);
members.forEach((member, index) => {
console.log(`${colors.CYAN}${index + 1}. ${colors.YELLOW}${member.login}${colors.CYAN} (${colors.YELLOW}${member.type}${colors.CYAN})${colors.NC}`);
});
}
} else {
console.warn(`${colors.YELLOW}Error fetching organization members: ${membersData.message || 'Unknown error'}${colors.NC}`);
}
// Fetch organization repositories
const reposData = await apiCall(`${API_URL}/orgs/${orgName}/repos?per_page=100`);
let repos = [];
if (Array.isArray(reposData)) {
repos = reposData;
console.log(`${colors.GREEN}Found ${colors.YELLOW}${repos.length}${colors.GREEN} organization repositories${colors.NC}`);
// Update result with repositories
result.repositories = repos.map(repo => ({
name: repo.name,
description: repo.description,
language: repo.language,
fork: repo.fork,
created_at: repo.created_at,
updated_at: repo.updated_at,
url: repo.html_url
}));
saveRealTime(result, orgName, 'github_org', outputDir);
if (verbose && repos.length > 0) {
console.log(`${colors.YELLOW}Organization Repositories:${colors.NC}`);
repos.slice(0, 10).forEach((repo, index) => {
console.log(`${colors.CYAN}${index + 1}. ${colors.YELLOW}${repo.name}${colors.CYAN} - ${colors.DIM}${repo.description || 'No description'}${colors.NC}`);
});
if (repos.length > 10) {
console.log(`${colors.CYAN}... and ${repos.length - 10} more repositories${colors.NC}`);
}
}
} else {
console.warn(`${colors.YELLOW}Error fetching organization repositories: ${reposData.message || 'Unknown error'}${colors.NC}`);
}
// Collect emails from repositories and members
const allLeakedEmails = [];
const emailsToName = new Map();
const emailsToRepo = new Map();
const emailsToMember = new Map();
// Only scan a subset of repos to avoid rate limiting
const reposToScan = repos.slice(0, 10);
const totalRepos = reposToScan.length;
if (totalRepos > 0) {
console.log(`${colors.GREEN}Scanning ${colors.YELLOW}${totalRepos}${colors.GREEN} repositories for leaked emails${colors.NC}`);
for (let i = 0; i < totalRepos; i++) {
const repo = reposToScan[i];
process.stdout.write(`${colors.GREEN}Scanning repository ${colors.YELLOW}${i + 1}/${totalRepos}${colors.GREEN}: ${colors.CYAN}${repo.name}${colors.GREEN}...${colors.NC}`);
try {
// Scan commits in this repository
let commitPageCounter = 1;
let seenCommits = new Set();
let newEmailsFound = false;
while (true) {
let continueCommitLoop = true;
const commitsUrl = `${API_URL}/repos/${orgName}/${repo.name}/commits?per_page=100&page=${commitPageCounter}`;
const commitsResult = await apiCall(commitsUrl);
// Handle empty repositories and API errors
if ('message' in commitsResult) {
if (commitsResult.message === 'Git Repository is empty.' || commitsResult.message === 'No commit found') {
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
console.log(`${colors.YELLOW}Repository ${colors.CYAN}${repo.name}${colors.YELLOW} is empty - skipping${colors.NC}`);
break; // Skip this repo
}
if (commitsResult.message.includes('API rate limit exceeded for ')) {
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
console.error(`${colors.RED}API rate limit exceeded - saving current results${colors.NC}`);
// Save current results before exiting
const emailDetails = Array.from(emailsToName.entries()).map(([email, namesSet]) => ({
email,
names: Array.from(namesSet),
sources: Array.from(emailsToRepo.get(email) || []),
github_username: emailsToMember.get(email) || null
}));
result.leaked_emails = allLeakedEmails;
result.email_details = emailDetails;
result.last_updated = new Date().toISOString();
result.scan_interrupted = true;
result.scan_progress = `${i + 1}/${totalRepos} repositories scanned`;
saveRealTime(result, orgName, 'github_org', outputDir);
break; // Exit scanning loop
}
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
console.warn(`${colors.YELLOW}Error for repository ${colors.CYAN}${repo.name}${colors.YELLOW}: ${commitsResult.message}${colors.NC}`);
break; // Skip to next repo
}
if (!Array.isArray(commitsResult)) {
break;
}
for (const commit of commitsResult) {
if (!commit || !commit.sha) continue;
const sha = commit.sha;
if (seenCommits.has(sha)) {
continueCommitLoop = false;
break;
}
seenCommits.add(sha);
if (!commit.commit) continue;
const { author, committer } = commit.commit;
if (author && author.email) {
if (!emailsToName.has(author.email)) {
emailsToName.set(author.email, new Set());
newEmailsFound = true;
}
emailsToName.get(author.email).add(author.name || "Unknown");
// Map email to GitHub username if available
if (commit.author && commit.author.login) {
emailsToMember.set(author.email, commit.author.login);
}
// Track the repository where this email was found
if (!emailsToRepo.has(author.email)) {
emailsToRepo.set(author.email, new Set());
allLeakedEmails.push(author.email);
}
emailsToRepo.get(author.email).add(repo.name);
}
if (committer && committer.email && committer.email !== author.email) {
if (!emailsToName.has(committer.email)) {
emailsToName.set(committer.email, new Set());
newEmailsFound = true;
}
emailsToName.get(committer.email).add(committer.name || "Unknown");
// Map email to GitHub username if available
if (commit.committer && commit.committer.login) {
emailsToMember.set(committer.email, commit.committer.login);
}
// Track the repository where this email was found
if (!emailsToRepo.has(committer.email)) {
emailsToRepo.set(committer.email, new Set());
allLeakedEmails.push(committer.email);
}
emailsToRepo.get(committer.email).add(repo.name);
}
}
if (continueCommitLoop && commitsResult.length === 100) {
commitPageCounter += 1;
} else {
break;
}
}
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
// If new emails were found in this repo, update and save
if (newEmailsFound) {
// Prepare email details for real-time saving
const emailDetails = Array.from(emailsToName.entries()).map(([email, namesSet]) => ({
email,
names: Array.from(namesSet),
sources: Array.from(emailsToRepo.get(email) || []),
github_username: emailsToMember.get(email) || null
}));
result.leaked_emails = allLeakedEmails;
result.email_details = emailDetails;
result.scan_progress = `${i + 1}/${totalRepos} repositories scanned`;
saveRealTime(result, orgName, 'github_org', outputDir);
}
console.log(`${colors.GREEN}Scanned repository ${colors.YELLOW}${i + 1}/${totalRepos}${colors.GREEN}: ${colors.CYAN}${repo.name}${colors.GREEN} - Found ${colors.YELLOW}${Array.from(emailsToRepo.keys()).filter(email => emailsToRepo.get(email).has(repo.name)).length}${colors.GREEN} emails${colors.NC}`);
} catch (error) {
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
console.error(`${colors.RED}Error scanning ${repo.name}: ${error.message}${colors.NC}`);
}
}
}
// Prepare email details for display and output
const emailDetails = Array.from(emailsToName.entries()).map(([email, namesSet]) => ({
email,
names: Array.from(namesSet),
sources: Array.from(emailsToRepo.get(email) || []),
github_username: emailsToMember.get(email) || null
}));
// Display results
console.log(`\n${colors.GREEN}Reconnaissance completed:${colors.NC}`);
console.log(`${colors.GREEN}Organization: ${colors.YELLOW}${orgInfo.login} (${orgInfo.name || 'No name'})${colors.NC}`);
console.log(`${colors.GREEN}URL: ${colors.CYAN}https://github.com/${orgName}${colors.NC}`);
console.log(`${colors.GREEN}Members: ${colors.YELLOW}${members.length}${colors.NC}`);
console.log(`${colors.GREEN}Repositories: ${colors.YELLOW}${repos.length}${colors.NC}`);
console.log(`${colors.GREEN}Leaked Emails: ${colors.YELLOW}${allLeakedEmails.length}${colors.NC}`);
if (allLeakedEmails.length > 0) {
console.log(`\n${colors.YELLOW}Leaked Emails:${colors.NC}`);
// Create a more organized table format for output
const emailTable = emailDetails.map(detail => ({
email: detail.email,
names: Array.from(detail.names).join(', ').substring(0, 30) + (Array.from(detail.names).join(', ').length > 30 ? '...' : ''),
username: detail.github_username || 'Unknown',
sources: Array.from(detail.sources).length
}));
console.table(emailTable);
}
// Final update to result object
result.scan_completed_at = new Date().toISOString();
// Download avatar if requested
if (downloadAvatarFlag && orgInfo.avatar_url) {
await downloadAvatar(orgInfo.avatar_url, orgName, 'github_org');
}
// Save final output if requested
if (outputFormat) {
saveOutput(result, outputFormat, orgName, 'github_org');
}
return result;
};
// Function to run GitLab reconnaissance
const runGitlabRecon = async (username, options = {}) => {
const { downloadAvatarFlag = false, outputFormat = null } = options;
console.info(`${colors.GREEN}Running GitLab reconnaissance on user "${colors.YELLOW}${username}${colors.GREEN}"${colors.NC}`);
// Create output directory if it doesn't exist
const outputDir = path.join(process.cwd(), 'gitrecon-results');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Prepare result object with basic structure
let result = {
username: username,
scan_started_at: new Date().toISOString(),
leaked_emails: [],
email_details: [],
keys: []
};
// Fetch user ID first
const userData = await apiCall(`${GITLAB_API_URL}/users?username=${username}`);
if (!userData || userData.error || !Array.isArray(userData) || userData.length === 0) {
console.error(`${colors.RED}Error: GitLab user "${username}" not found${colors.NC}`);
return null;
}
const userId = userData[0].id;
console.log(`${colors.GREEN}Found GitLab user ID: ${colors.YELLOW}${userId}${colors.NC}`);
// Fetch profile info
const userInfo = await apiCall(`${GITLAB_API_URL}/users/${userId}`);
if (userInfo.error) {
console.error(`${colors.RED}Error fetching user info: ${userInfo.message}${colors.NC}`);
return null;
}
// Update result with user info
Object.assign(result, {
username: userInfo.username,
name: userInfo.name,
id: userInfo.id,
avatar_url: userInfo.avatar_url,
public_email: userInfo.public_email,
location: userInfo.location,
bio: userInfo.bio,
organization: userInfo.organization,
job_title: userInfo.job_title,
web_url: userInfo.web_url,
state: userInfo.state,
twitter: userInfo.twitter,
linkedin: userInfo.linkedin,
skype: userInfo.skype,
created_at: userInfo.created_at
});
// Save initial data
saveRealTime(result, username, 'gitlab', outputDir);
console.log(`${colors.GREEN}Found GitLab user: ${colors.YELLOW}${userInfo.username || username}${colors.GREEN} (${colors.YELLOW}${userInfo.name || 'No name'}${colors.GREEN})${colors.NC}`);
// Fetch status
const userStatus = await apiCall(`${GITLAB_API_URL}/users/${userId}/status`);
if (userStatus && !userStatus.error) {
result.status = userStatus.message;
saveRealTime(result, username, 'gitlab', outputDir);
}
// Fetch keys
const keys = await apiCall(`${GITLAB_API_URL}/users/${userId}/keys`);
if (Array.isArray(keys) && keys.length > 0) {
console.log(`${colors.GREEN}Found ${colors.YELLOW}${keys.length}${colors.GREEN} public SSH keys${colors.NC}`);
// Update result with keys
result.keys = keys.map(key => ({
title: key.title,
created_at: key.created_at,
expires_at: key.expires_at,
key: key.key
}));
saveRealTime(result, username, 'gitlab', outputDir);
} else {
console.log(`${colors.GREEN}No public SSH keys found${colors.NC}`);
}
// For email leaks, we need to check projects
const projects = await apiCall(`${GITLAB_API_URL}/users/${userId}/projects`);
let allLeakedEmails = [];
const emailsToName = new Map();
const emailsToProject = new Map();
if (Array.isArray(projects)) {
console.log(`${colors.GREEN}Found ${colors.YELLOW}${projects.length}${colors.GREEN} public projects${colors.NC}`);
// Limit to 10 projects to avoid rate limiting
const projectsToScan = projects.slice(0, 10);
const totalProjects = projectsToScan.length;
for (let i = 0; i < totalProjects; i++) {
const project = projectsToScan[i];
process.stdout.write(`${colors.GREEN}Scanning project ${colors.YELLOW}${i + 1}/${totalProjects}${colors.GREEN}: ${colors.CYAN}${project.name || `Project ${project.id}`}${colors.GREEN}...${colors.NC}`);
try {
const commits = await apiCall(`${GITLAB_API_URL}/projects/${project.id}/repository/commits`);
process.stdout.write('\r' + ' '.repeat(100) + '\r'); // Clear the line
// Handle empty repositories or API errors
if (!Array.isArray(commits)) {
if (commits && commits.message) {
if (commits.message.includes("404 Project Not Found") ||
commits.message.includes("Empty repository") ||
commits.message.includes("No commits")) {
console.log(`${colors.YELLOW}Project ${colors.CYAN}${project.name || `Project ${project.id}`}${colors.YELLOW} is empty or not accessible - skipping${colors.NC}`);
} else {
console.warn(`${colors.YELLOW}Error for project ${colors.CYAN}${project.name || `Project ${project.id}`}${colors.YELLOW}: ${commits.message}${colors.NC}`);
}
} else {
console.warn(`${colors.YELLOW}Error fetching commits for project ${colors.CYAN}${project.name || `Project ${project.id}`}${colors.YELLOW}${colors.NC}`);
}
continue;
}
let newEmailsCount = 0;
for (const commit of commits) {
if (commit.author_email) {
// Track the project where this email was found