From 7e848073cfdb8d5bb2d793f816195c1b1e1c30df Mon Sep 17 00:00:00 2001 From: techgaun Date: Fri, 28 Aug 2026 16:41:55 -0500 Subject: [PATCH] chore: revive project foundations --- .github/workflows/docker-build.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 21 +++++++ .gitignore | 7 +++ Dockerfile | 18 ++---- README.md | 14 ++++- github-dork.py | 33 +++++++---- requirements.txt | 4 +- setup.py | 5 +- tests/test_github_dork.py | 94 ++++++++++++++++++++++++++++++ 10 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 tests/test_github_dork.py diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 8476c21..54cc305 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -17,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . load: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 038f261..b616495 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.12' - name: Install flake8 run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..491c773 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: Tests + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install . + - run: python -m unittest discover -s tests -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..179ad9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ +dist/ +build/ +*.egg-info/ diff --git a/Dockerfile b/Dockerfile index 0d8ecb9..92e04ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,15 @@ -# Use Python 3.8 as base - this version has good compatibility with older packages -FROM python:3.8-slim +FROM python:3.12-slim # Set working directory WORKDIR /app -# Install git (needed for pip install from git repos) -RUN apt-get update && \ - apt-get install -y git && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# Copy only the necessary files +# Copy only the files needed to install and run the project COPY github-dork.py /app/ COPY github-dorks.txt /app/ COPY setup.py /app/ COPY README.md /app/ -COPY requirements.txt /app/ -# Install dependencies -# Using the specific version of github3.py that's known to work -RUN pip install --no-cache-dir github3.py==1.0.0a2 feedparser==6.0.2 +RUN pip install --no-cache-dir . # Set environment variables ENV PYTHONUNBUFFERED=1 @@ -28,4 +18,4 @@ ENV PYTHONIOENCODING=UTF-8 # Create volume for potential output files VOLUME ["/app/output"] -ENTRYPOINT ["python", "github-dork.py"] \ No newline at end of file +ENTRYPOINT ["python", "github-dork.py"] diff --git a/README.md b/README.md index eb36e4f..a9f2f74 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Docker Build & Test](https://github.com/techgaun/github-dorks/actions/workflows/docker-build.yml/badge.svg)](https://github.com/techgaun/github-dorks/actions/workflows/docker-build.yml) -# Github Dorks +# GitHub Dorks [Github Search](https://github.com/search) is a quite powerful and useful feature that can be used to search for sensitive data on repositories. Collection of Github dorks can reveal sensitive personal and/or organizational information such as private keys, credentials, authentication tokens, etc. This list is supposed to be useful for assessing security and performing pen-testing of systems. @@ -61,10 +61,20 @@ GH_TOKEN= github-dork.py -u dev-nepal # search using GH_URL=https://github.example.com github-dork.py -u dev-nepal # search a GitHub Enterprise instance ``` +### Development + +Run the dependency-free unit test suite with: + +```shell +python -m unittest discover -s tests -v +``` + +The CI test matrix covers Python 3.10 through 3.13. + ### Limitations - Authenticated requests get a higher rate limit. But, since this tool waits for the api rate limit to be reset (which is usually less than a minute), it can be slightly slow. -- Output formatting is not great. PR welcome +- Search results can be printed to the terminal or written as CSV. - ~~Handle rate limit and retry. PR welcome~~ ### Contribution diff --git a/github-dork.py b/github-dork.py index 2941b04..72e1bea 100644 --- a/github-dork.py +++ b/github-dork.py @@ -4,11 +4,15 @@ import github3 as github import os import argparse +import csv import time import feedparser from copy import copy +from contextlib import nullcontext from sys import stderr, prefix +__version__ = '0.1.1' + gh_user = os.getenv('GH_USER', None) gh_pass = os.getenv('GH_PWD', None) gh_token = os.getenv('GH_TOKEN', None) @@ -93,7 +97,7 @@ def search(repo_to_search=None, gh_dorks_file = filename break - if not os.path.isfile(gh_dorks_file): + if gh_dorks_file is None or not os.path.isfile(gh_dorks_file): raise Exception('Error, the dorks file path is not valid') if user_to_search: print("Scanning User: ", user_to_search) @@ -101,14 +105,20 @@ def search(repo_to_search=None, print("Scanning Repo: ", repo_to_search) found = False - outputFile = None - if output_filename: - outputFile = open(output_filename, 'w') + output_context = ( + open(output_filename, 'w', newline='', encoding='utf-8') + if output_filename else nullcontext(None) + ) - with open(gh_dorks_file, 'r') as dork_file: + with open(gh_dorks_file, 'r', encoding='utf-8') as dork_file, output_context as output_file: # Write CSV Header - if outputFile: - outputFile.write('Issue Type (Dork), Text Matches, File Path, Score/Relevance, URL of File\n') + csv_writer = None + if output_file: + csv_writer = csv.writer(output_file) + csv_writer.writerow([ + 'Issue Type (Dork)', 'Text Matches', 'File Path', + 'Score/Relevance', 'URL of File' + ]) for dork in dork_file: dork = dork.strip() if not dork or dork[0] in '#;': @@ -133,8 +143,11 @@ def search(repo_to_search=None, } # Either write to file or print output - if outputFile: - outputFile.write('{dork}, {text_matches}, {path}, {score}, {url}\n'.format(**fmt_args)) + if csv_writer: + csv_writer.writerow([ + fmt_args['dork'], fmt_args['text_matches'], + fmt_args['path'], fmt_args['score'], fmt_args['url'] + ]) else: result = '\n'.join([ 'Found result for {dork}', @@ -161,7 +174,7 @@ def main(): epilog='Use responsibly, Enjoy pentesting') parser.add_argument( - '-v', '--version', action='version', version='%(prog)s 0.1.1') + '-v', '--version', action='version', version='%(prog)s ' + __version__) group = parser.add_mutually_exclusive_group(required=True) group.add_argument( diff --git a/requirements.txt b/requirements.txt index cfe346b..31ad16d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -github3.py==1.0.0a2 -feedparser==6.0.2 +github3.py==4.0.1 +feedparser>=6.0.12,<7 diff --git a/setup.py b/setup.py index 979e932..8563c59 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='github-dorks', - version='0.1', + version='0.1.1', description='Find leaked secrets via github search.', license='Apache License 2.0', long_description=long_description, @@ -13,8 +13,9 @@ long_description_content_type='text/markdown', scripts=['github-dork.py'], data_files=[('github-dorks', ['github-dorks.txt'])], + python_requires='>=3.10', install_requires=[ 'github3.py==4.0.1', - 'feedparser==6.0.2', + 'feedparser>=6.0.12,<7', ], ) diff --git a/tests/test_github_dork.py b/tests/test_github_dork.py new file mode 100644 index 0000000..2127a7e --- /dev/null +++ b/tests/test_github_dork.py @@ -0,0 +1,94 @@ +import csv +import importlib.util +import io +import sys +import tempfile +import types +import unittest +from contextlib import redirect_stdout +from pathlib import Path +from unittest.mock import patch + + +class FakeGitHubError(Exception): + pass + + +class FakeForbiddenError(FakeGitHubError): + pass + + +fake_github3 = types.ModuleType('github3') +fake_github3.GitHub = lambda **kwargs: None +fake_github3.GitHubEnterprise = lambda **kwargs: None +fake_github3.exceptions = types.SimpleNamespace( + ForbiddenError=FakeForbiddenError, + GitHubError=FakeGitHubError, +) +sys.modules.setdefault('github3', fake_github3) +sys.modules.setdefault('feedparser', types.ModuleType('feedparser')) + +spec = importlib.util.spec_from_file_location( + 'github_dork', Path(__file__).parents[1] / 'github-dork.py' +) +github_dork = importlib.util.module_from_spec(spec) +spec.loader.exec_module(github_dork) + + +class SearchResult: + text_matches = ['secret, with comma'] + path = 'config/file.env' + score = 42 + html_url = 'https://github.example/result' + + +class GitHubClient: + def search_code(self, query): + self.query = query + return iter([SearchResult()]) + + +class SearchTests(unittest.TestCase): + def test_writes_valid_csv_and_scopes_query_to_repository(self): + client = GitHubClient() + with tempfile.TemporaryDirectory() as directory: + dorks = Path(directory) / 'dorks.txt' + output = Path(directory) / 'results.csv' + dorks.write_text('# comment\nfilename:.env PASSWORD\n', encoding='utf-8') + + with patch.object(github_dork, 'gh', client): + github_dork.search( + repo_to_search='owner/repo', + gh_dorks_file=str(dorks), + output_filename=str(output), + ) + + with output.open(newline='', encoding='utf-8') as output_file: + rows = list(csv.reader(output_file)) + + self.assertEqual(client.query, 'filename:.env PASSWORD repo:owner/repo') + self.assertEqual(len(rows), 2) + self.assertEqual(rows[1][0], client.query) + self.assertEqual(rows[1][1], "['secret, with comma']") + + def test_reports_when_no_results_are_found(self): + client = GitHubClient() + client.search_code = lambda query: iter([]) + with tempfile.TemporaryDirectory() as directory: + dorks = Path(directory) / 'dorks.txt' + dorks.write_text('filename:.env\n', encoding='utf-8') + stdout = io.StringIO() + with patch.object(github_dork, 'gh', client), redirect_stdout(stdout): + github_dork.search( + user_to_search='example', gh_dorks_file=str(dorks) + ) + + self.assertIn('No results for your dork search user:example', stdout.getvalue()) + + def test_rejects_missing_dorks_file_with_clear_error(self): + with self.assertRaisesRegex(Exception, 'dorks file path is not valid'): + github_dork.search(gh_dorks_file='/does/not/exist') + + +if __name__ == '__main__': + unittest.main()