├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── logo.png ├── cmd └── root.go ├── config.example.yml ├── git-hound-installation-linux.sh ├── go.mod ├── go.sum ├── internal └── app │ ├── api_tracker.go │ ├── dig.go │ ├── github.go │ ├── keyword_scan.go │ ├── match_pool.go │ ├── options.go │ ├── programmingwords.go │ ├── regex_decoder.go │ ├── search_api.go │ ├── search_ui.go │ ├── util.go │ ├── websocket.go │ └── worker_pool.go ├── main.go ├── regex.yml ├── rules-generator └── import_gitleaks_rules.py ├── rules ├── LICENSE ├── adobe.yml ├── age.yml ├── artifactory.yml ├── aws.yml ├── azure.yml ├── codeclimate.yml ├── crates.io.yml ├── dependency_track.yml ├── digitalocean.yml ├── dockerconfig.yml ├── dockerhub.yml ├── doppler.yml ├── dropbox.yml ├── dynatrace.yml ├── facebook.yml ├── figma.yml ├── generic.yml ├── github.yml ├── gitlab.yml ├── google.yml ├── gradle.yml ├── grafana.yml ├── hashes.yml ├── heroku.yml ├── huggingface.yml ├── jenkins.yml ├── jwt.yml ├── linkedin.yml ├── mailchimp.yml ├── mailgun.yml ├── mapbox.yml ├── microsoft_teams.yml ├── netrc.yml ├── newrelic.yml ├── npm.yml ├── nuget.yml ├── odbc.yml ├── okta.yml ├── openai.yml ├── pem.yml ├── postman.yml ├── psexec.yml ├── pypi.yml ├── react.yml ├── rubygems.yml ├── rules-gitleaks.toml ├── salesforce.yml ├── sauce.yml ├── segment.yml ├── sendgrid.yml ├── shopify.yml ├── slack.yml ├── sonarqube.yml ├── square.yml ├── stackhawk.yml ├── stripe.yml ├── telegram.yml ├── truenas.yml ├── twilio.yml ├── twitter.yml └── wireguard.yml └── tests └── search_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: tillson 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | git-hound 3 | .goreleaser.yaml 4 | 5 | dist/ 6 | .venv 7 | output* 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch file", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "debug", 12 | "program": "${file}", 13 | "args": [ 14 | "searchKeyword", 15 | "weka.io", 16 | "--regex-file", 17 | "regexes_example.txt" 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at tillsongalloway@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Golang image as a parent image 2 | FROM golang:latest 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Install git-hound 8 | RUN git clone https://github.com/tillson/git-hound.git 9 | RUN cd git-hound && go build -o /usr/local/bin/git-hound 10 | 11 | # Copy the locally required files to the container 12 | COPY . . 13 | 14 | # Set up a directory for .githound 15 | RUN mkdir -p /root/.githound 16 | 17 | # Set up volume for input files 18 | VOLUME /data 19 | VOLUME /root/.githound 20 | 21 | # Set the default command for the container 22 | ENTRYPOINT ["git-hound"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tillson Galloway 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHound 2 | 3 | A pattern-matching, patch-attacking, batch-catching secret snatcher. 4 | 5 | 6 | 🚀 New in v3.0.0! Try the GitHound Web Dashboard 7 | Visualize and manage your search results in real-time with the new GitHound Explore dashboard. Get started now for free at https://githoundexplore.com or by using the `--dashboard` flag. Learn how to use this with a local installation of GitHound or TruffleHog at the [Wiki page](https://github.com/tillson/git-hound/wiki/GitHound-Explore-%E2%80%93%C2%A0UI-for-result-filtering-&-cloud-scans). Keep in mind you can still use GitHound without the dashboard. 8 | 9 | 10 | 11 | ![GitHound](assets/logo.png) 12 | 13 | GitHound hunts down exposed API keys and other sensitive information on GitHub using GitHub code search, pattern matching, and commit history searching. Unlike other secret-finding tools, GitHound's use of of GitHub code search enables it to search all of GitHub and isn't limited to specific repos, users, or orgs. 14 | More information is available in the [accompanying blog post](https://tillsongalloway.com/finding-sensitive-information-on-github/). 15 | 16 | ## Features 17 | 18 | - GitHub/Gist code search. This enables GitHound to locate sensitive information exposed across all of GitHub, uploaded by any user. 19 | - Sensitive data detection using pattern matching, contextual information, and string entropy 20 | - Commit history digging to find improperly deleted sensitive information 21 | - Scoring system that filters common false positives and optimizes intensive repo digging 22 | - Base64 detection and decoding 23 | - Options to build GitHound into larger systems, including JSON output and custom regexes 24 | 25 | ## Usage 26 | 27 | `echo "AKIA" | git-hound` or `git-hound --query "AKIA"` 28 | 29 | ## Setup 30 | 31 | 1. Download latest version of GitHound from https://github.com/tillson/git-hound/releases (with wget [url] or from the web browser). 32 | 2. Make sure an API key is set in `config.yml` 33 | 4. Run `./git-hound` to test (make sure you're in the correct directory!) 34 | 35 | **Configuration:** 36 | 37 | GitHound primarily uses `config.yml` (located in the current directory or `$HOME/.githound/`) for configuration. See `config.example.yml` for an example. 38 | 39 | Alternatively, you can use environment variables, which will override values in `config.yml`: 40 | - `GITHOUND_GITHUB_TOKEN`: Sets the GitHub API access token. 41 | - `GITHOUND_INSERT_KEY`: Sets the GitHoundExplore Insert Key for the `--dashboard` feature. 42 | 43 | ### Two-Factor Authentication 44 | 45 | If GitHound is logged into your GitHub account, two-factor authentication may kick in. You can pass 2FA codes to GitHound with `--otp-code`. 46 | Otherwise, GitHound will prompt you for it when it starts up. 47 | You can also [supply your 2FA seed](https://github.com/tillson/git-hound/pull/24) in the config and you'll never have to worry about 2FA again. 48 | Grab the 2FA seed by decoding the barcode that GitHub shows during the 2FA setup process. 49 | 50 | ## API Key Regexes 51 | GitHound utilizes a database of API key regexes maintained by the [Gitleaks](https://github.com/zricethezav/gitleaks) authors. 52 | 53 | ## Use cases 54 | 55 | ### Corporate: Searching for exposed customer API keys 56 | 57 | Knowing the pattern for a specific service's API keys enables you to search GitHub for these keys. You can then pipe matches for your custom key regex into your own script to test the API key against the service and to identify the at-risk account. 58 | 59 | `echo "api.halcorp.biz" | githound --dig-files --dig-commits --many-results --rules halcorp-api-regexes.txt --results-only | python halapitester.py` 60 | 61 | For detecting future API key leaks, GitHub offers [Push Token Scanning](https://help.github.com/en/articles/about-token-scanning) to immediately detect API keys as they are posted. 62 | 63 | ### Bug Bounty Hunters: Searching for leaked employee API tokens 64 | 65 | My primary use for GitHound is for finding sensitive information for Bug Bounty programs. For high-profile targets, the `--many-results` hack and `--languages` flag are useful for scraping >100 pages of results. 66 | 67 | `echo "\"uberinternal.com\"" | githound --dig-files --dig-commits --many-results --languages common-languages.txt --threads 100` 68 | 69 | ## How does GitHound find API keys? 70 | 71 | https://github.com/tillson/git-hound/blob/master/internal/app/keyword_scan.go 72 | GitHound finds API keys with a combination of exact regexes for common services like Slack and AWS and a context-sensitive generic API regex. This finds long strings that look like API keys surrounded by keywords like "Authorization" and "API-Token". GitHound assumes that these are false positives and then proves their legitimacy with Shannon entropy, dictionary word checks, uniqueness calculations, and encoding detection. GitHound then outputs high certainty positives. 73 | For files that encode secrets, decodes base64 strings and searches the encoded strings for API keys. 74 | 75 | Check out this [blog post](https://tillsongalloway.com/finding-sensitive-information-on-github/) for more details on use cases and methodologies. 76 | 77 | ## Flags 78 | GitHound makes it easy to find exposed API keys on GitHub using pattern matching, targetted querying, and a robust scoring system. 79 | ``` 80 | Usage: 81 | -h, --help help for githound 82 | --dashboard Stream results to web dashboard (see https://githoundexplore.com) 83 | --all-results Print all results, even if they do not contain secrets 84 | --api-debug Prints details about GitHub API requests and counts them. 85 | --config-file string Supply the path to a config file. 86 | --debug Enables verbose debug logging. 87 | --dig-commits Dig through commit history to find more secrets (CPU intensive). 88 | --dig-files Dig through the repo's files to find more secrets (CPU intensive). 89 | --fast Skip file grepping and only return search preview 90 | --json Print results in JSON format 91 | --many-results Search >100 pages with filtering hack 92 | --no-api-keys Don't search for generic API keys. 93 | --no-files Don't search for interesting files. 94 | --no-gists Don't search Gists 95 | --no-keywords Don't search for built-in keywords 96 | --no-repos Don't search repos 97 | --no-scoring Don't use scoring to filter out false positives. 98 | --otp-code string Github account 2FA token used for sign-in. (Only use if you have 2FA enabled on your account via authenticator app) 99 | --pages int Maximum pages to search per query (default 100) 100 | --profile Enable pprof profiling on localhost:6060 101 | --profile-addr string Address to serve pprof profiles (default "localhost:6060") 102 | --query string A query string (default: stdin) 103 | --query-file string A file containing a list of subdomains (or other queries). 104 | --results-only Only print match strings. 105 | --rules string Path to a list of regexes or a GitLeaks rules folder. (default "rules/") 106 | --search-type api Search interface (api or `ui`). 107 | --threads int Threads to dig with (default 20) 108 | ``` 109 | 110 | ## Development 111 | ### Sending flags on VS Code 112 | 113 | On launch.json send the needed flags as args 114 | "args": [ 115 | "searchKeyword", 116 | "tillsongalloway.com", 117 | "--regex-file", 118 | "regexes.txt" 119 | ] 120 | 121 | ## Building the project 122 | 123 | From the main folder: `go build .` 124 | 125 | --- 126 | 127 | ## Building the Docker Image 128 | To build the Docker image for Git-Hound, use the following command: 129 | 130 | ```bash 131 | docker build -t my-githound-container . 132 | ``` 133 | 134 | This command builds the Docker image with the tag `my-githound-container`. You can change the tag name to your preference. 135 | 136 | #### Running the Container 137 | To run the Git-Hound Docker container, you'll need to provide your `config.yaml` file and any input files (like `subdomains.txt`) via Docker volumes. 138 | 139 | #### Mounting `config.yaml` 140 | Place your `config.yaml` file at a known location on your host machine. This file should contain your Git-Hound configuration, including GitHub credentials. 141 | 142 | Example `config.yaml`: 143 | 144 | ```yaml 145 | # config.yaml 146 | github_username: "your_username" 147 | github_password: "your_password" 148 | # Optional: GitHub TOTP seed 149 | # github_totp_seed: "ABCDEF1234567890" 150 | ``` 151 | 152 | #### Mounting Input Files 153 | If you have a file like `subdomains.txt`, place it in a directory on your host machine. 154 | 155 | #### Running the Command 156 | Use the following command to run the container with your configuration and input files: 157 | 158 | ```bash 159 | docker run -v /path/to/config.yaml:/root/.githound/config.yaml -v $(pwd)/data:/data my-githound-container --subdomain-file /data/subdomains.txt 160 | ``` 161 | 162 | Replace `/path/to/config.yaml` with the actual path to your `config.yaml` file. The `-v $(pwd)/data:/data` part mounts a directory containing your input files (`subdomains.txt`) into the container. 163 | 164 | --- 165 | 166 | ## References 167 | 168 | - [How Bad Can It Git? Characterizing Secret Leakage in Public GitHub Repositories (Meli, McNiece, Reaves)](https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_04B-3_Meli_paper.pdf) 169 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillson/git-hound/2da82e38e3b54a3846b9f5cc8379d6efc27042dc/assets/logo.png -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | # Instructions: rename this file to config.yml. 2 | # DO NOT CHECK YOUR USERNAME AND PASSWORD INTO GIT! 3 | 4 | # GitHub Access Token 5 | # Instructions: Create a GitHub access token at https://github.com/settings/personal-access-tokens. 6 | github_access_token: "API_TOKEN" 7 | 8 | 9 | # WebSocket URL for GitHound Explore (--dashboard) 10 | websocket_url: "wss://githoundexplore.com/ws" 11 | 12 | 13 | # Deprecated UI search (Use with `--search-type ui` flag) 14 | # github_username: "username" 15 | # github_password: "your_password" 16 | # github_totp_seed: "ABCDEF1234567890" # Obtained via https://github.com/settings/security -------------------------------------------------------------------------------- /git-hound-installation-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Base URL for git-hound releases 6 | RELEASES_URL="https://github.com/tillson/git-hound/releases" 7 | FILE_BASENAME="git-hound" 8 | 9 | # Determine system architecture 10 | ARCHITECTURE="$(uname -m)" 11 | case "$ARCHITECTURE" in 12 | x86_64) 13 | ARCHITECTURE="amd64" 14 | ;; 15 | i386|i686) 16 | ARCHITECTURE="386" 17 | ;; 18 | aarch64) 19 | ARCHITECTURE="arm64" 20 | ;; 21 | *) 22 | echo "Unsupported architecture: $ARCHITECTURE. git-hound may not provide a pre-compiled binary for this architecture." 23 | exit 1 24 | ;; 25 | esac 26 | 27 | OS_NAME="linux" # Assuming the script is intended for Linux; adjust as necessary for other OSes 28 | 29 | # Fetch the latest git-hound version 30 | VERSION="$(curl -sfL -o /dev/null -w %{url_effective} "$RELEASES_URL/latest" | rev | cut -f1 -d'/' | rev)" 31 | VERSION="${VERSION#v}" 32 | 33 | if [ -z "$VERSION" ]; then 34 | echo "Unable to determine the latest git-hound version." >&2 35 | exit 1 36 | fi 37 | 38 | echo "Latest git-hound version: $VERSION" 39 | 40 | # Construct the download URL 41 | DOWNLOAD_URL="$RELEASES_URL/download/v$VERSION/${FILE_BASENAME}_${VERSION}_${OS_NAME}_${ARCHITECTURE}.tar.gz" 42 | 43 | TMPDIR="$(mktemp -d)" 44 | TAR_FILE="$TMPDIR/${FILE_BASENAME}.tar.gz" 45 | 46 | echo "Downloading git-hound $VERSION for $ARCHITECTURE..." 47 | curl -sfLo "$TAR_FILE" "$DOWNLOAD_URL" 48 | 49 | # Extract the tar.gz file 50 | tar -xzf "$TAR_FILE" -C "$TMPDIR" 51 | 52 | # Move the binary to /usr/bin (or another directory in your PATH) 53 | sudo mv "$TMPDIR/${FILE_BASENAME}" /usr/bin/ 54 | 55 | 56 | # Setup git-hound configuration 57 | CONFIG_DIR="$HOME/.githound" 58 | mkdir -p "$CONFIG_DIR" 59 | CONFIG_FILE="$CONFIG_DIR/config.yml" 60 | 61 | echo "Setting up git-hound configuration..." 62 | read -p "Enter your GitHub username (press enter to skip): " github_username 63 | 64 | if [ -n "$github_username" ]; then 65 | read -sp "Enter your GitHub password (press enter to skip): " github_password 66 | echo "" 67 | if [ -z "$github_password" ]; then 68 | echo "Since you've skipped entering the password, default values will be used for both username and password." 69 | github_username="username" 70 | github_password="your_password" 71 | fi 72 | else 73 | echo "You've skipped entering the username. Default values will be used for both username and password." 74 | github_username="username" 75 | github_password="your_password" 76 | fi 77 | 78 | # Create or update the config.yml file 79 | # This is the config.example.yml in the repository. 80 | { 81 | echo "# DO NOT CHECK YOUR USERNAME AND PASSWORD INTO GIT!" 82 | echo "" 83 | echo "# Required" 84 | echo "github_username: \"$github_username\"" 85 | echo "github_password: \"$github_password\"" 86 | echo "" 87 | echo "# Optional (comment out if not using)" 88 | echo "# github_totp_seed: \"ABCDEF1234567890\" # Obtained via https://github.com/settings/security" 89 | } > "$CONFIG_FILE" 90 | 91 | echo "git-hound configuration has been set." 92 | 93 | # Cleanup 94 | rm -rf "$TMPDIR" 95 | 96 | echo "git-hound has been downloaded and installed successfully." 97 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tillson/git-hound 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/BurntSushi/toml v1.4.0 9 | github.com/GRbit/go-pcre v1.0.1 10 | github.com/fatih/color v1.18.0 11 | github.com/go-git/go-git/v5 v5.13.1 12 | github.com/google/go-github/v57 v57.0.0 13 | github.com/gorilla/websocket v1.5.3 14 | github.com/pquerna/otp v1.4.0 15 | github.com/spf13/cobra v1.8.1 16 | github.com/spf13/viper v1.19.0 17 | github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d 18 | golang.org/x/crypto v0.35.0 19 | gopkg.in/yaml.v2 v2.4.0 20 | ) 21 | 22 | require ( 23 | dario.cat/mergo v1.0.1 // indirect 24 | github.com/Microsoft/go-winio v0.6.2 // indirect 25 | github.com/ProtonMail/go-crypto v1.1.3 // indirect 26 | github.com/boombuler/barcode v1.0.2 // indirect 27 | github.com/cloudflare/circl v1.5.0 // indirect 28 | github.com/cyphar/filepath-securejoin v0.3.6 // indirect 29 | github.com/emirpasic/gods v1.18.1 // indirect 30 | github.com/fsnotify/fsnotify v1.8.0 // indirect 31 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 32 | github.com/go-git/go-billy/v5 v5.6.1 // indirect 33 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 34 | github.com/google/go-querystring v1.1.0 // indirect 35 | github.com/hashicorp/hcl v1.0.0 // indirect 36 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 37 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 38 | github.com/kevinburke/ssh_config v1.2.0 // indirect 39 | github.com/magiconair/properties v1.8.9 // indirect 40 | github.com/mattn/go-colorable v0.1.13 // indirect 41 | github.com/mattn/go-isatty v0.0.20 // indirect 42 | github.com/mitchellh/mapstructure v1.5.0 // indirect 43 | github.com/mmcloughlin/avo v0.6.0 // indirect 44 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 45 | github.com/pjbgf/sha1cd v0.3.1 // indirect 46 | github.com/sagikazarmark/locafero v0.6.0 // indirect 47 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 48 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 49 | github.com/skeema/knownhosts v1.3.0 // indirect 50 | github.com/sourcegraph/conc v0.3.0 // indirect 51 | github.com/spf13/afero v1.11.0 // indirect 52 | github.com/spf13/cast v1.7.1 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/subosito/gotenv v1.6.0 // indirect 55 | github.com/xanzy/ssh-agent v0.3.3 // indirect 56 | go.uber.org/multierr v1.11.0 // indirect 57 | golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect 58 | golang.org/x/mod v0.22.0 // indirect 59 | golang.org/x/net v0.36.0 // indirect 60 | golang.org/x/sync v0.11.0 // indirect 61 | golang.org/x/sys v0.30.0 // indirect 62 | golang.org/x/term v0.29.0 // indirect 63 | golang.org/x/text v0.22.0 // indirect 64 | golang.org/x/tools v0.28.0 // indirect 65 | gopkg.in/ini.v1 v1.67.0 // indirect 66 | gopkg.in/warnings.v0 v0.1.2 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | ) 69 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 4 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 5 | github.com/GRbit/go-pcre v1.0.1 h1:8F7Wj1rxIq8ejKSXVVW2wE+4I4VnZbuOemrMk8kn3hc= 6 | github.com/GRbit/go-pcre v1.0.1/go.mod h1:0g7qVGbMpd2Odevd92x1RpaLpR3c3F/Gv2HEnI7CwEA= 7 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 8 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 9 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 10 | github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= 11 | github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 12 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 13 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 14 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 15 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 16 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 17 | github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= 18 | github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 19 | github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= 20 | github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 21 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 22 | github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= 23 | github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= 29 | github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= 30 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 31 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 32 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 33 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 34 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 35 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 36 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 37 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 38 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 39 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 40 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 41 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 42 | github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= 43 | github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= 44 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 45 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 46 | github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= 47 | github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= 48 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 49 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 50 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 52 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 53 | github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= 54 | github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= 55 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 56 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 57 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 58 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 59 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 60 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 61 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 62 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 63 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 64 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 65 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 66 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 67 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 68 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 69 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 70 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 71 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 72 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 73 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 74 | github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= 75 | github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 76 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 77 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 78 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 79 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 80 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 81 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 82 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 83 | github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY= 84 | github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8= 85 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 86 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 87 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 88 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 89 | github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI= 90 | github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= 91 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 92 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 95 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 96 | github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= 97 | github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 98 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 99 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 100 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 101 | github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= 102 | github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= 103 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 104 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 105 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 106 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 107 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 108 | github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= 109 | github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= 110 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 111 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 112 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 113 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 114 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 115 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 116 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 117 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 118 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 119 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 120 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 121 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 122 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 123 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 124 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 125 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 126 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 127 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 128 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 129 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 130 | github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d h1:xQcF7b7cZLWZG/+7A4G7un1qmEDYHIvId9qxRS1mZMs= 131 | github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d/go.mod h1:BzSc3WEF8R+lCaP5iGFRxd5kIXy4JKOZAwNe1w0cdc0= 132 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 133 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 134 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 135 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 136 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 137 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 138 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 139 | golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= 140 | golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 141 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 142 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 143 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 144 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 145 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 146 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 147 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 148 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 157 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 158 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 159 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 160 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 161 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 162 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 163 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 164 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 165 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 166 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 167 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 171 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 172 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 173 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 174 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 175 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 176 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 178 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 179 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 180 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 | -------------------------------------------------------------------------------- /internal/app/api_tracker.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | var ( 11 | apiRequestCount int 12 | apiMutex sync.Mutex 13 | ) 14 | 15 | // TrackAPIRequest logs and counts a GitHub API request. 16 | // If the APIDebug flag is enabled, it will print details about the request. 17 | func TrackAPIRequest(endpoint string, details string) { 18 | apiMutex.Lock() 19 | defer apiMutex.Unlock() 20 | 21 | apiRequestCount++ 22 | 23 | if GetFlags().APIDebug { 24 | timestamp := time.Now().Format("15:04:05.000") 25 | color.Cyan("[API Request #%d @ %s] %s %s", apiRequestCount, timestamp, endpoint, details) 26 | } 27 | } 28 | 29 | // GetAPIRequestCount returns the current count of API requests. 30 | func GetAPIRequestCount() int { 31 | apiMutex.Lock() 32 | defer apiMutex.Unlock() 33 | 34 | return apiRequestCount 35 | } 36 | 37 | // PrintAPIRequestSummary prints a summary of all API requests made. 38 | func PrintAPIRequestSummary() { 39 | if GetFlags().APIDebug { 40 | color.Green("Total GitHub API Requests: %d", GetAPIRequestCount()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/app/dig.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/go-git/go-git/v5" 16 | "github.com/go-git/go-git/v5/plumbing/object" 17 | "github.com/waigani/diffparser" 18 | ) 19 | 20 | var ( 21 | queue []RepoSearchResult 22 | reposStored = 0 23 | finishedRepos []string 24 | reposMutex sync.Mutex 25 | // Cache for already scanned files 26 | fileCache = make(map[string]bool) 27 | fileCacheMutex sync.Mutex 28 | // Skip these file extensions 29 | skipExtensions = map[string]bool{ 30 | ".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".ico": true, 31 | ".pdf": true, ".zip": true, ".tar": true, ".gz": true, ".bz2": true, 32 | ".mp3": true, ".mp4": true, ".mov": true, ".wav": true, ".ogg": true, 33 | ".ttf": true, ".woff": true, ".woff2": true, ".eot": true, 34 | } 35 | ) 36 | 37 | // Dig into the secrets of a repo 38 | func Dig(result RepoSearchResult) []*Match { 39 | startTime := time.Now() 40 | if GetFlags().Debug { 41 | fmt.Printf("[DEBUG] Starting Dig for repo: %s\n", result.Repo) 42 | } 43 | 44 | matchChannel := make(chan []*Match, 1) 45 | if GetFlags().Debug { 46 | fmt.Printf("[DEBUG] Submitting job to worker pool for repo: %s\n", result.Repo) 47 | } 48 | GetGlobalPool().Submit(func() { 49 | if GetFlags().Debug { 50 | fmt.Printf("[DEBUG] Worker started processing repo: %s\n", result.Repo) 51 | } 52 | matchChannel <- digHelper(result) 53 | if GetFlags().Debug { 54 | fmt.Printf("[DEBUG] Worker completed processing repo: %s\n", result.Repo) 55 | } 56 | close(matchChannel) 57 | }) 58 | 59 | if GetFlags().Debug { 60 | fmt.Printf("[DEBUG] Waiting for results from repo: %s\n", result.Repo) 61 | } 62 | matches := <-matchChannel 63 | if GetFlags().Debug { 64 | fmt.Printf("[DEBUG] Received results for repo: %s in %v\n", result.Repo, time.Since(startTime)) 65 | } 66 | return matches 67 | } 68 | 69 | func digHelper(result RepoSearchResult) []*Match { 70 | startTime := time.Now() 71 | matches := make([]*Match, 0, 10) 72 | matchMap := make(map[string]bool) 73 | 74 | if GetFlags().Debug { 75 | fmt.Printf("[DEBUG] Starting digHelper for repo: %s\n", result.Repo) 76 | } 77 | 78 | var repo *git.Repository 79 | var err error 80 | if _, err = os.Stat("/tmp/githound/" + result.Repo); os.IsNotExist(err) { 81 | cloneStart := time.Now() 82 | context, cancel := context.WithTimeout(context.Background(), 30*time.Second) 83 | defer cancel() 84 | 85 | if GetFlags().Debug { 86 | fmt.Printf("[DEBUG] Cloning repo: %s\n", result.Repo) 87 | } 88 | 89 | repo, err = git.PlainCloneContext(context, "/tmp/githound/"+result.Repo, false, &git.CloneOptions{ 90 | URL: "https://github.com/" + result.Repo, 91 | SingleBranch: true, 92 | Depth: 20, 93 | }) 94 | 95 | if err != nil { 96 | if GetFlags().Debug { 97 | fmt.Printf("[DEBUG] Error cloning repo %s: %v\n", result.Repo, err) 98 | } 99 | return matches 100 | } 101 | 102 | if GetFlags().Debug { 103 | fmt.Printf("[DEBUG] Clone completed in %v\n", time.Since(cloneStart)) 104 | } 105 | 106 | // Update repo storage stats 107 | reposMutex.Lock() 108 | reposStored++ 109 | if reposStored%10 == 0 { 110 | size, err := DirSize("/tmp/githound") 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | if size > 20e+6 { 115 | reposMutex.Unlock() 116 | if GetFlags().Debug { 117 | fmt.Printf("[DEBUG] Storage size exceeded, clearing finished repos\n") 118 | } 119 | ClearFinishedRepos() 120 | reposMutex.Lock() 121 | } 122 | } 123 | reposMutex.Unlock() 124 | 125 | ref, err := repo.Head() 126 | if err != nil { 127 | if GetFlags().Debug { 128 | fmt.Printf("[DEBUG] Error accessing repo head %s: %v\n", result.Repo, err) 129 | } 130 | return matches 131 | } 132 | 133 | if GetFlags().DigRepo { 134 | scanStart := time.Now() 135 | root := "/tmp/githound/" + result.Repo 136 | var files []string 137 | if GetFlags().Debug { 138 | fmt.Printf("[DEBUG] Starting file walk for %s\n", result.Repo) 139 | } 140 | 141 | err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 142 | if strings.HasPrefix(path, root+"/.git/") { 143 | return nil 144 | } 145 | ext := strings.ToLower(filepath.Ext(path)) 146 | if skipExtensions[ext] { 147 | if GetFlags().Debug { 148 | fmt.Printf("[DEBUG] Skipping file with blacklisted extension: %s\n", path) 149 | } 150 | return nil 151 | } 152 | files = append(files, path) 153 | return nil 154 | }) 155 | if err != nil { 156 | fmt.Printf("[DEBUG] Error walking directory: %v\n", err) 157 | } 158 | 159 | if GetFlags().Debug { 160 | fmt.Printf("[DEBUG] Found %d files to scan in %v\n", len(files), time.Since(scanStart)) 161 | } 162 | 163 | // Process files in parallel 164 | var wg sync.WaitGroup 165 | matchesChan := make(chan []*Match, len(files)) 166 | semaphore := make(chan struct{}, 10) // Limit concurrent file processing 167 | processedFiles := 0 168 | 169 | for _, file := range files { 170 | wg.Add(1) 171 | go func(file string) { 172 | defer wg.Done() 173 | semaphore <- struct{}{} 174 | defer func() { <-semaphore }() 175 | 176 | // Check cache first 177 | fileCacheMutex.Lock() 178 | if fileCache[file] { 179 | fileCacheMutex.Unlock() 180 | if GetFlags().Debug { 181 | fmt.Printf("[DEBUG] Skipping cached file: %s\n", file) 182 | } 183 | matchesChan <- nil 184 | return 185 | } 186 | fileCache[file] = true 187 | fileCacheMutex.Unlock() 188 | 189 | fileStart := time.Now() 190 | if GetFlags().Debug { 191 | fmt.Printf("[DEBUG] Scanning file: %s\n", file) 192 | } 193 | fileMatches := processFile(file, result) 194 | if GetFlags().Debug { 195 | fmt.Printf("[DEBUG] Processed file %s in %v\n", file, time.Since(fileStart)) 196 | } 197 | matchesChan <- fileMatches 198 | }(file) 199 | } 200 | 201 | // Collect results 202 | go func() { 203 | wg.Wait() 204 | close(matchesChan) 205 | }() 206 | 207 | for fileMatches := range matchesChan { 208 | processedFiles++ 209 | if fileMatches != nil { 210 | for _, match := range fileMatches { 211 | matchKey := fmt.Sprintf("%s|%s|%s", match.Text, match.File, match.Line.Text) 212 | if !matchMap[matchKey] { 213 | matchMap[matchKey] = true 214 | matches = append(matches, match) 215 | } 216 | } 217 | } 218 | } 219 | 220 | if GetFlags().Debug { 221 | fmt.Printf("[DEBUG] Processed %d files in %v\n", processedFiles, time.Since(scanStart)) 222 | } 223 | } 224 | 225 | if GetFlags().DigCommits { 226 | commitStart := time.Now() 227 | if GetFlags().Debug { 228 | fmt.Printf("[DEBUG] Starting commit scanning for %s\n", result.Repo) 229 | } 230 | 231 | commit, err := repo.CommitObject(ref.Hash()) 232 | if err != nil { 233 | if GetFlags().Debug { 234 | fmt.Printf("[DEBUG] Error getting commit object %s: %v\n", result.Repo, err) 235 | } 236 | return matches 237 | } 238 | 239 | commitIter, err := repo.Log(&git.LogOptions{From: commit.Hash}) 240 | if err != nil { 241 | fmt.Printf("[DEBUG] Error getting commit log: %v\n", err) 242 | return matches 243 | } 244 | 245 | lastHash, err := commit.Tree() 246 | if err != nil { 247 | fmt.Printf("[DEBUG] Error getting commit tree: %v\n", err) 248 | return matches 249 | } 250 | 251 | number := 0 252 | commitIter.ForEach(func(c *object.Commit) error { 253 | if number > 30 { 254 | return nil 255 | } 256 | number++ 257 | commitTree, err := c.Tree() 258 | if err != nil { 259 | return err 260 | } 261 | diffMatches := ScanDiff(lastHash, commitTree, result) 262 | for _, match := range diffMatches { 263 | match.Commit = c.Hash.String() 264 | matchKey := fmt.Sprintf("%s|%s|%s|%s", match.Text, match.File, match.Line.Text, match.Commit) 265 | if !matchMap[matchKey] { 266 | matchMap[matchKey] = true 267 | matches = append(matches, match) 268 | } 269 | } 270 | lastHash = commitTree 271 | return nil 272 | }) 273 | 274 | if GetFlags().Debug { 275 | fmt.Printf("[DEBUG] Commit scanning completed in %v\n", time.Since(commitStart)) 276 | } 277 | } 278 | 279 | finishedRepos = append(finishedRepos, result.Repo) 280 | if GetFlags().Debug { 281 | fmt.Printf("[DEBUG] Total processing time for %s: %v\n", result.Repo, time.Since(startTime)) 282 | } 283 | } 284 | 285 | return matches 286 | } 287 | 288 | func processFile(file string, result RepoSearchResult) []*Match { 289 | readStart := time.Now() 290 | data, err := ioutil.ReadFile(file) 291 | if err != nil { 292 | if GetFlags().Debug { 293 | fmt.Printf("[DEBUG] Error reading file %s: %v\n", file, err) 294 | } 295 | return nil 296 | } 297 | 298 | // Quick check for binary files 299 | if len(data) > 0 && data[0] == 0 { 300 | if GetFlags().Debug { 301 | fmt.Printf("[DEBUG] Skipping binary file: %s\n", file) 302 | } 303 | return nil 304 | } 305 | 306 | // Convert to ASCII efficiently 307 | ascii := make([]byte, 0, len(data)) 308 | for _, b := range data { 309 | if 0 < b && b < 127 { 310 | ascii = append(ascii, b) 311 | } 312 | } 313 | 314 | // Skip if too much binary content 315 | if float32(len(ascii))/float32(len(data)) < 0.9 { 316 | if GetFlags().Debug { 317 | fmt.Printf("[DEBUG] Skipping file with too much binary content: %s\n", file) 318 | } 319 | return nil 320 | } 321 | 322 | if GetFlags().Debug { 323 | fmt.Printf("[DEBUG] Read and processed file %s in %v\n", file, time.Since(readStart)) 324 | } 325 | 326 | fileResult := result 327 | fileResult.File = file 328 | score := 0 329 | var newMatches []*Match 330 | 331 | // Check file extensions first 332 | extStart := time.Now() 333 | fileExtMatches := MatchFileExtensions(file, fileResult) 334 | for _, match := range fileExtMatches { 335 | newMatches = append(newMatches, match) 336 | score += 5 337 | } 338 | PutMatches(fileExtMatches) 339 | if GetFlags().Debug { 340 | fmt.Printf("[DEBUG] File extension check for %s took %v\n", file, time.Since(extStart)) 341 | } 342 | 343 | // Only do full text search if we haven't found anything yet 344 | if score == 0 { 345 | searchStart := time.Now() 346 | searchMatches, searchScore := GetMatchesForString(string(ascii), result, true) 347 | score += searchScore 348 | if searchScore > -1 { 349 | newMatches = append(newMatches, searchMatches...) 350 | } 351 | if GetFlags().Debug { 352 | fmt.Printf("[DEBUG] Text search for %s took %v\n", file, time.Since(searchStart)) 353 | } 354 | } 355 | 356 | if score > 1 { 357 | for _, match := range newMatches { 358 | relPath := strings.Join(strings.Split(file[len("/tmp/githound/"):], "/")[2:], "/") 359 | match.CommitFile = relPath 360 | match.File = relPath 361 | } 362 | return newMatches 363 | } 364 | 365 | return nil 366 | } 367 | 368 | // ScanDiff finds secrets in the diff between two Git trees. 369 | func ScanDiff(from *object.Tree, to *object.Tree, result RepoSearchResult) (matches []*Match) { 370 | if from == to || from == nil || to == nil { 371 | return matches 372 | } 373 | diff, err := from.Diff(to) 374 | if err != nil { 375 | log.Fatal(err) 376 | } 377 | for _, change := range diff { 378 | if change == nil { 379 | continue 380 | } 381 | //temporary response to: https://github.com/sergi/go-diff/issues/89 382 | //reference: https://github.com/codeEmitter/gitrob/commit/c735767e86d40a0015756a299e4daeb136c7126b 383 | defer func() error { 384 | if err := recover(); err != nil { 385 | return errors.New(fmt.Sprintf("Panic occurred while retrieving change content: %s", err)) 386 | } 387 | return nil 388 | }() 389 | 390 | patch, err := change.Patch() 391 | if err != nil { 392 | if GetFlags().Debug { 393 | fmt.Println("Diff scan error: Patch error.") 394 | fmt.Println(err) 395 | } 396 | continue 397 | } 398 | patchStr := patch.String() 399 | diffData, err := diffparser.Parse(patchStr) 400 | if err != nil { 401 | log.Fatal(err) 402 | } 403 | matches, _ = GetMatchesForString(patchStr, result, true) 404 | for _, diffFile := range diffData.Files { 405 | fileExtMatches := MatchFileExtensions(diffFile.NewName, result) 406 | // Convert pointer matches to value matches before appending 407 | for _, ptrMatch := range fileExtMatches { 408 | matches = append(matches, ptrMatch) 409 | } 410 | // Don't forget to return the matches to the pool 411 | PutMatches(fileExtMatches) 412 | } 413 | } 414 | return matches 415 | } 416 | 417 | // DirSize gets the size of a diretory. 418 | func DirSize(path string) (int64, error) { 419 | var size int64 420 | err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { 421 | if err != nil { 422 | return err 423 | } 424 | if !info.IsDir() { 425 | size += info.Size() 426 | } 427 | return err 428 | }) 429 | return size, err 430 | } 431 | 432 | // ClearFinishedRepos clears finished repos from disk 433 | func ClearFinishedRepos() { 434 | // Lock for thread safety 435 | reposMutex.Lock() 436 | defer reposMutex.Unlock() 437 | 438 | // More aggressive cleanup - remove all repos 439 | err := os.RemoveAll("/tmp/githound") 440 | if err != nil { 441 | fmt.Println(err) 442 | } 443 | 444 | // Reset counters 445 | reposStored = 0 446 | finishedRepos = []string{} 447 | 448 | // Recreate the base directory 449 | err = os.MkdirAll("/tmp/githound", 0755) 450 | if err != nil { 451 | fmt.Println(err) 452 | } 453 | } 454 | 455 | // ClearRepoStorage deletes all stored repos from the disk. 456 | func ClearRepoStorage() { 457 | os.RemoveAll("/tmp/githound") 458 | } 459 | -------------------------------------------------------------------------------- /internal/app/github.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "os" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | "sync" 18 | "time" 19 | 20 | "github.com/google/go-github/v57/github" 21 | "github.com/pquerna/otp/totp" 22 | ) 23 | 24 | // GitHubCredentials stores a GitHub username and password 25 | type GitHubCredentials struct { 26 | Username string 27 | Password string 28 | OTP string 29 | } 30 | 31 | // SearchOptions are the options that the GitHub search will use. 32 | type SearchOptions struct { 33 | MaxPages int 34 | github.SearchOptions 35 | } 36 | 37 | // LoginToGitHub logs into GitHub with the given 38 | // credentials and returns an HTTTP client. 39 | func LoginToGitHub(credentials GitHubCredentials) (httpClient *http.Client, err error) { 40 | 41 | jar, err := cookiejar.New(nil) 42 | if err != nil { 43 | return nil, err 44 | } 45 | client := http.Client{ 46 | Jar: jar, 47 | } 48 | rt := WithHeader(client.Transport) 49 | rt.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36") 50 | client.Transport = rt 51 | csrf, err := GrabCSRFToken("https://github.com/login", &client) 52 | if err != nil { 53 | return nil, err 54 | } 55 | resp, err := client.PostForm("https://github.com/session", url.Values{ 56 | "authenticity_token": {csrf}, 57 | "login": {credentials.Username}, 58 | "password": {credentials.Password}, 59 | }) 60 | // fmt.Println(resp.StatusCode) 61 | data, err := ioutil.ReadAll(resp.Body) 62 | dataStr := string(data) 63 | // fmt.Println(dataStr) 64 | if strings.Index(dataStr, "Incorrect username or password.") > -1 { 65 | return nil, fmt.Errorf("Incorrect username or password.") 66 | } 67 | if strings.Index(dataStr, "app_otp") > -1 { 68 | csrf, err = GrabCSRFTokenBody(dataStr) 69 | if err != nil { 70 | return nil, err 71 | } 72 | // fmt.Println(csrf) 73 | otp := HandleOTPCode(credentials) 74 | 75 | if strings.Index(resp.Request.URL.String(), "verified-device") > -1 { 76 | resp, err = client.PostForm("https://github.com/sessions/verified-device", url.Values{ 77 | 78 | "authenticity_token": {csrf}, 79 | "otp": {otp}, 80 | }) 81 | data, err = ioutil.ReadAll(resp.Body) 82 | } else { 83 | resp, err = client.PostForm("https://github.com/sessions/two-factor", url.Values{ 84 | 85 | "authenticity_token": {csrf}, 86 | "otp": {otp}, 87 | }) 88 | data, err = ioutil.ReadAll(resp.Body) 89 | } 90 | } 91 | 92 | return &client, err 93 | } 94 | 95 | // HandleOTPCode returns a user's OTP code for authenticating with Github by searching 96 | // config values, then CLI arguments, then prompting the user for input 97 | func HandleOTPCode(credentials GitHubCredentials) string { 98 | var otp string 99 | if credentials.OTP != "" { 100 | // Generate a TOTP code based on TOTP seed in config 101 | otp, _ = totp.GenerateCode(credentials.OTP, time.Now()) 102 | } else if GetFlags().OTPCode != "" { 103 | // Use the provided CLI argument (--otp-code) for OTP code 104 | otp = GetFlags().OTPCode 105 | } else { 106 | // Prompt the user for OTP code 107 | tty, err := os.Open("/dev/tty") 108 | if err != nil { 109 | log.Fatalf("can't open /dev/tty: %s", err) 110 | } 111 | fmt.Printf("Enter your GitHub 2FA code: ") 112 | scanner := bufio.NewScanner(tty) 113 | _ = scanner.Scan() 114 | otp = scanner.Text() 115 | } 116 | return otp 117 | } 118 | 119 | // GrabCSRFToken grabs the CSRF token from a GitHub page 120 | func GrabCSRFToken(csrfURL string, client *http.Client) (token string, err error) { 121 | resp, err := client.Get(csrfURL) 122 | if err != nil { 123 | log.Println("Error getting CSRF token page.") 124 | log.Println(err) 125 | } 126 | data, err := ioutil.ReadAll(resp.Body) 127 | resp.Body.Close() 128 | dataStr := string(data) 129 | return GrabCSRFTokenBody(dataStr) 130 | } 131 | 132 | // GrabCSRFTokenBody grabs the CSRF token from a GitHub page 133 | func GrabCSRFTokenBody(pageBody string) (token string, err error) { 134 | re := regexp.MustCompile("authenticity_token\"\\svalue\\=\"([0-9A-z/=\\+\\-_]{32,})\"") 135 | match := re.FindStringSubmatch(pageBody) 136 | if len(match) == 2 { 137 | return match[1], err 138 | } 139 | return "", err 140 | } 141 | 142 | // DownloadRawFile downloads files from the githubusercontent CDN. 143 | func DownloadRawFile(client *http.Client, base string, searchResult RepoSearchResult) (data []byte, err error) { 144 | // If the raw URL contains '%' character, gracefully skip it 145 | if strings.Contains(searchResult.Raw, "%") { 146 | // Return empty byte array with nil error to gracefully skip this file 147 | return []byte{}, nil 148 | } 149 | 150 | // Create a context with timeout 151 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 152 | defer cancel() 153 | 154 | // Split the path and encode each component 155 | pathParts := strings.Split(searchResult.Raw, "/") 156 | encodedParts := make([]string, len(pathParts)) 157 | for i, part := range pathParts { 158 | encodedParts[i] = url.PathEscape(part) 159 | } 160 | encodedPath := strings.Join(encodedParts, "/") 161 | 162 | // Construct the full URL with encoded path 163 | fullURL := base + "/" + encodedPath 164 | 165 | // Create a request with the timeout context 166 | req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | TrackAPIRequest("GitHub Web", fmt.Sprintf("GET %s (download raw file)", fullURL)) 172 | 173 | // Perform the GET request 174 | resp, err := client.Do(req) 175 | if err != nil { 176 | return nil, err 177 | } 178 | defer resp.Body.Close() 179 | 180 | // Check response code 181 | if resp.StatusCode >= 400 { 182 | fmt.Println(fullURL) 183 | return []byte{}, fmt.Errorf("HTTP error: %d", resp.StatusCode) 184 | } 185 | 186 | // Read the response body with a size limit to prevent memory explosions 187 | // Limit to 10MB max file size 188 | const maxSize = 10 * 1024 * 1024 189 | return ioutil.ReadAll(io.LimitReader(resp.Body, maxSize)) 190 | } 191 | 192 | // Cache for repository popularity to avoid repeated HTTP requests 193 | var repoPopularityCache = make(map[string]bool) 194 | var repoCacheMutex sync.RWMutex 195 | 196 | // RepoIsUnpopular uses stars/forks/watchers to determine the popularity of a repo. 197 | func RepoIsUnpopular(client *http.Client, result RepoSearchResult) bool { 198 | // Check cache first 199 | repoCacheMutex.RLock() 200 | if isUnpopular, exists := repoPopularityCache[result.Repo]; exists { 201 | repoCacheMutex.RUnlock() 202 | return isUnpopular 203 | } 204 | repoCacheMutex.RUnlock() 205 | 206 | // Default timeout context 207 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 208 | defer cancel() 209 | 210 | // Create a request with timeout context 211 | req, err := http.NewRequestWithContext(ctx, "GET", "https://github.com/"+result.Repo, nil) 212 | if err != nil { 213 | // If we can't create the request, assume unpopular to be safe 214 | return true 215 | } 216 | 217 | TrackAPIRequest("GitHub Web", fmt.Sprintf("GET https://github.com/%s (check popularity)", result.Repo)) 218 | resp, err := client.Do(req) 219 | if err != nil { 220 | // Network error, assume unpopular to be safe 221 | return true 222 | } 223 | defer resp.Body.Close() 224 | 225 | // Only read a limited amount of the response 226 | bodyBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, 100*1024)) // Limit to 100KB 227 | if err != nil { 228 | return true 229 | } 230 | 231 | // Convert to string for regex search 232 | strData := string(bodyBytes) 233 | 234 | // Parse star count 235 | regex := regexp.MustCompile("aria\\-label\\=\"(\\d+)\\suser(s?)\\sstarred\\sthis") 236 | match := regex.FindStringSubmatch(strData) 237 | 238 | isUnpopular := true 239 | if len(match) > 1 { 240 | stars, err := strconv.Atoi(match[1]) 241 | if err == nil && stars > 6 { 242 | isUnpopular = false 243 | } 244 | } 245 | 246 | // Cache the result 247 | repoCacheMutex.Lock() 248 | repoPopularityCache[result.Repo] = isUnpopular 249 | repoCacheMutex.Unlock() 250 | 251 | return isUnpopular 252 | } 253 | 254 | // GetRawGistPage gets the source code for a Gist. 255 | func GetRawGistPage(client *http.Client, gist string) string { 256 | TrackAPIRequest("GitHub Web", fmt.Sprintf("GET https://gist.github.com/%s (get gist)", gist)) 257 | resp, err := client.Get("https://gist.github.com/" + gist) 258 | if err != nil { 259 | log.Fatal(err) 260 | } 261 | escaped := regexp.QuoteMeta(gist) 262 | regex := regexp.MustCompile("href\\=\"\\/(" + escaped + "\\/raw\\/[0-9a-z]{40}\\/[\\w_\\-\\.\\/\\%]{1,255})\"") 263 | body, err := ioutil.ReadAll(resp.Body) 264 | if err != nil { 265 | log.Fatal(err) 266 | } 267 | resp.Body.Close() 268 | match := regex.FindStringSubmatch(string(body)) 269 | if len(match) == 2 { 270 | return match[1] 271 | } 272 | return "" 273 | } 274 | 275 | // ConstructSearchURL serializes its parameters into a search URL 276 | func ConstructSearchURL(base string, query string, options SearchOptions) string { 277 | var sb strings.Builder 278 | sb.WriteString(base) 279 | sb.WriteString("?q=" + url.QueryEscape(query)) 280 | sb.WriteString("&p=" + strconv.Itoa(options.Page)) 281 | // sb.WriteString("&o=desc") // + options.Order) 282 | sb.WriteString("&s=indexed") // + options.Sort) 283 | sb.WriteString("&type=code") 284 | return sb.String() 285 | } 286 | 287 | type withHeader struct { 288 | http.Header 289 | rt http.RoundTripper 290 | } 291 | 292 | func WithHeader(rt http.RoundTripper) withHeader { 293 | if rt == nil { 294 | rt = http.DefaultTransport 295 | } 296 | 297 | return withHeader{Header: make(http.Header), rt: rt} 298 | } 299 | 300 | func (h withHeader) RoundTrip(req *http.Request) (*http.Response, error) { 301 | for k, v := range h.Header { 302 | req.Header[k] = v 303 | } 304 | 305 | return h.rt.RoundTrip(req) 306 | } 307 | -------------------------------------------------------------------------------- /internal/app/match_pool.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // MatchPool implements an object pool for Match structs 8 | type MatchPool struct { 9 | pool sync.Pool 10 | } 11 | 12 | // Global match pool singleton 13 | var globalMatchPool = &MatchPool{ 14 | pool: sync.Pool{ 15 | New: func() interface{} { 16 | // Create a new Match with pre-allocated attributes slice 17 | return &Match{ 18 | Attributes: make([]string, 0, 4), // Pre-allocate with capacity for typical use 19 | } 20 | }, 21 | }, 22 | } 23 | 24 | // GetMatch gets a Match from the pool or creates a new one if none are available 25 | func GetMatch() *Match { 26 | return globalMatchPool.pool.Get().(*Match) 27 | } 28 | 29 | // PutMatch returns a Match to the pool for reuse 30 | func PutMatch(m *Match) { 31 | // Reset the match to avoid leaking data 32 | m.Text = "" 33 | m.Attributes = m.Attributes[:0] // Keep capacity but reset length to 0 34 | m.Line = Line{} 35 | m.Commit = "" 36 | m.CommitFile = "" 37 | m.File = "" 38 | m.Expression = "" 39 | 40 | // Return to pool 41 | globalMatchPool.pool.Put(m) 42 | } 43 | 44 | // GetMatches gets multiple matches at once 45 | func GetMatches(count int) []*Match { 46 | matches := make([]*Match, count) 47 | for i := 0; i < count; i++ { 48 | matches[i] = GetMatch() 49 | } 50 | return matches 51 | } 52 | 53 | // PutMatches returns multiple matches to the pool 54 | func PutMatches(matches []*Match) { 55 | for _, m := range matches { 56 | if m != nil { 57 | PutMatch(m) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/app/options.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | // Flags stores the program options. 4 | type Flags struct { 5 | QueryFile string 6 | Query string 7 | SearchID string 8 | DigRepo bool 9 | DigCommits bool 10 | RegexFile string 11 | ConfigFile string 12 | Pages int 13 | ResultsOnly bool 14 | NoAPIKeys bool 15 | NoScoring bool 16 | NoFiles bool 17 | NoKeywords bool 18 | AllResults bool 19 | FastMode bool 20 | Threads int 21 | Debug bool 22 | APIDebug bool 23 | NoGists bool 24 | NoRepos bool 25 | ManyResults bool 26 | JsonOutput bool 27 | Dashboard bool 28 | SearchType string 29 | OTPCode string 30 | TextRegexes []Rule 31 | WebSocketURL string 32 | EnableProfiling bool // Enable pprof profiling 33 | ProfileAddr string // Address to serve pprof profiles (host:port) 34 | GithubAccessToken string // GitHub API token 35 | InsertKey string // GitHoundExplore Insert Key 36 | Trufflehog bool // Ingest trufflehog output without scanning 37 | } 38 | 39 | var flags Flags 40 | 41 | // GetFlags is a singleton that returns the program flags. 42 | func GetFlags() *Flags { 43 | return &flags 44 | } 45 | -------------------------------------------------------------------------------- /internal/app/regex_decoder.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type RuleConfig struct { 10 | Rules []Rule `yaml:"rules,omitempty"` 11 | } 12 | 13 | type Rule struct { 14 | ID string `yaml:"id" toml:"id"` 15 | Pattern *regexp.Regexp `yaml:"pattern"` 16 | StringPattern string `toml:"regex"` 17 | Description string `yaml:"name" toml:"description"` 18 | SmartFiltering bool `yaml:"smart_filtering" toml:"smart_filtering"` 19 | } 20 | 21 | // UnmarshalYAML implements the yaml.Unmarshaler interface for Rule 22 | func (r *Rule) UnmarshalYAML(unmarshal func(interface{}) error) error { 23 | // Create a temporary type to avoid infinite recursion 24 | type RuleAlias Rule 25 | 26 | // Create a temporary struct with all fields 27 | temp := struct { 28 | *RuleAlias 29 | Pattern string `yaml:"pattern"` 30 | Name string `yaml:"name"` 31 | ID string `yaml:"id"` 32 | }{ 33 | RuleAlias: (*RuleAlias)(r), 34 | } 35 | 36 | // Unmarshal into the temporary struct 37 | if err := unmarshal(&temp); err != nil { 38 | return fmt.Errorf("failed to unmarshal rule: %w", err) 39 | } 40 | 41 | // Skip empty patterns 42 | if strings.TrimSpace(temp.Pattern) == "" { 43 | return nil 44 | } 45 | 46 | // Compile with Go's regexp package 47 | compiled, err := regexp.Compile(temp.Pattern) 48 | if err != nil { 49 | return fmt.Errorf("failed to compile regex '%s': %w", temp.Pattern, err) 50 | } 51 | 52 | r.Pattern = compiled 53 | r.StringPattern = temp.Pattern 54 | r.Description = temp.Name 55 | r.ID = temp.ID 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/app/search_api.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "net/http" 8 | "os" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/fatih/color" 16 | "github.com/google/go-github/v57/github" 17 | ) 18 | 19 | // Semaphore to limit concurrent HTTP requests 20 | var httpSemaphore chan struct{} 21 | var httpSemaphoreOnce sync.Once 22 | 23 | func initHTTPSemaphore() { 24 | httpSemaphoreOnce.Do(func() { 25 | threads := GetFlags().Threads 26 | if threads <= 0 { 27 | threads = 10 28 | } 29 | httpSemaphore = make(chan struct{}, threads) 30 | }) 31 | } 32 | 33 | func SearchWithAPI(queries []string) { 34 | // Initialize HTTP request limiter 35 | initHTTPSemaphore() 36 | 37 | token := GetFlags().GithubAccessToken 38 | if token == "" { 39 | color.Red("[!] GitHub access token not found. Please set it using GITHOUND_GITHUB_TOKEN environment variable or in your config file.") 40 | os.Exit(1) 41 | } 42 | 43 | client := github.NewClient(nil).WithAuthToken(token) 44 | if client == nil { 45 | color.Red("[!] Unable to create GitHub client. Please check your configuration.") 46 | os.Exit(1) 47 | } 48 | 49 | // Test the token by making a simple API call 50 | _, _, err := client.Users.Get(context.Background(), "") 51 | if err != nil { 52 | if strings.Contains(err.Error(), "401") { 53 | color.Red("[!] Invalid GitHub access token. Please check that your token is correct and has the necessary permissions.") 54 | } else { 55 | color.Red("[!] Error authenticating with GitHub: %v", err) 56 | } 57 | os.Exit(1) 58 | } 59 | 60 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput && GetFlags().Debug { 61 | color.Cyan("[*] Logged into GitHub using API key") 62 | } 63 | 64 | options := github.SearchOptions{ 65 | Sort: "indexed", 66 | ListOptions: github.ListOptions{ 67 | PerPage: 100, 68 | }, 69 | } 70 | 71 | http_client := http.Client{} 72 | rt := WithHeader(http_client.Transport) 73 | rt.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36") 74 | http_client.Transport = rt 75 | 76 | for _, query := range queries { 77 | if GetFlags().Dashboard && GetFlags().InsertKey != "" { 78 | BrokerSearchCreation(query) 79 | } 80 | for page := 0; page < int(math.Min(10, float64(GetFlags().Pages))); page++ { 81 | options.Page = page 82 | if GetFlags().Debug { 83 | TrackAPIRequest("Search.Code", fmt.Sprintf("Query: %s, Page: %d", query, page)) 84 | } 85 | result, _, err := client.Search.Code(context.Background(), query, &options) 86 | for err != nil { 87 | fmt.Println(err) 88 | resetTime := extractResetTime(err.Error()) 89 | sleepDuration := resetTime + 3 90 | color.Yellow("[!] GitHub API rate limit exceeded. Waiting %d seconds...", sleepDuration) 91 | time.Sleep(time.Duration(sleepDuration) * time.Second) 92 | if GetFlags().Debug { 93 | TrackAPIRequest("Search.Code", fmt.Sprintf("Query: %s, Page: %d (retry)", query, page)) 94 | } 95 | result, _, err = client.Search.Code(context.Background(), query, &options) 96 | } 97 | 98 | // If we get an empty page of results, stop searching 99 | if len(result.CodeResults) == 0 { 100 | if GetFlags().Debug { 101 | fmt.Println("No more results found, stopping search...") 102 | } 103 | break 104 | } 105 | 106 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput && GetFlags().Debug { 107 | fmt.Println("Analyzing " + strconv.Itoa(len(result.CodeResults)) + " repos on page " + strconv.Itoa(page+1) + "...") 108 | } 109 | 110 | // Initialize the worker pool if not already done 111 | workerPool := GetGlobalPool() 112 | 113 | for _, code_result := range result.CodeResults { 114 | // fmt.Println(code_result.GetPath()) 115 | author_repo_str := code_result.GetRepository().GetOwner().GetLogin() + "/" + code_result.GetRepository().GetName() 116 | re := regexp.MustCompile(`\/([a-f0-9]{40})\/`) 117 | matches := re.FindStringSubmatch(code_result.GetHTMLURL()) 118 | 119 | sha := "" 120 | if len(matches) > 1 { 121 | sha = matches[1] 122 | } 123 | 124 | // Create a repo result object to pass to the worker 125 | repoResult := RepoSearchResult{ 126 | Repo: author_repo_str, 127 | File: code_result.GetPath(), 128 | Raw: author_repo_str + "/" + sha + "/" + code_result.GetPath(), 129 | Source: "repo", 130 | Query: query, 131 | URL: "https://github.com/" + author_repo_str + "/blob/" + sha + "/" + code_result.GetPath(), 132 | } 133 | 134 | // Increment the wait group before submitting the job 135 | SearchWaitGroup.Add(1) 136 | 137 | // Submit the job to the worker pool instead of creating a goroutine directly 138 | workerPool.Submit(func() { 139 | // Process the repository in the worker pool 140 | ScanAndPrintResult(&http_client, repoResult) 141 | }) 142 | } 143 | } 144 | 145 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 146 | color.Green("Finished searching... Now waiting for scanning to finish.") 147 | } 148 | 149 | SearchWaitGroup.Wait() 150 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 151 | color.Green("Finished scanning.") 152 | } 153 | } 154 | } 155 | 156 | // extractResetTime extracts the number of seconds until the rate limit resets from the error message. 157 | func extractResetTime(errorMessage string) int { 158 | re := regexp.MustCompile(`rate reset in (\d+)s`) 159 | matches := re.FindStringSubmatch(errorMessage) 160 | if len(matches) > 1 { 161 | seconds, err := strconv.Atoi(matches[1]) 162 | if err == nil { 163 | return seconds 164 | } 165 | } 166 | return 0 167 | } 168 | -------------------------------------------------------------------------------- /internal/app/search_ui.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "os" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/fatih/color" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | // RepoSearchResult represents a result in GitHub/Gist code search. 21 | type RepoSearchResult struct { 22 | Repo string 23 | File string 24 | Raw string 25 | Source string 26 | Contents string 27 | Query string 28 | URL string 29 | SourceFileLastUpdated string 30 | SourceFileLastAuthorEmail string 31 | searchOptions *SearchOptions 32 | } 33 | 34 | type NewSearchPayload struct { 35 | Payload struct { 36 | Results []struct { 37 | RepoNwo string `json:"repo_nwo"` 38 | RepoName string `json:"repo_name"` 39 | Path string `json:"path"` 40 | CommitSha string `json:"commit_sha"` 41 | // Repository struct { 42 | // } 43 | } `json:"results"` 44 | PageCount int `json:"page_count"` 45 | } `json:"payload"` 46 | } 47 | 48 | var SearchWaitGroup sync.WaitGroup 49 | 50 | func SearchWithUI(queries []string) { 51 | client, err := LoginToGitHub(GitHubCredentials{ 52 | Username: viper.GetString("github_username"), 53 | Password: viper.GetString("github_password"), 54 | OTP: viper.GetString("github_totp_seed"), 55 | }) 56 | 57 | if err != nil { 58 | fmt.Println(err) 59 | color.Red("[!] Unable to login to GitHub. Please check your username/password credentials.") 60 | os.Exit(1) 61 | } 62 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 63 | color.Cyan("[*] Logged into GitHub as " + viper.GetString("github_username")) 64 | } 65 | for _, query := range queries { 66 | if GetFlags().Dashboard && GetFlags().InsertKey != "" { 67 | BrokerSearchCreation(query) 68 | } 69 | _, err = Search(query, client) 70 | if err != nil { 71 | color.Red("[!] Unable to collect search results for query '" + query + "'.") 72 | break 73 | } 74 | } 75 | 76 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 77 | color.Green("Finished searching... Now waiting for scanning to finish.") 78 | } 79 | 80 | SearchWaitGroup.Wait() 81 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 82 | color.Green("Finished scanning.") 83 | } 84 | } 85 | 86 | // Search Everything 87 | func Search(query string, client *http.Client) (results []RepoSearchResult, err error) { 88 | 89 | options := SearchOptions{ 90 | MaxPages: 100, 91 | } 92 | 93 | resultMap := make(map[string]bool) 94 | 95 | // Rich GitHub search 96 | if !GetFlags().NoRepos { 97 | err = SearchGitHub(query, options, client, &results, resultMap) 98 | if err != nil { 99 | color.Red("[!] Error searching GitHub for `" + query + "`") 100 | } 101 | } 102 | 103 | // Gist search 104 | if !GetFlags().NoGists { 105 | resultMap = make(map[string]bool) 106 | err = SearchGist(query, options, client, &results, resultMap) 107 | if err != nil { 108 | color.Red("[!] Error searching Gist for `" + query + "`") 109 | } 110 | } 111 | return results, err 112 | } 113 | 114 | // SearchGitHub searches GitHub code results for the given query 115 | func SearchGitHub(query string, options SearchOptions, client *http.Client, results *[]RepoSearchResult, resultSet map[string]bool) (err error) { 116 | base := "https://github.com/search" 117 | page, pages := 0, 1 118 | var delay = 5 119 | orders := []string{"asc"} 120 | rankings := []string{"indexed"} 121 | for i := 0; i < len(orders); i++ { 122 | for j := 0; j < len(rankings); j++ { 123 | if i == 1 && j == 1 { 124 | continue 125 | } 126 | for page < pages { 127 | str := ConstructSearchURL(base, query, options) 128 | // fmt.Println(str) 129 | // fmt.Println(str) 130 | response, err := client.Get(str) 131 | // fmt.Println(response.StatusCode) 132 | // fmt.Println(err) 133 | if err != nil { 134 | if response != nil { 135 | // fmt.Println(response.StatusCode) 136 | if response.StatusCode == 403 { 137 | response.Body.Close() 138 | delay += 5 139 | color.Yellow("[!] Rate limited by GitHub. Waiting " + strconv.Itoa(delay) + "s...") 140 | time.Sleep(time.Duration(delay) * time.Second) 141 | } else if response.StatusCode == 503 { 142 | break 143 | } 144 | } else { 145 | fmt.Println(err) 146 | } 147 | continue 148 | } 149 | if delay > 10 { 150 | delay-- 151 | } 152 | responseData, err := ioutil.ReadAll(response.Body) 153 | responseStr := string(responseData) 154 | 155 | // fmt.Println(responseStr) 156 | 157 | if err != nil { 158 | log.Fatal(err) 159 | } 160 | response.Body.Close() 161 | resultRegex := regexp.MustCompile("href=\"\\/((.*)\\/blob\\/([0-9a-f]{40}\\/([^#\"]+)))\">") 162 | matches := resultRegex.FindAllStringSubmatch(responseStr, -1) 163 | if page == 0 { 164 | if len(matches) == 0 { 165 | resultRegex = regexp.MustCompile("(?s)react-app\\.embeddedData\">(.*?)<\\/script>") 166 | match := resultRegex.FindStringSubmatch(responseStr) 167 | // fmt.Println(match) 168 | var resultPayload NewSearchPayload 169 | 170 | if len(match) == 0 { 171 | page++ 172 | continue 173 | } 174 | json.Unmarshal([]byte(match[1]), &resultPayload) 175 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 176 | if pages != resultPayload.Payload.PageCount { 177 | color.Cyan("[*] Searching " + strconv.Itoa(resultPayload.Payload.PageCount) + " pages of results for '" + query + "'...") 178 | } 179 | } 180 | pages = resultPayload.Payload.PageCount 181 | } else { 182 | regex := regexp.MustCompile("\\bdata\\-total\\-pages\\=\"(\\d+)\"") 183 | match := regex.FindStringSubmatch(responseStr) 184 | if err != nil { 185 | log.Fatal(err) 186 | } 187 | if len(match) == 2 { 188 | newPages, err := strconv.Atoi(match[1]) 189 | if err == nil { 190 | if newPages > GetFlags().Pages { 191 | newPages = GetFlags().Pages 192 | } 193 | pages = newPages 194 | if pages > 99 && GetFlags().ManyResults { 195 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 196 | color.Cyan("[*] Searching 100+ pages of results for '" + query + "'...") 197 | } 198 | orders = append(orders, "desc") 199 | rankings = append(orders, "") 200 | } else { 201 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 202 | color.Cyan("[*] Searching " + strconv.Itoa(pages) + " pages of results for '" + query + "'...") 203 | } 204 | } 205 | } else { 206 | color.Red("[!] An error occurred while parsing the page count.") 207 | fmt.Println(err) 208 | } 209 | } else { 210 | if strings.Index(responseStr, "Sign in to GitHub") > -1 { 211 | color.Red("[!] Unable to log into GitHub.") 212 | log.Fatal() 213 | } else if len(matches) > 0 { 214 | if !GetFlags().ResultsOnly { 215 | color.Cyan("[*] Searching 1 page of results for '" + query + "'...") 216 | } 217 | } 218 | } 219 | } 220 | } 221 | page++ 222 | if len(matches) == 0 { 223 | resultRegex = regexp.MustCompile("(?s)react-app\\.embeddedData\">(.*?)<\\/script>") 224 | match := resultRegex.FindStringSubmatch(responseStr) 225 | var resultPayload NewSearchPayload 226 | if len(match) > 0 { 227 | // fmt.Println(match[1]/) 228 | // fmt.Println(match[1]) 229 | json.Unmarshal([]byte(match[1]), &resultPayload) 230 | for _, result := range resultPayload.Payload.Results { 231 | if resultSet[(result.RepoName+result.Path)] == true { 232 | continue 233 | } 234 | if result.RepoName == "" { 235 | result.RepoName = result.RepoNwo 236 | } 237 | resultSet[(result.RepoName + result.Path)] = true 238 | SearchWaitGroup.Add(1) 239 | 240 | // Use worker pool instead of creating a goroutine directly 241 | workerPool := GetGlobalPool() 242 | 243 | // Create a repo result to pass to the worker 244 | repoResult := RepoSearchResult{ 245 | Repo: result.RepoName, 246 | File: result.Path, 247 | Raw: result.RepoName + "/" + result.CommitSha + "/" + result.Path, 248 | Contents: result.RepoName + "/" + result.CommitSha + "/" + result.Path, 249 | Source: "repo", 250 | Query: query, 251 | URL: "https://github.com/" + result.RepoName + "/blob/" + result.CommitSha + "/" + result.Path, 252 | } 253 | 254 | // Submit the job to the worker pool 255 | workerPool.Submit(func() { 256 | ScanAndPrintResult(client, repoResult) 257 | }) 258 | // fmt.Println(result.RepoName + "/" + result.DefaultBranch + "/" + result.Path) 259 | } 260 | } 261 | } 262 | options.Page = (page + 1) 263 | } 264 | 265 | } 266 | } 267 | return nil 268 | } 269 | 270 | // SearchGist searches Gist results for the given query 271 | func SearchGist(query string, options SearchOptions, client *http.Client, results *[]RepoSearchResult, resultSet map[string]bool) (err error) { 272 | // TODO: A lot of this code is shared between GitHub and Gist searches, 273 | // so we should rework the logic 274 | base := "https://gist.github.com/search" 275 | page, pages := 0, 1 276 | var delay = 5 277 | for page < pages { 278 | options.Page = (page + 1) 279 | response, err := client.Get(ConstructSearchURL(base, query, options)) 280 | if err != nil { 281 | if response != nil { 282 | if response.StatusCode == 403 { 283 | delay += 5 284 | color.Yellow("[!] Rate limited by GitHub. Waiting " + strconv.Itoa(delay) + "s...") 285 | time.Sleep(time.Duration(delay) * time.Second) 286 | } else if response.StatusCode == 503 { 287 | break 288 | } 289 | } else { 290 | fmt.Println(err) 291 | } 292 | continue 293 | } 294 | if delay > 10 { 295 | delay-- 296 | } 297 | responseData, err := ioutil.ReadAll(response.Body) 298 | responseStr := string(responseData) 299 | if err != nil { 300 | log.Fatal(err) 301 | } 302 | if page == 0 { 303 | regex := regexp.MustCompile("\\bdata\\-total\\-pages\\=\"(\\d+)\"") 304 | match := regex.FindStringSubmatch(responseStr) 305 | if err != nil { 306 | log.Fatal(err) 307 | } 308 | if len(match) == 2 { 309 | newPages, err := strconv.Atoi(match[1]) 310 | if err == nil { 311 | if newPages > GetFlags().Pages { 312 | newPages = GetFlags().Pages 313 | } 314 | pages = newPages 315 | if pages > 99 { 316 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 317 | color.Cyan("[*] Searching 100+ pages of Gist results for '" + query + "'...") 318 | } 319 | } else { 320 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 321 | color.Cyan("[*] Searching " + strconv.Itoa(pages) + " pages of Gist results for '" + query + "'...") 322 | } 323 | } 324 | } else { 325 | color.Red("[!] An error occurred while parsing the Gist page count.") 326 | fmt.Println(err) 327 | } 328 | } else { 329 | if strings.Index(responseStr, "Sign in to GitHub") > -1 { 330 | color.Red("[!] Unable to log into GitHub.") 331 | log.Fatal() 332 | } else { 333 | if !GetFlags().ResultsOnly && !GetFlags().JsonOutput { 334 | color.Cyan("[*] Searching 1 page of Gist results for '" + query + "'...") 335 | } 336 | } 337 | } 338 | } 339 | page++ 340 | resultRegex := regexp.MustCompile("href=\"\\/(\\w+\\/[0-9a-z]{5,})\">") 341 | matches := resultRegex.FindAllStringSubmatch(responseStr, -1) 342 | for _, element := range matches { 343 | if len(element) == 2 { 344 | if resultSet[element[1]] == true { 345 | continue 346 | } 347 | resultSet[element[1]] = true 348 | SearchWaitGroup.Add(1) 349 | 350 | // Use worker pool instead of creating a goroutine directly 351 | workerPool := GetGlobalPool() 352 | 353 | // Create a gist result to pass to the worker 354 | gistResult := RepoSearchResult{ 355 | Repo: "gist:" + element[1], 356 | File: element[1], 357 | Raw: GetRawGistPage(client, element[1]), 358 | Contents: GetRawGistPage(client, element[1]), 359 | Source: "gist", 360 | Query: query, 361 | URL: "https://gist.github.com/" + element[1] + "#file-" + element[1], 362 | } 363 | 364 | // Submit the job to the worker pool 365 | workerPool.Submit(func() { 366 | ScanAndPrintResult(client, gistResult) 367 | }) 368 | } 369 | } 370 | time.Sleep(time.Duration(delay) * time.Second) 371 | } 372 | return nil 373 | } 374 | -------------------------------------------------------------------------------- /internal/app/util.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | // GetFileLines takes a file path and returns its lines, stringified. 12 | func GetFileLines(file string) (lines []string) { 13 | dat, err := ioutil.ReadFile(file) 14 | if err != nil { 15 | color.Red("[!] File '" + file + "' does not exist.") 16 | log.Fatal() 17 | } 18 | linesUnparsed := strings.Split(string(dat), "\n") 19 | for _, line := range linesUnparsed { 20 | if line != "" { 21 | lines = append(lines, line) 22 | } 23 | } 24 | return lines 25 | } 26 | 27 | // Abs returns the absolute value of x. 28 | func Abs(x int) int { 29 | if x < 0 { 30 | return -x 31 | } 32 | return x 33 | } 34 | 35 | // CheckErr checks if an error is not null, and 36 | // exits if it is not null. 37 | func CheckErr(err error) { 38 | if err != nil { 39 | color.Red("[!] An error has occurred.") 40 | log.Fatal(err) 41 | } 42 | } 43 | 44 | // GetRepoURLForSearchResult returns the URL of the repo depending on 45 | // RepoSearchResult source 46 | func GetRepoURLForSearchResult(repo RepoSearchResult) string { 47 | if repo.Source == "repo" { 48 | return "https://github.com/" + repo.Repo 49 | } else if repo.Source == "gist" { 50 | return "https://gist.github.com/" + repo.Repo 51 | } 52 | // Left this way in case other Source values ever exist 53 | return "" 54 | } 55 | 56 | // GetRawURLForSearchResult returns a raw data URL for a RepoSearchResult 57 | func GetRawURLForSearchResult(repo RepoSearchResult) string { 58 | if repo.Source == "repo" { 59 | return "https://raw.githubusercontent.com" 60 | } else if repo.Source == "gist" { 61 | return "https://gist.githubusercontent.com" 62 | } 63 | // Left this way in case other Source values ever exist 64 | return "" 65 | } 66 | 67 | func slice_contains(slice []string, item string) bool { 68 | for _, s := range slice { 69 | if s == item { 70 | return true 71 | } 72 | } 73 | return false 74 | } 75 | -------------------------------------------------------------------------------- /internal/app/websocket.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/fatih/color" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | var wsConn *websocket.Conn 17 | var wsMessageChannel chan string 18 | var InsertKey string // Global variable for InsertKey 19 | 20 | func StartWebSocket(url string) { 21 | dialer := websocket.Dialer{ 22 | HandshakeTimeout: 10 * time.Second, 23 | EnableCompression: false, 24 | } 25 | var err error 26 | wsConn, _, err = dialer.Dial(url, nil) 27 | if err != nil { 28 | color.Red("Error connecting to GitHound Explore connector: %v", err) 29 | time.Sleep(5 * time.Second) 30 | return 31 | } 32 | wsMessageChannel = make(chan string) 33 | 34 | go func() { 35 | defer wsConn.Close() 36 | for { 37 | message := <-wsMessageChannel 38 | err := wsConn.WriteMessage(websocket.TextMessage, []byte(message)) 39 | if err != nil { 40 | color.Red("Error writing to WebSocket: %v", err) 41 | return 42 | } 43 | } 44 | }() 45 | 46 | // Use Insert Key from environment variable if available 47 | if GetFlags().InsertKey != "" { 48 | InsertKey = GetFlags().InsertKey 49 | return 50 | } 51 | 52 | // Fall back to reading from token file 53 | homeDir, err := os.UserHomeDir() 54 | if err != nil { 55 | color.Red("Error getting home directory: %v", err) 56 | time.Sleep(5 * time.Second) 57 | return 58 | } 59 | 60 | gitHoundDir := filepath.Join(homeDir, ".githound") 61 | tokenFilePath := filepath.Join(gitHoundDir, "insert_token.txt") 62 | 63 | var token string 64 | if _, err := os.Stat(tokenFilePath); err == nil { 65 | // Token file exists, load the token 66 | tokenBytes, err := ioutil.ReadFile(tokenFilePath) 67 | if err != nil { 68 | color.Red("Error accessing cached GitHound token at ~/.githound/insert_token.txt: %v", err) 69 | time.Sleep(5 * time.Second) 70 | return 71 | } 72 | token = string(tokenBytes) 73 | 74 | // Send the token to the WebSocket 75 | payload := fmt.Sprintf(`{"event": "gh_banner", "ghVersion": "1.0.0", "insertToken": "%s"}`, token) 76 | err = wsConn.WriteMessage(websocket.TextMessage, []byte(payload)) 77 | if err != nil { 78 | color.Red("Error sending WebSocket message: %v", err) 79 | log.Fatal(err) 80 | } 81 | for { 82 | _, message, err := wsConn.ReadMessage() 83 | if err != nil { 84 | color.Red("Error reading WebSocket message: %v", err) 85 | fmt.Println(message) 86 | log.Fatal(err) 87 | } 88 | 89 | var response map[string]interface{} 90 | err = json.Unmarshal(message, &response) 91 | if err != nil { 92 | color.Red("Error unmarshalling WebSocket message: %v", err) 93 | log.Fatal(err) 94 | } 95 | 96 | if loggedIn, ok := response["logged_in"].(bool); ok && loggedIn { 97 | break 98 | } else { 99 | ConnectToAccount(response) 100 | } 101 | } 102 | } else { 103 | payload := fmt.Sprintf(`{"event": "gh_banner", "ghVersion": "1.0.0"}`) 104 | err = wsConn.WriteMessage(websocket.TextMessage, []byte(payload)) 105 | if err != nil { 106 | color.Red("Error sending WebSocket message: %v", err) 107 | log.Fatal(err) 108 | } 109 | _, message, err := wsConn.ReadMessage() 110 | if err != nil { 111 | color.Red("Error reading WebSocket message: %v", err) 112 | log.Fatal(err) 113 | } 114 | 115 | var response map[string]interface{} 116 | _ = json.Unmarshal(message, &response) 117 | token = ConnectToAccount(response) 118 | } 119 | InsertKey = token 120 | } 121 | 122 | func ConnectToAccount(response map[string]interface{}) string { 123 | if wsConn == nil { 124 | color.Red("WebSocket connection is not established") 125 | return "" 126 | } 127 | var first = true 128 | var message string 129 | var token string 130 | for { 131 | if !first { 132 | _, message, err := wsConn.ReadMessage() 133 | if err != nil { 134 | color.Red("Error reading WebSocket message: %v", err) 135 | log.Fatal(err) 136 | } 137 | 138 | _ = json.Unmarshal(message, &response) 139 | } else { 140 | first = false 141 | } 142 | 143 | if loggedIn, ok := response["logged_in"].(bool); ok && !loggedIn { 144 | if url, ok := response["url"].(string); ok { 145 | color.Cyan("Please visit the following URL to link your account: %s", url) 146 | color.Cyan("Waiting for verification...") 147 | for i := 0; i < 3; i++ { 148 | fmt.Print(".") 149 | time.Sleep(500 * time.Millisecond) 150 | } 151 | fmt.Println() 152 | } 153 | } else if loggedIn, ok := response["logged_in"].(bool); ok && loggedIn { 154 | if insertToken, ok := response["insert_token"].(string); ok { 155 | token = insertToken 156 | 157 | homeDir, err := os.UserHomeDir() 158 | if err != nil { 159 | color.Red("Error getting home directory: %v", err) 160 | log.Fatal(err) 161 | } 162 | 163 | gitHoundDir := filepath.Join(homeDir, ".githound") 164 | tokenFilePath := filepath.Join(gitHoundDir, "insert_token.txt") 165 | 166 | // Create the .githound directory if it doesn't exist 167 | if _, err := os.Stat(gitHoundDir); os.IsNotExist(err) { 168 | err = os.Mkdir(gitHoundDir, 0700) 169 | if err != nil { 170 | color.Red("Error creating .githound directory: %v", err) 171 | log.Fatal(err) 172 | } 173 | } 174 | 175 | // Save the token to the file 176 | err = ioutil.WriteFile(tokenFilePath, []byte(token), 0600) 177 | if err != nil { 178 | color.Red("Error writing token file: %v", err) 179 | log.Fatal(err) 180 | } 181 | 182 | break 183 | } 184 | } else { 185 | color.Red("Unexpected WebSocket response: %s", string(message)) 186 | log.Fatal("Unexpected WebSocket response") 187 | } 188 | 189 | } 190 | 191 | return token 192 | } 193 | 194 | func SendMessageToWebSocket(message string) { 195 | if wsMessageChannel != nil { 196 | wsMessageChannel <- message 197 | } 198 | } 199 | 200 | // SendToWebSocket sends a message to the WebSocket connection 201 | func SendToWebSocket(message string) { 202 | if wsMessageChannel != nil { 203 | wsMessageChannel <- message 204 | } else { 205 | color.Yellow("[!] WebSocket not initialized") 206 | } 207 | } 208 | 209 | func BrokerSearchCreation(query string) { 210 | if wsConn == nil { 211 | color.Red("WebSocket connection is not established") 212 | return 213 | } 214 | 215 | // First send the insert key to authenticate 216 | authPayload := fmt.Sprintf(`{"event": "gh_banner", "ghVersion": "1.0.0", "insertToken": "%s"}`, InsertKey) 217 | err := wsConn.WriteMessage(websocket.TextMessage, []byte(authPayload)) 218 | if err != nil { 219 | color.Red("Error sending authentication message: %v", err) 220 | return 221 | } 222 | 223 | // Wait for authentication response 224 | _, message, err := wsConn.ReadMessage() 225 | if err != nil { 226 | color.Red("Error reading authentication response: %v", err) 227 | return 228 | } 229 | 230 | var authResponse map[string]interface{} 231 | err = json.Unmarshal(message, &authResponse) 232 | if err != nil { 233 | color.Red("Error unmarshalling authentication response: %v", err) 234 | return 235 | } 236 | 237 | if loggedIn, ok := authResponse["logged_in"].(bool); !ok || !loggedIn { 238 | color.Red("Error authenticating with insert key") 239 | return 240 | } 241 | 242 | // Skip start_search if in dashboard mode with search ID 243 | if GetFlags().Dashboard && GetFlags().SearchID != "" { 244 | color.Green("Connected to GitHound Explore! Using existing search ID: %s", GetFlags().SearchID) 245 | return 246 | } 247 | 248 | // Now send the search query 249 | escapedQuery, err := json.Marshal(query) 250 | if err != nil { 251 | color.Red("Error escaping search query") 252 | return 253 | } 254 | payload := fmt.Sprintf(`{"event": "start_search", "insertToken": "%s", "searchQuery": %s}`, InsertKey, escapedQuery) 255 | err = wsConn.WriteMessage(websocket.TextMessage, []byte(payload)) 256 | if err != nil { 257 | color.Red("Error sending search message: %v", err) 258 | return 259 | } 260 | 261 | _, message, err = wsConn.ReadMessage() 262 | if err != nil { 263 | color.Red("Error reading search response: %v", err) 264 | return 265 | } 266 | 267 | var response map[string]interface{} 268 | err = json.Unmarshal(message, &response) 269 | if err != nil { 270 | color.Red("Error unmarshalling search response: %v", err) 271 | return 272 | } 273 | 274 | if event, ok := response["event"].(string); ok && event == "search_ack" { 275 | if _, ok := response["searchID"].(string); ok { 276 | if url, ok := response["url"].(string); ok { 277 | color.Green("Connected to GitHound Explore! View search results at: %s", url) 278 | } 279 | } 280 | } else if errorMsg, ok := response["error"].(string); ok { 281 | color.Red("Error starting search: %s", errorMsg) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /internal/app/worker_pool.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | // JobFunc represents a job to be executed by the worker pool 10 | type JobFunc func() 11 | 12 | // WorkerPool manages a pool of workers for concurrent execution 13 | type WorkerPool struct { 14 | workQueue chan JobFunc 15 | wg sync.WaitGroup 16 | once sync.Once 17 | closed atomic.Bool 18 | } 19 | 20 | var ( 21 | // Global worker pool instance 22 | globalPool *WorkerPool 23 | globalPoolOnce sync.Once 24 | ) 25 | 26 | // GetGlobalPool returns the global worker pool instance 27 | func GetGlobalPool() *WorkerPool { 28 | globalPoolOnce.Do(func() { 29 | // Default to CPUs/2 for number of workers, minimum 1 30 | numWorkers := 1 31 | if numWorkers < 1 { 32 | numWorkers = 1 33 | } 34 | 35 | // Allow override via flags if set 36 | if GetFlags().Threads > 0 { 37 | numWorkers = GetFlags().Threads 38 | } 39 | 40 | globalPool = NewWorkerPool(numWorkers) 41 | globalPool.Start() 42 | }) 43 | return globalPool 44 | } 45 | 46 | // NewWorkerPool creates a new worker pool with the specified number of workers 47 | func NewWorkerPool(numWorkers int) *WorkerPool { 48 | // Buffer the channel to allow more queueing (10x the worker count) 49 | return &WorkerPool{ 50 | workQueue: make(chan JobFunc, numWorkers*10), 51 | } 52 | } 53 | 54 | // Start launches the worker pool 55 | func (p *WorkerPool) Start() { 56 | p.once.Do(func() { 57 | numWorkers := cap(p.workQueue) / 10 58 | p.wg.Add(numWorkers) 59 | 60 | for i := 0; i < numWorkers; i++ { 61 | go func(workerID int) { 62 | defer p.wg.Done() 63 | if GetFlags().Debug { 64 | LogInfo("Worker %d started", workerID) 65 | } 66 | for job := range p.workQueue { 67 | if job != nil { 68 | if GetFlags().Debug { 69 | LogInfo("Worker %d processing job", workerID) 70 | } 71 | job() 72 | if GetFlags().Debug { 73 | LogInfo("Worker %d completed job", workerID) 74 | } 75 | } 76 | } 77 | if GetFlags().Debug { 78 | LogInfo("Worker %d shutting down", workerID) 79 | } 80 | }(i) 81 | } 82 | 83 | if GetFlags().Debug { 84 | LogInfo("Started worker pool with %d workers", numWorkers) 85 | } 86 | }) 87 | } 88 | 89 | // Submit adds a job to the worker pool 90 | func (p *WorkerPool) Submit(job JobFunc) { 91 | if p.closed.Load() { 92 | // If pool is closed, execute job directly 93 | job() 94 | return 95 | } 96 | 97 | if GetFlags().Debug { 98 | LogInfo("Submitting job to worker pool (queue length: %d/%d)", len(p.workQueue), cap(p.workQueue)) 99 | } 100 | 101 | select { 102 | case p.workQueue <- job: 103 | // Job submitted successfully 104 | if GetFlags().Debug { 105 | LogInfo("Job submitted successfully") 106 | } 107 | default: 108 | // Channel is full, execute job directly 109 | if GetFlags().Debug { 110 | LogInfo("Worker pool queue full, executing job directly") 111 | } 112 | job() 113 | } 114 | } 115 | 116 | // Wait waits for all jobs to complete 117 | func (p *WorkerPool) Wait() { 118 | if p.closed.Load() { 119 | return 120 | } 121 | p.closed.Store(true) 122 | close(p.workQueue) 123 | p.wg.Wait() 124 | } 125 | 126 | // Printf formats and prints a message 127 | func Printf(format string, args ...interface{}) { 128 | fmt.Printf(format+"\n", args...) 129 | } 130 | 131 | // LogInfo logs informational messages if debug mode is enabled 132 | func LogInfo(format string, args ...interface{}) { 133 | if GetFlags().Debug { 134 | Printf(format, args...) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "net/http/pprof" 5 | 6 | "github.com/tillson/git-hound/cmd" 7 | ) 8 | 9 | func main() { 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /regex.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tillson/git-hound/2da82e38e3b54a3846b9f5cc8379d6efc27042dc/regex.yml -------------------------------------------------------------------------------- /rules-generator/import_gitleaks_rules.py: -------------------------------------------------------------------------------- 1 | # Import GitLeaks (https://github.com/zricethezav/gitleaks) regex rules into the GitHub rules file 2 | # Note that many of the rules require post-processing to work correctly with GitHound. This is already done in the included version. 3 | 4 | import requests 5 | import sys 6 | import datetime 7 | 8 | 9 | if len(sys.argv) < 2: 10 | print("Usage: python import_gitleaks_rules.py ") 11 | sys.exit(1) 12 | 13 | out_file = open(sys.argv[1], "a") 14 | out_file.write("\n# The following rules are from GitLeaks (https://github.com/zricethezav/gitleaks), which is released under an MIT license (https://github.com/zricethezav/gitleaks/blob/master/LICENSE)\n") 15 | 16 | now = datetime.datetime.now() 17 | date = now.strftime("%Y-%m-%d") 18 | out_file.write(f"### BEGIN GITLEAKS RULES {date}\n") 19 | data = requests.get("https://raw.githubusercontent.com/zricethezav/gitleaks/master/config/gitleaks.toml").text.split("\n") 20 | 21 | 22 | for i in range(0, len(data)): 23 | if data[i] == "[[rules]]": 24 | out_file.write("[[rules]]\n") 25 | i += 1 26 | out_file.write(data[i] + "\n") 27 | i += 1 28 | out_file.write(data[i] + "\n") 29 | i += 1 30 | out_file.write(data[i] + "\n") 31 | 32 | out_file.write("### END GITLEAKS RULES\n") 33 | out_file.close() -------------------------------------------------------------------------------- /rules/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /rules/adobe.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Adobe OAuth Client Secret 4 | id: np.adobe.1 5 | 6 | pattern: (?i:\b(p8e-[a-z0-9-]{32})(?:[^a-z0-9-]|$)) 7 | 8 | 9 | references: 10 | - https://developer.adobe.com/developer-console/docs/guides/authentication/ 11 | - https://developer.adobe.com/developer-console/docs/guides/authentication/OAuthIntegration/ 12 | - https://developer.adobe.com/developer-console/docs/guides/authentication/OAuth/ 13 | 14 | examples: 15 | - | 16 | { 17 | "client_credentials": { 18 | "client_id": "a65b0146769d433a835f36660881db50", 19 | "client_secret": "p8e-ibndcvsmAp9ZgPBZ606FSlYIZVlsZ-g5" 20 | }, 21 | -------------------------------------------------------------------------------- /rules/age.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Age Recipient (X25519 public key) 4 | id: np.age.1 5 | pattern: '\b(age1[0-9a-z]{58})\b' 6 | 7 | examples: 8 | - 'age1zvkyg2lqzraa2lnjvqej32nkuu0ues2s82hzrye869xeexvn73equnujwj' 9 | 10 | references: 11 | - https://age-encryption.org 12 | - https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/main/doc/age.1.html 13 | - https://github.com/C2SP/C2SP/blob/8b6a842e0360d35111c46be2a8019b2276295914/age.md#the-x25519-recipient-type 14 | 15 | 16 | - name: Age Identity (X22519 secret key) 17 | id: np.age.2 18 | pattern: '\b(AGE-SECRET-KEY-1[0-9A-Z]{58})\b' 19 | 20 | examples: 21 | - | 22 | # created: 2022-09-26T21:55:47-05:00 23 | # public key: age1epzmwwzw8n09slh0c7z1z52x43nnga7lkksx3qrh07tqz5v7lcys45428t 24 | AGE-SECRET-KEY-1HJCRJVK7EE3A5N8CRP8YSEUGZKNW90Y5UR2RGYAS8L279LFP6LCQU5ADNR 25 | - 'AGE-SECRET-KEY-1GFPYYSJZGFPYYSJZGFPYYSJZGFPYYSJZGFPYYSJZGFPYYSJZGFPQ4EGAEX' 26 | 27 | references: 28 | - https://age-encryption.org 29 | - https://htmlpreview.github.io/?https://github.com/FiloSottile/age/blob/main/doc/age.1.html 30 | - https://github.com/C2SP/C2SP/blob/8b6a842e0360d35111c46be2a8019b2276295914/age.md#the-x25519-recipient-type 31 | -------------------------------------------------------------------------------- /rules/artifactory.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Artifactory API Key 4 | id: np.artifactory.1 5 | # FIXME: all the real onces start with `AKC`? 6 | pattern: '(?i)artifactory.{0,50}\b([a-z0-9]{73})\b' 7 | 8 | examples: 9 | - | 10 | export HOMEBREW_ARTIFACTORY_API_TOKEN=AKCp8igrDNFerC357m4422e4tmu7xB983QLPxJhKFcSMfoux2RFvp8rc4jC8t9ncdmYCMFD8W 11 | export HOMEBREW_ARTIFACTORY_API_USER=kashorn 12 | - 'jfrog rt dl --url=http://localhost:8071/artifactory --apikey=AKCp2WXX7SDvcsmny528sSDnaB3zACkNQoRcD8D1WmxhMV9gk6Wp8mVWC8bh38kJQbXagUT8Z generic-local/hello.txt' 13 | 14 | references: 15 | - https://jfrog.com/help/r/jfrog-rest-apis/introduction-to-the-artifactory-rest-apis 16 | -------------------------------------------------------------------------------- /rules/aws.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: AWS API Key 4 | id: np.aws.1 5 | 6 | pattern: '\b((?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})\b' 7 | 8 | references: 9 | - https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html 10 | - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html 11 | - https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html 12 | - https://docs.aws.amazon.com/accounts/latest/reference/credentials-access-keys-best-practices.html 13 | 14 | examples: 15 | - 'A3T0ABCDEFGHIJKLMNOP' 16 | - 'AKIADEADBEEFDEADBEEF' 17 | 18 | negative_examples: 19 | - 'A3T0ABCDEFGHIJKLMNO' 20 | - 'A3T0ABCDEFGHIjklmnop' 21 | - '======================' 22 | - '//////////////////////' 23 | - '++++++++++++++++++++++' 24 | 25 | 26 | - name: AWS Secret Access Key 27 | id: np.aws.2 28 | 29 | pattern: "(?i)\\baws_?(?:secret)?_?(?:access)?_?(?:key)?[\"'']?\\s{0,30}(?::|=>|=)\\s{0,30}[\"'']?([a-z0-9/+=]{40})\\b" 30 | 31 | references: 32 | - https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html 33 | - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html 34 | - https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html 35 | - https://docs.aws.amazon.com/accounts/latest/reference/credentials-access-keys-best-practices.html 36 | 37 | examples: 38 | - 'aws_secret_access_key:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 39 | - 'aws_secret_access_key => aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 40 | 41 | negative_examples: 42 | - 'export AWS_SECRET_ACCESS_KEY=ded7db27a4558e2ea9bbf0bf36ae0e8521618f366c' 43 | - '"aws_secret_access_key" = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaend' 44 | - '"aws_secret_access_key" = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaendbbbbbbb' 45 | - '"aws_sEcReT_key" = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaend' 46 | # FIXME: modify the pattern to detect cases like the following 47 | - 'aws_secret_key: OOzkR1+hF+1ABCsIFDJMEUtqmtnZ1234567890' 48 | - '======================' 49 | - '//////////////////////' 50 | - '++++++++++++++++++++++' 51 | 52 | 53 | - name: AWS Account ID 54 | id: np.aws.3 55 | 56 | pattern: '(?i)aws_?(?:account)_?(?:id)?["''`]?\s{0,30}(?::|=>|=)\s{0,30}["''`]?([0-9]{4}-?[0-9]{4}-?[0-9]{4})' 57 | 58 | examples: 59 | - | 60 | KeyMetadata: { 61 | AWSAccountId: "324320755747", 62 | Arn: "arn:aws:kms:us-east-2:324320755747:key/54348bc1-6e3b-4cda-8b18-c6033ca7d328", 63 | CreationDate: 2019-07-12 18:23:13 +0000 UTC, 64 | Description: "", 65 | Enabled: true, 66 | KeyId: "54348bc1-6e3b-4cda-8b18-c6033ca7d328", 67 | KeyManager: "CUSTOMER", 68 | KeyState: "Enabled", 69 | KeyUsage: "ENCRYPT_DECRYPT", 70 | Origin: "AWS_KMS" 71 | } 72 | - | 73 | 4. login into ecr 74 | 75 | ```bash 76 | aws_region=eu-central-1 77 | aws_account_id=891511536143 78 | aws_profile=serverless-bert 79 | 80 | aws ecr get-login-password \ 81 | --region $aws_region \ 82 | --profile $aws_profile \ 83 | | docker login \ 84 | --username AWS \ 85 | --password-stdin $aws_account_id.dkr.ecr.$aws_region.amazonaws.com 86 | ``` 87 | 88 | negative_examples: 89 | - '======================' 90 | - '//////////////////////' 91 | - '++++++++++++++++++++++' 92 | 93 | 94 | - name: AWS Session Token 95 | id: np.aws.4 96 | pattern: '(?i)(?:aws.?session|aws.?session.?token|aws.?token)["''`]?\s{0,30}(?::|=>|=)\s{0,30}["''`]?([a-z0-9/+=]{16,200})[^a-z0-9/+=]' 97 | 98 | negative_examples: 99 | - '======================' 100 | - '//////////////////////' 101 | - '++++++++++++++++++++++' 102 | 103 | examples: 104 | - | 105 | export AWS_ACCESS_KEY_ID="I08BCX2ACV45ED1DOC9J" 106 | export AWS_SECRET_ACCESS_KEY="0qk+o7XctJMmG6ydO8537c9+TofLJU1K0PiVBXSg" 107 | export AWS_SESSION_TOKEN="eyJhbGciOiJIUzUxMi53InR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJJMDhCQ1gySkpWNDVFRDFET0M5SiIsImFjciI6Ij53LCJhdWQiOiJhY2NvdW50IiwiYXV0aF90aW1lIjowLCJhenAiOiJtaW5pbyIsImVtYWlsIjoiYWlkYW4uY29wZUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImV4cCI6MTU4MDUwMDIzOCwiZmFtaWx5X25hbWUiOiJDb3BlIiwiZ2l2ZW5fbmFtZSI6IkFpZGFuIENvcGUiLCJpYXQiOjE1ODA0OTk5MzgsImlzcyI6Imh0dHBzOi8vYXV0aHN0Zy5wb3BkYXRhLmJjLmNhL2F1dGgvcmVhbG1zL3NhbXBsZSIsImp0aSI6IjU5ZTM5ODAxLWQxMmUtNDVhYS04NmQzLWVhMmNmZDU0NmE2MiIsIm1pbmlvX3BvbGljeSI6ImRhdGFzZXRfMV9ybyIsIm5hbWUiOiJBaWRhbiBDb3BlIENvcGUiLCJuYmYiOjAsInByZWZlcnJlZF91c2VybmFtZSI6ImFjb3BlLTk5LXQwNSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2Vzc2lvbl9zdGF0ZSI6IjcxYjczZWJjLThlMzMtNGMyMi04NmE2LWI0MzhhNDM4ZmI2MiIsInN1YiI6IjVkOTBlOTgzLTA1NDItNDYyYS1hZWIwLWYxZWVmNjcwYzdlNSIsInR5cCI6IkJlYXJlciJ9.J-a9PORJToz7MUrnPQlOywcqtVMNkXy53Gedp_V4PW-Gbf1_BAMjwuw_X7fKRd6hkNfEn43CKKju7muzi_d1Ig" 108 | 109 | 110 | # Note that this service is being deprecated on March 31, 2024 111 | - name: Amazon MWS Auth Token 112 | id: np.aws.5 113 | pattern: '(?i)(amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})' 114 | 115 | examples: 116 | - | 117 | AWS Access Key:AKIAIDQJ6PTGDFFWYX7A 118 | secret key:IwJz1SHMccAKUKuskdVoHFfkre73BTyF80nRmcWc 119 | MWS Authorisation Token: amzn.mws.dab428a1-ed97-fd8d-e045-950d712f6f58 120 | 121 | references: 122 | - https://docs.developer.amazonservices.com/en_US/dev_guide/index.html 123 | 124 | 125 | # - name: AWS S3 Bucket (subdomain style) 126 | # id: np.s3.1 127 | 128 | # pattern: | 129 | # (?x) 130 | # (?: ^ | [\s/"'] | %2F ) 131 | # ( 132 | # (?: [a-zA-Z0-9_-]+ \. )+ (?# bucket name as subdomain ) 133 | # (?: s3 134 | # | s3-af-south-1 135 | # | s3-ap-east-1 136 | # | s3-ap-northeast-1 137 | # | s3-ap-northeast-2 138 | # | s3-ap-northeast-3 139 | # | s3-ap-south-1 140 | # | s3-ap-south-2 141 | # | s3-ap-southeast-1 142 | # | s3-ap-southeast-2 143 | # | s3-ap-southeast-3 144 | # | s3-ap-southeast-4 145 | # | s3-ca-central-1 146 | # | s3-eu-central-1 147 | # | s3-eu-central-2 148 | # | s3-eu-north-1 149 | # | s3-eu-south-1 150 | # | s3-eu-south-2 151 | # | s3-eu-west-1 152 | # | s3-eu-west-2 153 | # | s3-eu-west-3 154 | # | s3-me-central-1 155 | # | s3-me-south-1 156 | # | s3-sa-east-1 157 | # | s3-us-east-1 158 | # | s3-us-east-2 159 | # | s3-us-gov-east-1 160 | # | s3-us-gov-west-1 161 | # | s3-us-west-1 162 | # | s3-us-west-2 163 | # ) 164 | # \.amazonaws\.com 165 | # ) \b 166 | 167 | # references: 168 | # - https://docs.aws.amazon.com/general/latest/gr/rande.html 169 | 170 | # examples: 171 | # - 'example-bucket.s3.amazonaws.com' 172 | # - 'http://bucket.s3-us-east-2.amazonaws.com' 173 | # - 'http%2F%2Fsome-bucket.s3.amazonaws.com' 174 | 175 | # negative_examples: 176 | # - '.s3.amazonaws.com' 177 | # - 's3.amazonaws.com' 178 | 179 | 180 | # - name: AWS S3 Bucket (path style) 181 | # id: np.s3.2 182 | 183 | # pattern: | 184 | # (?x) 185 | # (?: ^ | [\s/"'] | %2F ) 186 | # ( 187 | # (?: s3 188 | # | s3-af-south-1 189 | # | s3-ap-east-1 190 | # | s3-ap-northeast-1 191 | # | s3-ap-northeast-2 192 | # | s3-ap-northeast-3 193 | # | s3-ap-south-1 194 | # | s3-ap-south-2 195 | # | s3-ap-southeast-1 196 | # | s3-ap-southeast-2 197 | # | s3-ap-southeast-3 198 | # | s3-ap-southeast-4 199 | # | s3-ca-central-1 200 | # | s3-eu-central-1 201 | # | s3-eu-central-2 202 | # | s3-eu-north-1 203 | # | s3-eu-south-1 204 | # | s3-eu-south-2 205 | # | s3-eu-west-1 206 | # | s3-eu-west-2 207 | # | s3-eu-west-3 208 | # | s3-me-central-1 209 | # | s3-me-south-1 210 | # | s3-sa-east-1 211 | # | s3-us-east-1 212 | # | s3-us-east-2 213 | # | s3-us-gov-east-1 214 | # | s3-us-gov-west-1 215 | # | s3-us-west-1 216 | # | s3-us-west-2 217 | # ) 218 | # \.amazonaws\.com 219 | # / 220 | # [a-zA-Z0-9_][a-zA-Z0-9_-]* (?: \. [a-zA-Z0-9_-]+)* (?# bucket name as path ) 221 | # ) 222 | # (?: [^a-zA-Z0-9_-] | $ ) (?# this instead of a \b anchor because that doesn't play nicely with `-` ) 223 | 224 | # references: 225 | # - https://docs.aws.amazon.com/general/latest/gr/rande.html 226 | 227 | # examples: 228 | # - 's3.amazonaws.com/example-bucket' 229 | # - 'http://s3-us-east-2.amazonaws.com/example-bucket' 230 | 231 | # negative_examples: 232 | # - '.s3.amazonaws.com' 233 | # - 's3.amazonaws.com' 234 | # - 's3.amazonaws.com/' 235 | # - 'some-bucket-name.s3.amazonaws.com/171ea24dd241f8a2178b0374-username-Reponame-3-0' 236 | # - 'some-bucket.s3.amazonaws.com/some-object-here' 237 | 238 | 239 | # - name: Amazon Resource Name 240 | # id: np.arn.1 241 | 242 | # pattern: | 243 | # (?x) 244 | # \b 245 | # ( 246 | # arn 247 | # : 248 | # (?: aws | aws-cn | aws-us-gov ) (?# partition ) 249 | # : 250 | # [a-zA-Z0-9_-]{2,} (?# service ) 251 | # : 252 | # (?: af-south-1 253 | # | ap-east-1 254 | # | ap-northeast-1 255 | # | ap-northeast-2 256 | # | ap-northeast-3 257 | # | ap-south-1 258 | # | ap-south-2 259 | # | ap-southeast-1 260 | # | ap-southeast-2 261 | # | ap-southeast-3 262 | # | ap-southeast-4 263 | # | ca-central-1 264 | # | eu-central-1 265 | # | eu-central-2 266 | # | eu-north-1 267 | # | eu-south-1 268 | # | eu-south-2 269 | # | eu-west-1 270 | # | eu-west-2 271 | # | eu-west-3 272 | # | me-central-1 273 | # | me-south-1 274 | # | sa-east-1 275 | # | us-east-1 276 | # | us-east-2 277 | # | us-gov-east-1 278 | # | us-gov-west-1 279 | # | us-west-1 280 | # | us-west-2 281 | # )? (?# region ) 282 | # : 283 | # (?: \d{12} )? (?# account ID sans hyphens ) 284 | # : 285 | # (?: [a-zA-Z0-9_-]+ [:/])? (?# resource type) 286 | # [^\s"'&<>\\%]+ (?# resource ID) 287 | # ) 288 | # (?: [\s"'&<>\\%] | $ ) 289 | 290 | # references: 291 | # - https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html 292 | 293 | # examples: 294 | # - 'arn:aws:s3:::my_corporate_bucket/*' 295 | # - 'arn:aws:s3:::my_corporate_bucket/Development/*' 296 | # - 'arn:aws:iam::123456789012:user/Development/product_1234/*' 297 | # - 'alerts: "arn:aws:sns:us-west-2:123456789023:CloudwatchMetricAlarm"' 298 | # - '"Principal":{"Federated":["arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"]},' 299 | # - '"KeyId": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",' 300 | # - '"aws-kms://arn:aws:kms:us-east-1:003084325827:key/84a66985-f968-4bac-82c2-365518adf157";' 301 | # - 'return f"arn:aws:s3:::{bucket_name}"' 302 | # - 'return f"arn:aws:s3:::${bucket_name}"' 303 | -------------------------------------------------------------------------------- /rules/azure.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Azure Connection String 4 | id: np.azure.1 5 | 6 | # XXX There are a bunch of other keys that seem to have secret content assigned to them: 7 | # 8 | # - SharedAccessSignature 9 | # - Authorization 10 | # - UserToken 11 | # - ApplicationToken 12 | # 13 | # Maybe we can generalize this rule one day. 14 | pattern: (?i:(?:AccountName|SharedAccessKeyName|SharedSecretIssuer)\s*=\s*([^;]{1,80})\s*;\s*.{0,10}\s*(?:AccountKey|SharedAccessKey)\s*=\s*([^;]{1,80})) 15 | 16 | references: 17 | - https://azure.microsoft.com/en-us/blog/windows-azure-web-sites-how-application-strings-and-connection-strings-work/ 18 | - https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string 19 | - https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-sas#best-practices-when-using-sas 20 | 21 | examples: 22 | - | 23 | # Azure Storage Connection String 24 | AzureWebJobsStorage=DefaultEndpointsProtocol=https;AccountName=hanatour9833;AccountKey=6jqh42QQjWWBwoPGGR/Jr0PZjhBMZVbHm/gkhEfHvOj8aV6+oI8ed6ZAAwB5a6993WqyQDiuJJB0QpseJwqYxw==;EndpointSuffix=core.windows.net 25 | - | 26 | DefaultEndpointsProtocol=http;AccountName=testacc1; 27 | AccountKey=1gy3lpE7Du1j5ljKiupgKzywSw2isjsa69sfsdfsdsgfsgfdgfdgfd/YThisv/OVVLfIOv9kQ==; 28 | BlobEndpoint=http://127.0.0.1:8440/testacc1; 29 | TableEndpoint=http://127.0.0.1:8440/testacc1; 30 | QueueEndpoint=http://127.0.0.1:8440/testacc1; 31 | 32 | - | 33 | "IOTHUB_CONNECTION_STRING": { 34 | "value": "HostName=d1-vi-ioth521.azure-devices.net;SharedAccessKeyName=registryReadWrite;SharedAccessKey=S8ii67l3Gd1Ba69az78iP9UksewzhjvUfh1DIuDs30w=" 35 | } 36 | 37 | - | 38 | "AZURE_STORAGE_CONNECTION_STRING": { 39 | "value": "DefaultEndpointsProtocol=https;AccountName=d1biblobstor521;AccountKey=NjEwGHd9+piK+iCi2C2XURWPmeDDjif9UKN1HAszYptL4iQ+yD7/dgjLMZc3VOpURsa53aJ4HZfbVWzL429C5g==;EndpointSuffix=core.windows.net" 40 | } 41 | 42 | negative_examples: 43 | # https://docs.microsoft.com/en-us/azure/azure-monitor/app/sdk-connection-string 44 | - 'InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;' 45 | - 'InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://custom.com:111/;LiveEndpoint=https://custom.com:222/;ProfilerEndpoint=https://custom.com:333/;SnapshotEndpoint=https://custom.com:444/;' 46 | 47 | 48 | - name: Azure App Configuration Connection String 49 | id: np.azure.2 50 | 51 | pattern: (https://[a-zA-Z0-9-]+\.azconfig\.io);Id=(.{4}-.{2}-.{2}:[a-zA-Z0-9+/]{18,22});Secret=([a-zA-Z0-9+/]{36,50}=) 52 | 53 | examples: 54 | - 'Endpoint=https://foo-nonprod-appconfig.azconfig.io;Id=ABCD-E6-s0:tl6ABcdefGHi7kLMno/p;Secret=abCD1EF+GHIJxLMnOPqRSa53VWX05zaBCdE/fg9hi4k=' 55 | - 'https://foo-nonprod-appconfig.azconfig.io;Id=ABCD-E6-s0:tl6ABcdefGHi7kLMno/p;Secret=abCD1EF+GHIJxLMnOA53ST8uVWX05zaBCdE/fg9hi4k=' 56 | - 'Endpoint=https://appconfig-test01.azconfig.io;Id=09pv-l0-s0:opFCQMC6+9485xJgN5Ws;Secret=GcoEA53t7GLRNJ910M46IrbHO/Vg0tt4HujRdsaCoTY=' 57 | - ' private static string appConfigurationConnectionString = "Endpoint=https://appcs-fg-pwc.azconfig.io;Id=pi5x-l9-s0:SZLlhHA53Nz2MpAl04cU;Secret=CQ+mlfQqkzfZv4XA53gigJ/seeXMKwNsqW/rM3wmtuE=";' 58 | 59 | negative_examples: 60 | - | 61 | text: 62 | az appconfig feature delete --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --label MyLabel 63 | 64 | references: 65 | - https://docs.microsoft.com/en-us/azure/azure-app-configuration/ 66 | - https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-best-practices 67 | - https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_utils.py 68 | -------------------------------------------------------------------------------- /rules/codeclimate.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | # This rule detects the appearance of a or CodeClimate Reporter ID (aka Repo Token). 4 | # Such a token only has write access for coverage info from the corresponding 5 | # repository, and has no other access than that. 6 | # 7 | # However, a leaked token could still be used to upload fraudulent code 8 | # coverage data or cause abuse of services. 9 | - name: CodeClimate Reporter ID 10 | id: np.codeclimate.1 11 | 12 | pattern: (?:CODECLIMATE_REPO_TOKEN|CC_TEST_REPORTER_ID)\s*[:=]\s*([a-f0-9]{64})\b 13 | 14 | 15 | references: 16 | # Old reporters use `CODECLIMATE_REPO_TOKEN` 17 | - https://github.com/codeclimate/javascript-test-reporter 18 | - https://github.com/codeclimate/php-test-reporter 19 | - https://github.com/codeclimate/python-test-reporter 20 | - https://github.com/codeclimate/ruby-test-reporter 21 | - https://github.com/codeclimate/ruby-test-reporter/issues/34 22 | 23 | # New reporter uses `CC_TEST_REPORTER_ID` 24 | - https://docs.codeclimate.com/docs/finding-your-test-coverage-token#should-i-keep-my-test-reporter-id-secret 25 | 26 | examples: 27 | - ' - RAILS_ENV=test CODECLIMATE_REPO_TOKEN=d37a8b9e09642cb73cfcf4e1284815fc3d6a55a7714110187ac59856ae4ab5ad' 28 | 29 | - | 30 | - uses: paambaati/codeclimate-action@v2.2.4 31 | env: 32 | CC_TEST_REPORTER_ID: 945dfb58a832d233a3caeb84e3e6d3be212e8c7abcb48117fce63b9adcb43647 33 | 34 | 35 | 36 | # XXX: should add rules for CodeClimate API keys too: https://developer.codeclimate.com/#authentication 37 | -------------------------------------------------------------------------------- /rules/crates.io.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: crates.io API Key 4 | id: np.cratesio.1 5 | 6 | # It's a 32-character alphanumeric identifier prefixed by `cio` 7 | pattern: '\b(cio[a-zA-Z0-9]{32})\b' 8 | 9 | references: 10 | - https://crates.io/data-access 11 | - https://github.com/rust-lang/crates.io/blob/master/src/util/token.rs 12 | 13 | examples: 14 | - 'Bearer: ciotgp8BGZBlX192iExSQPm0SrUlBunG8zd' 15 | -------------------------------------------------------------------------------- /rules/dependency_track.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | # This detects occurrences of a Dependency-Track API key that uses the default 4 | # `odt_` key prefix. This prefix was set as the default in the Dependency-Track 5 | # v4.9.0 release on October 16, 2023. 6 | - name: Dependency-Track API Key 7 | id: np.dtrack.1 8 | pattern: '\b(odt_[A-Za-z0-9]{32,255})\b' 9 | 10 | examples: 11 | - 'odt_KTJlDq2AGGGlqG4riKdT7p980AW8RlU5' 12 | - 'odt_ABCDDq2AGxGlrF4ribBT7p98AOM9TlU8' 13 | - 'odt_FHxhQGh77JAHHIYpZ818UQ0aYjXIdMIxxgeR' 14 | 15 | negative_examples: 16 | - 'KTJlDq2AGGGlqG8riKdT7p980AW8RlU5' 17 | - 'ABCDDq2AGxGlqG 4ribBT7p98AOM9TlU8' 18 | - 'FHxhQGh77_JAHHIYpZ818UQ0aYjXIdMIxxgeR' 19 | 20 | references: 21 | - https://docs.dependencytrack.org/integrations/rest-api/ 22 | - https://docs.dependencytrack.org/getting-started/configuration/ 23 | 24 | # Code that implements stuff related to the API key 25 | - https://github.com/stevespringett/Alpine/blob/92fdb7de7e5623b8c986de08997480036af5f472/alpine-model/src/main/java/alpine/model/ApiKey.java 26 | 27 | # Issue about adding support for the `odt_` default key prefix 28 | - https://github.com/DependencyTrack/dependency-track/pull/3047 29 | -------------------------------------------------------------------------------- /rules/digitalocean.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: DigitalOcean Application Access Token 4 | id: np.digitalocean.1 5 | 6 | pattern: \b(doo_v1_[a-f0-9]{64})\b 7 | 8 | references: 9 | - https://docs.digitalocean.com/reference/api/ 10 | 11 | examples: 12 | - 'curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer doo_v1_c46dde8bd620fcc382e70d5d43be6eebb141fa2452e8f8fa882433377898ebf2" "https://api.digitalocean.com/v2/cdn/endpoints"' 13 | 14 | 15 | - name: DigitalOcean Personal Access Token 16 | id: np.digitalocean.2 17 | 18 | pattern: \b(dop_v1_[a-f0-9]{64})\b 19 | 20 | references: 21 | - https://docs.digitalocean.com/reference/api/ 22 | 23 | examples: 24 | - 'token = "dop_v1_ef0e04edc13918192246e0c90f0735c7f4db7a5a036a857e48d6cc98f1c9576b"' 25 | 26 | 27 | - name: DigitalOcean Refresh Token 28 | id: np.digitalocean.3 29 | 30 | pattern: \b(dor_v1_[a-f0-9]{64})\b 31 | 32 | references: 33 | - https://docs.digitalocean.com/reference/api/ 34 | 35 | examples: 36 | - ' "refresh_token": "dor_v1_d6ce5b93104521c47be0b580e9296454ef4a319b02b5513469f0ec71d99af2e2",' 37 | -------------------------------------------------------------------------------- /rules/dockerconfig.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | - name: Docker/kube secret 3 | id: dockersecret.custom 4 | pattern: \b"auth":\s*"[A-Za-z0-9+/=]+"\b -------------------------------------------------------------------------------- /rules/dockerhub.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Docker Hub Personal Access Token 4 | id: np.dockerhub.1 5 | 6 | pattern: \b(dckr_pat_[a-zA-Z0-9_-]{27})(?:$|[^a-zA-Z0-9_-]) 7 | 8 | examples: 9 | - docker login -u gemesa -p dckr_pat_hc8VxYclixyTr2rDFsa2rqzkP3Y 10 | - docker login -u gemesa -p dckr_pat_tkzBYxjNNC3R_Yg6jd_O-G8FbrJ 11 | - docker login -u gemesa -p dckr_pat_1q8yKET1VDJTpfCwseUDzT8vFh- 12 | 13 | references: 14 | - https://docs.docker.com/security/for-developers/access-tokens/ 15 | -------------------------------------------------------------------------------- /rules/doppler.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Doppler CLI Token 4 | id: np.doppler.1 5 | 6 | pattern: \b(dckr_pat_[a-zA-Z0-9_-]{27})(?:$|[^a-zA-Z0-9_-]) 7 | 8 | examples: 9 | - dp.ct.bAqhcVzrhy5cRHkOlNTc0Ve6w5NUDCpcutm8vGE9myi 10 | 11 | references: 12 | - https://docs.doppler.com/reference/api 13 | - https://docs.doppler.com/reference/auth-token-formats 14 | 15 | - name: Doppler Personal Token 16 | id: np.doppler.2 17 | 18 | pattern: \b(dp\.pt\.[a-zA-Z0-9]{40,44})\b 19 | 20 | examples: 21 | - dp.pt.bAqhcVzrhy5cRHkOlNTc0Ve6w5NUDCpcutm8vGE9myi 22 | 23 | references: 24 | - https://docs.doppler.com/reference/api 25 | - https://docs.doppler.com/reference/auth-token-formats 26 | 27 | - name: Doppler Service Token 28 | id: np.doppler.3 29 | 30 | pattern: \b(dp\.st\.(?:[a-z0-9\-_]{2,35}\.)?[a-zA-Z0-9]{40,44})\b 31 | 32 | examples: 33 | - dp.st.dev.bAqhcVzrhy5cRHkOlNTc0Ve6w5NUDCpcutm8vGE9myi 34 | 35 | references: 36 | - https://docs.doppler.com/reference/api 37 | - https://docs.doppler.com/reference/auth-token-formats 38 | 39 | - name: Doppler Service Account Token 40 | id: np.doppler.4 41 | 42 | pattern: \b(dp\.sa\.[a-zA-Z0-9]{40,44})\b 43 | 44 | examples: 45 | - dp.sa.bAqhcVzrhy5cRHkOlNTc0Ve6w5NUDCpcutm8vGE9myi 46 | 47 | references: 48 | - https://docs.doppler.com/reference/api 49 | - https://docs.doppler.com/reference/auth-token-formats 50 | 51 | - name: Doppler SCIM Token 52 | id: np.doppler.5 53 | 54 | pattern: \b(dp\.scim\.[a-zA-Z0-9]{40,44})\b 55 | 56 | examples: 57 | - dp.scim.bAqhcVzrhy5cRHkOlNTc0Ve6w5NUDCpcutm8vGE9myi 58 | 59 | references: 60 | - https://docs.doppler.com/reference/api 61 | - https://docs.doppler.com/reference/auth-token-formats 62 | 63 | - name: Doppler Audit Token 64 | id: np.doppler.6 65 | 66 | pattern: \b(dp\.audit\.[a-zA-Z0-9]{40,44})\b 67 | 68 | examples: 69 | - dp.audit.bAqhcVzrhy5cRHkOlNTc0Ve6w5NUDCpcutm8vGE9myi 70 | 71 | references: 72 | - https://docs.doppler.com/reference/api 73 | - https://docs.doppler.com/reference/auth-token-formats 74 | -------------------------------------------------------------------------------- /rules/dropbox.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Dropbox Short-lived access token 4 | id: np.dropbox.1 5 | 6 | pattern: "\\b(sl\\.[a-zA-Z0-9_-]{130,152})(?:$|[^a-zA-Z0-9_-])" 7 | 8 | references: 9 | - https://www.dropbox.com/developers/reference/auth-types 10 | - https://www.dropbox.com/developers/reference/oauth-guide 11 | 12 | examples: 13 | - | 14 | dropbox_token = "sl.BW62fci8fGC5u9zLk493zOrOZOxz3stPQnNSsx6QeCwFuD1zYaS5jlZLsNooMnY-fvUBTkx4xX0HqgDl4WMB-Lq5zf4xqSJ5ZnIfSYtYCmDVAw-q96ciBCURlWmQtEhHnXLnXRX" 15 | - | 16 | var dbx = new Dropbox.Dropbox({ 17 | accessToken: "sl.BW62fci8fGC5u9zLk493zOrOZOxz3stPQnNSsx6QeCwFuD1zYaS5jlZLsNooMnY-fvUBTkx4xX0HqgDl4WMB-Lq5zf4xqSJ5ZnIfSYtYCmDVAw-q96ciBCURlWmQtEhHnXLnXRX" 18 | }); 19 | -------------------------------------------------------------------------------- /rules/dynatrace.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Dynatrace Token 4 | id: np.dynatrace.1 5 | 6 | pattern: '\b(dt0[a-zA-Z]{1}[0-9]{2}\.[A-Z0-9]{24}\.[A-Z0-9]{64})\b' 7 | 8 | examples: 9 | - | 10 | helmCharts: 11 | - name: dynatrace-operator 12 | namespace: dynatrace 13 | version: 0.4.1 14 | repo: https://raw.githubusercontent.com/Dynatrace/helm-charts/master/repos/stable 15 | releaseName: dynatrace-operator 16 | includeCRDs: true 17 | valuesInline: 18 | apiUrl: https://fqp43822.live.dynatrace.com/api 19 | apiToken: dt0c01.FJEGSO2NBAXCOEA7WOSKOA2G.GGMUK6GJDH2TWLNKQT6F68FH22252VXP2F3QAMBUVUDV5TSYYHAWZVVFCUQLF2UA 20 | paasToken: dt0c01.QS7G6CAS5G64DLXFMEDEJ2O7.XVJQTFD2H7XG45V5RTDGA78GAI5W44MFTLZTUOMH4JEXPAV6NSEHUNGAYPIZGEIV 21 | 22 | references: 23 | - https://www.dynatrace.com/support/help/dynatrace-api 24 | - https://www.dynatrace.com/support/help/dynatrace-api/basics/dynatrace-api-authentication 25 | -------------------------------------------------------------------------------- /rules/facebook.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Facebook Secret Key 4 | id: np.facebook.1 5 | 6 | pattern: (?i:\b(?:facebook|fb).?(?:api|app|application|client|consumer|customer|secret|key).?(?:key|oauth|sec|secret)?.{0,2}\s{0,20}.{0,2}\s{0,20}.{0,2}\b([a-z0-9]{32})\b) 7 | 8 | 9 | references: 10 | - https://developers.facebook.com/docs/facebook-login/access-tokens/ 11 | 12 | examples: 13 | - ' # config.facebook.key = "34cebc81c056a21bc66e212f947d73ec"' 14 | - " var fbApiKey = '0278fc1adf6dc1d82a156f306ce2c5cc';" 15 | - ' fbApiKey: "171e84fd57f430fc59afa8fad3dbda2a",' 16 | 17 | negative_examples: 18 | # XXX would be nice if the following matched 19 | - '\"fbconnectkey\";s:32:\"8f52d1586bd18a18e152289b00ed7d29\";' 20 | 21 | 22 | - name: Facebook Access Token 23 | id: np.facebook.2 24 | 25 | pattern: '\b(EAACEdEose0cBA[a-zA-Z0-9]+)\b' 26 | 27 | references: 28 | - https://developers.facebook.com/docs/facebook-login/access-tokens/ 29 | 30 | examples: 31 | - "url = 'https://graph.facebook.com/me/friends?access_token=EAACEdEose0cBAD5XZCz5JXYvqyeJzcSvFZC42toHiWyfjhcZCMZBZCpE3uRJnEBsrhUEMRK1wWs6SsdiDCaCI1mYwyoNuMix2XZCpvsKbZB9TumtZBlcLeIpl4pa931Ce9rTinEAhtyVVZAAZAX4NmfpBUqWtzCRC0fX5GZBn7ZC28mPKAZDZD'" 32 | -------------------------------------------------------------------------------- /rules/figma.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Figma Personal Access Token 4 | id: np.figma.1 5 | 6 | # The key material looks like a v4 UUID with an extra 4 hex digits up front 7 | pattern: "(?i)figma.{0,20}\\b([0-9a-f]{4}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\b" 8 | 9 | references: 10 | - https://www.figma.com/developers/api 11 | - https://www.figma.com/developers/api#access-tokens 12 | 13 | examples: 14 | - "--header='X-Figma-Token: 1394-0ca7a5be-8e22-40ee-8c40-778d41ab2313'" 15 | -------------------------------------------------------------------------------- /rules/generic.yml: -------------------------------------------------------------------------------- 1 | # The rules in this file detect "generic" secrets. 2 | # Generic secrets do not have well-defined structure like most new API token schemes do. 3 | # In particular, there isn't some notable prefix to search for, nor is there a 4 | # fixed length and alphabet for the secret content. 5 | # Because of all this, the patterns in these rules tend to be pretty complex, in 6 | # an attempt to avoid false positives. 7 | # These rules all have relatively bad precision and recall. 8 | 9 | rules: 10 | 11 | - name: Generic Secret 12 | id: np.generic.1 13 | 14 | pattern: (?i:secret.{0,20}\b([0-9a-z]{32,64})\b) 15 | 16 | 17 | examples: 18 | - ' private static String CLIENT_SECRET = "6fb1cff7690db9ac066cadbbde8e3c078efdabcf";' 19 | 20 | # FIXME: extend this rule so these examples get matched 21 | negative_examples: 22 | - " client_credential='5pX8Q~MmTI8OMBJFVqMlFR4DE3Spz6Qm.xO.Gbf-'" 23 | - " secret_access_key = 'abcdefg12346+FJQCK'" 24 | - ' Ldap password ---- H7IKC85R#@4$' 25 | 26 | 27 | - name: Generic API Key 28 | id: np.generic.2 29 | 30 | pattern: (?i:(?:api_key|apikey|access_key|accesskey).{0,3}[\ \t]*(?:=|:=|=>|:|,|'|")[\ \t]*.{0,3}\b([0-9a-z][0-9a-z\-._/+]{30,62}[0-9a-z])\b) 31 | 32 | 33 | examples: 34 | - 'API_KEY = "951bc382db9abad29c68634761dd6e19"' 35 | - 'buildConfigField ''String'' , ''API_KEY'' , ''"951bc382db9cfee29c68634761dd6e19"'' API_KEY ' 36 | 37 | negative_examples: 38 | - 'name="ws_plugin__s2member_amazon_s3_comp_files_access_key" id="ws-plugin--s2member-amazon-s3-comp-files-access-key"' 39 | 40 | 41 | - name: Generic Username and Password (quoted) 42 | id: np.generic.3 43 | 44 | pattern: (?:username|USERNAME|user|USER)[\ \t]*=[\ \t]*["']([a-zA-Z0-9.@_\-+]{3,30})["']\s*[,;]?\s*(?:\s*(?:\#|//)[^\n\r]*[\n\r])*?(?:password|pass|PASSWORD|PASS)[\ \t]*=[\ \t]*["']([^"']{5,30})["'] 45 | 46 | 47 | examples: 48 | - | 49 | credential = UsernamePasswordCredential( 50 | client_id='da34859b-2ae4-48c3-bfe0-1b28b7cf2eed', 51 | username='donjuandemarco', 52 | password='1qay@WXS????', 53 | tenant_id='bc877b20-f135-4c13-a266-8ed26b8f0f4b') 54 | 55 | - | 56 | hostname = '10.11.12.13' 57 | username = 'donjuandemarco@example.com' 58 | password = '`123QWERasdf' 59 | 60 | - | 61 | hostname = '10.11.12.13' 62 | USERNAME = 'donjuandemarco@example.com' 63 | # some comment 64 | # some other comment 65 | PASS = '`123QWERasdf' 66 | 67 | - | 68 | user = 'abuser' # some comment 69 | password = 'abuser123456' # some other comment 70 | 71 | - | 72 | user = 'Aladdin' 73 | password = 'open sesame' 74 | 75 | 76 | negative_examples: 77 | - | 78 | USERNAME=donjuan 79 | PASSWORD=$($(dirname $0)/../bin/get-django-setting LOCAL_DATABASE_PASSWORD) 80 | - ":authn_dbd_params => 'host=db_host port=3306 user=apache password=###### dbname=apache_auth'," 81 | 82 | # FIXME: extend this rule so this actually gets matched 83 | - | 84 | #if DEBUG 85 | string backend_host = "amazon-subdomain-for-database.string.us-east-1.rds.amazonaws.com"; 86 | string backend_user = "root"; 87 | string backend_pass = "XXXXXXXXXXXXX"; 88 | string backend_db = "database_db"; 89 | string backend_port = "1234"; 90 | 91 | 92 | 93 | - name: Generic Username and Password (unquoted) 94 | id: np.generic.4 95 | pattern: (?:username|USERNAME|user|USER)[\ \t]*=[\ \t]*([a-zA-Z0-9.@_\-+]{3,30})\s*;?\s*(?:\s*(?:\#|//)[^\n\r]*[\n\r])*?(?:password|pass|PASSWORD|PASS)[\ \t]*=[\ \t]*(\S{5,30})(?:\s|$) 96 | 97 | examples: 98 | - | 99 | user = Aladdin 100 | password = open_sesame 101 | 102 | - | 103 | user = Aladdin 104 | // some comment 105 | // some other comment 106 | password = open_sesame 107 | 108 | - ":authn_dbd_params => 'host=db_host port=3306 user=apache password=###### dbname=apache_auth'," 109 | 110 | negative_examples: 111 | - | 112 | user = 'Aladdin' 113 | password = 'open_sesame' 114 | 115 | 116 | - name: Generic Password (double quoted) 117 | id: np.generic.5 118 | 119 | pattern: (?i:password["']?[\ \t]*(?:=|:|:=|=>)[\ \t]*"([^$<%@.,\s+'"(){}&/\#\-][^\s+'"(){}/]{4,})") 120 | 121 | 122 | examples: 123 | - | 124 | password = "super$ecret" 125 | - | 126 | password="super$ecret" 127 | - | 128 | String usernamePassword = "application:" + appKey + ":" + appSecret; 129 | - | 130 | my_password: "super$ecret" 131 | - | 132 | "password": "super$ecret", 133 | - | 134 | my_password := "super$ecret" 135 | - | 136 | password => "super$ecret" 137 | - | 138 | "ApplicationServicesConnection" : { 139 | "ServiceAddress" : "https://services-dev.examples.com", 140 | "AdminPassword" : "thisismypassword" 141 | } 142 | - | 143 | private const string DevFolkoosComPfxPassword = "thisismypassword"; 144 | - | 145 | "password": "YOURPASSWROD" 146 | - | 147 | create_random_name('sfrp-cli-cert2', 24), 148 | 'cluster_name': self.create_random_name('sfrp-cli-', 24), 149 | 'vm_password': "Pass123!@#", 150 | 'policy_path': os.path.join(TEST_DIR, 'policy.json') 151 | }) 152 | 153 | negative_examples: 154 | - | 155 | password = "123" 156 | - | 157 | password = super$ecret 158 | - | 159 | password = 'super$ecret' 160 | - | 161 | "password": "$super$ecret", 162 | - | 163 | sb.append("MasterUserPassword: " + getMasterUserPassword() + ","); 164 | - | 165 | "//localhost:1337/:_password = "+new Buffer("feast").toString("base64") 166 | - | 167 | export PGPASSWORD="$gdcapi_db_password" 168 | - | 169 | define wget::authfetch($source,$destination,$user,$password="",$timeout="0",$verbose=false) { 170 | - | 171 | - echo 'export DATABASE_PASSWORD="'$PRECOMPILE_PASSWORD'"' >> .env 172 | - | 173 | "/en/enterprise/3.0/authentication/keeping-your-account-and-data-secure/creating-a-strong-password":"/en/enterprise-server@3.0/auth" 174 | - | 175 | "password": "<YOURPASSWROD>" 176 | - | 177 | as: 'cms_user_password' 178 | get '/passwords/:id/edit' => "cms/sites/passwords#edit", as: 'edit_password' 179 | put '/forgot-password' => "cms/sites/passwords#update", as: 'update_password' 180 | end 181 | - | 182 | IAMUserChangePassword = "arn:aws:iam::aws:policy/IAMUserChangePassword" 183 | - | 184 | this.addPassword = "#add-password"; 185 | 186 | 187 | 188 | - name: Generic Password (single quoted) 189 | id: np.generic.6 190 | 191 | pattern: (?i:password["']?[\ \t]*(?:=|:|:=|=>)[\ \t]*'([^$<%@.,\s+'"(){}&/\#\-][^\s+'"(){}/]{4,})') 192 | 193 | 194 | examples: 195 | - | 196 | :password => '4ian1234', 197 | - | 198 | common.then_log_in({username: 'geronimo', password: '52VeZqtHDCdAr5yM'}); 199 | 200 | - | 201 | beta => { 202 | host => 'foo.example.com', 203 | user => 'joe', 204 | password => 'thisismypassword', 205 | } 206 | 207 | negative_examples: 208 | - | 209 | echo 'password = '.$p['config']['daemon_password']."\n"; 210 | - | 211 | usernameLabel:"Username or email:",passwordLabel:"Password:",rememberMeLabel:"Remember me:" 212 | - | 213 | this.addPassword = '#add-password'; 214 | -------------------------------------------------------------------------------- /rules/github.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: GitHub Personal Access Token 4 | id: np.github.1 5 | pattern: '\b(ghp_[a-zA-Z0-9]{36})\b' 6 | 7 | references: 8 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github 9 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token 10 | - https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ 11 | 12 | examples: 13 | - 'GITHUB_KEY=ghp_XIxB7KMNdAr3zqWtQqhE94qglHqOzn1D1stg' 14 | - "let g:gh_token='ghp_4U3LSowpDx8XvYE7A8GH56oxU5aWnY2mzIbV'" 15 | - | 16 | ## git devaloper settings 17 | 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 18 | 19 | 20 | - name: GitHub OAuth Access Token 21 | id: np.github.2 22 | pattern: '\b(gho_[a-zA-Z0-9]{36})\b' 23 | 24 | references: 25 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github 26 | - https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 27 | - https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ 28 | 29 | examples: 30 | - ' "url": "git+https://FelipeMestre:gho_psT9pqNFsehnc4se0ZzzR0HBxapxZD35hNHi@github.com/gontarz/PW_2021_Website-FelipeMestre.git"' 31 | - ' oauth_token: gho_fq75OMU7UVbS9pTZmoCCzJT6TM5d1w099FgG' 32 | 33 | 34 | - name: GitHub App Token 35 | id: np.github.3 36 | # Note: `ghu_` prefix is for user-to-server tokens; `ghs_` is for server-to-server tokens 37 | pattern: '\b((?:ghu|ghs)_[a-zA-Z0-9]{36})\b' 38 | 39 | references: 40 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github 41 | - https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 42 | - https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ 43 | 44 | examples: 45 | - ' "token": "ghu_16C7e42F292c69C2E7C10c838347Ae178B4a",' 46 | - | 47 | Example usage: 48 | git clone http://ghs_RguXIkihJjwHAP6eXEYxaPNvywurTr5IOAbg@github.com/username/repo.git 49 | 50 | 51 | - name: GitHub Refresh Token 52 | id: np.github.4 53 | pattern: '\b(ghr_[a-zA-Z0-9]{76})\b' 54 | references: 55 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github 56 | - https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 57 | - https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ 58 | 59 | examples: 60 | - ' "refresh_token": "ghr_1B4a2e77838347a7E420ce178F2E7c6912E169246c3CE1ccbF66C46812d16D5B1A9Dc86A1498",' 61 | 62 | 63 | - name: GitHub Client ID 64 | id: np.github.5 65 | pattern: (?i:(?:github).?(?:api|app|application|client|consumer|customer)?.?(?:id|identifier|key).{0,2}\s{0,20}.{0,2}\s{0,20}.{0,2}\b([a-z0-9]{20})\b) 66 | 67 | 68 | references: 69 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github 70 | - https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 71 | 72 | examples: 73 | - | 74 | GITHUB_CLIENT_ID=ac58d6da7d7a84c039b7 75 | GITHUB_SECRET=37d02377a3e9d849e18704c3ec883f9c5787d857 76 | 77 | 78 | - name: GitHub Secret Key 79 | id: np.github.6 80 | pattern: (?i:github.?(?:api|app|application|client|consumer|customer|secret|key).?(?:key|oauth|sec|secret)?.{0,2}\s{0,20}.{0,2}\s{0,20}.{0,2}\b([a-z0-9]{40})\b) 81 | 82 | references: 83 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github 84 | - https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 85 | 86 | examples: 87 | - | 88 | GITHUB_CLIENT_ID=ac58d6da7d7a84c039b7 89 | GITHUB_SECRET=37d02377a3e9d849e18704c3ec883f9c5787d857 90 | 91 | 92 | - name: GitHub Personal Access Token (fine-grained permissions) 93 | id: np.github.7 94 | pattern: \b(github_pat_[0-9a-zA-Z_]{82})\b 95 | 96 | 97 | references: 98 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github 99 | - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token 100 | 101 | examples: 102 | - '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' 103 | -------------------------------------------------------------------------------- /rules/gitlab.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: GitLab Runner Registration Token 4 | id: np.gitlab.1 5 | pattern: '\b(GR1348941[0-9a-zA-Z_-]{20})(?:\b|$)' 6 | 7 | references: 8 | - https://docs.gitlab.com/runner/security/ 9 | - https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens-deprecated 10 | - https://docs.gitlab.com/ee/security/token_overview.html#security-considerations 11 | 12 | examples: 13 | - | 14 | sudo gitlab-runner register \ 15 | --non-interactive \ 16 | --url "https://gitlab.com/" \ 17 | --registration-token "GR1348941_iAgdMy7a3NhZaa5oNoH" \ 18 | --executor "docker" \ 19 | --docker-image ubuntu:latest \ 20 | --description "docker-runner" \ 21 | --tag-list "docker, CICD, App" \ 22 | --run-untagged="true" \ 23 | --locked="false" \ 24 | --access-level="not_protected" 25 | 26 | - name: GitLab Personal Access Token 27 | id: np.gitlab.2 28 | pattern: '\b(glpat-[0-9a-zA-Z_-]{20})(?:\b|$)' 29 | 30 | references: 31 | - https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html 32 | 33 | examples: 34 | - | 35 | docker build -t tweedledee \ 36 | -f Dockerfile \ 37 | --build-arg 'GO_REPO_TOKEN=glpat-tFrjFXD7soVU2fqxuDMh' \ 38 | 39 | - name: GitLab Pipeline Trigger Token 40 | id: np.gitlab.3 41 | pattern: '\b(glptt-[0-9a-f]{40})\b' 42 | 43 | references: 44 | - https://docs.gitlab.com/ee/ci/triggers/ 45 | - https://gitlab.com/gitlab-org/gitlab/-/issues/371396 46 | - https://gitlab.com/gitlab-org/gitlab/-/issues/388379 47 | 48 | examples: 49 | - | 50 | curl \ 51 | -X POST \ 52 | --fail \ 53 | --no-progress-meter \ 54 | -F token=glptt-0d66598d696a02da33fb65e2a041f607c68ea50d \ 55 | -F ref=main 56 | -------------------------------------------------------------------------------- /rules/google.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Google Client ID 4 | id: np.google.1 5 | 6 | pattern: \b([0-9]+-[a-z0-9_]{32})\.apps\.googleusercontent\.com\b 7 | 8 | examples: 9 | - " 'clientID' : '231545488769-4d1mcev9vifvlncrern52id2pqqf5u5l.apps.googleusercontent.com'," 10 | - " //$google_client_id = '244082345999-o6m8f1pmb1e76tjfj9v7b96j31e53ps5.apps.googleusercontent.com';" 11 | - " GOOGLE_OAUTH2_CLIENT_ID = '607830223128-4qgthc7ofdqce232dk690t5jgkm1ce33.apps.googleusercontent.com'" 12 | - ' $cordovaOauth.google("653512027492-5u9blotr1521fa0lo1172nhv4pmqgttq.apps.googleusercontent.com", ["email"]).then(function(result) {' 13 | 14 | 15 | - name: Google OAuth Client Secret (prefixed) 16 | id: np.google.2 17 | 18 | pattern: \b(GOCSPX-[a-zA-Z0-9_-]{28})(?:[^a-zA-Z0-9_-]|$) 19 | 20 | examples: 21 | - 'const CLIENTSECRET = "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"' 22 | 23 | - name: Google OAuth Client Secret 24 | id: np.google.3 25 | 26 | pattern: client.?secret.{0,10}\b([a-z0-9_-]{24})(?:[^a-z0-9_-]|$) 27 | 28 | examples: 29 | - '"client_secret":"aaaaaaaaaaaaaaaaaaaaaaa-"' 30 | - " //$google_client_secret = 'fnhqAakzWrX-mtFQ4PRdMoy0';" 31 | - " 'clientSecret' : 'Ufvuj-d6alhwGKvvLh_8Nq0K'" 32 | 33 | 34 | - name: Google OAuth Access Token 35 | id: np.google.4 36 | 37 | pattern: \b(ya29\.[0-9A-Za-z_-]{20,100})(?:[^0-9A-Za-z_-]|$) 38 | 39 | examples: 40 | - | 41 | const setupCredentials = () => { 42 | const { encryptedData, iv } = encrypt({ 43 | expiry_date: 1642441058842, 44 | access_token: 45 | 'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzCu947fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod', 46 | // This token is linked to a test Google account (typebot.test.user@gmail.com) 47 | refresh_token: 48 | '1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29Ga91EhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek', 49 | }) 50 | - | 51 | -- Clear login if it's a new connection. 52 | --propertyTable.access_token = 'ya29.Ci_UA7aEsvT6-oVI8f96kvB6i8oO13WgdZUviLaCVtpEPYZqhQcQycR-u2X9xtmYGA' 53 | 54 | 55 | - name: Google API Key 56 | id: np.google.5 57 | 58 | pattern: \b(AIza[0-9A-Za-z_-]{35})\b 59 | references: 60 | - https://cloud.google.com/docs/authentication/api-keys#securing 61 | - https://support.google.com/googleapi/answer/6310037 62 | 63 | examples: 64 | - " var DEVELOPER_KEY = 'AIzaSyB4sU8lU15bR_87qNb7eUVQN72_vv8mpbU';" 65 | 66 | 67 | - name: Google Cloud Storage Bucket (subdomain style) 68 | id: np.gcs.1 69 | 70 | pattern: (?:^|[\s/"']|%2F)((?:[a-zA-Z0-9_-]+\.)+storage\.googleapis\.com)\b 71 | 72 | references: 73 | - https://cloud.google.com/storage/docs/request-endpoints 74 | 75 | examples: 76 | - 'c.storage.googleapis.com' 77 | - 'some-bucket.example.com.storage.googleapis.com' 78 | 79 | negative_examples: 80 | - 'https://storage.googleapis.com' 81 | 82 | 83 | - name: Google Cloud Storage Bucket (path style) 84 | id: np.gcs.2 85 | 86 | pattern: (?:^|[\s/"']|%2F)(storage\.googleapis\.com/[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)(?:[^a-zA-Z0-9_-]|$) 87 | 88 | references: 89 | - https://cloud.google.com/storage/docs/request-endpoints 90 | 91 | negative_examples: 92 | - 'c.storage.googleapis.com/some_object' 93 | - 'some-bucket.example.com.storage.googleapis.com/some_object' 94 | 95 | examples: 96 | - 'https://storage.googleapis.com/bucket_name/object_name' 97 | -------------------------------------------------------------------------------- /rules/gradle.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | # This is intended to detect hardcoded credentials that sometimes appear in Gradle files. 4 | - name: Hardcoded Gradle Credentials 5 | id: np.gradle.1 6 | 7 | pattern: credentials\s*\{[\s\S]*?(?:username|password)\s+['"]([^'"]{1,60})['"][\s\S]*?(?:username|password)\s+['"]([^'"]{1,60})['"] 8 | 9 | examples: 10 | - | 11 | credentials { 12 | username 'user' 13 | password 'password' 14 | } 15 | - | 16 | publishing { 17 | repositories { 18 | maven { 19 | url "http://us01cmsysart01.example.com:8081/artifactory/Mobile-Libs-Internal" 20 | credentials { 21 | // your password here 22 | 23 | username "SOME_USERNAME" 24 | password "SOME_PASSWORD" 25 | } 26 | } 27 | } 28 | - "credentials {\n username 'user'\n password 'password'\n}" 29 | - "credentials {\n username \"user\"\n password \"password\"\n}" 30 | -------------------------------------------------------------------------------- /rules/grafana.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Grafana API Token 4 | id: np.grafana.1 5 | 6 | pattern: \b(eyJrIjoi[A-Za-z0-9]{60,100})\b 7 | 8 | references: 9 | - https://grafana.com/docs/grafana/latest/developers/http_api/auth/ 10 | 11 | examples: 12 | - 'Authorization: Bearer eyJrIjoiWHZiSWd5NzdCYUZnNUtibE8obUpESmE2bzJYNDRIc1UiLCJuIjoibXlrZXkiLCJpZCI7MX1' 13 | - 'admin_client = GrafanaClient("eyJrIjoiY21sM1JRYjB6RnVYSTNLenRWQkFEaWN2bXI2V202U2IiLCJuIjoiYWRtaW5rZXkiLCJpZCI6MX0=", host=grafana_host, port=3000, protocol="http")' 14 | 15 | non_examples: 16 | - 'View the latest results [here](https://heyo.powerbi.com/view?r=eyJrIjoiYTZjMTk3YjEtMzQ4Yi00NTI5LTg6ZDItNmUyMGRlOTkwMGRlIiwidCI5IjcyZjk4OGJmLTg3ZjEtNDFhZi06MWFiLTJkN2NkMDExZGI0NyIsImMiOjV9&pageName=ReportSection9567390a89a2d30b0eda).' 17 | 18 | 19 | - name: Grafana Cloud API Token 20 | id: np.grafana.2 21 | 22 | pattern: \b(glc_eyJrIjoi[A-Za-z0-9]{60,100})\b 23 | 24 | references: 25 | - https://grafana.com/docs/grafana-cloud/api-reference/cloud-api/ 26 | 27 | examples: 28 | - ' "token": "glc_eyJrIjoiZjI0YzZkNGEwZDBmZmZjMmUzNTU3ODcxMmY0ZWZlNTQ1NTljMDFjOCIsIm6iOiJteXRva3VuIiwiaWQiOjF8"' 29 | 30 | 31 | - name: Grafana Service Account Token 32 | id: np.grafana.3 33 | 34 | pattern: \b(glsa_[a-zA-Z0-9]{32}_[a-fA-F0-9]{8})\b 35 | 36 | references: 37 | - https://grafana.com/docs/grafana/latest/administration/service-accounts/ 38 | 39 | examples: 40 | - | 41 | curl -H "Authorization: Bearer glsa_HOruNAb7SOiCdshU7algkrq7FDsNSLAa_55e2f8be" -X GET '/api/access-control/user/permissions' | jq 42 | 43 | - | 44 | // getData() 45 | // { 46 | // let url="http://localhost:4200/api/search" 47 | // const headers = new HttpHeaders({ 48 | // 'Content-Type': 'application/json', 49 | // 'Authorization': `Bearer glsa_Sof0HKi3agxrQP9qm5r2G98VacBNwV5P_9b638c45` 50 | // }) 51 | // return this.http.get(url, {headers: headers}); 52 | // } 53 | -------------------------------------------------------------------------------- /rules/hashes.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Password Hash (md5crypt) 4 | id: np.pwhash.1 5 | pattern: (\$1\$[./A-Za-z0-9]{8}\$[./A-Za-z0-9]{22}) 6 | 7 | references: 8 | - https://en.wikipedia.org/wiki/Crypt_(C)#MD5-based_scheme 9 | - https://unix.stackexchange.com/a/511017 10 | - https://hashcat.net/wiki/doku.php?id=example_hashes 11 | - https://passwordvillage.org/salted.html#md5crypt 12 | 13 | examples: 14 | # generated with `openssl passwd -1 -salt 'OKgLCmVl' 'a'` 15 | - '$1$OKgLCmVl$d02jECa4DXn/oXX0R.MoQ/' 16 | - '$1$28772684$iEwNOgGugqO9.bIz5sk8k/' 17 | 18 | 19 | - name: Password Hash (bcrypt) 20 | id: np.pwhash.2 21 | # Format from Wikipedia: 22 | # $2$[cost]$[22 character salt][31 character hash] 23 | pattern: (\$2[abxy]\$\d+\$[./A-Za-z0-9]{53}) 24 | 25 | references: 26 | - https://en.wikipedia.org/wiki/Bcrypt 27 | - https://hashcat.net/wiki/doku.php?id=example_hashes 28 | 29 | examples: 30 | - '$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW' 31 | - '$2a$05$/VT2Xs2dMd8GJKfrXhjYP.DkTjOVrY12yDN7/6I8ZV0q/1lEohLru' 32 | - '$2a$05$Uo385Fa0g86uUXHwZxB90.qMMdRFExaXePGka4WGFv.86I45AEjmO' 33 | - '$2a$05$LhayLxezLhK1LhWvKxCyLOj0j1u.Kj0jZ0pEmm134uzrQlFvQJLF6' 34 | - '$2y$12$atWJ1Nx6ep65tNx0YIJ4I.jzgI86znQbNRI3lF0qIt/XCYnEPxSc2' 35 | 36 | 37 | - name: Password Hash (sha256crypt) 38 | id: np.pwhash.3 39 | pattern: (\$5(?:\$rounds=\d+)?\$[./A-Za-z0-9]{8,16}\$[./A-Za-z0-9]{43}) 40 | 41 | references: 42 | - https://en.wikipedia.org/wiki/Crypt_(C)#Key_derivation_functions_supported_by_crypt 43 | - https://hashcat.net/wiki/doku.php?id=example_hashes 44 | - https://passwordvillage.org/salted.html#sha256crypt 45 | 46 | examples: 47 | - '$5$rounds=5000$GX7BopJZJxPc/KEK$le16UF8I2Anb.rOrn22AUPWvzUETDGefUmAV8AZkGcD' 48 | - '$5$9ks3nNEqv31FX.F$gdEoLFsCRsn/WRN3wxUnzfeZLoooVlzeF4WjLomTRFD' 49 | - '$5$KAlz5SULZNybHwil$3UgmS1pmo2r5HG.tjbjzoVxISBh8IH81d.bJh4MCC19' 50 | 51 | 52 | - name: Password Hash (sha512crypt) 53 | id: np.pwhash.4 54 | pattern: (\$6(?:\$rounds=\d+)?\$[./A-Za-z0-9]{8,16}\$[./A-Za-z0-9]{86}) 55 | 56 | references: 57 | - https://en.wikipedia.org/wiki/Crypt_(C)#Key_derivation_functions_supported_by_crypt 58 | - https://hashcat.net/wiki/doku.php?id=example_hashes 59 | - https://passwordvillage.org/salted.html#sha512crypt 60 | 61 | examples: 62 | - '$6$52450745$k5ka2p8bFuSmoVT1tzOyyuaREkkKBcCNqoDKzYiJL9RaE8yMnPgh2XzzF0NDrUhgrcLwg78xs1w5pJiypEdFX/' 63 | - '$6$qoE2letU$wWPRl.PVczjzeMVgjiA8LLy2nOyZbf7Amj3qLIL978o18gbMySdKZ7uepq9tmMQXxyTIrS12Pln.2Q/6Xscao0' 64 | 65 | 66 | - name: Password Hash (Cisco IOS PBKDF2 with SHA256) 67 | id: np.pwhash.5 68 | pattern: (\$8\$[./A-Za-z0-9]{8,16}\$[./A-Za-z0-9]{43}) 69 | 70 | references: 71 | - https://en.wikipedia.org/wiki/Crypt_(C)#Key_derivation_functions_supported_by_crypt 72 | - https://hashcat.net/wiki/doku.php?id=example_hashes 73 | 74 | examples: 75 | - '$8$TnGX/fE4KGHOVU$pEhnEvxrvaynpi8j4f.EMHr6M.FzU8xnZnBr/tJdFWk' 76 | - '$8$mTj4RZG8N9ZDOk$elY/asfm8kD3iDmkBe3hD2r4xcA/0oWS5V3os.O91u.' 77 | -------------------------------------------------------------------------------- /rules/heroku.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Heroku API Key 4 | id: np.heroku.1 5 | pattern: '(?i)heroku.{0,20}key.{0,20}\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b' 6 | 7 | references: 8 | - https://devcenter.heroku.com/articles/authentication 9 | 10 | examples: 11 | - ' HEROKU_API_KEY: c55dbac4-e0e8-4a06-b892-75cac2387ce5' 12 | 13 | negative_examples: 14 | - 'curl https://kolkrabbi.heroku.com/apps/98fc74a8-ff56-4a21-85f6-7a1fcac895c9/github/push \' 15 | -------------------------------------------------------------------------------- /rules/huggingface.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: HuggingFace User Access Token 4 | id: np.huggingface.1 5 | 6 | pattern: '\b(hf_[a-zA-Z]{34})\b' 7 | 8 | references: 9 | - https://huggingface.co/docs/hub/security-tokens 10 | 11 | -------------------------------------------------------------------------------- /rules/jenkins.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Jenkins Token or Crumb 4 | id: np.jenkins.1 5 | 6 | pattern: '(?i)jenkins.{0,10}(?:crumb)?.{0,10}\b([0-9a-f]{32,36})\b' 7 | 8 | examples: 9 | - | 10 | jenkins_user = 'root' 11 | # jenkins_passwd = '116365fd86d63bf507aba962606a5c8956' Pre token 12 | jenkins_passwd = '11811f784531053132519844d047186074' # Dev Token 13 | jenkins_url = 'http://10.1.188.121' 14 | - | 15 | export JENKINS_USER=justin-admin-edit-view 16 | export JENKINS_TOKEN=11f4274ec59be12eace9a08b08ee13d54b 17 | export JENKINS=jenkins-cicd.apps.sno.openshiftlabs.net 18 | - | 19 | sh "curl -X POST 'http://jenkins.lsfusion.luxsoft.by/job/${Paths.updateParentVersionsJob}/build' --user ${USERPASS} -H 'Jenkins-Crumb:440561953171ba44ace9740562d172bb'" 20 | 21 | negative_examples: 22 | - '1. ~~Does not play well with [Build Token Root Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Build+Token+Root+Plugin) URL formats.~~ (added with [this commit](https://github.com/morficus/Parameterized-Remote-Trigger-Plugin/commit/f687dbe75d1c4f39f7e14b68220890384d7c5674) )' 23 | 24 | references: 25 | - https://www.jenkins.io/blog/2018/07/02/new-api-token-system/ 26 | - https://www.jenkins.io/doc/book/security/csrf-protection/ 27 | -------------------------------------------------------------------------------- /rules/jwt.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: JSON Web Token (base64url-encoded) 4 | id: np.jwt.1 5 | 6 | # `header . payload . signature`, all base64-encoded 7 | # Unencoded, the header and payload are JSON objects, usually starting with 8 | # `{"`, which gets base64-encoded starting with `ey`. 9 | pattern: \b(ey[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)(?:[^a-zA-Z0-9_-]|$) 10 | 11 | references: 12 | - https://en.wikipedia.org/wiki/JSON_Web_Token 13 | - https://datatracker.ietf.org/doc/html/rfc7519 14 | - https://en.wikipedia.org/wiki/Base64#URL_applications 15 | - https://datatracker.ietf.org/doc/html/rfc4648 16 | - https://developer.okta.com/blog/2018/06/20/what-happens-if-your-jwt-is-stolen 17 | 18 | examples: 19 | - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmEmMjLiuyu5CSpyHI' 20 | - 'NUCLEAR_SERVICES_ANON_KEY=eyJhbGciOiJIUzI1NiIsEnR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFqcnVqc2lzY2Nzdnl2am5xdG5xIiwicm9sZSI6ImEub24iLCJpYXQiOjE2NTY1OTY0NjEsImV4cCI6MTk3MjE3MjQ2MX0.WQWcwBAQFNE259f2o8ruFln_UMLTFEnEaUD7KHrs9Aw' 21 | -------------------------------------------------------------------------------- /rules/linkedin.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: LinkedIn Client ID 4 | id: np.linkedin.1 5 | 6 | pattern: linkedin.?((?:api|app|application|client|consumer|customer)?.?)?(?:id|identifier|key).{0,2}\s{0,20}.{0,2}\s{0,20}.{0,2}\b([a-z0-9]{12,14})\b 7 | 8 | references: 9 | - https://docs.microsoft.com/en-us/linkedin/shared/api-guide/best-practices/secure-applications 10 | 11 | examples: 12 | # FIXME: this example should not actually match 13 | - 'Email ID Last 5 Digits of your SSN LinkedIn ID Availability' 14 | - | 15 | LINKEDIN_KEY = "77yg7tx91p4lag" 16 | LINKEDIN_SECRET = "zt7GeN6IH911xvRj" 17 | 18 | 19 | - name: LinkedIn Secret Key 20 | id: np.linkedin.2 21 | 22 | pattern: linkedin.?((?:api|app|application|client|consumer|customer|secret|key)?.?)?(?:key|oauth|sec|secret)?.{0,2}\s{0,20}.{0,2}\s{0,20}.{0,2}\b([a-z0-9]{16})\b 23 | 24 | references: 25 | - https://docs.microsoft.com/en-us/linkedin/shared/api-guide/best-practices/secure-applications 26 | 27 | examples: 28 | - | 29 | LINKEDIN_KEY = "77yg7tx91p4lag" 30 | LINKEDIN_SECRET = "zt7GeN6IH911xvRj" 31 | -------------------------------------------------------------------------------- /rules/mailchimp.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: MailChimp API Key 4 | id: np.mailchimp.1 5 | 6 | pattern: (?:mailchimp|mc).{0,20}\b([a-f0-9]{32}-us[0-9]{1,3})\b 7 | 8 | references: 9 | - https://mailchimp.com/help/about-api-keys/ 10 | - https://mailchimp.com/help/about-api-keys/#API_key_security 11 | 12 | examples: 13 | - "MAILCHIMP_API='bd3777708aecfee66c5335f62a6246a4-us13'" 14 | -------------------------------------------------------------------------------- /rules/mailgun.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Mailgun API Key 4 | id: np.mailgun.1 5 | 6 | pattern: '(?i)(?:mailgun|mg).{0,20}key-([a-z0-9]{32})\b' 7 | 8 | examples: 9 | - "var apiKey = process.env.MAILGUN_API || 'key-46cebd38c59ac222e6cf991581411eaf'" 10 | 11 | references: 12 | - https://documentation.mailgun.com/en/latest/api-intro.html#authentication-1 13 | -------------------------------------------------------------------------------- /rules/mapbox.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Mapbox Public Access Token 4 | id: np.mapbox.1 5 | 6 | # NOTE: `pk` tokens are public and have read-only access. 7 | # `sk` tokens are secret and should never be shared. 8 | # `tk` tokens are temporary access tokens. 9 | pattern: '(?i)(?s)mapbox.{0,30}(pk\.[a-z0-9\-+/=]{32,128}\.[a-z0-9\-+/=]{20,30})(?:[^a-z0-9\-+/=]|$)' 10 | 11 | examples: 12 | - | 13 | mapboxApiKey: 14 | 'pk.eyJ1Ijoia3Jpc3R3IiwiYSI6ImNqbGg1N242NTFlczczdnBcf99iMjgzZ2sifQ.lUneM-o3NucXN189EYyXxQ', 15 | 16 | references: 17 | - https://docs.mapbox.com/api/accounts/tokens/#token-format 18 | - https://docs.mapbox.com/help/getting-started/access-tokens/ 19 | - https://docs.mapbox.com/help/troubleshooting/how-to-use-mapbox-securely 20 | 21 | 22 | - name: Mapbox Secret Access Token 23 | id: np.mapbox.2 24 | 25 | # NOTE: `pk` tokens are public and have read-only access. 26 | # `sk` tokens are secret and should never be shared. 27 | # `tk` tokens are temporary access tokens. 28 | pattern: '(?i)(?s)mapbox.{0,30}(sk\.[a-z0-9\-+/=]{32,128}\.[a-z0-9\-+/=]{20,30})(?:[^a-z0-9\-+/=]|$)' 29 | 30 | examples: 31 | - " //mapboxgl.accessToken = 'sk.eyJ1Ijoic2hlbmdsaWgiLCJhIjCf99ttaWF5bDBsMGNlaDJubGZyMGUwZXNmaCJ9.eI8KXNm5zKZXOKh0c8u9vg';" 32 | - 'export MAPBOX_SECRET_TOKEN=sk.eyJ1IjoiY2FwcGVsYWVyZSIsImEicf99c1BaTkZnIn0.P4lD1eHeSEx7AsBq1zbJ4g' 33 | 34 | references: 35 | - https://docs.mapbox.com/api/accounts/tokens/#token-format 36 | - https://docs.mapbox.com/help/getting-started/access-tokens/ 37 | - https://docs.mapbox.com/help/troubleshooting/how-to-use-mapbox-securely 38 | 39 | 40 | - name: Mapbox Temporary Access Token 41 | id: np.mapbox.3 42 | 43 | # NOTE: `pk` tokens are public and have read-only access. 44 | # `sk` tokens are secret and should never be shared. 45 | # `tk` tokens are temporary access tokens. 46 | pattern: '(?i)(?s)mapbox.{0,30}(tk\.[a-z0-9\-+/=]{32,128}\.[a-z0-9\-+/=]{20,30})(?:[^a-z0-9\-+/=]|$)' 47 | 48 | examples: 49 | - " //mapboxgl.accessToken = 'tk.eyJ1Ijoic2hlbmdsaWgiLCJhIjCf99ttaWF5bDBsMGNlaDJubGZyMGUwZXNmaCJ9.eI8KXNm5zKZXOKh0c8u9vg';" 50 | - 'export MAPBOX_SECRET_TOKEN=tk.eyJ1IjoiY2FwcGVsYWVyZSIsImEicf99c1BaTkZnIn0.P4lD1eHeSEx7AsBq1zbJ4g' 51 | 52 | references: 53 | - https://docs.mapbox.com/api/accounts/tokens/#token-format 54 | - https://docs.mapbox.com/help/getting-started/access-tokens/ 55 | - https://docs.mapbox.com/help/troubleshooting/how-to-use-mapbox-securely 56 | -------------------------------------------------------------------------------- /rules/microsoft_teams.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Microsoft Teams Webhook 4 | id: np.msteams.1 5 | 6 | pattern: (https://outlook\.office\.com/webhook/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}@[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/IncomingWebhook/[a-f0-9]{32}/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}) 7 | 8 | examples: 9 | - "//test //url = 'https://outlook.office.com/webhook/9da5da9c-4218-4c22-aed6-b5c8baebfff5@2f2b54b7-0141-4ba7-8fcd-ab7d17a60547/IncomingWebhook/1bf66ccbb8e745e791fa6e6de0cf465b/4361420b-8fde-48eb-b62a-0e34fec63f5c';" 10 | - " [T2`https://outlook.office.com/webhook/fa4983ab-49ea-4c1b-9297-2658ea56164c@f784fbed-7fc7-4c7a-aae9-d2f387b67c5d/IncomingWebhook/4d2b3a16113d47b080b7a083b5a5e533/74f315eb-1dde-4731-b6b5-2524b77f2acd`](https://outlook.office.com/webhook/fe4183ab-49ea-4c1b-9297-2658ea56164c%2540f784fbed-7fc7-4c7a-aae9-d2f387b67c5d/IncomingWebhook/4d2b3a16003d47b080b7a083b5a5e533/74f315eb-1dde-4731-b6b5-2524b77f2acd)" 11 | - 'curl -H "Content-Type: application/json" -d "{\"text\": \"Debut du script deploy.sh \"}" https://outlook.office.com/webhook/555aa7fc-ea71-4fb7-ae9e-755caa4404ed@72f988bf-86f1-41af-91ab-2d7cd011db47/IncomingWebhook/16085df23e564bb9076842605ede3af2/51dab674-ad95-4f0a-8964-8bdefc25b6d9' 12 | - ' webhooks: https://outlook.office.com/webhook/2f92c502-7feb-4a6c-86f1-477271ae576f@990414fa-d0a3-42f5-b740-21d865a44a28/IncomingWebhook/54e43eb586f14aa9984d5c0bec3d5050/539ce6fa-e9aa-413f-a79b-fb7e8998fcac' 13 | 14 | # FIXME: this example probably should actually match 15 | negative_examples: 16 | - " office365ConnectorSend message: 'Execucao Concluida.', status: 'End', webhookUrl: 'https://outlook.office.com/webhook/82fc2788-c6f4-4507-a657-36c91eccfd87@93f33571-550f-43cf-b09f-cd33c338d086/JenkinsCI/4f3bbf41e81a4f36887a1a4d7cbfb2c6/82fa2788-c6f4-45c7-a657-36f91eccfd87'" 17 | 18 | references: 19 | - https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors 20 | - https://github.com/praetorian-inc/nuclei-templates/blob/main/exposures/tokens/microsoft/microsoft-teams-webhook.yaml 21 | -------------------------------------------------------------------------------- /rules/netrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: netrc Credentials 4 | id: np.netrc.1 5 | 6 | pattern: (?:machine\s+[^\s]+|default)\s+login\s+([^\s]+)\s+password\s+([^\s]+) 7 | 8 | references: 9 | - https://everything.curl.dev/usingcurl/netrc 10 | - https://devcenter.heroku.com/articles/authentication#api-token-storage 11 | 12 | examples: 13 | - 'machine api.github.com login ziggy^stardust password 012345abcdef' 14 | - | 15 | ``` 16 | machine raw.github.com 17 | login visionmedia 18 | password pass123 19 | ``` 20 | 21 | - | 22 | """ 23 | machine api.wandb.ai 24 | login user 25 | password 7cc938e45e63e9014f88f811be240ba0395c02dd 26 | """ 27 | -------------------------------------------------------------------------------- /rules/newrelic.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: New Relic License Key 4 | id: np.newrelic.1 5 | 6 | pattern: \b([a-z0-9]{6}[a-f0-9]{30}nral)\b 7 | 8 | references: 9 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys 10 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#license-key 11 | 12 | examples: 13 | - | 14 | # Required license key associated with your New Relic account. 15 | license_key: 033f2f2072ca3f2cb2ec39024fa9e49cd640NRAL 16 | 17 | # Your application name. Renaming here affects where data displays in New 18 | 19 | - ' license_key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaNRAL' 20 | - ' license: eu01xxaa7460e1ea3abdfbbbd36e85c10cd0NRAL' 21 | 22 | negative_examples: 23 | - ' license_key: xxxxxxxxxxxxxxx' 24 | - ' --set global.licenseKey=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8NRAL `' 25 | 26 | 27 | - name: New Relic License Key (non-suffixed) 28 | id: np.newrelic.2 29 | pattern: associated\ with\ your\ New\ Relic\ account\.\s+license_key:\s*([a-f0-9]{40})\b 30 | 31 | references: 32 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys 33 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#license-key 34 | 35 | examples: 36 | - | 37 | # Required license key associated with your New Relic account. 38 | license_key: 0a14254db7a1e9d29c3370dacc798cb65d25c9af 39 | 40 | # Your application name. Renaming here affects where data displays in New 41 | 42 | negative_examples: 43 | - | 44 | # Required license key associated with your New Relic account. 45 | license_key: 033f2f2072ca3f2cb2ec39019fa9e49cd640NRAL 46 | 47 | - | 48 | license_key: '<%= ENV["NEW_RELIC_LICENSE_KEY"] %>' 49 | 50 | 51 | - name: New Relic API Service Key 52 | id: np.newrelic.3 53 | pattern: \b(nrak-[a-z0-9]{27})\b 54 | 55 | references: 56 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys 57 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#user-key 58 | 59 | examples: 60 | - " PS> Get-NR1Catalog -PersonalAPIKey 'NRAK-123456788ABCDEFGHIJKLMNOPQR'" 61 | - ' placeholder="e.g: NRAK-CIH1YVYWKA9ZP6E49WP5XYJH1G9">' 62 | - | 63 | ENV NODE_ENV "production" 64 | ENV PORT 8079 65 | #ENV NEW_RELIC_LICENSE_KEY=NRAK-7JCF597RJ492YP6MZWST3HWRNY2 66 | 67 | 68 | - name: New Relic Admin API Key 69 | id: np.newrelic.4 70 | pattern: \b(nraa-[a-f0-9]{27})\b 71 | 72 | references: 73 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys 74 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#admin-keys 75 | 76 | examples: 77 | - 'admin_access:NRAA-4780f48c47df5882dbec3fd82c7' 78 | 79 | 80 | - name: New Relic Insights Insert Key 81 | id: np.newrelic.5 82 | pattern: \b(nrii-[a-z0-9_-]{32})(?:[^a-z0-9_-]|$) 83 | 84 | references: 85 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys 86 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#insights-insert-key 87 | 88 | examples: 89 | - ' insertKey: "NRII-3nbcrMjHHs0RrT3GhRNqpd16YVMFHdcI")' 90 | - ' "Api-Key": "NRII-7a6SL_Pau5Dz923jEuBEylu3clzXzfby"' 91 | 92 | 93 | - name: New Relic Insights Query Key 94 | id: np.newrelic.6 95 | pattern: \b(nriq-[a-z0-9_-]{32})(?:[^a-z0-9_-]|$) 96 | 97 | references: 98 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys 99 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#insights-query-key 100 | 101 | examples: 102 | - ' "querykey": "NRIQ-pD-yUGl9Z3ACIJ89V-zGkhMxFJE5O121",' 103 | 104 | 105 | - name: New Relic REST API Key 106 | id: np.newrelic.7 107 | pattern: \b(nrra-[a-f0-9]{42})\b 108 | 109 | references: 110 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys 111 | - https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#rest-api-key 112 | 113 | examples: 114 | - | 115 | curl -X POST "https://api.newrelic.com/v2/applications/380836898/deployments.json" \ 116 | -H "X-Api-Key:NRRA-e270623d47659ff6a48ac5bde6bba223bef47c8c26" \ 117 | -i \ 118 | -H "Content-Type: application/json" \ 119 | -d "{ \"deployment\": { \"revision\": \"${rev}\" }}" 120 | 121 | 122 | - name: New Relic Pixie API Key 123 | id: np.newrelic.8 124 | pattern: \b(px-api-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\b 125 | 126 | references: 127 | - https://docs.px.dev/reference/admin/api-keys/ 128 | 129 | examples: 130 | - 'MW_PX_DEPLOY_KEY=px-dep-f43ae612-dc8a-4049-9553-4af1b0e17620 MW_PX_API_KEY=px-api-c20a3cba-d3c9-45c1-a557-8864040b8f79' 131 | 132 | negative_examples: 133 | - ' --set newrelic-pixie.apiKey=px-api-a1b2c3d4-e5f6-g7h8-i8j0-k0l3m3n4o0p5 `' 134 | 135 | 136 | - name: New Relic Pixie Deploy Key 137 | id: np.newrelic.9 138 | pattern: \b(px-dep-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\b 139 | 140 | references: 141 | - https://docs.px.dev/reference/admin/deploy-keys/ 142 | 143 | examples: 144 | - 'MW_PX_DEPLOY_KEY=px-dep-f43ae612-dc8a-4049-9553-4af1b0e17620 MW_PX_API_KEY=px-api-c20a2cba-d3c9-45c1-a556-8864040b8f79' 145 | 146 | negative_examples: 147 | - ' --set pixie-chart.deployKey=px-dep-d4c3b2a1-f6e5-h8g7-j1i8-p5o0n5m3l2k1 `' 148 | -------------------------------------------------------------------------------- /rules/npm.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: NPM Access Token (fine-grained) 4 | id: np.npm.1 5 | 6 | pattern: \b(npm_[A-Za-z0-9]{36})\b 7 | 8 | references: 9 | - https://docs.npmjs.com/about-access-tokens 10 | - https://github.com/github/roadmap/issues/557 11 | - https://github.blog/changelog/2022-12-06-limit-scope-of-npm-tokens-with-the-new-granular-access-tokens/ 12 | 13 | examples: 14 | - 'npm_TCllNwh2WLQlMWVhybM1iQrsTj6rMQ0BOh6d' 15 | 16 | # There are also NPM Legacy Access Tokens, which appear to be non-prefixed v4 UUIDs. 17 | # Matching these would require a pattern that uses heuristics against surrounding context. 18 | negative_examples: 19 | - '-export NPM_TOKEN="007e65c7-635d-4d54-8294-f360cb8e2e3f"' 20 | -------------------------------------------------------------------------------- /rules/nuget.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: NuGet API Key 4 | id: np.nuget.1 5 | 6 | pattern: '\b(oy2[a-z0-9]{43})\b' 7 | 8 | references: 9 | - https://docs.microsoft.com/en-us/nuget/nuget-org/publish-a-package#create-api-keys 10 | 11 | examples: 12 | - 'nuget push %filename% oy2dgb333j35kjjybcf99yzxo7hjyloera4anxn4ivcvle -Source https://api.nuget.org/v3/index.json' 13 | - 'find . -name "*.nupkg"|xargs -I {} dotnet nuget push "{}" --api-key oy2l53fxd7xcf99dnyrqewssedgopshuticofclpespbyi -s https://api.nuget.org/v3/index.json --skip-duplicate' 14 | -------------------------------------------------------------------------------- /rules/odbc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Credentials in ODBC Connection String 4 | id: np.odbc.1 5 | 6 | pattern: (?:User|User\ Id|UserId|Uid)\s*=\s*([^\s;]{3,100})\s*;[ \t]*.{0,10}[ \t]*(?:Password|Pwd)\s*=\s*([^\s;]{3,100})\s*(?:[;"']|$) 7 | 8 | examples: 9 | - 'Server=host;Port=5432;User Id=username;Password=secret;Database=databasename;' 10 | - 'Server=host;Port=5432;SomeOtherKey=SomeOtherValue;User Id=username;Password=secret;Database=databasename;' 11 | - 'Data Source=190.190.200.100,1433;Network Library=DBMSSOCN;Initial Catalog=myDataBase;User ID=myUsername;Password=myPassword;' 12 | - 'Data Source=190.190.200.100,1433;Network_library=DBMSSOCN;Initial Catalog=myDataBase;User ID=myUsername;Password=myPassword;' 13 | - 'Provider=SQLNCLI;Server=myServerName,myPortNumber;Database=myDataBase;Uid=myUsername;Pwd=myPassword;' 14 | - ' adoConn.Open("Provider=SQLOLEDB.1;User ID=specialbill_user; " & "Password =specialbill_user;Initial Catalog=SpecialBill_PROD;Data Source=uszdba01;")' 15 | - | 16 | "driver={SQL Server};server=(#{datastore['DBHOST']});database=#{datastore['DBNAME']};uid=#{datastore['DBUID']};pwd=#{datastore['DBPASSWORD']}" 17 | 18 | negative_examples: 19 | - "def login(self, user = '', password = '', domain = ''):" 20 | - | 21 | if datastore['VERBOSE'] 22 | text = '' 23 | text << "User=#{username}, " 24 | text << "Password=#{password}, " 25 | text << "Domain=#{domain}, " 26 | text << "Full Name=#{full_name}, " 27 | text << "E-mail=#{e_mail}" 28 | print_good(text) 29 | - | 30 | if (len < ulen + wlen + 2) 31 | break; 32 | user = (char *) (p + 1); 33 | pwd = (char *) (p + ulen + 2); 34 | p += ulen + wlen + 2; 35 | 36 | - | 37 | /* Set default values */ 38 | server = xmalloc(sizeof(*server)); 39 | server->user = "anonymous"; 40 | server->password = "busybox@"; 41 | 42 | - | 43 | System.out.println("Here we go..."); 44 | String url = "jdbc:msf:sql://127.0.0.1:8080/sample"; 45 | String userid = "userid"; 46 | String password = "password"; 47 | 48 | - | 49 | char *domain = NULL; 50 | char *user = NULL; 51 | char *password = NULL; 52 | 53 | - | 54 | 59 | 60 | references: 61 | - https://docs.aws.amazon.com/redshift/latest/mgmt/configure-odbc-connection.html 62 | - https://docs.microsoft.com/en-us/azure/data-explorer/kusto/api/connection-strings/kusto 63 | - https://docs.microsoft.com/en-us/azure/mariadb/howto-connection-string 64 | - https://docs.microsoft.com/en-us/azure/mysql/single-server/how-to-connection-string 65 | - https://www.connectionstrings.com/ 66 | -------------------------------------------------------------------------------- /rules/okta.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Okta API Token 4 | id: np.okta.1 5 | 6 | # Note: looks like `00` followed by a 40-character base-62 payload. We are a 7 | # bit more restrictive here, not allowing `-` at the end, so that we can use 8 | # the word boundary `\b` zero-width assertion, and also requiring "okta" or 9 | # "ssws" out front. 10 | pattern: '(?i)(?s)(?:okta|ssws).{0,40}\b(00[a-z0-9_-]{39}[a-z0-9_])\b' 11 | references: 12 | - https://devforum.okta.com/t/api-token-length/5519 13 | - https://developer.okta.com/docs/guides/create-an-api-token/main/ 14 | 15 | examples: 16 | - 'okta_api_token = 00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 17 | - 'OKTA_API_KEY = "00-aaaaaaaaaaaaa-aaaaaaaaaaaaaaaaaaaaaaaaa"' 18 | - 'okta_secret: 00QCjAl4MlV-WPXM-ABCDEFGHIJKL-0HmjFx-vbGua' 19 | - 'Authorization: SSWS 00QCjAl4MlV-WPXM-ABCDEFGHIJKL-0HmjFx-vbGua' 20 | - | 21 | variable "corp_okta_api_token" { 22 | default = "004EWTpRQT_HJtG_nL-agxacgzYHjxPcF99kJsFzWg" 23 | } 24 | 25 | negative_examples: 26 | - '000000000000000000000000000000000000000000' 27 | - 'okta_api_token: 000000000000000000000000000000000000000000aa' 28 | - 'okta: 00QCjAl4MlV-WPXM-ABCDEFGHIJKL-0HmjFx-vbGu--' 29 | - 'okta_api_key: 00QCjAl4MlV-WPXM-ABCDEFGHIJKL-0HmjFx-vbGu-' 30 | 31 | 32 | # FIXME: also add rules for Okta OAuth 2.0 tokens 33 | -------------------------------------------------------------------------------- /rules/openai.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: OpenAI API Key 4 | id: np.openai.1 5 | 6 | pattern: \b(sk-[a-zA-Z0-9]{48})\b 7 | 8 | examples: 9 | - | 10 | curl https://api.openai.com/v1/images/generations -H 'Content-Type: application/json' -H "Authorization: Bearer sk-mxIt5s1tyfCJyIKHwrqOT4BlbkFJT3VVmv6VdSwB7XXIq1TO" 11 | 12 | references: 13 | - https://platform.openai.com/docs/api-reference 14 | - https://platform.openai.com/docs/api-reference/authentication 15 | -------------------------------------------------------------------------------- /rules/pem.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | # FIXME: add for `-----BEGIN CERTIFICATE-----` 4 | 5 | - name: PEM-Encoded Private Key 6 | id: np.pem.1 7 | 8 | # Note: This is intended to match many PEM-encoded base64 payloads 9 | pattern: -----BEGIN\ .{0,20}\ ?PRIVATE\ KEY\ ?.{0,20}-----\s*((?:[a-zA-Z0-9+/=\s"',]|\\r|\\n){50,})\s*-----END\ .{0,20}\ ?PRIVATE\ KEY\ ?.{0,20}----- 10 | 11 | references: 12 | - https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail 13 | - https://datatracker.ietf.org/doc/html/rfc7468 14 | 15 | examples: 16 | - | 17 | -----BEGIN RSA PRIVATE KEY----- 18 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn 19 | NhAAAAAwEAAQAAAIEAtDSHFO5tfN+jYMJuiNvBaplkSI3eFqKMLOvXyVu+dmSEic6xyKWQ 20 | qjQiFpXogArvAq2tBxWOq7F+a6rNhDKdICD2amRwDHqKD1bzXVSZ5c1XnpCFsBiQaEyX2i 21 | qyjScnntFHIpTCVHNxILDxsStocj64YS0C7hfCGVhft/Ts/O0AAAIQJOKnUyTip1MAAAAH 22 | c3NoLXJzYQAAAIEAtDSHFO5tfN+jYMJuiNvBaplkSI3eFqKMLOvXyVu+dmSEic6xyKWQqj 23 | QiFpXogArvAq2tBxWOq7F+a6rNhDKdICD2amRwDHqKD1bzXVSZ5c1XnpCFsBiQaEyX2iqy 24 | jScnntFHIpTCVHNxILDxsStocj6Cf99C7hfCGVhft/Ts/O0AAAADAQABAAAAgBcaTN8gGi 25 | VSPo3fH3CoS8mw1KyAk6JvQG1Z5xZHjsl65YsNVrmUkFFh0aT3nxEbVb0QKwineN0GKmD/ 26 | Ss3R91a573gzli7TJPFCHhhBbE7FRC4KQMTc1/UANwFYQVcfZ4n9IVHr3jiWToSY3XbC66 27 | Zcd0sg+d+YRjIxUktuNFHBAAAAQQCOOKbSUJAWzcTDbxImwDCAfBMlEeMAnJrwobL/zxbT 28 | GhKdnqnomoreFdYL8vOcOlwZG0hUKIA6AM1GsMzp6aCwAAAAQQDmAABpOQnkDy8v8kTDhP 29 | dW3lAqRGOU4WRWj7WystQv/VjuJpceekhOyhNJBuNHDKZ3IT1agAZHIhhL+webE2S1AAAA 30 | QQDIk4H1agCohlHUg50PcyKzE/zZ85Gw0ErTmgqIIGd4B1AqUtjwVe1qFoqHuZPtq2cbVF 31 | 1HTHh6GX//J6rKWVJZAAAAGWJsYXJzZW5AYnJhZGZvcmRzLW1icC5sYW4B 32 | -----END RSA PRIVATE KEY----- 33 | 34 | # Sometimes keys are written as string concatenation in source code; 35 | # this rule can match those too. 36 | - | 37 | "-----BEGIN RSA PRIVATE KEY-----" + 38 | "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn" + 39 | "NhAAAAAwEAAQAAAIEAtDSHFO5tfN+jYMJuiNvBaplkSI3eFqKMLOvXyVu+dmSEic6xyKWQ" + 40 | "qjQiFpXogArvAq2tBxWOq7F+a6rNhDKdICD2amRwDHqKD1bzXVSZ5c1XnpCFsBiQaEyX2i" + 41 | "qyjScnntFHIpTCVHNxILDxsStocj64YS0C7hfCGVhft/Ts/O0AAAIQJOKnUyTip1MAAAAH" + 42 | "c3NoLXJzYQAAAIEAtDSHFO5tfN+jYMJuiNvBaplkSI3eFqKMLOvXyVu+dmSEic6xyKWQqj" + 43 | "QiFpXogArvAq2tBxWOq7F+a6rNhDKdICD2amRwDHqKD1bzXVSZ5c1XnpCFsBiQaEyX2iqy" + 44 | "jScnntFHIpTCVHNxILDxsStocj6Cf99C7hfCGVhft/Ts/O0AAAADAQABAAAAgBcaTN8gGi" + 45 | "VSPo3fH3CoS8mw1KyAk6JvQG1Z5xZHjsl65YsNVrmUkFFh0aT3nxEbVb0QKwineN0GKmD/" + 46 | "Ss3R91a573gzli7TJPFCHhhBbE7FRC4KQMTc1/UANwFYQVcfZ4n9IVHr3jiWToSY3XbC66" + 47 | "Zcd0sg+d+YRjIxUktuNFHBAAAAQQCOOKbSUJAWzcTDbxImwDCAfBMlEeMAnJrwobL/zxbT" + 48 | "GhKdnqnomoreFdYL8vOcOlwZG0hUKIA6AM1GsMzp6aCwAAAAQQDmAABpOQnkDy8v8kTDhP" + 49 | "dW3lAqRGOU4WRWj7WystQv/VjuJpceekhOyhNJBuNHDKZ3IT1agAZHIhhL+webE2S1AAAA" + 50 | "QQDIk4H1agCohlHUg50PcyKzE/zZ85Gw0ErTmgqIIGd4B1AqUtjwVe1qFoqHuZPtq2cbVF" + 51 | "1HTHh6GX//J6rKWVJZAAAAGWJsYXJzZW5AYnJhZGZvcmRzLW1icC5sYW4B" + 52 | "-----END RSA PRIVATE KEY-----" 53 | 54 | # Other times keys are embedded as literal strings in source code; 55 | # this rule can match those too. 56 | - | 57 | "-----BEGIN RSA PRIVATE KEY-----\r\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn\r\nNhAAAAAwEAAQAAAIEAtDSHFO5tfN+jYMJuiNvBaplkSI3eFqKMLOvXyVu+dmSEic6xyKWQ\r\nqjQiFpXogArvAq2tBxWOq7F+a6rNhDKdICD2amRwDHqKD1bzXVSZ5c1XnpCFsBiQaEyX2i\r\nqyjScnntFHIpTCVHNxILDxsStocj64YS0C7hfCGVhft/Ts/O0AAAIQJOKnUyTip1MAAAAH\r\nc3NoLXJzYQAAAIEAtDSHFO5tfN+jYMJuiNvBaplkSI3eFqKMLOvXyVu+dmSEic6xyKWQqj\r\nQiFpXogArvAq2tBxWOq7F+a6rNhDKdICD2amRwDHqKD1bzXVSZ5c1XnpCFsBiQaEyX2iqy\r\njScnntFHIpTCVHNxILDxsStocj6Cf99C7hfCGVhft/Ts/O0AAAADAQABAAAAgBcaTN8gGi\r\nVSPo3fH3CoS8mw1KyAk6JvQG1Z5xZHjsl65YsNVrmUkFFh0aT3nxEbVb0QKwineN0GKmD/\r\nSs3R91a573gzli7TJPFCHhhBbE7FRC4KQMTc1/UANwFYQVcfZ4n9IVHr3jiWToSY3XbC66\r\nZcd0sg+d+YRjIxUktuNFHBAAAAQQCOOKbSUJAWzcTDbxImwDCAfBMlEeMAnJrwobL/zxbT\r\nGhKdnqnomoreFdYL8vOcOlwZG0hUKIA6AM1GsMzp6aCwAAAAQQDmAABpOQnkDy8v8kTDhP\r\ndW3lAqRGOU4WRWj7WystQv/VjuJpceekhOyhNJBuNHDKZ3IT1agAZHIhhL+webE2S1AAAA\r\nQQDIk4H1agCohlHUg50PcyKzE/zZ85Gw0ErTmgqIIGd4B1AqUtjwVe1qFoqHuZPtq2cbVF\r\n1HTHh6GX//J6rKWVJZAAAAGWJsYXJzZW5AYnJhZGZvcmRzLW1icC5sYW4B\r\n-----END RSA PRIVATE KEY-----" 58 | -------------------------------------------------------------------------------- /rules/postman.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Postman API Key 4 | id: np.postman.1 5 | 6 | pattern: \b(PMAK-[a-zA-Z0-9]{24}-[a-zA-Z0-9]{34})\b 7 | 8 | 9 | examples: 10 | - "// ('x-api-key', 'PMAK-629c73facbc064567cbf6970-f56e8b4cd0bb14d00962f17afc158dc2a2')" 11 | 12 | references: 13 | - https://learning.postman.com/docs/developer/intro-api/ 14 | - https://learning.postman.com/docs/developer/postman-api/authentication/ 15 | - https://learning.postman.com/docs/administration/managing-your-team/managing-api-keys/ 16 | -------------------------------------------------------------------------------- /rules/psexec.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Credentials in PsExec 4 | id: np.psexec.1 5 | 6 | pattern: (?i:psexec.{0,100}-u\s*(\S+)\s+-p\s*(\S+)) 7 | 8 | examples: 9 | - 'cmd.exe /C PSEXEC \\10.0.94.120 -u Administrator -p dev_admin CMD /C ECHO' 10 | - 'PSEXEC.EXE \\LocalComputerIPAddress -u DOMAIN\my-user -p mypass CMD' 11 | - 'psExec \\OAIJCTDU8024272 -u User -p $Password -i -d calc.exe' 12 | - | 13 | :: satmodel2 14 | %RUNTIMEDIR%\PsExec.exe \\satmodel2 -u SATMODEL2\MTCPB -p %nothing% -i 2 -c -f %TEMP%\psexec_helper.bat %RUNTIMEDIR% .\JavaOnly_runNode2.cmd 15 | %RUNTIMEDIR%\pslist.exe \\satmodel2 java 16 | if %ERRORLEVEL% NEQ 0 goto done 17 | - | 18 | ASSEMBLE THE BATCH FILE TO COPY THE FILE ACROSS THE DOMAIN 19 | start PsExec.exe /accepteula @C:\share$\comps1.txt -u DOMAIN\ADMINISTRATOR -p PASSWORD cmd /c COPY "\PRIMARY DOMAIN CONTROLLER\share$\fx166.exe" "C:\windows\temp\" 20 | SAVE IT AS "COPY.BAT" 21 | - 'system("psexec \\\\192.168.3.77 -u Administrator -p braksha shutdown -r -f -t 0");' 22 | 23 | references: 24 | - https://learn.microsoft.com/en-us/sysinternals/downloads/psexec 25 | -------------------------------------------------------------------------------- /rules/pypi.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: PyPI Upload Token 4 | id: np.pypi.1 5 | 6 | # NOTE: these can actually be arbitrarily long 7 | pattern: \b(pypi-AgEIcHlwaS5vcmc[a-zA-Z0-9_-]{50,})(?:[^a-zA-Z0-9_-]|$) 8 | 9 | references: 10 | # GitHub Secrets Scanning implementation issue and discussion 11 | - https://github.com/pypa/warehouse/issues/6051 12 | # A library that generates PyPI tokens (which are b64-encoded macaroons) 13 | - https://pypi.org/project/pypitoken/ 14 | # The library that PyPi uses in its backend? 15 | - https://github.com/ecordell/pymacaroons 16 | - https://en.wikipedia.org/wiki/Macaroons_(computer_science) 17 | - https://github.com/pypa/warehouse/blob/82815b06d9f98deed5f205c66e054de59d22a10d/docs/development/token-scanning.rst 18 | - https://research.google/pubs/pub41892/ 19 | 20 | examples: 21 | - '# password = pypi-AgEIcHlwaS5vcmcCJDkwNzYwNzU1LWMwOTUtNGNkOC1iYjQzLTU3OWNhZjI1NDQ1MwACJXsicGVybWCf99lvbnMiOiAidXNlciIsICJ2ZXJzaW9uIjogMX0AAAYgSpW5PAywXvchMUQnkF5H6-SolJysfUvIWopMsxE4hCM' 22 | - | 23 | - name: Publish package 24 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 25 | with: 26 | user: santoshp 27 | password: ${{ secrets.pypi-AgEIcHlwaS5vcmcCJDA1NTdiYzI2LTQ3N2QtNDAyYy04YzBjLTVmODU4ZTFkMjACf99COXsicGVybWlzc2lvbnMiOiB7InByb2plY3RzIjogWyJlbXB5cmlhbCJdfSwgInZlcnNpb24iOiAxfQAABiAx85KUjr83dNyI9uO0RVMmH7DKqoXNH4_rMkO5SQYItA}} 28 | - 'password: pypi-AgEIcHlwaS5vcmcCJGExMDIxZjRhLTFhZDMtNDc4YS1iOWNmLWQwCf99OTIwZjFjNwACSHsicGVybWlzc2lvbnMiOiB7InByb2plY3RzIjogWyJkamFuZ28tY2hhbm5lbHMtanNvbnJwYyJdfSwgInZlcnNpb24iOiAxfQAABiBZg48cIBQt7HckwM4G3q-462xphsLbm7IZvjqMS4jvQw' 29 | -------------------------------------------------------------------------------- /rules/react.yml: -------------------------------------------------------------------------------- 1 | # These rules are designed to detect certain username and password patterns 2 | # that sometimes appear within .env files used within React apps via the 3 | # `create-react-app` program. 4 | # 5 | # Note that secrets are _not_ supposed to appear in such .env files, even if 6 | # they are .gitignored, as the contents will be embedded within the generated 7 | # React code (which is visible to clients). 8 | # 9 | # The variable names within one of these .env files are arbitrary, other than 10 | # that they have to start with `REACT_APP_`. 11 | 12 | 13 | rules: 14 | 15 | 16 | - name: React App Username 17 | id: np.reactapp.1 18 | 19 | pattern: \bREACT_APP(?:_[A-Z0-9]+)*_USER(?:NAME)?\s*=\s*['"]?([^\s'"$]{3,})(?:[\s'"$]|$) 20 | 21 | references: 22 | - https://create-react-app.dev/docs/adding-custom-environment-variables/ 23 | - https://stackoverflow.com/questions/48699820/how-do-i-hide-an-api-key-in-create-react-app 24 | 25 | examples: 26 | - '# REACT_APP_GUEST_USERNAME=guest' 27 | - '# REACT_APP_USER=postgres' 28 | - 'REACT_APP_AUTH_USER=postgres' 29 | - 'REACT_APP_AUTH_USERNAME=bowie' 30 | - ' REACT_APP_AUTH_USERNAME=bowie # some comment' 31 | - 'REACT_APP_MAILER_USERNAME=smtp_username # Enter your SMTP email username' 32 | 33 | negative_examples: 34 | - 'REACT_APP_FRONTEND_LOGIN_FORGOT_USERNAME=$REACT_APP_MATRIX_BASE_URL/classroom/#/forgot_username' 35 | 36 | 37 | - name: React App Password 38 | id: np.reactapp.2 39 | 40 | pattern: \bREACT_APP(?:_[A-Z0-9]+)*_PASS(?:WORD)?\s*=\s*['"]?([^\s'"$]{6,})(?:[\s'"$]|$) 41 | 42 | references: 43 | - https://create-react-app.dev/docs/adding-custom-environment-variables/ 44 | - https://stackoverflow.com/questions/48699820/how-do-i-hide-an-api-key-in-create-react-app 45 | 46 | examples: 47 | - '# REACT_APP_GUEST_PASSWORD=mycoin!1' 48 | - '# REACT_APP_PASS=whiteduke' 49 | - 'REACT_APP_AUTH_PASS=whiteduke' 50 | - 'REACT_APP_AUTH_PASSWORD=whiteduke' 51 | - ' REACT_APP_AUTH_PASSWORD=whiteduke # some comment' 52 | - 'REACT_APP_MAILER_PASSWORD=smtp_password # Enter your SMTP email password' 53 | 54 | negative_examples: 55 | - ' const password = process.env.REACT_APP_FIREBASE_DEV_PASSWORD || "not-set"' 56 | - 'REACT_APP_FRONTEND_LOGIN_FORGOT_PASSWORD=$REACT_APP_MATRIX_BASE_URL/classroom/#/forgot_password' 57 | -------------------------------------------------------------------------------- /rules/rubygems.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: RubyGems API Key 4 | id: np.rubygems.1 5 | 6 | pattern: \b(rubygems_[a-f0-9]{48})\b 7 | 8 | references: 9 | - https://guides.rubygems.org/rubygems-org-api/ 10 | - https://guides.rubygems.org/api-key-scopes/ 11 | 12 | examples: 13 | - | 14 | $ curl -H 'Authorization:rubygems_b9ce70c306b3a2e248679fbbbd66723d408d3c8c5f00566c' \ 15 | https://rubygems.org/api/v1/web_hooks.json 16 | -------------------------------------------------------------------------------- /rules/salesforce.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Salesforce Access Token 4 | id: np.salesforce.1 5 | pattern: \b(00[a-zA-Z0-9]{13}![a-zA-Z0-9._]{96})(?:\b|$|[^a-zA-Z0-9._]) 6 | 7 | references: 8 | - https://help.salesforce.com/s/articleView?id=sf.remoteaccess_access_tokens.htm&type=5 9 | - https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/quickstart_oauth.htm 10 | 11 | examples: 12 | - 00DE0X0A0M0PeLE!CJoAQOx1GCLf1UIt4UU9y0VOPLUZAYN6I8DsdGEDyHh5cO02egObcAhIDHYiGCfi94c53oFbr4HB.xZfuYRGhvNuxobAAXRe 13 | - | 14 | === Org Description 15 | KEY VALUE 16 | ──────────────── ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── 17 | Access Token 00DE0X0A0M0PeLE!AQcAQH0dMHEXAMPLEzmpkb58urFRkgeBGsxL_QJWwYMfAbUeeG7c1EXAMPLEDUkWe6H34r1AAwOR8B8fLEz6nEXAMPLEAAAA 18 | Client Id PlatformCLI 19 | Connected Status Connected 20 | Id 00D5fORGIDEXAMPLE 21 | Instance Url https://MyDomainName.my.salesforce.com 22 | Username juliet.capulet@empathetic-wolf-g5qddtr.com 23 | -------------------------------------------------------------------------------- /rules/sauce.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Sauce Token 4 | id: np.sauce.1 5 | 6 | pattern: '(?i)sauce.{0,50}\b([a-f0-9-]{36})\b' 7 | 8 | examples: 9 | - | 10 | - SAUCE_USERNAME=vitess 11 | - SAUCE_ACCESS_KEY=2397f603-c2c4-4897-a8ca-587ace5dc8dd 12 | 13 | # FIXME: add references for this rule 14 | # FIXME: review the stuff about tokens in https://docs.saucelabs.com/test-results/sharing-test-results and make sure this pattern is both good and comprehensive 15 | -------------------------------------------------------------------------------- /rules/segment.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Segment Public API Token 4 | id: np.segment.1 5 | 6 | pattern: \b(sgp_[a-zA-Z0-9]{64})\b 7 | 8 | references: 9 | - https://segment.com/docs/api/public-api/ 10 | - https://segment.com/blog/how-segment-proactively-protects-customer-api-tokens/ 11 | 12 | examples: 13 | - '"token": "sgp_b8eaD5d9Ae59a15a407bb7C88350bc85dc959EBE8277883d50Bc84dc960eE826"' 14 | -------------------------------------------------------------------------------- /rules/sendgrid.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: SendGrid API Key 4 | id: np.sendgrid.1 5 | 6 | pattern: '\b(SG\.[0-9A-Za-z_-]{22}\.[0-9A-Za-z_-]{43})\b' 7 | 8 | examples: 9 | - " 'SENDGRID_API_KEYSID': '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'" 10 | - "var sendgrid = require('sendgrid')('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');" 11 | 12 | references: 13 | - https://docs.sendgrid.com/ui/account-and-settings/api-keys 14 | -------------------------------------------------------------------------------- /rules/shopify.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | # - name: Shopify Domain 4 | # id: np.shopify.1 5 | # catgegories: [identifier] 6 | 7 | # pattern: | 8 | # (?x) 9 | # \b 10 | # ( 11 | # (?:[a-zA-Z0-9-]+\.)* [a-zA-Z0-9-]+ \.myshopify\.com 12 | # ) 13 | # \b 14 | 15 | # references: 16 | # - https://help.shopify.com/en/manual/domains 17 | 18 | # examples: 19 | # - 'handsomestranger.myshopify.com' 20 | # - 'store.handsomestranger.myshopify.com' 21 | 22 | 23 | - name: Shopify App Secret 24 | id: np.shopify.2 25 | catgegories: [secret] 26 | 27 | pattern: '\b(shpss_[a-fA-F0-9]{32})\b' 28 | 29 | references: 30 | - https://shopify.dev/apps/auth 31 | - https://shopify.dev/changelog/app-secret-key-length-has-increased 32 | 33 | examples: 34 | - | 35 | SHOPIFY_API_KEY='66eaacb546afcad32162d40acb6bd2b0' 36 | SHOPIFY_API_SECRET_KEY='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' 37 | - | 38 | SHOPIFY_API_KEY: 38d5b9a8b6c0a3d3ad3f2c422c77db80 39 | SHOPIFY_API_SECRET: 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 40 | 41 | 42 | - name: Shopify Access Token (Public App) 43 | id: np.shopify.3 44 | pattern: '\b(shpat_[a-fA-F0-9]{32})\b' 45 | 46 | references: 47 | - https://shopify.dev/apps/auth 48 | - https://shopify.dev/changelog/length-of-the-shopify-access-token-is-increasing 49 | 50 | examples: 51 | - | 52 | include('layouts/header.php'); 53 | $shop = $_GET['shop']; 54 | $token = "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"; 55 | $query = array( 56 | 57 | 58 | - name: Shopify Access Token (Custom App) 59 | id: np.shopify.4 60 | pattern: '\b(shpca_[a-fA-F0-9]{32})\b' 61 | 62 | references: 63 | - https://shopify.dev/apps/auth 64 | - https://shopify.dev/changelog/length-of-the-shopify-access-token-is-increasing 65 | 66 | examples: 67 | - "const TEMP_CONTENT = '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 handsomestranger.myshopify.com'" 68 | 69 | 70 | - name: Shopify Access Token (Legacy Private App) 71 | id: np.shopify.5 72 | pattern: '\b(shppa_[a-fA-F0-9]{32})\b' 73 | 74 | references: 75 | - https://shopify.dev/apps/auth 76 | - https://shopify.dev/changelog/length-of-the-shopify-access-token-is-increasing 77 | 78 | examples: 79 | - 'SHOP_PASSWORD=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' 80 | -------------------------------------------------------------------------------- /rules/slack.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | # XXX what are these? 4 | # pattern: '\b(xoxa-[0-9]{12}-[0-9]{12}-[a-f0-9]{32})\b' 5 | # pattern: '\b(xoxa-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-f0-9]{32})\b' 6 | # pattern: '\b(xoxr-[0-9]{12}-[0-9]{12}-[a-z0-9]{24})\b' 7 | 8 | - name: Slack Bot Token 9 | id: np.slack.2 10 | pattern: '\b(xoxb-[0-9]{12}-[0-9]{12}-[a-zA-Z0-9]{24})\b' 11 | 12 | references: 13 | - https://api.slack.com/authentication 14 | - https://api.slack.com/authentication/best-practices 15 | - https://api.slack.com/authentication/token-types 16 | 17 | examples: 18 | - 'SLACK_API_TOKEN=xoxb-893582989554-899326518131-JRHeVv1o9Cf99fwDpuortR2D' 19 | 20 | negative_examples: 21 | - 'python log_announce.py xoxp-513768634356-513201028496-513937500594-185e196ace562dd6443b5d29b1d817c2 "This is a test run. Ignore"' 22 | - | 23 | this is the api token to connect to the bot user 24 | 25 | xoxb-153445930147-Tjy11gGxUW6Cf99YOYwtzG0K 26 | - | 27 | def send_slack_notification(message): 28 | token = "xoxb-47834520726-N3otsrwj8Cf99cs8GhiRZsX1" 29 | 30 | 31 | - name: Slack User Token 32 | id: np.slack.4 33 | pattern: '\b(xoxp-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-f0-9]{32})\b' 34 | 35 | references: 36 | - https://api.slack.com/authentication 37 | - https://api.slack.com/authentication/best-practices 38 | - https://api.slack.com/authentication/token-types 39 | 40 | examples: 41 | - 'python log_announce.py xoxp-513768634356-513201028496-513937500594-185e196ace562dd6443b5d29b1d817c2 "This is a test run. Ignore"' 42 | - 'curl -X POST -H "Content-type: application/json" -H "Authorization: Bearer xoxp-283316862324-298911817009-298923149681-44f585044dace54f5701618e97cd1c0b" --data @data.json https://wirecard-issuing.slack.com/api/chat.postMessage' 43 | - ' url := "https://slack.com/api/channels.history?token=xoxp-113726990690-113803571044-155105854433-53ffb9d16ace50aa79aa1c425a68b131&channel=C4D8D3XMX&count=1&pretty=1"' 44 | 45 | negative_examples: 46 | - | 47 | this is the api token to connect to the bot user 48 | 49 | xoxb-153445930147-Tjy11gGxUW6Cf99YOYwtzG0K 50 | - 'SLACK_API_TOKEN=xoxb-893582989554-899326518131-JRHeVv1o9Cf99fwDpuortR2D' 51 | - | 52 | def send_slack_notification(message): 53 | token = "xoxb-47834520726-N3otsrwj8Cf99cs8GhiRZsX1" 54 | 55 | 56 | - name: Slack App Token 57 | id: np.slack.5 58 | pattern: '\b(xapp-[0-9]{12}-[a-zA-Z0-9/+]{24})\b' 59 | 60 | references: 61 | - https://api.slack.com/authentication 62 | - https://api.slack.com/authentication/best-practices 63 | - https://api.slack.com/authentication/token-types 64 | 65 | examples: 66 | - 'ENV SLACK_TOKEN="xapp-083452001657-ShAYwge/87H4lC3j7lZ48pAL" \' 67 | 68 | - name: Slack Legacy Bot Token 69 | id: np.slack.6 70 | pattern: '\b(xoxb-[0-9]{10,13}-[a-zA-Z0-9]{24})\b' 71 | 72 | references: 73 | - https://api.slack.com/authentication 74 | - https://api.slack.com/authentication/best-practices 75 | - https://api.slack.com/authentication/token-types 76 | - https://api.slack.com/legacy/custom-integrations/legacy-tokens 77 | 78 | examples: 79 | - | 80 | this is the api token to connect to the bot user 81 | 82 | xoxb-153445930147-Tjy11gGxUW6Cf99YOYwtzG0K 83 | - | 84 | def send_slack_notification(message): 85 | token = "xoxb-47834520726-N3otsrwj8Cf99cs8GhiRZsX1" 86 | 87 | negative_examples: 88 | - 'SLACK_API_TOKEN=xoxb-893582989554-899326518131-JRHeVv1o9Cf99fwDpuortR2D' 89 | - 'python log_announce.py xoxp-513768634356-513201028496-513937500594-185e196ace562dd6443b5d29b1d817c2 "This is a test run. Ignore"' 90 | - 'curl -X POST -H "Content-type: application/json" -H "Authorization: Bearer xoxp-283316862324-298911817009-298923149681-44f585044dace54f5701618e97cd1c0b" --data @data.json https://wirecard-issuing.slack.com/api/chat.postMessage' 91 | - ' url := "https://slack.com/api/channels.history?token=xoxp-113726990690-113803571044-155105854433-53ffb9d16ace50aa79aa1c425a68b131&channel=C4D8D3XMX&count=1&pretty=1"' 92 | 93 | 94 | - name: Slack Webhook 95 | id: np.slack.3 96 | pattern: '(?i)(https://hooks.slack.com/services/T[a-z0-9_]{8}/B[a-z0-9_]{8,12}/[a-z0-9_]{24})' 97 | 98 | references: 99 | - https://api.slack.com/messaging/webhooks 100 | 101 | examples: 102 | - '#notifications_marcus: https://hooks.slack.com/services/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' 103 | - | 104 | // Import and Configure Console.Slack (Thanks David <3) 105 | // const slack = require('console-slack'); 106 | // slack.options = { 107 | // webhook : "https://hooks.slack.com/services/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", 108 | // username: "console.slack.bot", 109 | // emoji : ":trollface:", 110 | // channel : "#payx-logs" 111 | // }; 112 | -------------------------------------------------------------------------------- /rules/sonarqube.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: SonarQube Token 4 | id: np.sonarqube.1 5 | 6 | pattern: '(?i)sonar.{0,5}login.{0,5}\s*\b([a-f0-9]{40})\b' 7 | 8 | references: 9 | - https://docs.sonarqube.org/latest/user-guide/user-token/ 10 | 11 | examples: 12 | - 'sonar.host.url=https://sonarcloud.io -Dsonar.login=5524bf449ca45fcace54698371466398321f3a82' 13 | - "sonar.login', '826de5590c75919a8317fdface58206eebe7ebbc" 14 | - '$sonarLogin = "4924be8f51f3e738c97db2c4ace51db7e938f28b"' 15 | 16 | negative_examples: 17 | - 'sonarqube-reporter-1.2.4.tgz#3b335d612137949d2f21fcc6c8c8164db7603227' 18 | - 'sonarqube-reporter-1.4.0.tgz#eb9e15deb83e4ca532989df12b40fedd434ef89a' 19 | - 'sonarqube-scanner/-/sonarqube-scanner-2.5.0.tgz#ff704cbddf355d38a52c5e9479d6bb5c1ff28eac' 20 | - | 21 | /d:sonar.host.url=$(SONAR_HOST) /d:sonar.login=$(SONAR_LOGIN) \ 22 | /d:sonar.coverage.exclusions="**Tests*.cs" 23 | -------------------------------------------------------------------------------- /rules/square.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Square Access Token 4 | id: np.square.1 5 | pattern: '(?i)\b(sq0atp-[a-z0-9_-]{22})\b' 6 | examples: 7 | - ' personal access token sq0atp-qUlZzae8wVMc5P5NZdf5DA
' 8 | - | 9 | var applicationId = 'sq0idp-r34HdSnJVWaCesH3dnJrGA'; 10 | var accessToken = 'sq0atp-RdSPeJa5qDMaCesxHOjeRQ'; 11 | 12 | 13 | - name: Square OAuth Secret 14 | id: np.square.2 15 | pattern: '(?i)\b(sq0csp-[a-z0-9_-]{43})\b' 16 | examples: 17 | - | 18 | app_secret: sq0csp-VQgEphNJFVxfoEtJ1M_2KaCesfzP2_ugNWnlMPwZaZk 19 | sandbox_app_id: sandbox-sq0idp-wWAaCesVx0PhRbXkdUUg9Q 20 | sandbox_access_token: sandbox-sq0atb-KVmmWPaCesnJkFsvje76sQ 21 | production_app_id: sq0idp-wWACO1oVx0aCesXkdUUg9Q 22 | - | 23 | private String accessTokenEndpoint = "https://connect.squareup.com/oauth2/token"; 24 | private String baseURL = "https://connect.squareup.com"; 25 | private String clientId = "sq0idp-Ux0S-9iMfaCeszTkDpSjDw"; 26 | private String clientSecret = "sq0csp-lBGGHNQmcaCesLfa3x6W7jJj8SQ-Fx5Y0yQiCrUWM40"; 27 | -------------------------------------------------------------------------------- /rules/stackhawk.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: StackHawk API Key 4 | id: np.stackhawk.1 5 | 6 | pattern: '\b(hawk\.[0-9A-Za-z_-]{20}\.[0-9A-Za-z_-]{20})\b' 7 | 8 | examples: 9 | - 'HAWK_API_KEY="hawk.nHAOHdJjXoNyzAcTDC5M.R2gqQh2aCesrh0yCGB7q"' 10 | 11 | references: 12 | - https://docs.stackhawk.com/web-app/ 13 | -------------------------------------------------------------------------------- /rules/stripe.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | # FIXME: also add an entry for stripe "Publishable Keys"? Those are public, but having it along with the secret key would be problematic. 4 | # Example: STRIPE_PUBLISHABLE_KEY=pk_test_aQbfVWeaCesES5FRSY7iIjk9 5 | 6 | - name: Stripe API Key 7 | id: np.stripe.1 8 | 9 | pattern: '(?i)\b((?:sk|rk)_live_[a-z0-9]{24})\b' 10 | 11 | references: 12 | - https://stripe.com/docs/keys 13 | 14 | examples: 15 | - 'Stripe.api_key = "sk_live_dhhfUUyfrAace5dBAZ10JrAD"' 16 | - 'var stripe = require("stripe")("sk_live_qdyFazIVmace52bThiOzbEVT");' 17 | 18 | - name: Stripe API Test Key 19 | id: np.stripe.2 20 | 21 | pattern: '(?i)\b((?:sk|rk)_test_[a-z0-9]{24})\b' 22 | 23 | references: 24 | - https://stripe.com/docs/keys 25 | 26 | examples: 27 | - '//var stripe = require("stripe")("sk_test_nxOdTTuEace5Ajbh3svpG32m");' 28 | -------------------------------------------------------------------------------- /rules/telegram.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Telegram Bot Token 4 | id: np.telegram.1 5 | 6 | pattern: \b(\d+:AA[a-zA-Z0-9_-]{32,33})(?:[^a-zA-Z0-9_-]|$) 7 | 8 | examples: 9 | - '4839574813:AAFD39kkdpWt3ywyRZergyOLMaJhac61qc' 10 | - '4839574813:AAE4A6Rz0CSnIGzeu897OjQnjzsMEG2_uso' 11 | 12 | references: 13 | - https://core.telegram.org/bots/api 14 | - https://core.telegram.org/bots/features#botfather 15 | -------------------------------------------------------------------------------- /rules/truenas.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: TrueNAS API Key (WebSocket) 4 | id: np.truenas.1 5 | 6 | pattern: '"params"\s*:\s*\[\s*"(\d+-[a-zA-Z0-9]{64})"\s*\]' 7 | 8 | examples: 9 | - '{"id":"3286a508-a6ca-278a-c078-85b2b515d8d2", "msg":"method", "method":"auth.login_with_api_key", "params":["8-Lp22ov7halMBLUpG97Wg4y7fibQi3CW19VJiZcCu746zgCs0mdDdTCoOcpgEucgu"]}' 10 | - '{"id":"677d9914-f598-f497-e77e-2a3aadbb822e", "msg":"method", "method":"auth.login_with_api_key", "params" : ["9-hTSZDBPyg0PjRZvWb8omoxJ7X2gAjRGmiPKql9ENGIUP9OPtEAzz5f6g9YIMVbZT"]}' 11 | - '{"id":"2755dad4-cc12-94bb-a894-ba0f85c3fdbf", "msg":"method", "method":"auth.login_with_api_key", "params" : [ "10-6LZBVhNq8zze0rzXJptfSWDBoskWuThnQb3fUVw4sVNgJ7GKT3ITVIovhwPf34oL" ]}' 12 | - | 13 | { 14 | "id": "2755dad4-cc12-94bb-a894-ba0f85c3fdbf", 15 | "msg": "method", 16 | "method": "auth.login_with_api_key", 17 | "params": [ 18 | "10-6LZBVhNq8zze0rzXJptfSWDBoskWuThnQb3fUVw4sVNgJ7GKT3ITVIovhwPf34oL" 19 | ] 20 | } 21 | 22 | references: 23 | - https://www.truenas.com/docs/api/core_websocket_api.html 24 | - https://www.truenas.com/docs/api/scale_rest_api.html 25 | - https://www.truenas.com/docs/scale/scaletutorials/toptoolbar/managingapikeys/ 26 | - https://www.truenas.com/docs/scale/scaleclireference/auth/cliapikey/ 27 | - https://www.truenas.com/docs/scale/api/ 28 | - https://www.truenas.com/community/threads/api-examples-in-perl-python.108053/ 29 | 30 | - name: TrueNAS API Key (REST API) 31 | id: np.truenas.2 32 | 33 | pattern: Bearer\s*(\d+-[a-zA-Z0-9]{64})\b 34 | 35 | examples: 36 | # only "Bearer" is accepted by TrueNAS API (no "bearer" etc.) 37 | - 'curl -X POST "http://192.168.0.30/api/v2.0/device/get_info" -H "Content-Type: application/json" -H "Authorization: Bearer 8-Lp22ov7halMBLUpG97Wg4y7fibQi3CW19VJiZcCu746zgCs0mdDdTCoOcpgEucgu" -d "\"SERIAL\""' 38 | 39 | references: 40 | - https://www.truenas.com/docs/api/core_websocket_api.html 41 | - https://www.truenas.com/docs/api/scale_rest_api.html 42 | - https://www.truenas.com/docs/scale/scaletutorials/toptoolbar/managingapikeys/ 43 | - https://www.truenas.com/docs/scale/scaleclireference/auth/cliapikey/ 44 | - https://www.truenas.com/docs/scale/api/ 45 | - https://www.truenas.com/community/threads/api-examples-in-perl-python.108053/ 46 | -------------------------------------------------------------------------------- /rules/twilio.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Twilio API Key 4 | id: np.twilio.1 5 | 6 | pattern: '(?i)twilio.{0,20}\b(sk[a-f0-9]{32})\b' 7 | 8 | examples: 9 | - | 10 | const twilioAccountSid = 'AC712594f590c0d8ace55c04858f7398f9' // Your Account SID from www.twilio.com/console 11 | const twilioApiKeySID = 'SK9b4cc552783500ace5414a1ed3e9fd1a' 12 | const twilioApiKeySecret = 'l6LUelKF2BUtMLace5oShZSmRppadYqI' 13 | - | 14 | // https://www.twilio.com/console/video/dev-tools/api-keys 15 | 'API' => env('TWILIO_API','SK6e84981d07ace5c9df33e1ab043a2fb2'), 16 | 'API_KEY' => env('TWILIO_API_KEY', 'wbTs1SUt6Aace5eKeNCxuYvJa6PhaRd0') 17 | 18 | references: 19 | - https://www.twilio.com/docs/usage/api 20 | - https://www.twilio.com/docs/usage/api#authenticate-with-http 21 | - https://www.twilio.com/docs/usage/api#authenticate-using-the-twilio-sdks 22 | -------------------------------------------------------------------------------- /rules/twitter.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | - name: Twitter Client ID 4 | id: np.twitter.1 5 | 6 | pattern: \btwitter.?(?:api|app|application|client|consumer|customer)?.?(?:id|identifier|key).{0,2}\s{0,20}.{0,2}\s{0,20}.{0,2}\b([a-z0-9]{18,25})\b 7 | 8 | references: 9 | - https://developer.twitter.com/en/docs/authentication/overview 10 | 11 | examples: 12 | - ' TWITTER_ID: "DkWLqcP3ace3wHuJ7fiw",' 13 | - | 14 | # TWITTER_API_KEY = 'UZYoBAfBzNace3mBwPOGYw' 15 | # TWITTER_API_SECRET = 'ngHaeaRPKA5BDQNXace3LWA1PvTA1kBGDaAJmc517E' 16 | 17 | 18 | - name: Twitter Secret Key 19 | id: np.twitter.2 20 | 21 | pattern: twitter.?(?:api|app|application|client|consumer|customer|secret|key)?.?(?:key|oauth|sec|secret)?.{0,2}\s{0,20}.{0,2}\s{0,20}.{0,2}\b([a-z0-9]{35,44})\b 22 | 23 | references: 24 | - https://developer.twitter.com/en/docs/authentication/overview 25 | 26 | examples: 27 | - | 28 | # TWITTER_API_KEY = 'UZYoBAfBzNace3mBwPOGYw' 29 | # TWITTER_API_SECRET = 'ngHaeaRPKA5BDQNXace3LWA1PvTA1kBGDaAJmc517E' 30 | 31 | # XXX It would be nice if this actually matched 32 | negative_examples: 33 | - | 34 | Twitter(auth=OAuth('MjuHWoGbzYmJv3ZuHaBvSENfyevu00NQuBc40VM', 35 | 'anJLBCOALCXl7aXeybmNA5oae9E03Cm23cKNMLaScuXwk', 36 | 'kl3E14NQx84qxO1dy247V0b2W', 37 | '5VFVXVMq9bDJzFAKPfWOiYmJZin2F7YLhSfoyLBXf6Bc9ngX3g')) 38 | -------------------------------------------------------------------------------- /rules/wireguard.yml: -------------------------------------------------------------------------------- 1 | # These rules are specifically designed to identify WireGuard .conf files, 2 | # with a focus on detecting private and preshared keys contained within them. 3 | 4 | rules: 5 | 6 | - name: WireGuard Private Key 7 | id: np.wireguard.1 8 | 9 | pattern: PrivateKey\s*=\s*([A-Za-z0-9+/]{43}=) 10 | 11 | examples: 12 | - | 13 | [Interface] 14 | Address = 10.200.200.3/32 15 | PrivateKey = AsaFot43bfs1fEWjvtty+rGcjh3rP1H6sug1l3u19ix= 16 | DNS = 8.8.8.8 17 | 18 | references: 19 | - https://www.wireguard.com/quickstart/ 20 | - https://manpages.debian.org/testing/wireguard-tools/wg.8.en.html 21 | - https://gist.github.com/lanceliao/5d2977f417f34dda0e3d63ac7e217fd6 22 | 23 | - name: WireGuard Preshared Key 24 | id: np.wireguard.2 25 | 26 | pattern: PresharedKey\s*=\s*([A-Za-z0-9+/]{43}=) 27 | 28 | examples: 29 | - | 30 | [Peer] 31 | PublicKey = [Server's public key] 32 | PresharedKey = uRsfsZ2Ts1rach4Zv3hhwcx6wa5fuIo2u3w7sa+7j81= 33 | AllowedIPs = 0.0.0.0/0, ::/0 34 | Endpoint = [Server Addr:Server Port] 35 | 36 | references: 37 | - https://www.wireguard.com/quickstart/ 38 | - https://manpages.debian.org/testing/wireguard-tools/wg.8.en.html 39 | - https://gist.github.com/lanceliao/5d2977f417f34dda0e3d63ac7e217fd6 40 | -------------------------------------------------------------------------------- /tests/search_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/GRbit/go-pcre" 7 | "github.com/tillson/git-hound/internal/app" 8 | ) 9 | 10 | func TestMatchKeywords(t *testing.T) { 11 | matches := app.MatchKeywords("odt_KTJlDq2AGGGlqG4riKdT7p980AW8RlU5") 12 | if len(matches) < 1 { 13 | t.Errorf("Keyword was not found in string.") 14 | } 15 | } 16 | 17 | func TestPCRERegex(t *testing.T) { 18 | regex := `odt_[A-Za-z0-9]{32}` 19 | str := "odt_KTJlDq2AGGGlqG4riKdT7p980AW8RlU5odt_KTJlDq2AGGGlqG4riKdT7p980AW8RlU5odt_KTJlDq2AGGGlqG4riKdT7p980AW8RlU5" 20 | 21 | re, err := pcre.Compile(regex, 0) 22 | if err != nil { 23 | t.Fatalf("Failed to compile regex: %s", err) 24 | } 25 | 26 | matched := re.FindAllIndex([]byte(str), 0) 27 | for _, match := range matched { 28 | t.Logf("Matched: %s", str[match[0]:match[1]]) 29 | } 30 | if len(matched) != 3 { 31 | t.Errorf("Regex did not match string.") 32 | } 33 | } 34 | 35 | func TestBase64EncodedKeyword(t *testing.T) { 36 | matches := app.MatchKeywords("This is a test. This is a test") 37 | if len(matches) < 1 { 38 | t.Errorf("Keyword was not found in base64 encoded string") 39 | } 40 | } 41 | --------------------------------------------------------------------------------