Conceit
I’m working on a large scale project that will ultimately use a combo of regex and structural search to scan a set of repositories to find indicators of vulnerability- which will then be fed to an AI agent that will evaluate attack feasibility and draft issues and CVE reports for me.
Big dreams- so we’re starting small.
I decided to experiment with querying via 2 APIs some more straightforward patterns, ergo: API keys.
If I’m considering using an AI agent for some of the end result- why not focus on OpenAI api keys!
Source Graph
My first choice was Sourcegraph, because I used it for my BELSKYS GAMBIT project. Familiar terrain, easy to iterate, and the UI makes it quick to gut-check a pattern before I touch code.
patterntype:regexp (?i)\bsk-(?:proj|admin|live|test)-[A-Za-z0-9_-]{40,}\b count:9000Searching this in the GUI gives us the following results:

It looks light, and that tracks; Sourcegraph doesn’t index everything, and coverage can lag. Still, I wanted to see how it behaves from the API because “feels low” isn’t data. The Docs!
We use the following request.
search_query = f'(?i)\\bsk-(?:proj|admin|live|test)-[A-Za-z0-9_-]{{40,}}\\b["\'] count:{limit} patternType:regexp'
...
params = {
"q": search_query,
"v": "V3",
"display": 10000,
"cm": "true",
"cl": "2"
}
Run it, dump the payloads, dedupe a bit:
Extracted 67 unique API keys
Found 129 total key occurrences across 55 repositories
Search completed in 31.78 seconds.
Found 128 matching documents in total.
Not nothing! But it’s also obviously not “the internet.” Between index coverage, forks, and how people name files, it’s a slice, not the pie. Time to widen the aperture.
GitHub
Same pattern, bigger lake. Start with the direct match:
/(?i)\bsk-(?:proj|admin|live|test)-[A-Za-z0-9_-]{40,}\b/last search 21.1k results

GitHub has secret scanning, but it’s not universally on for everything/everyone, and even when it is, it’s not an API you can just hoover through at will. The general code search API does work, with the usual guardrails:
Either of these following methods DO work, but my limitation is that the API only allows for requesting up to 1000 results:
┌ [eli•ryuk] []
└ gh search code 'sk-proj-' --language python --limit 1000curl -H "Authorization: Bearer KEY" \
-H "Accept: application/vnd.GitHub.text-match+json" \
"https://api.GitHub.com/search/code?q=sk-proj-+language:python&per_page=100&page=1"So focusing on just the first 1000 results of searching just language:python
Saved 806 results to output/GitHub_keys_20250820-013547.json
Saved 672 unique keys to output/GitHub_keys_20250820-013547_keys.txt
Found 806 total matches with API keys
Unique repositories: 671
Unique keys: 672
How can we expand this? Ultimately this was accomplished via sharding the search to some degree.
# Different programming languages where API keys might be found
languages = [
'python', 'javascript', 'typescript', 'java', 'php', 'ruby', 'go',
'rust', 'csharp', 'cpp', 'c', 'shell', 'powershell'
]
# Common file extensions where API keys are stored
extensions = [
'env', 'config', 'ini', 'conf', 'properties', 'settings', 'cfg',
'toml', 'yml', 'yaml', 'json', 'txt', 'log', 'py', 'js', 'ts'
]In this way we yield roughly 6000 keys. Now THAT’S a dataset. Is it perfect? No. Is it interesting enough to validate the approach? Absolutely.
Testing
Now, how many of those are Valid? Let’s pass them through a request that will test a simple prompt. Just a ping to sort the pile.
client = OpenAI(api_key=api_key)
try:
resp = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Say hello!"}],
max_tokens=5,
)Some of these ARE valid to varying degrees- Lots of quotas met- lots of organization or other permission limitations errors.

And the final verdict is the following:

That success rate is thin, but that’s not the headline. The headline is: if I can do this over a weekend with a few scripts and some rate-limit tap-dancing, imagine what an APT with infinite funds can do. Keys rotate, sure. Quotas get burned, sure. But if you’re scraping continuously and pruning aggressively, you don’t need one golden key; you need a conveyor belt. Spread the work, keep the belt moving.
Couple of notes:
- Lots of dupes. People commit the same file to N forks. Deduping is mandatory if you don’t want a junk dataset.
- Extensions matter. Secrets love
.envand friends, but they also hide in logs, sample configs, and random JSON. Shard smart. - Index coverage is real. Sourcegraph gave me directional data; GitHub filled in the rest. Use both.
- Ethics (obvious but stated): I’m not using anyone’s keys for anything beyond validity checks. If I see a high-risk leak, I’d rather report it than be that person. Play Nice.
This was the “keys” chapter of the broader idea. The next chapters are where it gets fun: composable patterns that point at vuln classes, light structural search to reduce false positives, and an agent that doesn’t just yell “maybe bad!” but also drafts a coherent issue with reproduction steps. That’s the goal. This is the proof that the plumbing is worth building.