github_secrets_dumper - #16
Open
begumakyuz wants to merge 7 commits into
Open
Conversation
Reviewer's GuideImplements a new standalone github_secrets_dumper crate that scans GitHub code search results and critical repo files for leaked secrets using regex signatures, with GitHub API access configured via an environment-provided token and a Tokio/reqwest async client. Sequence diagram for github_secrets_dumper scanning workflowsequenceDiagram
actor User
participant Main as main
participant RepoScanner
participant GitHubAPI
participant RawContent as RawGit
User->>Main: run binary
Main->>Main: std::env::var GITHUB_TOKEN
Main->>RepoScanner: RepoScanner::new token
Main->>RepoScanner: run target_user
loop global_dorks
RepoScanner->>RepoScanner: generate_global_dorks target_user
RepoScanner->>GitHubAPI: execute_search_with_retry q
GitHubAPI-->>RepoScanner: SearchResponse
loop search items
RepoScanner->>RawContent: fetch_raw_file repo path
RawContent-->>RepoScanner: file content
RepoScanner->>RepoScanner: analyze_content content repo path
end
end
RepoScanner->>GitHubAPI: get_user_repositories target_user
GitHubAPI-->>RepoScanner: repo list
loop critical_files
RepoScanner->>RawContent: fetch_raw_file repo file_path
RawContent-->>RepoScanner: file content
RepoScanner->>RepoScanner: analyze_content content repo file_path
end
RepoScanner-->>Main: Vec<LeakResult>
Main-->>User: print summary and detailed findings
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 8 issues, and left some high level feedback:
- RepoScanner::new currently uses several
unwrap()calls when building headers and the client, which will crash the process on malformed input; consider returning a Result fromnewand handling these failures more gracefully. - The
target_useris hard-coded inmain, which makes the binary inflexible; it would be better to accept this as a CLI argument or environment variable so callers can specify the target at runtime. - You’ve added
env_loggeras a dependency but still useprintln!for operational output; either removeenv_loggeror wire it up and replace the prints with structured logging to keep output consistent and configurable.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- RepoScanner::new currently uses several `unwrap()` calls when building headers and the client, which will crash the process on malformed input; consider returning a Result from `new` and handling these failures more gracefully.
- The `target_user` is hard-coded in `main`, which makes the binary inflexible; it would be better to accept this as a CLI argument or environment variable so callers can specify the target at runtime.
- You’ve added `env_logger` as a dependency but still use `println!` for operational output; either remove `env_logger` or wire it up and replace the prints with structured logging to keep output consistent and configurable.
## Individual Comments
### Comment 1
<location path="src/repo_scanner.rs" line_range="46-47" />
<code_context>
+ headers.insert("X-GitHub-Api-Version", HeaderValue::from_static("2022-11-28"));
+
+ // Terminalden gelen çift tırnak (") ve tek tırnak (') işaretlerini tamamen temizler
+ let clean_token = github_token.trim().trim_matches('"').trim_matches('\'');
+ let auth_header_value = format!("Bearer {}", clean_token);
+ headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header_value).unwrap());
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid panicking on invalid AUTHORIZATION header construction.
`HeaderValue::from_str(&auth_header_value).unwrap()` will panic if the token contains invalid header characters (e.g., non-ASCII or control chars). Prefer handling this error explicitly, e.g. by returning a `Result<Self, Error>` from `RepoScanner::new` and propagating it, or by logging the failure and exiting cleanly so a misconfigured env var doesn’t crash the process with a panic.
</issue_to_address>
### Comment 2
<location path="src/repo_scanner.rs" line_range="50-53" />
<code_context>
+ let auth_header_value = format!("Bearer {}", clean_token);
+ headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header_value).unwrap());
+
+ let client = reqwest::Client::builder()
+ .default_headers(headers)
+ .timeout(Duration::from_secs(15))
+ .build()
+ .unwrap();
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Returning `RepoScanner::new` as infallible hides possible HTTP client setup errors.
Both the header construction and `Client::builder().build()` can fail, and the current `unwrap` use will panic on error. Since this type is the entry point to the scanner, consider changing `RepoScanner::new` to return `Result<Self, E>` and propagating initialization errors so the caller (e.g. `main`) can handle them and exit cleanly instead of panicking.
Suggested implementation:
```rust
// Terminalden gelen çift tırnak (") ve tek tırnak (') işaretlerini tamamen temizler
let clean_token = github_token.trim().trim_matches('"').trim_matches('\'');
let auth_header_value = format!("Bearer {}", clean_token);
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header_value)?);
```
```rust
let client = reqwest::Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(15))
.build()?;
```
1. Change the signature of `RepoScanner::new` to return a `Result` instead of `Self`. For example:
- From:
`pub fn new(github_token: &str, /* ... */) -> Self {`
- To:
`pub fn new(github_token: &str, /* ... */) -> anyhow::Result<Self> {`
or another error type appropriate for your codebase.
2. Add the corresponding import at the top of the file if you use `anyhow` (or adjust for your error type):
- `use anyhow::Result;`
3. Ensure that the body of `RepoScanner::new` now returns `Ok(RepoScanner { /* fields, including client */ })` at the end, so the `?` operator compiles correctly.
4. Update all call sites of `RepoScanner::new` (e.g. in `main`) to handle the `Result`, typically with `?` in functions that return `Result`, or by matching/`expect`ing an error to exit cleanly.
</issue_to_address>
### Comment 3
<location path="src/repo_scanner.rs" line_range="82-91" />
<code_context>
+ 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;
+
+ if let Ok(resp) = res {
+ if resp.status() == reqwest::StatusCode::OK {
+ return resp.json::<SearchResponse>().await.ok();
+ } else if resp.status() == reqwest::StatusCode::FORBIDDEN || resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
+ sleep(current_delay).await;
+ current_delay *= 2;
+ }
+ }
+ }
+ None
+ }
+
</code_context>
<issue_to_address>
**suggestion:** Search retry logic silently swallows error details and ignores `Retry-After` hints.
Right now the loop only special-cases 403/429 with a fixed backoff and drops all response details. I’d suggest:
- When the final attempt fails, log/return the status and a truncated body so callers can see why the search failed.
- For 403/429, read and honor the `Retry-After` header when present instead of always using the exponential delay. This will play nicer with GitHub’s rate limiting and abuse detection.
Suggested implementation:
```rust
async fn execute_search_with_retry(&self, query: &str) -> Option<SearchResponse> {
let url = "https://api.github.com/search/code";
let mut current_delay = Duration::from_secs(3);
// Retry up to 3 times, honoring GitHub rate limiting hints when possible.
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 {
// Successful response; parse and return.
return resp.json::<SearchResponse>().await.ok();
}
// Handle rate limiting / abuse detection with Retry-After when present.
if status == reqwest::StatusCode::FORBIDDEN
|| status == reqwest::StatusCode::TOO_MANY_REQUESTS
{
// Try to honor Retry-After, falling back to exponential backoff.
let retry_after = resp
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.map(Duration::from_secs);
let delay = retry_after.unwrap_or(current_delay);
sleep(delay).await;
current_delay *= 2;
// Continue to next attempt.
continue;
}
// For non-retriable statuses, read and log a truncated body on the final attempt.
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 out; nothing more to do for other status codes.
break;
}
Err(err) => {
// Network / client error; log on final attempt and then give up.
if attempt == 3 {
eprintln!(
"GitHub code search request errored on attempt {}: {}",
attempt,
err
);
} else {
// Brief backoff before retrying transient errors.
sleep(current_delay).await;
current_delay *= 2;
}
}
}
}
None
}
```
If your project uses a structured logging framework (e.g., `log`, `tracing`), replace the `eprintln!` calls with the appropriate logging macros (`warn!`, `error!`, etc.) and ensure the corresponding `use` statements and dependencies are in place to match your existing logging conventions.
</issue_to_address>
### Comment 4
<location path="src/repo_scanner.rs" line_range="126-135" />
<code_context>
+ }
+
+ async fn get_user_repositories(&self, target_user: &str) -> Vec<String> {
+ let url = format!("https://api.github.com/users/{}/repos", target_user);
+ let mut repo_list = Vec::new();
+
+ #[derive(Deserialize)]
+ struct RepoInfo { name: String }
+
+ if let Ok(resp) = self.client.get(&url).send().await {
+ if let Ok(repos) = resp.json::<Vec<RepoInfo>>().await {
+ for r in repos {
+ repo_list.push(r.name);
+ }
+ }
+ }
+ repo_list
+ }
+
</code_context>
<issue_to_address>
**issue:** User repositories listing does not handle pagination and may miss many repos.
This endpoint is paginated (30 repos by default, max 100 per page), so this function only processes the first page and ignores additional repos. Please add pagination handling (e.g., follow the `Link` header or loop over `page`/`per_page`) so all public repositories are included.
</issue_to_address>
### Comment 5
<location path="src/repo_scanner.rs" line_range="98-94" />
<code_context>
+ }
+
+ async fn fetch_raw_file(&self, repo: &str, path: &str) -> Option<String> {
+ let branches = vec!["main", "master"];
+ for branch in branches {
+ let raw_url = format!("https://raw.githubusercontent.com/{}/{}/{}", repo, branch, path);
+ if let Ok(resp) = self.client.get(&raw_url).send().await {
+ if resp.status().is_success() {
+ return resp.text().await.ok();
+ }
+ }
+ }
+ None
+ }
+
</code_context>
<issue_to_address>
**suggestion:** Hard-coding `main`/`master` may miss files in repos with different default branches.
Because this only checks `main` and `master`, repos with other default branches (e.g., `develop`, `trunk`, custom names) will never resolve files. Consider looking up `default_branch` from the repo API and using that, or falling back to the API contents endpoint instead of raw URLs.
Suggested implementation:
```rust
async fn fetch_raw_file(&self, repo: &str, path: &str) -> Option<String> {
// First, try to resolve the repository's default branch via the GitHub API.
#[derive(Deserialize)]
struct GitHubRepo {
default_branch: String,
}
// Try to get the default branch from the repo metadata
let api_url = format!("https://api.github.com/repos/{}", repo);
if let Ok(resp) = self.client.get(&api_url).send().await {
if resp.status().is_success() {
if let Ok(repo_info) = resp.json::<GitHubRepo>().await {
let raw_url = format!(
"https://raw.githubusercontent.com/{}/{}/{}",
repo, repo_info.default_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();
}
}
}
}
}
// Fallback: use the contents API, which resolves the correct branch server-side.
#[derive(Deserialize)]
struct ContentResponse {
content: String,
encoding: String,
}
let contents_url = format!("https://api.github.com/repos/{}/contents/{}", repo, path);
if let Ok(resp) = self.client.get(&contents_url).send().await {
if resp.status().is_success() {
if let Ok(content_resp) = resp.json::<ContentResponse>().await {
if content_resp.encoding == "base64" {
if let Ok(bytes) =
base64::engine::general_purpose::STANDARD.decode(content_resp.content)
{
if let Ok(text) = String::from_utf8(bytes) {
return Some(text);
}
}
}
}
}
}
None
}
```
1. Ensure `base64` is included as a dependency in `Cargo.toml` (e.g., `base64 = "0.22"` or the version already used in your project).
2. Add the necessary imports at the top of `src/repo_scanner.rs` if they are not present yet:
- `use base64::engine::general_purpose;`
- `use base64::Engine;`
3. If your project already defines shared structs for GitHub repo or contents responses, reuse those instead of the inline `GitHubRepo`/`ContentResponse` definitions for consistency.
</issue_to_address>
### Comment 6
<location path="src/main.rs" line_range="17-21" />
<code_context>
+ }
+ };
+
+ // 🎯 BURAYA TARAMAK İSTEDİĞİN KİŞİNİN KULLANICI ADINI YAZABİLİRSİN
+ let target_user = "hedef_kullanici_adi";
+
+ let scanner = repo_scanner::RepoScanner::new(&token);
</code_context>
<issue_to_address>
**suggestion:** Hard-coded target user limits usability and encourages editing source to run scans.
Requiring source edits to set the target username hurts automation and integration. Consider reading the target from CLI args (e.g. `std::env::args()`), a config file, or an environment variable, and exiting with a clear error if it’s not provided. This also avoids accidentally committing a hard-coded target.
```suggestion
// 🎯 Hedef kullanıcı adını komut satırı argümanlarından al
// Örnek: cargo run -- hedef_kullanici_adi
let target_user = match std::env::args().nth(1) {
Some(arg) if !arg.trim().is_empty() => arg,
_ => {
println!("[!] HATA: Hedef kullanıcı adı belirtilmedi.");
println!("[*] Kullanım: cargo run -- <github_kullanici_adi>");
return;
}
};
let scanner = repo_scanner::RepoScanner::new(&token);
let sonuclar = scanner.run(target_user.as_str()).await;
```
</issue_to_address>
### Comment 7
<location path="src/main.rs" line_range="11-13" />
<code_context>
+
+ let token = match std::env::var("GITHUB_TOKEN") {
+ Ok(t) => t,
+ Err(_) => {
+ println!("[!] HATA: GITHUB_TOKEN tanımlanmamış!");
+ return;
+ }
+ };
</code_context>
<issue_to_address>
**issue (bug_risk):** Exiting with a success status when `GITHUB_TOKEN` is missing can mislead automation.
Because `main` returns normally here, the process exits with code 0, so CI/scripts will treat this as success even though the scan never ran. Return a non‑zero exit code instead (e.g., via `std::process::exit(1)` or by propagating an error) so misconfiguration is detectable.
</issue_to_address>
### Comment 8
<location path="src/repo_scanner.rs" line_range="142" />
<code_context>
+ repo_list
+ }
+
+ pub async fn run(&self, target_user: &str) -> Vec<LeakResult> {
+ let mut all_leaks = Vec::new();
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting a pure scanning function from `run` and using a `HashSet` for duplicate detection to separate concerns and avoid repeated linear scans.
You can reduce complexity without changing behavior by:
### 1. Split scanning from logging in `run`
`run` is currently doing orchestration, scanning, rate limiting, and user-facing output. You can keep the public API intact and still separate concerns by introducing a pure “scan only” method and making `run` a thin wrapper that only handles printing.
```rust
pub async fn scan(&self, target_user: &str) -> Vec<LeakResult> {
let mut all_leaks = Vec::new();
let dorks = self.generate_global_dorks(target_user);
for dork in dorks {
if let Some(search_result) = self.execute_search_with_retry(&dork).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.analyze_content(
&file_content,
&item.repository.full_name,
&item.path,
);
all_leaks.extend(leaks);
}
sleep(Duration::from_millis(400)).await;
}
}
}
let repos = self.get_user_repositories(target_user).await;
let critical_files = vec![".env", "config.json", "database.yml", "src/config.js", "README.md"];
for repo in repos {
let full_repo_name = format!("{}/{}", target_user, repo);
for file_path in &critical_files {
if let Some(file_content) = self.fetch_raw_file(&full_repo_name, file_path).await {
let leaks = self.analyze_content(&file_content, &full_repo_name, file_path);
all_leaks.extend(leaks);
}
}
}
all_leaks
}
```
Then `run` becomes responsible only for presentation (keeping the same behavior):
```rust
pub async fn run(&self, target_user: &str) -> Vec<LeakResult> {
println!("[*] AŞAMA 1: Global İndeks Araması Başlatılıyor...");
println!("\n[*] AŞAMA 2: İndeks Dışı Riskli Dosyalar Taranıyor (Derin Tarama)...");
let mut all_leaks = self.scan(target_user).await;
// print global leaks
for leak in &all_leaks {
println!(
"[🚨 SIZINTI] Tür: {} | Konum: {}\n └── Yakalanan Key: \x1b[31m{}\x1b[0m",
leak.secret_type, leak.file_path, leak.matched_content
);
}
all_leaks
}
```
This keeps the external behavior and output intact, but makes it easier to test `scan` independently, reuse the scanning logic (e.g., from a library API), and adjust logging/formatting without touching the core logic.
### 2. Simplify duplicate detection with a `HashSet`
The deep-scan duplicate check:
```rust
if !all_leaks.iter().any(|l| l.repo == leak.repo && l.file_path == leak.file_path) {
// ...
all_leaks.push(leak);
}
```
does an O(n) scan for every leak. Using a `HashSet` of keys makes intent clearer and avoids repeated scans:
```rust
use std::collections::HashSet;
// before loops
let mut seen_keys = HashSet::new();
// inside deep scan, instead of iter().any(...)
let key = (leak.repo.clone(), leak.file_path.clone());
if seen_keys.insert(key) {
println!(
"[🚨 DERİN TARAMA SIZINTISI] Tür: {} | Konum: {}\n └── Yakalanan Key: \x1b[31m{}\x1b[0m",
leak.secret_type, leak.file_path, leak.matched_content
);
all_leaks.push(leak);
}
```
You can also reuse the same `seen_keys` in the global phase to enforce the same “no duplicates by (repo, path)” rule across both phases.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Replace return with std::process::exit(1) for error handling.
Updated target user input to accept command line argument.
Refactor RepoScanner with improved error handling and logging.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR introduces an advanced hybrid GitHub Secrets Dumper module to identify critical data exposure and log leakage risks under the specified scope. The core implementation scans both global code indexes via optimized GitHub Search API queries (with exponential backoff retry mechanics) and local critical environment configurations (e.g.,
.env,config.json) recursively.All sensitive operational tokens are completely isolated and passed securely via standard environment variables ($env:GITHUB_TOKEN), preventing hardcoded credential leakage.
Fixes # (No specific issue, task assignment implementation)
Type of change
How Has This Been Tested?
The module has been rigorously verified on a local system architecture to guarantee stability and prevent rate-limiting or unwrap panics during the API handshake.
cargo check.cargo build.Checklist:
cargo fmt)docs/)cargo clippy)Summary by Sourcery
Introduce a new github_secrets_dumper crate that scans GitHub repositories for leaked secrets using the GitHub API and local file heuristics.
New Features:
Enhancements: