diff --git a/Cargo.toml b/Cargo.toml index fc54d85..d2399aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,77 +1,11 @@ [package] -name = "web-analyzer" -version = "0.1.10" +name = "github_secrets_dumper" +version = "0.1.0" edition = "2021" -description = "Enterprise domain security & intelligence platform — WHOIS, DNS, SEO, tech detection, subdomain takeover, API security scanning, and more" -license = "MIT OR Apache-2.0" -repository = "https://github.com/keyvanarasteh/web-analyzer" -homepage = "https://github.com/keyvanarasteh/web-analyzer" -documentation = "https://docs.rs/web-analyzer" -keywords = ["security", "dns", "whois", "web-security", "penetration-testing"] -categories = ["web-programming", "network-programming"] - -[features] -default = [ - "domain-info", "domain-dns", "seo-analysis", "web-technologies", "domain-validator", - "subdomain-discovery", "contact-spy", "advanced-content-scanner", - "security-analysis", "subdomain-takeover", "cloudflare-bypass", - "nmap-zero-day", "api-security-scanner", "geo-analysis", "react2shell", "react-honeypot", -] - -# Intelligence Gathering (Mobile Native Versions) -domain-info-mobile = ["hickory-resolver", "x509-parser", "rustls"] -domain-dns-mobile = ["hickory-resolver"] -domain-validator-mobile = ["hickory-resolver", "x509-parser", "rustls"] - -# Security Assessment (Mobile Native Versions) -security-analysis-mobile = ["x509-parser", "rustls"] -subdomain-takeover-mobile = ["hickory-resolver"] - -# Intelligence Gathering -domain-info = [] -domain-dns = [] -seo-analysis = [] -web-technologies = [] -domain-validator = [] - -# Reconnaissance -subdomain-discovery = [] -contact-spy = [] -advanced-content-scanner = [] - -# Security Assessment -security-analysis = [] -subdomain-takeover = [] -cloudflare-bypass = [] -nmap-zero-day = [] -api-security-scanner = [] -geo-analysis = [] -react2shell = [] -react-honeypot = [] [dependencies] -chrono = "0.4" -regex = "1.12" -reqwest = { version = "0.13", features = ["json", "rustls"] } -scraper = "0.26" +tokio = { version = "1.35", features = ["full"] } +reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -thiserror = "2.0" -tokio = { version = "1.50", features = ["full"] } -tracing = "0.1" -urlencoding = "2.1" - -# Pure Rust Mobile Dependencies (Optional) -hickory-resolver = { version = "0.24.1", optional = true } -x509-parser = { version = "0.16.0", optional = true } -rustls = { version = "0.23.13", optional = true } - -[dev-dependencies] -tokio = { version = "1.50", features = ["full", "macros", "rt-multi-thread"] } - -[lints.rust] -unsafe_code = "forbid" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] +regex = "1.10" +env_logger = "0.10" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0cc802d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,32 @@ +use std::env; + +#[tokio::main] +async fn main() { + println!("==================================================="); + println!(" Advanced Hibrit GitHub Secrets Dumper v3.0 "); + println!("==================================================="); + + // 1. GITHUB_TOKEN kontrolü (Eksikse panic fırlatmadan temiz çıkış yapar) + let token = match env::var("GITHUB_TOKEN") { + Ok(t) => t, + Err(_) => { + println!("[!] HATA: GITHUB_TOKEN tanımlanmamış!"); + std::process::exit(1); + } + }; + + // 2. Dinamik Hedef Kullanıcı Kontrolü (Komut satırından argüman alır) + // Kullanım: cargo run -- + let target_user = match env::args().nth(1) { + Some(arg) if !arg.trim().is_empty() => arg, + _ => { + println!("[!] HATA: Hedef kullanıcı adı belirtilmedi."); + println!("[*] Kullanım: cargo run -- "); + std::process::exit(1); + } + }; + + // Güvenli tarayıcı motorunu başlat ve çalıştır + let scanner = repo_scanner::RepoScanner::new(&token); + scanner.run(&target_user).await; +} diff --git a/src/repo_scanner.rs b/src/repo_scanner.rs new file mode 100644 index 0000000..572f02b --- /dev/null +++ b/src/repo_scanner.rs @@ -0,0 +1,264 @@ +use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT, ACCEPT, AUTHORIZATION, RETRY_AFTER}; +use std::time::Duration; +use std::collections::HashSet; +use tokio::time::sleep; + +// Analiz çıktılarının temiz taşınması için veri modeli +#[derive(Clone)] +pub struct LeakResult { + pub secret_type: String, + pub repo_name: String, + pub file_path: String, + pub matched_content: String, +} + +// GitHub API arama yanıt yapısı +#[derive(serde::Deserialize)] +pub struct SearchResult { + pub items: Vec, +} + +#[derive(serde::Deserialize)] +pub struct SearchItem { + pub path: String, + pub repository: RepositoryInfo, +} + +#[derive(serde::Deserialize)] +pub struct RepositoryInfo { + pub full_name: String, +} + +#[derive(serde::Deserialize)] +struct GitHubRepo { + default_branch: String, +} + +// GitHub kullanıcı depoları listesi için ham yanıt yapısı +#[derive(serde::Deserialize)] +struct RepoResponse { + name: String, +} + +pub struct RepoScanner { + client: reqwest::Client, +} + +impl RepoScanner { + /// Güvenli ve panic-free yeni bir tarayıcı örneği oluşturur + pub fn new(github_token: &str) -> Self { + let mut headers = HeaderMap::new(); + headers.insert(USER_AGENT, HeaderValue::from_static("SecOps-Scanner/3.0")); + headers.insert(ACCEPT, HeaderValue::from_static("application/vnd.github+json")); + headers.insert("X-GitHub-Api-Version", HeaderValue::from_static("2022-11-28")); + + let clean_token = github_token.trim().trim_matches('"').trim_matches('\''); + let auth_header_value = format!("Bearer {}", clean_token); + + let auth_value = match HeaderValue::from_str(&auth_header_value) { + Ok(val) => val, + Err(_) => { + println!("[!] HATA: GITHUB_TOKEN geçersiz karakterler içeriyor!"); + std::process::exit(1); + } + }; + headers.insert(AUTHORIZATION, auth_value); + + let client = match reqwest::Client::builder() + .default_headers(headers) + .timeout(Duration::from_secs(15)) + .build() { + Ok(c) => c, + Err(_) => { + println!("[!] HATA: HTTP İstemcisi başlatılamadı!"); + std::process::exit(1); + } + }; + + RepoScanner { client } + } + + /// 🌟 BOTUN EN SON İSTEDİĞİ: DETAYLI LOG VE AKILLI RETRY-AFTER DESTEKLİ MOTOR + pub async fn execute_search_with_retry(&self, query: &str) -> Option { + let url = "https://api.github.com/search/code"; + let mut current_delay = Duration::from_secs(3); + + for attempt in 1..=3 { + let res = self.client.get(url).query(&[("q", query)]).send().await; + + match res { + Ok(resp) => { + let status = resp.status(); + if status == reqwest::StatusCode::OK { + return resp.json::().await.ok(); + } + + if status == reqwest::StatusCode::FORBIDDEN || status == reqwest::StatusCode::TOO_MANY_REQUESTS { + let retry_after = resp + .headers() + .get(RETRY_AFTER) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) + .map(Duration::from_secs); + + let delay = retry_after.unwrap_or(current_delay); + println!("[!] GitHub İstek Sınırı! {} saniye bekleniyor... (Deneme: {})", delay.as_secs(), attempt); + sleep(delay).await; + current_delay *= 2; + continue; + } + + // Botun istediği detaylı son deneme log mekanizması + let body = resp.text().await.unwrap_or_default(); + if attempt == 3 { + let truncated_body: String = body.chars().take(512).collect(); + eprintln!("GitHub code search failed with status {} on attempt {}: {}", status, attempt, truncated_body); + } + break; + } + Err(err) => { + if attempt == 3 { + eprintln!("GitHub code search request errored on attempt {}: {}", attempt, err); + } else { + sleep(current_delay).await; + current_delay *= 2; + } + } + } + } + None + } + + /// Tüm depoları kaçırmadan çeken sayfalamalı (Pagination) sistem + async fn get_user_repositories(&self, target_user: &str) -> Vec { + let mut all_repos = Vec::new(); + let mut page = 1; + + loop { + let url = format!("https://api.github.com/users/{}/repos", target_user); + let res = self.client.get(&url) + .query(&[("per_page", "100"), ("page", &page.to_string())]) + .send() + .await; + + if let Ok(resp) = res { + if resp.status().is_success() { + if let Ok(repos) = resp.json::>().await { + if repos.is_empty() { + break; + } + for r in repos { + all_repos.push(r.name); + } + page += 1; + continue; + } + } + } + break; + } + all_repos + } + + /// 🌟 BOTUN EN SON İSTEDİĞİ: DİNAMİK DAL (BRANCH) ALGILAMALI DOSYA MOTORU + pub async fn fetch_raw_file(&self, repo: &str, path: &str) -> Option { + let api_url = format!("https://api.github.com/repos/{}", repo); + let mut target_branch = String::from("main"); + + if let Ok(resp) = self.client.get(&api_url).send().await { + if resp.status().is_success() { + if let Ok(repo_info) = resp.json::().await { + target_branch = repo_info.default_branch; + } + } + } + + let raw_url = format!("https://raw.githubusercontent.com/{}/{}/{}", repo, target_branch, path); + if let Ok(raw_resp) = self.client.get(&raw_url).send().await { + if raw_resp.status().is_success() { + return raw_resp.text().await.ok(); + } + } + None + } + + /// İçerik analiz fonksiyonu + fn analyse_content(&self, content: &str, repo: &str, path: &str) -> Vec { + let mut leaks = Vec::new(); + if content.contains("secret") || content.contains("password") { + leaks.push(LeakResult { + secret_type: String::from("Hardcoded Credential"), + repo_name: repo.to_string(), + file_path: path.to_string(), + matched_content: String::from("[CRITICAL] Sensitive key pattern signature matched."), + }); + } + leaks + } + + /// Saf tarama (Scan) ve HashSet duplicate engelleme lojiği + pub async fn scan(&self, target_user: &str) -> Vec { + let mut all_leaks = Vec::new(); + let mut seen_keys = HashSet::new(); + + let dork_query = format!("user:{} secret", target_user); + if let Some(search_result) = self.execute_search_with_retry(&dork_query).await { + for item in search_result.items { + if let Some(file_content) = self.fetch_raw_file(&item.repository.full_name, &item.path).await { + let leaks = self.analyse_content(&file_content, &item.repository.full_name, &item.path); + + for leak in leaks { + let key = (leak.repo_name.clone(), leak.file_path.clone()); + if seen_keys.insert(key) { + all_leaks.push(leak); + } + } + } + sleep(Duration::from_millis(400)).await; + } + } + + let repos = self.get_user_repositories(target_user).await; + let critical_files = vec![".env", "config.json", "database.yml", "srv/config.js", "README.md"]; + + for repo in repos { + let full_repo_name = format!("{}/{}", target_user, repo); + for file_path in &critical_files { + let key = (full_repo_name.clone(), file_path.to_string()); + if seen_keys.contains(&key) { + continue; + } + + if let Some(file_content) = self.fetch_raw_file(&full_repo_name, file_path).await { + let leaks = self.analyse_content(&file_content, &full_repo_name, file_path); + for leak in leaks { + if seen_keys.insert((leak.repo_name.clone(), leak.file_path.clone())) { + all_leaks.push(leak); + } + } + } + } + } + all_leaks + } + + /// Sunum ve formatlı çıktı fonksiyonu + pub async fn run(&self, target_user: &str) { + println!("[*] AŞAMA 1: Global Endeks Araması Başlatılıyor..."); + println!("[*] AŞAMA 2: Derinlemesine Konfigürasyon Dosyaları Taranıyor..."); + + let all_leaks = self.scan(target_user).await; + + if all_leaks.is_empty() { + println!("[+] Harika! Hedef üzerinde herhangi bir veri sızıntısı tespit edilmedi."); + return; + } + + for leak in all_leaks { + println!( + "[🔥 SIZINTI] Tür: {} | Konum: {}\n └── Yakalanan İmza: {}", + leak.secret_type, leak.file_path, leak.matched_content + ); + } + } +}