├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config.yaml ├── core ├── banner.go ├── config.go ├── git.go ├── github.go ├── log.go ├── match.go ├── options.go ├── session.go ├── signatures.go ├── spinner.go └── util.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── images ├── logo.png ├── shhgit-example.png ├── shhgit-live-example.png ├── shhgit.gif └── shhgit.png ├── main.go └── www ├── Dockerfile ├── nginx.conf └── public ├── index.html ├── logo.png ├── script.js ├── signatures.json └── style.css /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | LICENSE 3 | README.md 4 | config.yaml 5 | images/ 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: eth0izzle # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: GO111MODULE=on CGO_ENABLED=0 go build -v -i -o . 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | releases-matrix: 7 | name: Release Go Binary 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | goos: [linux, windows, darwin] 12 | goarch: [amd64] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: wangyoucao577/go-release-action@master 16 | with: 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | goos: ${{ matrix.goos }} 19 | goarch: ${{ matrix.goarch }} 20 | goversion: "https://dl.google.com/go/go1.15.2.linux-amd64.tar.gz" 21 | project_path: "." 22 | binary_name: "shhgit" 23 | extra_files: LICENSE README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | workspace.code-workspace 9 | build.sh 10 | vendor/ 11 | build/ 12 | .vscode/ 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | WORKDIR /go/src 3 | COPY . . 4 | 5 | RUN export CGO_ENABLED=0 && go install && go build -o / 6 | 7 | FROM golang:alpine AS runtime 8 | WORKDIR /app 9 | 10 | RUN apk update && apk add --no-cache git 11 | 12 | COPY --from=builder /shhgit /app 13 | 14 | ENTRYPOINT [ "/app/shhgit" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paul Price (@darkp0rt) 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 |

2 | 3 | 4 | # 🚨 shhgit is no longer maintained. If you need support or consultation for your secret scanning endeavours, drop me an e-mail paul@darkport.co.uk 🚨 5 | 6 | ## **shhgit helps secure forward-thinking development, operations, and security teams by finding secrets across their code before it leads to a security breach.** 7 | 8 | ![Go](https://github.com/eth0izzle/shhgit/workflows/Go/badge.svg) ![](https://img.shields.io/docker/cloud/build/eth0izzle/shhgit.svg) 9 | 10 | 11 |

12 | 13 | Accidentally leaking secrets — usernames and passwords, API tokens, or private keys — in a public code repository is a developers and security teams worst nightmare. Fraudsters constantly scan public code repositories for these secrets to gain a foothold in to systems. Code is more connected than ever so often these secrets provide access to private and sensitive data — cloud infrastructures, database servers, payment gateways, and file storage systems to name a few. 14 | 15 | shhgit can constantly scan your code repositories to find and alert you of these secrets. 16 | 17 | ## Installation 18 | 19 | You have two options. I'd recommend the first as it will give you access to the shhgit live web interface. Use the second option if you just want the command line interface. 20 | 21 | ### via Docker 22 | 23 | 1. Clone this repository: `git clone https://github.com/eth0izzle/shhgit.git` 24 | 2. Build via Docker compose: `docker-compose build` 25 | 3. Edit your `config.yaml` file (i.e. adding your GitHub tokens) 26 | 4. Bring up the stack: `docker-compose up` 27 | 5. Open up http://localhost:8080/ 28 | 29 | ### via Go get 30 | 31 | _Note_: this method does not include the shhgit web interface 32 | 33 | 1. Install [Go](https://golang.org/doc/install) for your platform. 34 | 2. `go get github.com/eth0izzle/shhgit` will download and build shhgit automatically. Or you can clone this repository and run `go build -v -i`. 35 | 3. Edit your `config.yaml` file and see usage below. 36 | 37 | ## Usage 38 | 39 | shhgit can work in two ways: consuming the public APIs of GitHub, Gist, GitLab and BitBucket or by processing files in a local directory. 40 | 41 | By default, shhgit will run in the former 'public mode'. For GitHub and Gist, you will need to obtain and provide an access token (see [this guide](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line); it doesn't require any scopes or permissions. And then place it under `github_access_tokens` in `config.yaml`). GitLab and BitBucket do not require any API tokens. 42 | 43 | You can also forgo the signatures and use shhgit with your own custom search query, e.g. to find all AWS keys you could use `shhgit --search-query AWS_ACCESS_KEY_ID=AKIA`. And to run in local mode (and perhaps integrate in to your CI pipelines) you can pass the `--local` flag (see usage below). 44 | 45 | ### Options 46 | 47 | ``` 48 | --clone-repository-timeout 49 | Maximum time it should take to clone a repository in seconds (default 10) 50 | --config-path 51 | Searches for config.yaml from given directory. If not set, tries to find if from shhgit binary's and current directory 52 | --csv-path 53 | Specify a path if you want to write found secrets to a CSV. Leave blank to disable 54 | --debug 55 | Print debugging information 56 | --entropy-threshold 57 | Finds high entropy strings in files. Higher threshold = more secret secrets, lower threshold = more false positives. Set to 0 to disable entropy checks (default 5.0) 58 | --local 59 | Specify local directory (absolute path) which to scan. Scans only given directory recursively. No need to have Github tokens with local run. 60 | --maximum-file-size 61 | Maximum file size to process in KB (default 512) 62 | --maximum-repository-size 63 | Maximum repository size to download and process in KB) (default 5120) 64 | --minimum-stars 65 | Only clone repositories with this many stars or higher. Set to 0 to ignore star count (default 0) 66 | --path-checks 67 | Set to false to disable file name/path signature checking, i.e. just match regex patterns (default true) 68 | --process-gists 69 | Watch and process Gists in real time. Set to false to disable (default true) 70 | --search-query 71 | Specify a search string to ignore signatures and filter on files containing this string (regex compatible) 72 | --silent 73 | Suppress all output except for errors 74 | --temp-directory 75 | Directory to store repositories/matches (default "%temp%\shhgit") 76 | --threads 77 | Number of concurrent threads to use (default number of logical CPUs) 78 | ``` 79 | 80 | ### Config 81 | 82 | The `config.yaml` file has 7 elements. A [default is provided](https://github.com/eth0izzle/shhgit/blob/master/config.yaml). 83 | 84 | ``` 85 | github_access_tokens: # provide at least one token 86 | - 'token one' 87 | - 'token two' 88 | webhook: '' # URL to a POST webhook. 89 | webhook_payload: '' # Payload to POST to the webhook URL 90 | blacklisted_strings: [] # list of strings to ignore 91 | blacklisted_extensions: [] # list of extensions to ignore 92 | blacklisted_paths: [] # list of paths to ignore 93 | blacklisted_entropy_extensions: [] # additional extensions to ignore for entropy checks 94 | signatures: # list of signatures to check 95 | - part: '' # either filename, extension, path or contents 96 | match: '' # simple text comparison (if no regex element) 97 | regex: '' # regex pattern (if no match element) 98 | name: '' # name of the signature 99 | ``` 100 | 101 | #### Signatures 102 | 103 | shhgit comes with 150 signatures. You can remove or add more by editing the `config.yaml` file. 104 | 105 | ``` 106 | 1Password password manager database file, Amazon MWS Auth Token, Apache htpasswd file, Apple Keychain database file, Artifactory, AWS Access Key ID, AWS Access Key ID Value, AWS Account ID, AWS CLI credentials file, AWS cred file info, AWS Secret Access Key, AWS Session Token, Azure service configuration schema file, Carrierwave configuration file, Chef Knife configuration file, Chef private key, CodeClimate, Configuration file for auto-login process, Contains a private key, Contains a private key, cPanel backup ProFTPd credentials file, Day One journal file, DBeaver SQL database manager configuration file, DigitalOcean doctl command-line client configuration file, Django configuration file, Docker configuration file, Docker registry authentication file, Environment configuration file, esmtp configuration, Facebook access token, Facebook Client ID, Facebook Secret Key, FileZilla FTP configuration file, FileZilla FTP recent servers file, Firefox saved passwords DB, git-credential-store helper credentials file, Git configuration file, GitHub Hub command-line client configuration file, Github Key, GNOME Keyring database file, GnuCash database file, Google (GCM) Service account, Google Cloud API Key, Google OAuth Access Token, Google OAuth Key, Heroku API key, Heroku config file, Hexchat/XChat IRC client server list configuration file, High entropy string, HockeyApp, Irssi IRC client configuration file, Java keystore file, Jenkins publish over SSH plugin file, Jetbrains IDE Config, KDE Wallet Manager database file, KeePass password manager database file, Linkedin Client ID, LinkedIn Secret Key, Little Snitch firewall configuration file, Log file, MailChimp API Key, MailGun API Key, Microsoft BitLocker recovery key file, Microsoft BitLocker Trusted Platform Module password file, Microsoft SQL database file, Microsoft SQL server compact database file, Mongoid config file, Mutt e-mail client configuration file, MySQL client command history file, MySQL dump w/ bcrypt hashes, netrc with SMTP credentials, Network traffic capture file, NPM configuration file, NuGet API Key, OmniAuth configuration file, OpenVPN client configuration file, Outlook team, Password Safe database file, PayPal/Braintree Access Token, PHP configuration file, Picatic API key, Pidgin chat client account configuration file, Pidgin OTR private key, PostgreSQL client command history file, PostgreSQL password file, Potential cryptographic private key, Potential Jenkins credentials file, Potential jrnl journal file, Potential Linux passwd file, Potential Linux shadow file, Potential MediaWiki configuration file, Potential private key (.asc), Potential private key (.p21), Potential private key (.pem), Potential private key (.pfx), Potential private key (.pkcs12), Potential PuTTYgen private key, Potential Ruby On Rails database configuration file, Private SSH key (.dsa), Private SSH key (.ecdsa), Private SSH key (.ed25519), Private SSH key (.rsa), Public ssh key, Python bytecode file, Recon-ng web reconnaissance framework API key database, remote-sync for Atom, Remote Desktop connection file, Robomongo MongoDB manager configuration file, Rubygems credentials file, Ruby IRB console history file, Ruby on Rails master key, Ruby on Rails secrets, Ruby On Rails secret token configuration file, S3cmd configuration file, Salesforce credentials, Sauce Token, Sequel Pro MySQL database manager bookmark file, sftp-deployment for Atom, sftp-deployment for Atom, SFTP connection configuration file, Shell command alias configuration file, Shell command history file, Shell configuration file (.bashrc, .zshrc, .cshrc), Shell configuration file (.exports), Shell configuration file (.extra), Shell configuration file (.functions), Shell profile configuration file, Slack Token, Slack Webhook, SonarQube Docs API Key, SQL Data dump file, SQL dump file, SQLite3 database file, SQLite database file, Square Access Token, Square OAuth Secret, SSH configuration file, SSH Password, Stripe API key, T command-line Twitter client configuration file, Terraform variable config file, Tugboat DigitalOcean management tool configuration, Tunnelblick VPN configuration file, Twilo API Key, Twitter Client ID, Twitter Secret Key, Username and password in URI, Ventrilo server configuration file, vscode-sftp for VSCode, Windows BitLocker full volume encrypted data file, WP-Config 107 | ``` 108 | 109 | ## Contributing 110 | 111 | 1. Fork it, baby! 112 | 2. Create your feature branch: `git checkout -b my-new-feature` 113 | 3. Commit your changes: `git commit -am 'Add some feature'` 114 | 4. Push to the branch: `git push origin my-new-feature` 115 | 5. Submit a pull request. 116 | 117 | ## Disclaimer 118 | 119 | I take no responsibility for how you use this tool. Don't be a dick. 120 | 121 | ## License 122 | 123 | MIT. See [LICENSE](https://github.com/eth0izzle/shhgit/blob/master/LICENSE) 124 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | github_access_tokens: 2 | - '' 3 | webhook: '' # URL to which the payload is POSTed 4 | 5 | # This default payload will work for Slack and MatterMost. 6 | # Consult your webhook API for additional configurations. 7 | webhook_payload: | 8 | { 9 | "text": "%s" 10 | } 11 | 12 | blacklisted_strings: ["AKIAIOSFODNN7EXAMPLE", "username:password", "sshpass -p $SSH_PASS"] # skip matches containing any of these strings (case insensitive 13 | blacklisted_extensions: [".exe", ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".psd", ".xcf", ".zip", ".tar.gz", ".ttf", ".lock"] 14 | blacklisted_paths: ["node_modules{sep}", "vendor{sep}bundle", "vendor{sep}cache"] # use {sep} for the OS' path seperator (i.e. / or \) 15 | blacklisted_entropy_extensions: [".pem", "id_rsa", ".asc", ".ovpn", ".sqlite", ".sqlite3", ".log"] # additional extensions to skip entropy checks 16 | signatures: 17 | - part: 'extension' 18 | match: '.pem' 19 | name: 'Potential cryptographic private key' 20 | - part: 'extension' 21 | match: '.log' 22 | name: 'Log file' 23 | - part: 'extension' 24 | match: '.pkcs12' 25 | name: 'Potential cryptographic key bundle' 26 | - part: 'extension' 27 | match: '.p12' 28 | name: 'Potential cryptographic key bundle' 29 | - part: 'extension' 30 | match: '.pfx' 31 | name: 'Potential cryptographic key bundle' 32 | - part: 'extension' 33 | match: '.asc' 34 | name: 'Potential cryptographic key bundle' 35 | - part: 'filename' 36 | match: 'otr.private_key' 37 | name: 'Pidgin OTR private key' 38 | - part: 'extension' 39 | match: '.ovpn' 40 | name: 'OpenVPN client configuration file' 41 | - part: 'extension' 42 | match: '.cscfg' 43 | name: 'Azure service configuration schema file' 44 | - part: 'extension' 45 | match: '.rdp' 46 | name: 'Remote Desktop connection file' 47 | - part: 'extension' 48 | match: '.mdf' 49 | name: 'Microsoft SQL database file' 50 | - part: 'extension' 51 | match: '.sdf' 52 | name: 'Microsoft SQL server compact database file' 53 | - part: 'extension' 54 | match: '.sqlite' 55 | name: 'SQLite database file' 56 | - part: 'extension' 57 | match: '.sqlite3' 58 | name: 'SQLite3 database file' 59 | - part: 'extension' 60 | match: '.bek' 61 | name: 'Microsoft BitLocker recovery key file' 62 | - part: 'extension' 63 | match: '.tpm' 64 | name: 'Microsoft BitLocker Trusted Platform Module password file' 65 | - part: 'extension' 66 | match: '.fve' 67 | name: 'Windows BitLocker full volume encrypted data file' 68 | - part: 'extension' 69 | match: '.jks' 70 | name: 'Java keystore file' 71 | - part: 'extension' 72 | match: '.psafe3' 73 | name: 'Password Safe database file' 74 | - part: 'filename' 75 | match: 'secret_token.rb' 76 | name: 'Ruby On Rails secret token configuration file' 77 | - part: 'filename' 78 | match: 'carrierwave.rb' 79 | name: 'Carrierwave configuration file' 80 | - part: 'filename' 81 | match: 'database.yml' 82 | name: 'Potential Ruby On Rails database configuration file' 83 | - part: 'filename' 84 | match: 'omniauth.rb' 85 | name: 'OmniAuth configuration file' 86 | - part: 'filename' 87 | match: 'settings.py' 88 | name: 'Django configuration file' 89 | - part: 'extension' 90 | match: '.agilekeychain' 91 | name: '1Password password manager database file' 92 | - part: 'extension' 93 | match: '.keychain' 94 | name: 'Apple Keychain database file' 95 | - part: 'extension' 96 | match: '.pcap' 97 | name: 'Network traffic capture file' 98 | - part: 'extension' 99 | match: '.gnucash' 100 | name: 'GnuCash database file' 101 | - part: 'filename' 102 | match: 'jenkins.plugins.publish_over_ssh.BapSshPublisherPlugin.xml' 103 | name: 'Jenkins publish over SSH plugin file' 104 | - part: 'filename' 105 | match: 'credentials.xml' 106 | name: 'Potential Jenkins credentials file' 107 | - part: 'extension' 108 | match: '.kwallet' 109 | name: 'KDE Wallet Manager database file' 110 | - part: 'filename' 111 | match: 'LocalSettings.php' 112 | name: 'Potential MediaWiki configuration file' 113 | - part: 'extension' 114 | match: '.tblk' 115 | name: 'Tunnelblick VPN configuration file' 116 | - part: 'filename' 117 | match: 'Favorites.plist' 118 | name: 'Sequel Pro MySQL database manager bookmark file' 119 | - part: 'filename' 120 | match: 'configuration.user.xpl' 121 | name: 'Little Snitch firewall configuration file' 122 | - part: 'extension' 123 | match: '.dayone' 124 | name: 'Day One journal file' 125 | - part: 'filename' 126 | match: 'journal.txt' 127 | name: 'Potential jrnl journal file' 128 | - part: 'filename' 129 | match: 'knife.rb' 130 | name: 'Chef Knife configuration file' 131 | - part: 'filename' 132 | match: 'proftpdpasswd' 133 | name: 'cPanel backup ProFTPd credentials file' 134 | - part: 'filename' 135 | match: 'robomongo.json' 136 | name: 'Robomongo MongoDB manager configuration file' 137 | - part: 'filename' 138 | match: 'filezilla.xml' 139 | name: 'FileZilla FTP configuration file' 140 | - part: 'filename' 141 | match: 'recentservers.xml' 142 | name: 'FileZilla FTP recent servers file' 143 | - part: 'filename' 144 | match: 'ventrilo_srv.ini' 145 | name: 'Ventrilo server configuration file' 146 | - part: 'filename' 147 | match: 'terraform.tfvars' 148 | name: 'Terraform variable config file' 149 | - part: 'filename' 150 | match: '.exports' 151 | name: 'Shell configuration file' 152 | - part: 'filename' 153 | match: '.functions' 154 | name: 'Shell configuration file' 155 | - part: 'filename' 156 | match: '.extra' 157 | name: 'Shell configuration file' 158 | - part: 'filename' 159 | regex: '^.*_rsa$' 160 | name: 'Private SSH key' 161 | - part: 'filename' 162 | regex: '^.*_dsa$' 163 | name: 'Private SSH key' 164 | - part: 'filename' 165 | regex: '^.*_ed25519$' 166 | name: 'Private SSH key' 167 | - part: 'filename' 168 | regex: '^.*_ecdsa$' 169 | name: 'Private SSH key' 170 | - part: 'path' 171 | regex: '\.?ssh/config$' 172 | name: 'SSH configuration file' 173 | - part: 'extension' 174 | regex: '^key(pair)?$' 175 | name: 'Potential cryptographic private key' 176 | - part: 'filename' 177 | regex: '^\.?(bash_|zsh_|sh_|z)?history$' 178 | name: 'Shell command history file' 179 | - part: 'filename' 180 | regex: '^\.?mysql_history$' 181 | name: 'MySQL client command history file' 182 | - part: 'filename' 183 | regex: '^\.?psql_history$' 184 | name: 'PostgreSQL client command history file' 185 | - part: 'filename' 186 | regex: '^\.?pgpass$' 187 | name: 'PostgreSQL password file' 188 | - part: 'filename' 189 | regex: '^\.?irb_history$' 190 | name: 'Ruby IRB console history file' 191 | - part: 'path' 192 | regex: '\.?purple/accounts\.xml$' 193 | name: 'Pidgin chat client account configuration file' 194 | - part: 'path' 195 | regex: '\.?xchat2?/servlist_?\.conf$' 196 | name: 'Hexchat/XChat IRC client server list configuration file' 197 | - part: 'path' 198 | regex: '\.?irssi/config$' 199 | name: 'Irssi IRC client configuration file' 200 | - part: 'path' 201 | regex: '\.?recon-ng/keys\.db$' 202 | name: 'Recon-ng web reconnaissance framework API key database' 203 | - part: 'filename' 204 | regex: '^\.?dbeaver-data-sources.xml$' 205 | name: 'DBeaver SQL database manager configuration file' 206 | - part: 'filename' 207 | regex: '^\.?muttrc$' 208 | name: 'Mutt e-mail client configuration file' 209 | - part: 'filename' 210 | regex: '^\.?s3cfg$' 211 | name: 'S3cmd configuration file' 212 | - part: 'path' 213 | regex: '\.?aws/credentials$' 214 | name: 'AWS CLI credentials file' 215 | - part: 'filename' 216 | regex: '^sftp-config(\.json)?$' 217 | name: 'SFTP connection configuration file' 218 | - part: 'filename' 219 | regex: '^\.?trc$' 220 | name: 'T command-line Twitter client configuration file' 221 | - part: 'filename' 222 | regex: '^\.?(bash|zsh|csh)rc$' 223 | name: 'Shell configuration file' 224 | - part: 'filename' 225 | regex: '^\.?(bash_|zsh_)?profile$' 226 | name: 'Shell profile configuration file' 227 | - part: 'filename' 228 | regex: '^\.?(bash_|zsh_)?aliases$' 229 | name: 'Shell command alias configuration file' 230 | - part: 'filename' 231 | regex: 'config(\.inc)?\.php$' 232 | name: 'PHP configuration file' 233 | - part: 'extension' 234 | regex: '^key(store|ring)$' 235 | name: 'GNOME Keyring database file' 236 | - part: 'extension' 237 | regex: '^kdbx?$' 238 | name: 'KeePass password manager database file' 239 | - part: 'extension' 240 | regex: '^sql(dump)?$' 241 | name: 'SQL dump file' 242 | - part: 'filename' 243 | regex: '^\.?htpasswd$' 244 | name: 'Apache htpasswd file' 245 | - part: 'filename' 246 | regex: '^(\.|_)?netrc$' 247 | name: 'Configuration file for auto-login process' 248 | - part: 'path' 249 | regex: '\.?gem/credentials$' 250 | name: 'Rubygems credentials file' 251 | - part: 'filename' 252 | regex: '^\.?tugboat$' 253 | name: 'Tugboat DigitalOcean management tool configuration' 254 | - part: 'path' 255 | regex: 'doctl/config.yaml$' 256 | name: 'DigitalOcean doctl command-line client configuration file' 257 | - part: 'filename' 258 | regex: '^\.?git-credentials$' 259 | name: 'git-credential-store helper credentials file' 260 | - part: 'path' 261 | regex: 'config/hub$' 262 | name: 'GitHub Hub command-line client configuration file' 263 | - part: 'filename' 264 | regex: '^\.?gitconfig$' 265 | name: 'Git configuration file' 266 | - part: 'path' 267 | regex: '\.?chef/(.*)\.pem$' 268 | name: 'Chef private key' 269 | - part: 'path' 270 | regex: 'etc/shadow$' 271 | name: 'Potential Linux shadow file' 272 | - part: 'path' 273 | regex: 'etc/passwd$' 274 | name: 'Potential Linux passwd file' 275 | comment: 'Contains system user information' 276 | - part: 'filename' 277 | regex: '^\.?dockercfg$' 278 | name: 'Docker configuration file' 279 | - part: 'filename' 280 | regex: '^\.?npmrc$' 281 | name: 'NPM configuration file' 282 | - part: 'filename' 283 | regex: '^\.?env$' 284 | name: 'Environment configuration file' 285 | - part: 'contents' 286 | regex: '(A3T[A-Z0-9]|AKIA|AGPA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}' 287 | name: 'AWS Access Key ID Value' 288 | - part: 'contents' 289 | regex: "((\\\"|'|`)?((?i)aws)?_?((?i)access)_?((?i)key)?_?((?i)id)?(\\\"|'|`)?\\\\s{0,50}(:|=>|=)\\\\s{0,50}(\\\"|'|`)?(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}(\\\"|'|`)?)" 290 | name: 'AWS Access Key ID' 291 | - part: 'contents' 292 | regex: "((\\\"|'|`)?((?i)aws)?_?((?i)account)_?((?i)id)?(\\\"|'|`)?\\\\s{0,50}(:|=>|=)\\\\s{0,50}(\\\"|'|`)?[0-9]{4}-?[0-9]{4}-?[0-9]{4}(\\\"|'|`)?)" 293 | name: 'AWS Account ID' 294 | - part: 'contents' 295 | regex: "((\\\"|'|`)?((?i)aws)?_?((?i)secret)_?((?i)access)?_?((?i)key)?_?((?i)id)?(\\\"|'|`)?\\\\s{0,50}(:|=>|=)\\\\s{0,50}(\\\"|'|`)?[A-Za-z0-9/+=]{40}(\\\"|'|`)?)" 296 | name: 'AWS Secret Access Key' 297 | - part: 'contents' 298 | regex: "((\\\"|'|`)?((?i)aws)?_?((?i)session)?_?((?i)token)?(\\\"|'|`)?\\\\s{0,50}(:|=>|=)\\\\s{0,50}(\\\"|'|`)?[A-Za-z0-9/+=]{16,}(\\\"|'|`)?)" 299 | name: 'AWS Session Token' 300 | - part: 'contents' 301 | regex: "(?i)artifactory.{0,50}(\\\"|'|`)?[a-zA-Z0-9=]{112}(\\\"|'|`)?" 302 | name: 'Artifactory' 303 | - part: 'contents' 304 | regex: "(?i)codeclima.{0,50}(\\\"|'|`)?[0-9a-f]{64}(\\\"|'|`)?" 305 | name: 'CodeClimate' 306 | - part: 'contents' 307 | regex: 'EAACEdEose0cBA[0-9A-Za-z]+' 308 | name: 'Facebook access token' 309 | - part: 'contents' 310 | regex: "((\\\"|'|`)?type(\\\"|'|`)?\\\\s{0,50}(:|=>|=)\\\\s{0,50}(\\\"|'|`)?service_account(\\\"|'|`)?,?)" 311 | name: 'Google (GCM) Service account' 312 | - part: 'contents' 313 | regex: '(?:r|s)k_[live|test]_[0-9a-zA-Z]{24}' 314 | name: 'Stripe API key' 315 | - part: 'contents' 316 | regex: '[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' 317 | name: 'Google OAuth Key' 318 | - part: 'contents' 319 | regex: 'AIza[0-9A-Za-z\\-_]{35}' 320 | name: 'Google Cloud API Key' 321 | - part: 'contents' 322 | regex: 'ya29\\.[0-9A-Za-z\\-_]+' 323 | name: 'Google OAuth Access Token' 324 | - part: 'contents' 325 | regex: 'sk_[live|test]_[0-9a-z]{32}' 326 | name: 'Picatic API key' 327 | - part: 'contents' 328 | regex: 'sq0atp-[0-9A-Za-z\-_]{22}' 329 | name: 'Square Access Token' 330 | - part: 'contents' 331 | regex: 'sq0csp-[0-9A-Za-z\-_]{43}' 332 | name: 'Square OAuth Secret' 333 | - part: 'contents' 334 | regex: 'access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}' 335 | name: 'PayPal/Braintree Access Token' 336 | - part: 'contents' 337 | regex: 'amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' 338 | name: 'Amazon MWS Auth Token' 339 | - part: 'contents' 340 | regex: 'SK[0-9a-fA-F]{32}' 341 | name: 'Twilo API Key' 342 | - part: 'contents' 343 | regex: 'SG\.[0-9A-Za-z\-_]{22}\.[0-9A-Za-z\-_]{43}' 344 | name: 'SendGrid API Key' 345 | - part: 'contents' 346 | regex: 'key-[0-9a-zA-Z]{32}' 347 | name: 'MailGun API Key' 348 | - part: 'contents' 349 | regex: '[0-9a-f]{32}-us[0-9]{12}' 350 | name: 'MailChimp API Key' 351 | - part: 'contents' 352 | regex: "sshpass -p.*['|\\\"]" 353 | name: 'SSH Password' 354 | - part: 'contents' 355 | regex: '(https\\://outlook\\.office.com/webhook/[0-9a-f-]{36}\\@)' 356 | name: 'Outlook team' 357 | - part: 'contents' 358 | regex: "(?i)sauce.{0,50}(\\\"|'|`)?[0-9a-f-]{36}(\\\"|'|`)?" 359 | name: 'Sauce Token' 360 | - part: 'contents' 361 | regex: '(xox[pboa]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})' 362 | name: 'Slack Token' 363 | - part: 'contents' 364 | regex: 'https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}' 365 | name: 'Slack Webhook' 366 | - part: 'contents' 367 | regex: "(?i)sonar.{0,50}(\\\"|'|`)?[0-9a-f]{40}(\\\"|'|`)?" 368 | name: 'SonarQube Docs API Key' 369 | - part: 'contents' 370 | regex: "(?i)hockey.{0,50}(\\\"|'|`)?[0-9a-f]{32}(\\\"|'|`)?" 371 | name: 'HockeyApp' 372 | - part: 'contents' 373 | regex: '([\w+]{1,24})(://)([^$<]{1})([^\s";]{1,}):([^$<]{1})([^\s";/]{1,})@[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,24}([^\s]+)' 374 | name: 'Username and password in URI' 375 | - part: 'contents' 376 | regex: 'oy2[a-z0-9]{43}' 377 | name: 'NuGet API Key' 378 | - part: 'contents' 379 | regex: 'hawk\.[0-9A-Za-z\-_]{20}\.[0-9A-Za-z\-_]{20}' 380 | name: 'StackHawk API Key' 381 | - part: 'extension' 382 | match: '.ppk' 383 | name: 'Potential PuTTYgen private key' 384 | - part: 'filename' 385 | match: 'heroku.json' 386 | name: 'Heroku config file' 387 | - part: 'extension' 388 | match: '.sqldump' 389 | name: 'SQL Data dump file' 390 | - part: 'filename' 391 | match: 'dump.sql' 392 | name: 'MySQL dump w/ bcrypt hashes' 393 | - part: 'filename' 394 | match: 'id_rsa_pub' 395 | name: 'Public ssh key' 396 | - part: 'filename' 397 | match: 'mongoid.yml' 398 | name: 'Mongoid config file' 399 | - part: 'filename' 400 | match: 'salesforce.js' 401 | name: 'Salesforce credentials in a nodejs project' 402 | - part: 'extension' 403 | match: '.netrc' 404 | name: 'netrc with SMTP credentials' 405 | - part: 'filename' 406 | regex: '.remote-sync.json$' 407 | name: 'Created by remote-sync for Atom, contains FTP and/or SCP/SFTP/SSH server details and credentials' 408 | - part: 'filename' 409 | regex: '.esmtprc$' 410 | name: 'esmtp configuration' 411 | - part: 'filename' 412 | regex: '^deployment-config.json?$' 413 | name: 'Created by sftp-deployment for Atom, contains server details and credentials' 414 | - part: 'filename' 415 | regex: '.ftpconfig$' 416 | name: 'Created by sftp-deployment for Atom, contains server details and credentials' 417 | - part: 'contents' 418 | regex: '-----BEGIN (EC|RSA|DSA|OPENSSH|PGP) PRIVATE KEY' 419 | name: 'Contains a private key' 420 | - part: 'contents' 421 | regex: 'define(.{0,20})?(DB_CHARSET|NONCE_SALT|LOGGED_IN_SALT|AUTH_SALT|NONCE_KEY|DB_HOST|DB_PASSWORD|AUTH_KEY|SECURE_AUTH_KEY|LOGGED_IN_KEY|DB_NAME|DB_USER)(.{0,20})?[''|"].{10,120}[''|"]' 422 | name: 'WP-Config' 423 | - part: 'contents' 424 | regex: '(?i)(aws_access_key_id|aws_secret_access_key)(.{0,20})?=.[0-9a-zA-Z\/+]{20,40}' 425 | name: 'AWS cred file info' 426 | - part: 'contents' 427 | regex: '(?i)(facebook|fb)(.{0,20})?(?-i)[''\"][0-9a-f]{32}[''\"]' 428 | name: 'Facebook Secret Key' 429 | - part: 'contents' 430 | regex: '(?i)(facebook|fb)(.{0,20})?[''\"][0-9]{13,17}[''\"]' 431 | name: 'Facebook Client ID' 432 | - part: 'contents' 433 | regex: '(?i)twitter(.{0,20})?[''\"][0-9a-z]{35,44}[''\"]' 434 | name: 'Twitter Secret Key' 435 | - part: 'contents' 436 | regex: '(?i)twitter(.{0,20})?[''\"][0-9a-z]{18,25}[''\"]' 437 | name: 'Twitter Client ID' 438 | - part: 'contents' 439 | regex: '(?i)github(.{0,20})?(?-i)[''\"][0-9a-zA-Z]{35,40}[''\"]' 440 | name: 'Github Key' 441 | - part: 'contents' 442 | regex: '(?i)heroku(.{0,20})?[''"][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[''"]' 443 | name: 'Heroku API key' 444 | - part: 'contents' 445 | regex: '(?i)linkedin(.{0,20})?(?-i)[''\"][0-9a-z]{12}[''\"]' 446 | name: 'Linkedin Client ID' 447 | - part: 'contents' 448 | regex: '(?i)linkedin(.{0,20})?[''\"][0-9a-z]{16}[''\"]' 449 | name: 'LinkedIn Secret Key' 450 | - part: 'path' 451 | regex: '\.?idea[\\\/]WebServers.xml$' 452 | name: 'Created by Jetbrains IDEs, contains webserver credentials with encoded passwords (not encrypted!)' 453 | - part: 'path' 454 | regex: '\.?vscode[\\\/]sftp.json$' 455 | name: 'Created by vscode-sftp for VSCode, contains SFTP/SSH server details and credentials' 456 | - part: 'path' 457 | regex: 'web[\\\/]ruby[\\\/]secrets.yml' 458 | name: 'Ruby on rails secrets.yml file (contains passwords)' 459 | - part: 'path' 460 | regex: '\.?docker[\\\/]config.json$' 461 | name: 'Docker registry authentication file' 462 | - part: 'path' 463 | regex: 'ruby[\\\/]config[\\\/]master.key$' 464 | name: 'Rails master key (used for decrypting credentials.yml.enc for Rails 5.2+)' 465 | - part: 'path' 466 | regex: '\.?mozilla[\\\/]firefox[\\\/]logins.json$' 467 | name: 'Firefox saved password collection (can be decrypted using keys4.db)' 468 | -------------------------------------------------------------------------------- /core/banner.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | const ( 4 | Name = "shhgit" 5 | Version = "0.4" 6 | Author = "Paul Price (@darkp0rt) - www.darkport.co.uk" 7 | ) 8 | 9 | const Banner = ` 10 | _ _ _ _ 11 | | | | | (_) | 12 | ___| |__ | |__ __ _ _| |_ 13 | / __| '_ \| '_ \ / _` + "`" + ` | | __| 14 | \__ \ | | | | | | (_| | | |_ 15 | |___/_| |_|_| |_|\__, |_|\__| 16 | __/ | 17 | v` + Version + ` |___/` 18 | -------------------------------------------------------------------------------- /core/config.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strings" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | type Config struct { 15 | GitHubAccessTokens []string `yaml:"github_access_tokens"` 16 | Webhook string `yaml:"webhook,omitempty"` 17 | WebhookPayload string `yaml:"webhook_payload,omitempty"` 18 | BlacklistedStrings []string `yaml:"blacklisted_strings"` 19 | BlacklistedExtensions []string `yaml:"blacklisted_extensions"` 20 | BlacklistedPaths []string `yaml:"blacklisted_paths"` 21 | BlacklistedEntropyExtensions []string `yaml:"blacklisted_entropy_extensions"` 22 | Signatures []ConfigSignature `yaml:"signatures"` 23 | } 24 | 25 | type ConfigSignature struct { 26 | Name string `yaml:"name"` 27 | Part string `yaml:"part"` 28 | Match string `yaml:"match,omitempty"` 29 | Regex string `yaml:"regex,omitempty"` 30 | Verifier string `yaml:"verifier,omitempty"` 31 | } 32 | 33 | func ParseConfig(options *Options) (*Config, error) { 34 | config := &Config{} 35 | var ( 36 | data []byte 37 | err error 38 | ) 39 | 40 | if len(*options.ConfigPath) > 0 { 41 | data, err = ioutil.ReadFile(path.Join(*options.ConfigPath, "config.yaml")) 42 | if err != nil { 43 | return config, err 44 | } 45 | } else { 46 | // Trying to first find the configuration next to executable 47 | // Helps e.g. with Drone where workdir is different than shhgit dir 48 | ex, err := os.Executable() 49 | dir := filepath.Dir(ex) 50 | data, err = ioutil.ReadFile(path.Join(dir, "config.yaml")) 51 | if err != nil { 52 | dir, _ = os.Getwd() 53 | data, err = ioutil.ReadFile(path.Join(dir, "config.yaml")) 54 | if err != nil { 55 | return config, err 56 | } 57 | } 58 | } 59 | 60 | err = yaml.Unmarshal(data, config) 61 | if err != nil { 62 | return config, err 63 | } 64 | 65 | if len(*options.Local) <= 0 && (len(config.GitHubAccessTokens) < 1 || strings.TrimSpace(strings.Join(config.GitHubAccessTokens, "")) == "") { 66 | return config, errors.New("You need to provide at least one GitHub Access Token. See https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line") 67 | } 68 | 69 | for i := 0; i < len(config.GitHubAccessTokens); i++ { 70 | config.GitHubAccessTokens[i] = os.ExpandEnv(config.GitHubAccessTokens[i]) 71 | } 72 | 73 | if len(config.Webhook) > 0 { 74 | config.Webhook = os.ExpandEnv(config.Webhook) 75 | } 76 | 77 | return config, nil 78 | } 79 | 80 | func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { 81 | *c = Config{} 82 | type plain Config 83 | 84 | err := unmarshal((*plain)(c)) 85 | 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /core/git.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "gopkg.in/src-d/go-git.v4" 9 | "gopkg.in/src-d/go-git.v4/plumbing" 10 | ) 11 | 12 | type GitResourceType int 13 | 14 | const ( 15 | LOCAL_SOURCE GitResourceType = iota 16 | GITHUB_SOURCE 17 | GITHUB_COMMENT 18 | GIST_SOURCE 19 | BITBUCKET_SOURCE 20 | GITLAB_SOURCE 21 | ) 22 | 23 | type GitResource struct { 24 | Id int64 25 | Type GitResourceType 26 | Url string 27 | Ref string 28 | } 29 | 30 | func CloneRepository(session *Session, url string, ref string, dir string) (*git.Repository, error) { 31 | timeout := time.Duration(*session.Options.CloneRepositoryTimeout) * time.Second 32 | localCtx, cancel := context.WithTimeout(context.Background(), timeout) 33 | defer cancel() 34 | 35 | session.Log.Debug("[%s] Cloning %s in to %s", url, ref, strings.Replace(dir, *session.Options.TempDirectory, "", -1)) 36 | opts := &git.CloneOptions{ 37 | Depth: 1, 38 | RecurseSubmodules: git.NoRecurseSubmodules, 39 | URL: url, 40 | SingleBranch: true, 41 | Tags: git.NoTags, 42 | } 43 | 44 | if ref != "" { 45 | opts.ReferenceName = plumbing.ReferenceName(ref) 46 | } 47 | 48 | repository, err := git.PlainCloneContext(localCtx, dir, false, opts) 49 | 50 | if err != nil { 51 | session.Log.Debug("[%s] Cloning failed: %s", url, err.Error()) 52 | return nil, err 53 | } 54 | 55 | return repository, nil 56 | } 57 | -------------------------------------------------------------------------------- /core/github.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/google/go-github/github" 10 | ) 11 | 12 | type GitHubClientWrapper struct { 13 | *github.Client 14 | Token string 15 | RateLimitedUntil time.Time 16 | } 17 | 18 | const ( 19 | perPage = 300 20 | sleep = 30 * time.Second 21 | ) 22 | 23 | func GetRepositories(session *Session) { 24 | localCtx, cancel := context.WithCancel(session.Context) 25 | defer cancel() 26 | 27 | observedKeys := map[string]bool{} 28 | var client *GitHubClientWrapper 29 | 30 | for c := time.Tick(sleep); ; { 31 | opt := &github.ListOptions{PerPage: perPage} 32 | 33 | for { 34 | if client != nil { 35 | session.FreeClient(client) 36 | } 37 | 38 | client = session.GetClient() 39 | events, resp, err := client.Activity.ListEvents(localCtx, opt) 40 | 41 | if err != nil { 42 | if _, ok := err.(*github.RateLimitError); ok { 43 | session.Log.Warn("Token %s[..] rate limited. Reset at %s", client.Token[:10], resp.Rate.Reset) 44 | client.RateLimitedUntil = resp.Rate.Reset.Time 45 | session.FreeClient(client) 46 | break 47 | } 48 | 49 | if _, ok := err.(*github.AbuseRateLimitError); ok { 50 | session.Log.Fatal("GitHub API abused detected. Quitting...") 51 | } 52 | 53 | session.Log.Warn("Error getting GitHub events: %s... trying again", err) 54 | } 55 | 56 | if opt.Page == 0 { 57 | tokenMessage := fmt.Sprintf("[?] Token %s[..] has %d/%d calls remaining.", client.Token[:10], resp.Rate.Remaining, resp.Rate.Limit) 58 | 59 | if resp.Rate.Remaining < 100 { 60 | session.Log.Warn(tokenMessage) 61 | } else { 62 | session.Log.Debug(tokenMessage) 63 | } 64 | } 65 | 66 | newEvents := make([]*github.Event, 0, len(events)) 67 | 68 | // remove duplicates 69 | for _, e := range events { 70 | if observedKeys[*e.ID] { 71 | continue 72 | } 73 | 74 | newEvents = append(newEvents, e) 75 | } 76 | 77 | for _, e := range newEvents { 78 | if *e.Type == "PushEvent" { 79 | observedKeys[*e.ID] = true 80 | 81 | dst := &github.PushEvent{} 82 | json.Unmarshal(e.GetRawPayload(), dst) 83 | session.Repositories <- GitResource{ 84 | Id: e.GetRepo().GetID(), 85 | Type: GITHUB_SOURCE, 86 | Url: e.GetRepo().GetURL(), 87 | Ref: dst.GetRef(), 88 | } 89 | } else if *e.Type == "IssueCommentEvent" { 90 | observedKeys[*e.ID] = true 91 | 92 | dst := &github.IssueCommentEvent{} 93 | json.Unmarshal(e.GetRawPayload(), dst) 94 | session.Comments <- *dst.Comment.Body 95 | } else if *e.Type == "IssuesEvent" { 96 | observedKeys[*e.ID] = true 97 | 98 | dst := &github.IssuesEvent{} 99 | json.Unmarshal(e.GetRawPayload(), dst) 100 | session.Comments <- dst.Issue.GetBody() 101 | } 102 | } 103 | 104 | if resp.NextPage == 0 { 105 | break 106 | } 107 | 108 | opt.Page = resp.NextPage 109 | time.Sleep(5 * time.Second) 110 | } 111 | 112 | select { 113 | case <-c: 114 | continue 115 | case <-localCtx.Done(): 116 | cancel() 117 | return 118 | } 119 | } 120 | } 121 | 122 | func GetGists(session *Session) { 123 | localCtx, cancel := context.WithCancel(session.Context) 124 | defer cancel() 125 | 126 | observedKeys := map[string]bool{} 127 | opt := &github.GistListOptions{} 128 | 129 | var client *GitHubClientWrapper 130 | for c := time.Tick(sleep); ; { 131 | if client != nil { 132 | session.FreeClient(client) 133 | } 134 | 135 | client = session.GetClient() 136 | gists, resp, err := client.Gists.ListAll(localCtx, opt) 137 | 138 | if err != nil { 139 | if _, ok := err.(*github.RateLimitError); ok { 140 | session.Log.Warn("Token %s[..] rate limited. Reset at %s", client.Token[:10], resp.Rate.Reset) 141 | client.RateLimitedUntil = resp.Rate.Reset.Time 142 | session.FreeClient(client) 143 | break 144 | } 145 | 146 | if _, ok := err.(*github.AbuseRateLimitError); ok { 147 | session.Log.Fatal("GitHub API abused detected. Quitting...") 148 | } 149 | 150 | session.Log.Warn("Error getting GitHub Gists: %s ... trying again", err) 151 | } 152 | 153 | newGists := make([]*github.Gist, 0, len(gists)) 154 | for _, e := range gists { 155 | if observedKeys[*e.ID] { 156 | continue 157 | } 158 | 159 | newGists = append(newGists, e) 160 | } 161 | 162 | for _, e := range newGists { 163 | observedKeys[*e.ID] = true 164 | session.Gists <- *e.GitPullURL 165 | } 166 | 167 | opt.Since = time.Now() 168 | 169 | select { 170 | case <-c: 171 | continue 172 | case <-localCtx.Done(): 173 | cancel() 174 | return 175 | } 176 | } 177 | } 178 | 179 | func GetRepository(session *Session, id int64) (*github.Repository, error) { 180 | client := session.GetClient() 181 | defer session.FreeClient(client) 182 | 183 | repo, resp, err := client.Repositories.GetByID(session.Context, id) 184 | 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | if resp.Rate.Remaining <= 1 { 190 | session.Log.Warn("Token %s[..] rate limited. Reset at %s", client.Token[:10], resp.Rate.Reset) 191 | client.RateLimitedUntil = resp.Rate.Reset.Time 192 | } 193 | 194 | return repo, nil 195 | } 196 | -------------------------------------------------------------------------------- /core/log.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/fatih/color" 12 | ) 13 | 14 | const ( 15 | FATAL = 5 16 | ERROR = 4 17 | IMPORTANT = 3 18 | WARN = 2 19 | INFO = 1 20 | DEBUG = 0 21 | ) 22 | 23 | var LogColors = map[int]*color.Color{ 24 | FATAL: color.New(color.FgRed).Add(color.Bold), 25 | ERROR: color.New(color.FgRed), 26 | WARN: color.New(color.FgYellow), 27 | IMPORTANT: color.New(), 28 | DEBUG: color.New(color.Faint), 29 | } 30 | 31 | type Logger struct { 32 | sync.Mutex 33 | 34 | debug bool 35 | silent bool 36 | } 37 | 38 | func (l *Logger) SetDebug(d bool) { 39 | l.debug = d 40 | } 41 | 42 | func (l *Logger) SetSilent(d bool) { 43 | l.silent = d 44 | } 45 | 46 | func (l *Logger) Log(level int, format string, args ...interface{}) { 47 | l.Lock() 48 | defer l.Unlock() 49 | 50 | if level == DEBUG && !l.debug { 51 | return 52 | } 53 | 54 | if l.silent && level < IMPORTANT { 55 | return 56 | } 57 | 58 | if c, ok := LogColors[level]; ok { 59 | c.Printf("\r"+format+"\n", args...) 60 | } else { 61 | fmt.Printf("\r"+format+"\n", args...) 62 | } 63 | 64 | if level > WARN && session.Config.Webhook != "" { 65 | text := colorStrip(fmt.Sprintf(format, args...)) 66 | payload := fmt.Sprintf(session.Config.WebhookPayload, text) 67 | http.Post(session.Config.Webhook, "application/json", strings.NewReader(payload)) 68 | } 69 | 70 | if level == FATAL { 71 | os.Exit(1) 72 | } 73 | } 74 | 75 | func (l *Logger) Fatal(format string, args ...interface{}) { 76 | l.Log(FATAL, format, args...) 77 | } 78 | 79 | func (l *Logger) Error(format string, args ...interface{}) { 80 | l.Log(ERROR, format, args...) 81 | } 82 | 83 | func (l *Logger) Warn(format string, args ...interface{}) { 84 | l.Log(WARN, format, args...) 85 | } 86 | 87 | func (l *Logger) Important(format string, args ...interface{}) { 88 | l.Log(IMPORTANT, format, args...) 89 | } 90 | 91 | func (l *Logger) Info(format string, args ...interface{}) { 92 | l.Log(INFO, format, args...) 93 | } 94 | 95 | func (l *Logger) Debug(format string, args ...interface{}) { 96 | l.Log(DEBUG, format, args...) 97 | } 98 | 99 | func colorStrip(str string) string { 100 | ansi := "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" 101 | re := regexp.MustCompile(ansi) 102 | return re.ReplaceAllString(str, "") 103 | } 104 | -------------------------------------------------------------------------------- /core/match.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | type MatchFile struct { 11 | Path string 12 | Filename string 13 | Extension string 14 | Contents []byte 15 | } 16 | 17 | func NewMatchFile(path string) MatchFile { 18 | path = filepath.ToSlash(path) 19 | _, filename := filepath.Split(path) 20 | extension := filepath.Ext(path) 21 | contents, _ := ioutil.ReadFile(path) 22 | 23 | return MatchFile{ 24 | Path: path, 25 | Filename: filename, 26 | Extension: extension, 27 | Contents: contents, 28 | } 29 | } 30 | 31 | func IsSkippableFile(path string) bool { 32 | extension := strings.ToLower(filepath.Ext(path)) 33 | 34 | for _, skippableExt := range session.Config.BlacklistedExtensions { 35 | if extension == skippableExt { 36 | return true 37 | } 38 | } 39 | 40 | for _, skippablePathIndicator := range session.Config.BlacklistedPaths { 41 | skippablePathIndicator = strings.Replace(skippablePathIndicator, "{sep}", string(os.PathSeparator), -1) 42 | if strings.Contains(path, skippablePathIndicator) { 43 | return true 44 | } 45 | } 46 | 47 | return false 48 | } 49 | 50 | func (match MatchFile) CanCheckEntropy() bool { 51 | if match.Filename == "id_rsa" { 52 | return false 53 | } 54 | 55 | for _, skippableExt := range session.Config.BlacklistedEntropyExtensions { 56 | if match.Extension == skippableExt { 57 | return false 58 | } 59 | } 60 | 61 | return true 62 | } 63 | 64 | func GetMatchingFiles(dir string) []MatchFile { 65 | fileList := make([]MatchFile, 0) 66 | maxFileSize := *session.Options.MaximumFileSize * 1024 67 | 68 | filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { 69 | if err != nil || f.IsDir() || uint(f.Size()) > maxFileSize || IsSkippableFile(path) { 70 | return nil 71 | } 72 | fileList = append(fileList, NewMatchFile(path)) 73 | return nil 74 | }) 75 | 76 | return fileList 77 | } 78 | -------------------------------------------------------------------------------- /core/options.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type Options struct { 10 | Threads *int 11 | Silent *bool 12 | Debug *bool 13 | MaximumRepositorySize *uint 14 | MaximumFileSize *uint 15 | CloneRepositoryTimeout *uint 16 | EntropyThreshold *float64 17 | MinimumStars *uint 18 | PathChecks *bool 19 | ProcessGists *bool 20 | TempDirectory *string 21 | CsvPath *string 22 | SearchQuery *string 23 | Local *string 24 | Live *string 25 | ConfigPath *string 26 | } 27 | 28 | func ParseOptions() (*Options, error) { 29 | options := &Options{ 30 | Threads: flag.Int("threads", 0, "Number of concurrent threads (default number of logical CPUs)"), 31 | Silent: flag.Bool("silent", false, "Suppress all output except for errors"), 32 | Debug: flag.Bool("debug", false, "Print debugging information"), 33 | MaximumRepositorySize: flag.Uint("maximum-repository-size", 5120, "Maximum repository size to process in KB"), 34 | MaximumFileSize: flag.Uint("maximum-file-size", 256, "Maximum file size to process in KB"), 35 | CloneRepositoryTimeout: flag.Uint("clone-repository-timeout", 10, "Maximum time it should take to clone a repository in seconds. Increase this if you have a slower connection"), 36 | EntropyThreshold: flag.Float64("entropy-threshold", 5.0, "Set to 0 to disable entropy checks"), 37 | MinimumStars: flag.Uint("minimum-stars", 0, "Only process repositories with this many stars. Default 0 will ignore star count"), 38 | PathChecks: flag.Bool("path-checks", true, "Set to false to disable checking of filepaths, i.e. just match regex patterns of file contents"), 39 | ProcessGists: flag.Bool("process-gists", true, "Will watch and process Gists. Set to false to disable."), 40 | TempDirectory: flag.String("temp-directory", filepath.Join(os.TempDir(), Name), "Directory to process and store repositories/matches"), 41 | CsvPath: flag.String("csv-path", "", "CSV file path to log found secrets to. Leave blank to disable"), 42 | SearchQuery: flag.String("search-query", "", "Specify a search string to ignore signatures and filter on files containing this string (regex compatible)"), 43 | Local: flag.String("local", "", "Specify local directory (absolute path) which to scan. Scans only given directory recursively. No need to have GitHub tokens with local run."), 44 | Live: flag.String("live", "", "Your shhgit live endpoint"), 45 | ConfigPath: flag.String("config-path", "", "Searches for config.yaml from given directory. If not set, tries to find if from shhgit binary's and current directory"), 46 | } 47 | 48 | flag.Parse() 49 | 50 | return options, nil 51 | } 52 | -------------------------------------------------------------------------------- /core/session.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "runtime" 10 | "sync" 11 | "time" 12 | 13 | "github.com/google/go-github/github" 14 | "golang.org/x/oauth2" 15 | ) 16 | 17 | type Session struct { 18 | sync.Mutex 19 | 20 | Version string 21 | Log *Logger 22 | Options *Options 23 | Config *Config 24 | Signatures []Signature 25 | Repositories chan GitResource 26 | Gists chan string 27 | Comments chan string 28 | Context context.Context 29 | Clients chan *GitHubClientWrapper 30 | ExhaustedClients chan *GitHubClientWrapper 31 | CsvWriter *csv.Writer 32 | } 33 | 34 | var ( 35 | session *Session 36 | sessionSync sync.Once 37 | err error 38 | ) 39 | 40 | func (s *Session) Start() { 41 | rand.Seed(time.Now().Unix()) 42 | 43 | s.InitLogger() 44 | s.InitThreads() 45 | s.InitSignatures() 46 | s.InitGitHubClients() 47 | s.InitCsvWriter() 48 | } 49 | 50 | func (s *Session) InitLogger() { 51 | s.Log = &Logger{} 52 | s.Log.SetDebug(*s.Options.Debug) 53 | s.Log.SetSilent(*s.Options.Silent) 54 | } 55 | 56 | func (s *Session) InitSignatures() { 57 | s.Signatures = GetSignatures(s) 58 | } 59 | 60 | func (s *Session) InitGitHubClients() { 61 | if len(*s.Options.Local) <= 0 { 62 | chanSize := *s.Options.Threads * (len(s.Config.GitHubAccessTokens) + 1) 63 | s.Clients = make(chan *GitHubClientWrapper, chanSize) 64 | s.ExhaustedClients = make(chan *GitHubClientWrapper, chanSize) 65 | for _, token := range s.Config.GitHubAccessTokens { 66 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 67 | tc := oauth2.NewClient(s.Context, ts) 68 | 69 | client := github.NewClient(tc) 70 | client.UserAgent = fmt.Sprintf("%s v%s", Name, Version) 71 | _, _, err := client.Users.Get(s.Context, "") 72 | 73 | if err != nil { 74 | if _, ok := err.(*github.ErrorResponse); ok { 75 | s.Log.Warn("Failed to validate token %s[..]: %s", token[:10], err) 76 | continue 77 | } 78 | } 79 | 80 | for i := 0; i <= *s.Options.Threads; i++ { 81 | s.Clients <- &GitHubClientWrapper{client, token, time.Now().Add(-1 * time.Second)} 82 | } 83 | } 84 | 85 | if len(s.Clients) < 1 { 86 | s.Log.Fatal("No valid GitHub tokens provided. Quitting!") 87 | } 88 | } 89 | } 90 | 91 | func (s *Session) GetClient() *GitHubClientWrapper { 92 | for { 93 | select { 94 | 95 | case client := <-s.Clients: 96 | s.Log.Debug("Using client with token: %s", client.Token[:10]) 97 | return client 98 | 99 | case client := <-s.ExhaustedClients: 100 | sleepTime := time.Until(client.RateLimitedUntil) 101 | s.Log.Warn("All GitHub tokens exhausted/rate limited. Sleeping for %s", sleepTime.String()) 102 | time.Sleep(sleepTime) 103 | s.Log.Debug("Returning client %s to pool", client.Token[:10]) 104 | s.FreeClient(client) 105 | 106 | default: 107 | s.Log.Debug("Available Clients: %d", len(s.Clients)) 108 | s.Log.Debug("Exhausted Clients: %d", len(s.ExhaustedClients)) 109 | time.Sleep(time.Millisecond * 1000) 110 | } 111 | } 112 | } 113 | 114 | // FreeClient returns the GitHub Client to the pool of available, 115 | // non-rate-limited channel of clients in the session 116 | func (s *Session) FreeClient(client *GitHubClientWrapper) { 117 | if client.RateLimitedUntil.After(time.Now()) { 118 | s.ExhaustedClients <- client 119 | } else { 120 | s.Clients <- client 121 | } 122 | } 123 | 124 | func (s *Session) InitThreads() { 125 | if *s.Options.Threads == 0 { 126 | numCPUs := runtime.NumCPU() 127 | s.Options.Threads = &numCPUs 128 | } 129 | 130 | runtime.GOMAXPROCS(*s.Options.Threads + 1) 131 | } 132 | 133 | func (s *Session) InitCsvWriter() { 134 | if *s.Options.CsvPath == "" { 135 | return 136 | } 137 | 138 | writeHeader := false 139 | if !PathExists(*s.Options.CsvPath) { 140 | writeHeader = true 141 | } 142 | 143 | file, err := os.OpenFile(*s.Options.CsvPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 144 | LogIfError("Could not create/open CSV file", err) 145 | 146 | s.CsvWriter = csv.NewWriter(file) 147 | 148 | if writeHeader { 149 | s.WriteToCsv([]string{"Repository name", "Signature name", "Matching file", "Matches"}) 150 | } 151 | } 152 | 153 | func (s *Session) WriteToCsv(line []string) { 154 | if *s.Options.CsvPath == "" { 155 | return 156 | } 157 | 158 | s.CsvWriter.Write(line) 159 | s.CsvWriter.Flush() 160 | } 161 | 162 | func GetSession() *Session { 163 | sessionSync.Do(func() { 164 | session = &Session{ 165 | Context: context.Background(), 166 | Repositories: make(chan GitResource, 1000), 167 | Gists: make(chan string, 100), 168 | Comments: make(chan string, 1000), 169 | } 170 | 171 | if session.Options, err = ParseOptions(); err != nil { 172 | fmt.Println(err) 173 | os.Exit(1) 174 | } 175 | 176 | if session.Config, err = ParseConfig(session.Options); err != nil { 177 | fmt.Println(err) 178 | os.Exit(1) 179 | } 180 | 181 | session.Start() 182 | }) 183 | 184 | return session 185 | } 186 | -------------------------------------------------------------------------------- /core/signatures.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "regexp" 5 | "regexp/syntax" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | TypeSimple = "simple" 11 | TypePattern = "pattern" 12 | 13 | PartExtension = "extension" 14 | PartFilename = "filename" 15 | PartPath = "path" 16 | PartContents = "contents" 17 | ) 18 | 19 | type Signature interface { 20 | Name() string 21 | Match(file MatchFile) (bool, string) 22 | GetContentsMatches(contents []byte) []string 23 | } 24 | 25 | type SimpleSignature struct { 26 | part string 27 | match string 28 | name string 29 | } 30 | 31 | type PatternSignature struct { 32 | part string 33 | match *regexp.Regexp 34 | name string 35 | } 36 | 37 | func (s SimpleSignature) Match(file MatchFile) (bool, string) { 38 | var ( 39 | haystack *string 40 | matchPart = "" 41 | ) 42 | 43 | switch s.part { 44 | case PartPath: 45 | haystack = &file.Path 46 | matchPart = PartPath 47 | case PartFilename: 48 | haystack = &file.Filename 49 | matchPart = PartPath 50 | case PartExtension: 51 | haystack = &file.Extension 52 | matchPart = PartPath 53 | default: 54 | return false, matchPart 55 | } 56 | 57 | return (s.match == *haystack), matchPart 58 | } 59 | 60 | func (s SimpleSignature) GetContentsMatches(contents []byte) []string { 61 | return nil 62 | } 63 | 64 | func (s SimpleSignature) Name() string { 65 | return s.name 66 | } 67 | 68 | func (s PatternSignature) Match(file MatchFile) (bool, string) { 69 | var ( 70 | haystack *string 71 | matchPart = "" 72 | ) 73 | 74 | switch s.part { 75 | case PartPath: 76 | haystack = &file.Path 77 | matchPart = PartPath 78 | case PartFilename: 79 | haystack = &file.Filename 80 | matchPart = PartFilename 81 | case PartExtension: 82 | haystack = &file.Extension 83 | matchPart = PartExtension 84 | case PartContents: 85 | return s.match.Match(file.Contents), PartContents 86 | default: 87 | return false, matchPart 88 | } 89 | 90 | return s.match.MatchString(*haystack), matchPart 91 | } 92 | 93 | func (s PatternSignature) GetContentsMatches(contents []byte) []string { 94 | matches := make([]string, 0) 95 | 96 | for _, match := range s.match.FindAllSubmatch(contents, -1) { 97 | match := string(match[0]) 98 | blacklistedMatch := false 99 | 100 | for _, blacklistedString := range session.Config.BlacklistedStrings { 101 | if strings.Contains(strings.ToLower(match), strings.ToLower(blacklistedString)) { 102 | blacklistedMatch = true 103 | } 104 | } 105 | 106 | if !blacklistedMatch { 107 | matches = append(matches, match) 108 | } 109 | } 110 | 111 | return matches 112 | } 113 | 114 | func (s PatternSignature) Name() string { 115 | return s.name 116 | } 117 | 118 | func GetSignatures(s *Session) []Signature { 119 | var signatures []Signature 120 | for _, signature := range s.Config.Signatures { 121 | if signature.Match != "" { 122 | signatures = append(signatures, SimpleSignature{ 123 | name: signature.Name, 124 | part: signature.Part, 125 | match: signature.Match, 126 | }) 127 | } else { 128 | if _, err := syntax.Parse(signature.Match, syntax.FoldCase); err == nil { 129 | signatures = append(signatures, PatternSignature{ 130 | name: signature.Name, 131 | part: signature.Part, 132 | match: regexp.MustCompile(signature.Regex), 133 | }) 134 | } 135 | } 136 | } 137 | 138 | return signatures 139 | } 140 | -------------------------------------------------------------------------------- /core/spinner.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | const SpinnerChars = `|/-\` 11 | 12 | type Spinner struct { 13 | mu sync.Mutex 14 | frames []rune 15 | length int 16 | pos int 17 | } 18 | 19 | func New() *Spinner { 20 | s := &Spinner{} 21 | s.frames = []rune(SpinnerChars) 22 | s.length = len(s.frames) 23 | 24 | return s 25 | } 26 | 27 | func (s *Spinner) Next() string { 28 | s.mu.Lock() 29 | defer s.mu.Unlock() 30 | r := s.frames[s.pos%s.length] 31 | s.pos++ 32 | return string(r) 33 | } 34 | 35 | func ShowSpinner() func() { 36 | spinner := New() 37 | done := make(chan bool) 38 | 39 | go func() { 40 | for { 41 | select { 42 | case <-done: 43 | default: 44 | fmt.Fprintf(os.Stderr, "\r%s", spinner.Next()) 45 | time.Sleep(150 * time.Millisecond) 46 | } 47 | } 48 | }() 49 | 50 | return func() { 51 | done <- true 52 | fmt.Fprintf(os.Stderr, "\033[%dD", 1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/util.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "math" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func GetTempDir(suffix string) string { 13 | dir := filepath.Join(*session.Options.TempDirectory, suffix) 14 | 15 | if _, err := os.Stat(dir); os.IsNotExist(err) { 16 | os.MkdirAll(dir, os.ModePerm) 17 | } else { 18 | os.RemoveAll(dir) 19 | } 20 | 21 | return dir 22 | } 23 | 24 | func PathExists(path string) bool { 25 | _, err := os.Stat(path) 26 | if err == nil { 27 | return true 28 | } 29 | 30 | if os.IsNotExist(err) { 31 | return false 32 | } 33 | 34 | return false 35 | } 36 | 37 | func LogIfError(text string, err error) { 38 | if err != nil { 39 | GetSession().Log.Error("%s (%s", text, err.Error()) 40 | } 41 | } 42 | 43 | func GetHash(s string) string { 44 | h := sha1.New() 45 | h.Write([]byte(s)) 46 | 47 | return hex.EncodeToString(h.Sum(nil)) 48 | } 49 | 50 | func Pluralize(count int, singular string, plural string) string { 51 | if count == 1 { 52 | return singular 53 | } 54 | 55 | return plural 56 | } 57 | 58 | func GetEntropy(data string) (entropy float64) { 59 | if data == "" { 60 | return 0 61 | } 62 | 63 | for i := 0; i < 256; i++ { 64 | px := float64(strings.Count(data, string(byte(i)))) / float64(len(data)) 65 | if px > 0 { 66 | entropy += -px * math.Log2(px) 67 | } 68 | } 69 | 70 | return entropy 71 | } 72 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | services: 4 | shhgit-www: 5 | build: ./www 6 | container_name: shhgit.www 7 | ports: 8 | - 8080:80 # if you change your local port update public/script.js:1273 9 | volumes: 10 | - ./www/public:/usr/share/nginx/html:ro 11 | 12 | shhgit-app: 13 | build: ./ 14 | container_name: shhgit.app 15 | entrypoint: ["/app/shhgit", "--live=http://shhgit-www/push"] 16 | depends_on: 17 | - shhgit-www 18 | volumes: 19 | - ./config.yaml:/app/config.yaml:ro -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eth0izzle/shhgit 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/fatih/color v1.7.0 7 | github.com/google/go-github v17.0.0+incompatible 8 | github.com/google/go-querystring v1.0.0 // indirect 9 | github.com/mattn/go-colorable v0.1.2 // indirect 10 | github.com/mattn/go-isatty v0.0.9 // indirect 11 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 12 | gopkg.in/src-d/go-git.v4 v4.13.1 13 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 3 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 4 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 5 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 6 | github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 h1:1fx+RA5lk1ZkzPAUP7DEgZnVHYxEcHO77vQO/V8z/2Q= 7 | github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1/go.mod h1:z0nyIb42Zs97wyX1V+8MbEFhHeTw1OgFQfR6q57ZuHc= 8 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 12 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 13 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 14 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 15 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 16 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 19 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 20 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 21 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 22 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 23 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 24 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 25 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 26 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 27 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 28 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 32 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 33 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 34 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 35 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 36 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 37 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 38 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 39 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 40 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 41 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 42 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 45 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 46 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 47 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 48 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 49 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 50 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 51 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 52 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 53 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 54 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 55 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 56 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 57 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 58 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= 62 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 63 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 64 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 65 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 69 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 70 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= 72 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 74 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 76 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 77 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 78 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 79 | golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 80 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 83 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 85 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 86 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= 87 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 88 | gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= 89 | gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 90 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 91 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 92 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA= 93 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 94 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eth0izzle/shhgit/bac0c7d39519203d230b6c9a2c6e3eba18346aba/images/logo.png -------------------------------------------------------------------------------- /images/shhgit-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eth0izzle/shhgit/bac0c7d39519203d230b6c9a2c6e3eba18346aba/images/shhgit-example.png -------------------------------------------------------------------------------- /images/shhgit-live-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eth0izzle/shhgit/bac0c7d39519203d230b6c9a2c6e3eba18346aba/images/shhgit-live-example.png -------------------------------------------------------------------------------- /images/shhgit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eth0izzle/shhgit/bac0c7d39519203d230b6c9a2c6e3eba18346aba/images/shhgit.gif -------------------------------------------------------------------------------- /images/shhgit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eth0izzle/shhgit/bac0c7d39519203d230b6c9a2c6e3eba18346aba/images/shhgit.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | "time" 15 | 16 | "github.com/eth0izzle/shhgit/core" 17 | "github.com/fatih/color" 18 | ) 19 | 20 | type MatchEvent struct { 21 | Url string 22 | Matches []string 23 | Signature string 24 | File string 25 | Stars int 26 | Source core.GitResourceType 27 | } 28 | 29 | var session = core.GetSession() 30 | 31 | func ProcessRepositories() { 32 | threadNum := *session.Options.Threads 33 | 34 | for i := 0; i < threadNum; i++ { 35 | go func(tid int) { 36 | for { 37 | timeout := time.Duration(*session.Options.CloneRepositoryTimeout) * time.Second 38 | _, cancel := context.WithTimeout(context.Background(), timeout) 39 | defer cancel() 40 | 41 | repository := <-session.Repositories 42 | 43 | repo, err := core.GetRepository(session, repository.Id) 44 | 45 | if err != nil { 46 | session.Log.Warn("Failed to retrieve repository %d: %s", repository.Id, err) 47 | continue 48 | } 49 | 50 | if repo.GetPermissions()["pull"] && 51 | uint(repo.GetStargazersCount()) >= *session.Options.MinimumStars && 52 | uint(repo.GetSize()) < *session.Options.MaximumRepositorySize { 53 | 54 | processRepositoryOrGist(repo.GetCloneURL(), repository.Ref, repo.GetStargazersCount(), core.GITHUB_SOURCE) 55 | } 56 | } 57 | }(i) 58 | } 59 | } 60 | 61 | func ProcessGists() { 62 | threadNum := *session.Options.Threads 63 | 64 | for i := 0; i < threadNum; i++ { 65 | go func(tid int) { 66 | for { 67 | gistUrl := <-session.Gists 68 | processRepositoryOrGist(gistUrl, "", -1, core.GIST_SOURCE) 69 | } 70 | }(i) 71 | } 72 | } 73 | 74 | func ProcessComments() { 75 | threadNum := *session.Options.Threads 76 | 77 | for i := 0; i < threadNum; i++ { 78 | go func(tid int) { 79 | for { 80 | commentBody := <-session.Comments 81 | dir := core.GetTempDir(core.GetHash(commentBody)) 82 | ioutil.WriteFile(filepath.Join(dir, "comment.ignore"), []byte(commentBody), 0644) 83 | 84 | if !checkSignatures(dir, "ISSUE", 0, core.GITHUB_COMMENT) { 85 | os.RemoveAll(dir) 86 | } 87 | } 88 | }(i) 89 | } 90 | } 91 | 92 | func processRepositoryOrGist(url string, ref string, stars int, source core.GitResourceType) { 93 | var ( 94 | matchedAny bool = false 95 | ) 96 | 97 | dir := core.GetTempDir(core.GetHash(url)) 98 | _, err := core.CloneRepository(session, url, ref, dir) 99 | 100 | if err != nil { 101 | session.Log.Debug("[%s] Cloning failed: %s", url, err.Error()) 102 | os.RemoveAll(dir) 103 | return 104 | } 105 | 106 | session.Log.Debug("[%s] Cloning %s in to %s", url, ref, strings.Replace(dir, *session.Options.TempDirectory, "", -1)) 107 | matchedAny = checkSignatures(dir, url, stars, source) 108 | if !matchedAny { 109 | os.RemoveAll(dir) 110 | } 111 | } 112 | 113 | func checkSignatures(dir string, url string, stars int, source core.GitResourceType) (matchedAny bool) { 114 | for _, file := range core.GetMatchingFiles(dir) { 115 | var ( 116 | matches []string 117 | relativeFileName string 118 | ) 119 | if strings.Contains(dir, *session.Options.TempDirectory) { 120 | relativeFileName = strings.Replace(file.Path, *session.Options.TempDirectory, "", -1) 121 | } else { 122 | relativeFileName = strings.Replace(file.Path, dir, "", -1) 123 | } 124 | 125 | if *session.Options.SearchQuery != "" { 126 | queryRegex := regexp.MustCompile(*session.Options.SearchQuery) 127 | for _, match := range queryRegex.FindAllSubmatch(file.Contents, -1) { 128 | matches = append(matches, string(match[0])) 129 | } 130 | 131 | if matches != nil { 132 | count := len(matches) 133 | m := strings.Join(matches, ", ") 134 | session.Log.Important("[%s] %d %s for %s in file %s: %s", url, count, core.Pluralize(count, "match", "matches"), color.GreenString("Search Query"), relativeFileName, color.YellowString(m)) 135 | session.WriteToCsv([]string{url, "Search Query", relativeFileName, m}) 136 | } 137 | } else { 138 | for _, signature := range session.Signatures { 139 | if matched, part := signature.Match(file); matched { 140 | if part == core.PartContents { 141 | if matches = signature.GetContentsMatches(file.Contents); len(matches) > 0 { 142 | count := len(matches) 143 | m := strings.Join(matches, ", ") 144 | publish(&MatchEvent{Source: source, Url: url, Matches: matches, Signature: signature.Name(), File: relativeFileName, Stars: stars}) 145 | matchedAny = true 146 | 147 | session.Log.Important("[%s] %d %s for %s in file %s: %s", url, count, core.Pluralize(count, "match", "matches"), color.GreenString(signature.Name()), relativeFileName, color.YellowString(m)) 148 | session.WriteToCsv([]string{url, signature.Name(), relativeFileName, m}) 149 | } 150 | } else { 151 | if *session.Options.PathChecks { 152 | publish(&MatchEvent{Source: source, Url: url, Matches: matches, Signature: signature.Name(), File: relativeFileName, Stars: stars}) 153 | matchedAny = true 154 | 155 | session.Log.Important("[%s] Matching file %s for %s", url, color.YellowString(relativeFileName), color.GreenString(signature.Name())) 156 | session.WriteToCsv([]string{url, signature.Name(), relativeFileName, ""}) 157 | } 158 | 159 | if *session.Options.EntropyThreshold > 0 && file.CanCheckEntropy() { 160 | scanner := bufio.NewScanner(bytes.NewReader(file.Contents)) 161 | 162 | for scanner.Scan() { 163 | line := scanner.Text() 164 | 165 | if len(line) > 6 && len(line) < 100 { 166 | entropy := core.GetEntropy(line) 167 | 168 | if entropy >= *session.Options.EntropyThreshold { 169 | blacklistedMatch := false 170 | 171 | for _, blacklistedString := range session.Config.BlacklistedStrings { 172 | if strings.Contains(strings.ToLower(line), strings.ToLower(blacklistedString)) { 173 | blacklistedMatch = true 174 | } 175 | } 176 | 177 | if !blacklistedMatch { 178 | publish(&MatchEvent{Source: source, Url: url, Matches: []string{line}, Signature: "High entropy string", File: relativeFileName, Stars: stars}) 179 | matchedAny = true 180 | 181 | session.Log.Important("[%s] Potential secret in %s = %s", url, color.YellowString(relativeFileName), color.GreenString(line)) 182 | session.WriteToCsv([]string{url, "High entropy string", relativeFileName, line}) 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | 193 | if !matchedAny && len(*session.Options.Local) <= 0 { 194 | os.Remove(file.Path) 195 | } 196 | } 197 | return 198 | } 199 | 200 | func publish(event *MatchEvent) { 201 | // todo: implement a modular plugin system to handle the various outputs (console, live, csv, webhooks, etc) 202 | if len(*session.Options.Live) > 0 { 203 | data, _ := json.Marshal(event) 204 | http.Post(*session.Options.Live, "application/json", bytes.NewBuffer(data)) 205 | } 206 | } 207 | 208 | func main() { 209 | session.Log.Info(color.HiBlueString(core.Banner)) 210 | session.Log.Info("\t%s\n", color.HiCyanString(core.Author)) 211 | session.Log.Info("[*] Loaded %s signatures. Using %s worker threads. Temp work dir: %s\n", color.BlueString("%d", len(session.Signatures)), color.BlueString("%d", *session.Options.Threads), color.BlueString(*session.Options.TempDirectory)) 212 | 213 | if len(*session.Options.Local) > 0 { 214 | session.Log.Info("[*] Scanning local directory: %s - skipping public repository checks...", color.BlueString(*session.Options.Local)) 215 | rc := 0 216 | if checkSignatures(*session.Options.Local, *session.Options.Local, -1, core.LOCAL_SOURCE) { 217 | rc = 1 218 | } else { 219 | session.Log.Info("[*] No matching secrets found in %s!", color.BlueString(*session.Options.Local)) 220 | } 221 | os.Exit(rc) 222 | } else { 223 | if *session.Options.SearchQuery != "" { 224 | session.Log.Important("Search Query '%s' given. Only returning matching results.", *session.Options.SearchQuery) 225 | } 226 | 227 | go core.GetRepositories(session) 228 | go ProcessRepositories() 229 | go ProcessComments() 230 | 231 | if *session.Options.ProcessGists { 232 | go core.GetGists(session) 233 | go ProcessGists() 234 | } 235 | 236 | spinny := core.ShowSpinner() 237 | select {} 238 | spinny() 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /www/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim AS builder 2 | 3 | RUN apt-get update -y \ 4 | && apt-get install --no-install-recommends -y wget git unzip lsb-release gnupg2 dpkg-dev ca-certificates \ 5 | && echo "deb-src http://nginx.org/packages/`lsb_release -is | tr '[:upper:]' '[:lower:]'` `lsb_release -cs` nginx" | tee /etc/apt/sources.list.d/nginx.list \ 6 | && wget http://nginx.org/keys/nginx_signing.key && apt-key add nginx_signing.key && rm nginx_signing.key \ 7 | && cd /tmp \ 8 | && apt-get update \ 9 | && apt-get source nginx \ 10 | && apt-get build-dep nginx --no-install-recommends -y \ 11 | && git clone https://github.com/wandenberg/nginx-push-stream-module.git nginx-push-stream-module \ 12 | && cd nginx-1* \ 13 | && sed -i "s@--with-stream_ssl_module@--with-stream_ssl_module --add-module=/tmp/nginx-push-stream-module @g" debian/rules \ 14 | && dpkg-buildpackage -uc -us -b \ 15 | && cd .. \ 16 | && mv nginx_1*~buster_amd64.deb nginx.deb 17 | 18 | FROM debian:buster-slim AS runner 19 | 20 | COPY --from=builder /tmp/nginx.deb /tmp 21 | 22 | RUN apt-get update -y \ 23 | && apt-get install --no-install-recommends -y libssl1.1 lsb-base \ 24 | && dpkg -i /tmp/nginx.deb \ 25 | && apt-mark hold nginx 26 | 27 | COPY nginx.conf /etc/nginx/nginx.conf 28 | 29 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /www/nginx.conf: -------------------------------------------------------------------------------- 1 | # uses the nginx and the stream module to proxy connections (shhgit > nginx < clients) 2 | # make sure to read https://github.com/wandenberg/nginx-push-stream-module 3 | worker_processes auto; 4 | 5 | events { 6 | worker_connections 1024; 7 | use epoll; 8 | } 9 | 10 | http { 11 | postpone_output 1; 12 | push_stream_shared_memory_size 128m; 13 | push_stream_max_channel_id_length 20; 14 | push_stream_max_messages_stored_per_channel 100; 15 | push_stream_message_ttl 30m; 16 | push_stream_ping_message_interval 10s; 17 | push_stream_longpolling_connection_ttl 30s; 18 | push_stream_timeout_with_body off; 19 | push_stream_authorized_channels_only off; # make sure to turn this on if your shhgit live instance is public 20 | push_stream_allowed_origins "*"; # change this if your shhgit live instance is public 21 | 22 | server { 23 | listen 80; 24 | server_name localhost; 25 | root /usr/share/nginx/html; 26 | index index.html; 27 | 28 | access_log off; 29 | error_log off; 30 | 31 | default_type application/octet-stream; 32 | include /etc/nginx/mime.types; 33 | 34 | location /push { 35 | push_stream_publisher admin; 36 | push_stream_channels_path "shhgit"; 37 | push_stream_store_messages on; 38 | 39 | #allow 127.0.0.1/32; 40 | #deny all; 41 | allow all; # make sure you only allow approved hosts to push messages if your shhgit live instance is public 42 | } 43 | 44 | location ~ /events/(.*) { 45 | push_stream_subscriber eventsource; 46 | push_stream_channels_path $1; 47 | push_stream_last_received_message_time "$arg_time"; 48 | push_stream_last_received_message_tag "$arg_tag"; 49 | push_stream_ping_message_interval 10s; 50 | push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":~text~,\"tag\":\"~tag~\",\"time\":\"~time~\"}"; 51 | 52 | client_max_body_size 32k; 53 | client_body_buffer_size 32k; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /www/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | shhgit: find secrets in real time across GitHub, GitLab and BitBucket 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 42 | 43 |
44 | 62 | 63 |
64 | 65 | 66 | 67 | 68 | shhgit 69 | 70 | 71 | 72 | 73 | 74 | 75 | @darkp0rt 76 | 77 | 78 | 79 | 80 | 81 | 82 | sponsor 83 | 84 | 85 | Connecting... 86 | 87 |
88 | 89 | 90 | 91 |
92 |
93 | 94 |
95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
FoundSignature NameMatchesFile
109 | 110 |
111 | Loading 112 |

Waiting for events...

113 |
114 |
115 |
116 |
117 |
118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /www/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eth0izzle/shhgit/bac0c7d39519203d230b6c9a2c6e3eba18346aba/www/public/logo.png -------------------------------------------------------------------------------- /www/public/script.js: -------------------------------------------------------------------------------- 1 | /*global PushStream WebSocketWrapper EventSourceWrapper EventSource*/ 2 | /*jshint evil: true, plusplus: false, regexp: false */ 3 | /** 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2010-2014 Wandenberg Peixoto , Rogério Carvalho Schneider 7 | 8 | This file is part of Nginx Push Stream Module. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | pushstream.js 29 | 30 | Created: Nov 01, 2011 31 | Authors: Wandenberg Peixoto , Rogério Carvalho Schneider 32 | */ 33 | (function (window, document, undefined) { 34 | "use strict"; 35 | 36 | /* prevent duplicate declaration */ 37 | if (window.PushStream) { return; } 38 | 39 | var Utils = {}; 40 | 41 | var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 42 | var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 43 | 44 | var valueToTwoDigits = function (value) { 45 | return ((value < 10) ? '0' : '') + value; 46 | }; 47 | 48 | Utils.dateToUTCString = function (date) { 49 | var time = valueToTwoDigits(date.getUTCHours()) + ':' + valueToTwoDigits(date.getUTCMinutes()) + ':' + valueToTwoDigits(date.getUTCSeconds()); 50 | return days[date.getUTCDay()] + ', ' + valueToTwoDigits(date.getUTCDate()) + ' ' + months[date.getUTCMonth()] + ' ' + date.getUTCFullYear() + ' ' + time + ' GMT'; 51 | }; 52 | 53 | var extend = function () { 54 | var object = arguments[0] || {}; 55 | for (var i = 0; i < arguments.length; i++) { 56 | var settings = arguments[i]; 57 | for (var attr in settings) { 58 | if (!settings.hasOwnProperty || settings.hasOwnProperty(attr)) { 59 | object[attr] = settings[attr]; 60 | } 61 | } 62 | } 63 | return object; 64 | }; 65 | 66 | var validChars = /^[\],:{}\s]*$/, 67 | validEscape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, 68 | validTokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, 69 | validBraces = /(?:^|:|,)(?:\s*\[)+/g; 70 | 71 | var trim = function(value) { 72 | return value.replace(/^\s*/, "").replace(/\s*$/, ""); 73 | }; 74 | 75 | Utils.parseJSON = function(data) { 76 | if (!data || !isString(data)) { 77 | return null; 78 | } 79 | 80 | // Make sure leading/trailing whitespace is removed (IE can't handle it) 81 | data = trim(data); 82 | 83 | // Attempt to parse using the native JSON parser first 84 | if (window.JSON && window.JSON.parse) { 85 | try { 86 | return window.JSON.parse( data ); 87 | } catch(e) { 88 | throw "Invalid JSON: " + data; 89 | } 90 | } 91 | 92 | // Make sure the incoming data is actual JSON 93 | // Logic borrowed from http://json.org/json2.js 94 | if (validChars.test(data.replace(validEscape, "@").replace( validTokens, "]").replace( validBraces, "")) ) { 95 | return (new Function("return " + data))(); 96 | } 97 | 98 | throw "Invalid JSON: " + data; 99 | }; 100 | 101 | var getControlParams = function(pushstream) { 102 | var data = {}; 103 | data[pushstream.tagArgument] = ""; 104 | data[pushstream.timeArgument] = ""; 105 | data[pushstream.eventIdArgument] = ""; 106 | if (pushstream.messagesControlByArgument) { 107 | data[pushstream.tagArgument] = Number(pushstream._etag); 108 | if (pushstream._lastModified) { 109 | data[pushstream.timeArgument] = pushstream._lastModified; 110 | } else if (pushstream._lastEventId) { 111 | data[pushstream.eventIdArgument] = pushstream._lastEventId; 112 | } 113 | } 114 | return data; 115 | }; 116 | 117 | var getTime = function() { 118 | return (new Date()).getTime(); 119 | }; 120 | 121 | var currentTimestampParam = function() { 122 | return { "_" : getTime() }; 123 | }; 124 | 125 | var objectToUrlParams = function(settings) { 126 | var params = settings; 127 | if (typeof(settings) === 'object') { 128 | params = ''; 129 | for (var attr in settings) { 130 | if (!settings.hasOwnProperty || settings.hasOwnProperty(attr)) { 131 | params += '&' + attr + '=' + escapeText(settings[attr]); 132 | } 133 | } 134 | params = params.substring(1); 135 | } 136 | 137 | return params || ''; 138 | }; 139 | 140 | var addParamsToUrl = function(url, params) { 141 | return url + ((url.indexOf('?') < 0) ? '?' : '&') + objectToUrlParams(params); 142 | }; 143 | 144 | var isArray = Array.isArray || function(obj) { 145 | return Object.prototype.toString.call(obj) === '[object Array]'; 146 | }; 147 | 148 | var isString = function(obj) { 149 | return Object.prototype.toString.call(obj) === '[object String]'; 150 | }; 151 | 152 | var isDate = function(obj) { 153 | return Object.prototype.toString.call(obj) === '[object Date]'; 154 | }; 155 | 156 | var Log4js = { 157 | logger: null, 158 | debug : function() { if (PushStream.LOG_LEVEL === 'debug') { Log4js._log.apply(Log4js._log, arguments); }}, 159 | info : function() { if ((PushStream.LOG_LEVEL === 'info') || (PushStream.LOG_LEVEL === 'debug')) { Log4js._log.apply(Log4js._log, arguments); }}, 160 | error : function() { Log4js._log.apply(Log4js._log, arguments); }, 161 | _initLogger : function() { 162 | var console = window.console; 163 | if (console && console.log) { 164 | if (console.log.apply) { 165 | Log4js.logger = console.log; 166 | } else if ((typeof console.log === "object") && Function.prototype.bind) { 167 | Log4js.logger = Function.prototype.bind.call(console.log, console); 168 | } else if ((typeof console.log === "object") && Function.prototype.call) { 169 | Log4js.logger = function() { 170 | Function.prototype.call.call(console.log, console, Array.prototype.slice.call(arguments)); 171 | }; 172 | } 173 | } 174 | }, 175 | _log : function() { 176 | if (!Log4js.logger) { 177 | Log4js._initLogger(); 178 | } 179 | 180 | if (Log4js.logger) { 181 | try { 182 | Log4js.logger.apply(window.console, arguments); 183 | } catch(e) { 184 | Log4js._initLogger(); 185 | if (Log4js.logger) { 186 | Log4js.logger.apply(window.console, arguments); 187 | } 188 | } 189 | } 190 | 191 | var logElement = document.getElementById(PushStream.LOG_OUTPUT_ELEMENT_ID); 192 | if (logElement) { 193 | var str = ''; 194 | for (var i = 0; i < arguments.length; i++) { 195 | str += arguments[i] + " "; 196 | } 197 | logElement.innerHTML += str + '\n'; 198 | 199 | var lines = logElement.innerHTML.split('\n'); 200 | if (lines.length > 100) { 201 | logElement.innerHTML = lines.slice(-100).join('\n'); 202 | } 203 | } 204 | } 205 | }; 206 | 207 | var Ajax = { 208 | _getXHRObject : function(crossDomain) { 209 | var xhr = false; 210 | if (crossDomain) { 211 | try { xhr = new window.XDomainRequest(); } catch (e) { } 212 | if (xhr) { 213 | return xhr; 214 | } 215 | } 216 | 217 | try { xhr = new window.XMLHttpRequest(); } 218 | catch (e1) { 219 | try { xhr = new window.ActiveXObject("Msxml2.XMLHTTP"); } 220 | catch (e2) { 221 | try { xhr = new window.ActiveXObject("Microsoft.XMLHTTP"); } 222 | catch (e3) { 223 | xhr = false; 224 | } 225 | } 226 | } 227 | return xhr; 228 | }, 229 | 230 | _send : function(settings, post) { 231 | settings = settings || {}; 232 | settings.timeout = settings.timeout || 30000; 233 | var xhr = Ajax._getXHRObject(settings.crossDomain); 234 | if (!xhr||!settings.url) { return; } 235 | 236 | Ajax.clear(settings); 237 | 238 | settings.xhr = xhr; 239 | 240 | if (window.XDomainRequest && (xhr instanceof window.XDomainRequest)) { 241 | xhr.onload = function () { 242 | if (settings.afterReceive) { settings.afterReceive(xhr); } 243 | if (settings.success) { settings.success(xhr.responseText); } 244 | }; 245 | 246 | xhr.onerror = xhr.ontimeout = function () { 247 | if (settings.afterReceive) { settings.afterReceive(xhr); } 248 | if (settings.error) { settings.error(xhr.status); } 249 | }; 250 | } else { 251 | xhr.onreadystatechange = function () { 252 | if (xhr.readyState === 4) { 253 | Ajax.clear(settings); 254 | if (settings.afterReceive) { settings.afterReceive(xhr); } 255 | if(xhr.status === 200) { 256 | if (settings.success) { settings.success(xhr.responseText); } 257 | } else { 258 | if (settings.error) { settings.error(xhr.status); } 259 | } 260 | } 261 | }; 262 | } 263 | 264 | if (settings.beforeOpen) { settings.beforeOpen(xhr); } 265 | 266 | var params = {}; 267 | var body = null; 268 | var method = "GET"; 269 | if (post) { 270 | body = objectToUrlParams(settings.data); 271 | method = "POST"; 272 | } else { 273 | params = settings.data || {}; 274 | } 275 | 276 | xhr.open(method, addParamsToUrl(settings.url, extend({}, params, currentTimestampParam())), true); 277 | 278 | if (settings.beforeSend) { settings.beforeSend(xhr); } 279 | 280 | var onerror = function() { 281 | Ajax.clear(settings); 282 | try { xhr.abort(); } catch (e) { /* ignore error on closing */ } 283 | settings.error(304); 284 | }; 285 | 286 | if (post) { 287 | if (xhr.setRequestHeader) { 288 | xhr.setRequestHeader("Accept", "application/json"); 289 | xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 290 | } 291 | } else { 292 | settings.timeoutId = window.setTimeout(onerror, settings.timeout + 2000); 293 | } 294 | 295 | xhr.send(body); 296 | return xhr; 297 | }, 298 | 299 | _clear_xhr : function(xhr) { 300 | if (xhr) { 301 | xhr.onreadystatechange = null; 302 | } 303 | }, 304 | 305 | _clear_script : function(script) { 306 | // Handling memory leak in IE, removing and dereference the script 307 | if (script) { 308 | script.onerror = script.onload = script.onreadystatechange = null; 309 | if (script.parentNode) { script.parentNode.removeChild(script); } 310 | } 311 | }, 312 | 313 | _clear_timeout : function(settings) { 314 | settings.timeoutId = clearTimer(settings.timeoutId); 315 | }, 316 | 317 | _clear_jsonp : function(settings) { 318 | var callbackFunctionName = settings.data.callback; 319 | if (callbackFunctionName) { 320 | window[callbackFunctionName] = function() { window[callbackFunctionName] = null; }; 321 | } 322 | }, 323 | 324 | clear : function(settings) { 325 | Ajax._clear_timeout(settings); 326 | Ajax._clear_jsonp(settings); 327 | Ajax._clear_script(document.getElementById(settings.scriptId)); 328 | Ajax._clear_xhr(settings.xhr); 329 | }, 330 | 331 | jsonp : function(settings) { 332 | settings.timeout = settings.timeout || 30000; 333 | Ajax.clear(settings); 334 | 335 | var head = document.head || document.getElementsByTagName("head")[0]; 336 | var script = document.createElement("script"); 337 | var startTime = getTime(); 338 | 339 | var onerror = function() { 340 | Ajax.clear(settings); 341 | var endTime = getTime(); 342 | settings.error(((endTime - startTime) > settings.timeout/2) ? 304 : 403); 343 | }; 344 | 345 | var onload = function() { 346 | Ajax.clear(settings); 347 | settings.load(); 348 | }; 349 | 350 | var onsuccess = function() { 351 | settings.afterSuccess = true; 352 | settings.success.apply(null, arguments); 353 | }; 354 | 355 | script.onerror = onerror; 356 | script.onload = script.onreadystatechange = function(eventLoad) { 357 | if (!script.readyState || /loaded|complete/.test(script.readyState)) { 358 | if (settings.afterSuccess) { 359 | onload(); 360 | } else { 361 | onerror(); 362 | } 363 | } 364 | }; 365 | 366 | if (settings.beforeOpen) { settings.beforeOpen({}); } 367 | if (settings.beforeSend) { settings.beforeSend({}); } 368 | 369 | settings.timeoutId = window.setTimeout(onerror, settings.timeout + 2000); 370 | settings.scriptId = settings.scriptId || getTime(); 371 | settings.afterSuccess = null; 372 | 373 | settings.data.callback = settings.scriptId + "_onmessage_" + getTime(); 374 | window[settings.data.callback] = onsuccess; 375 | 376 | script.setAttribute("src", addParamsToUrl(settings.url, extend({}, settings.data, currentTimestampParam()))); 377 | script.setAttribute("async", "async"); 378 | script.setAttribute("id", settings.scriptId); 379 | 380 | // Use insertBefore instead of appendChild to circumvent an IE6 bug. 381 | head.insertBefore(script, head.firstChild); 382 | return settings; 383 | }, 384 | 385 | load : function(settings) { 386 | return Ajax._send(settings, false); 387 | }, 388 | 389 | post : function(settings) { 390 | return Ajax._send(settings, true); 391 | } 392 | }; 393 | 394 | var escapeText = function(text) { 395 | return (text) ? window.encodeURIComponent(text) : ''; 396 | }; 397 | 398 | var unescapeText = function(text) { 399 | return (text) ? window.decodeURIComponent(text) : ''; 400 | }; 401 | 402 | Utils.parseMessage = function(messageText, keys) { 403 | var msg = messageText; 404 | if (isString(messageText)) { 405 | msg = Utils.parseJSON(messageText); 406 | } 407 | 408 | var message = { 409 | id : msg[keys.jsonIdKey], 410 | channel: msg[keys.jsonChannelKey], 411 | text : isString(msg[keys.jsonTextKey]) ? unescapeText(msg[keys.jsonTextKey]) : msg[keys.jsonTextKey], 412 | tag : msg[keys.jsonTagKey], 413 | time : msg[keys.jsonTimeKey], 414 | eventid: msg[keys.jsonEventIdKey] || "" 415 | }; 416 | 417 | return message; 418 | }; 419 | 420 | var getBacktrack = function(options) { 421 | return (options.backtrack) ? ".b" + Number(options.backtrack) : ""; 422 | }; 423 | 424 | var getChannelsPath = function(channels, withBacktrack) { 425 | var path = ''; 426 | for (var channelName in channels) { 427 | if (!channels.hasOwnProperty || channels.hasOwnProperty(channelName)) { 428 | path += "/" + channelName + (withBacktrack ? getBacktrack(channels[channelName]) : ""); 429 | } 430 | } 431 | return path; 432 | }; 433 | 434 | var getSubscriberUrl = function(pushstream, prefix, extraParams, withBacktrack) { 435 | var websocket = pushstream.wrapper.type === WebSocketWrapper.TYPE; 436 | var useSSL = pushstream.useSSL; 437 | var port = normalizePort(useSSL, pushstream.port); 438 | var url = (websocket) ? ((useSSL) ? "wss://" : "ws://") : ((useSSL) ? "https://" : "http://"); 439 | url += pushstream.host; 440 | url += (port ? (":" + port) : ""); 441 | url += prefix; 442 | 443 | var channels = getChannelsPath(pushstream.channels, withBacktrack); 444 | if (pushstream.channelsByArgument) { 445 | var channelParam = {}; 446 | channelParam[pushstream.channelsArgument] = channels.substring(1); 447 | extraParams = extend({}, extraParams, channelParam); 448 | } else { 449 | url += channels; 450 | } 451 | return addParamsToUrl(url, extraParams); 452 | }; 453 | 454 | var getPublisherUrl = function(pushstream) { 455 | var port = normalizePort(pushstream.useSSL, pushstream.port); 456 | var url = (pushstream.useSSL) ? "https://" : "http://"; 457 | url += pushstream.host; 458 | url += (port ? (":" + port) : ""); 459 | url += pushstream.urlPrefixPublisher; 460 | url += "?id=" + getChannelsPath(pushstream.channels, false); 461 | 462 | return url; 463 | }; 464 | 465 | Utils.extract_xss_domain = function(domain) { 466 | // if domain is an ip address return it, else return ate least the last two parts of it 467 | if (domain.match(/^(\d{1,3}\.){3}\d{1,3}$/)) { 468 | return domain; 469 | } 470 | 471 | var domainParts = domain.split('.'); 472 | // if the domain ends with 3 chars or 2 chars preceded by more than 4 chars, 473 | // we can keep only 2 parts, else we have to keep at least 3 (or all domain name) 474 | var keepNumber = Math.max(domainParts.length - 1, (domain.match(/(\w{4,}\.\w{2}|\.\w{3,})$/) ? 2 : 3)); 475 | 476 | return domainParts.slice(-1 * keepNumber).join('.'); 477 | }; 478 | 479 | var normalizePort = function (useSSL, port) { 480 | port = Number(port || (useSSL ? 443 : 80)); 481 | return ((!useSSL && port === 80) || (useSSL && port === 443)) ? "" : port; 482 | }; 483 | 484 | Utils.isCrossDomainUrl = function(url) { 485 | if (!url) { 486 | return false; 487 | } 488 | 489 | var parser = document.createElement('a'); 490 | parser.href = url; 491 | 492 | var srcPort = normalizePort(window.location.protocol === "https:", window.location.port); 493 | var dstPort = normalizePort(parser.protocol === "https:", parser.port); 494 | 495 | return (window.location.protocol !== parser.protocol) || 496 | (window.location.hostname !== parser.hostname) || 497 | (srcPort !== dstPort); 498 | }; 499 | 500 | var linker = function(method, instance) { 501 | return function() { 502 | return method.apply(instance, arguments); 503 | }; 504 | }; 505 | 506 | var clearTimer = function(timer) { 507 | if (timer) { 508 | window.clearTimeout(timer); 509 | } 510 | return null; 511 | }; 512 | 513 | /* common callbacks */ 514 | var onmessageCallback = function(event) { 515 | Log4js.info("[" + this.type + "] message received", arguments); 516 | var message = Utils.parseMessage(event.data, this.pushstream); 517 | if (message.tag) { this.pushstream._etag = message.tag; } 518 | if (message.time) { this.pushstream._lastModified = message.time; } 519 | if (message.eventid) { this.pushstream._lastEventId = message.eventid; } 520 | this.pushstream._onmessage(message.text, message.id, message.channel, message.eventid, true, message.time); 521 | }; 522 | 523 | var onopenCallback = function() { 524 | this.pushstream._onopen(); 525 | Log4js.info("[" + this.type + "] connection opened"); 526 | }; 527 | 528 | var onerrorCallback = function(event) { 529 | Log4js.info("[" + this.type + "] error (disconnected by server):", event); 530 | if ((this.pushstream.readyState === PushStream.OPEN) && 531 | (this.type === EventSourceWrapper.TYPE) && 532 | (event.type === 'error') && 533 | (this.connection.readyState === window.EventSource.CONNECTING)) { 534 | // EventSource already has a reconnection function using the last-event-id header 535 | return; 536 | } 537 | this._closeCurrentConnection(); 538 | this.pushstream._onerror({type: ((event && ((event.type === "load") || ((event.type === "close") && (event.code === 1006)))) || (this.pushstream.readyState === PushStream.CONNECTING)) ? "load" : "timeout"}); 539 | }; 540 | 541 | /* wrappers */ 542 | 543 | var WebSocketWrapper = function(pushstream) { 544 | if (!window.WebSocket && !window.MozWebSocket) { throw "WebSocket not supported"; } 545 | this.type = WebSocketWrapper.TYPE; 546 | this.pushstream = pushstream; 547 | this.connection = null; 548 | }; 549 | 550 | WebSocketWrapper.TYPE = "WebSocket"; 551 | 552 | WebSocketWrapper.prototype = { 553 | connect: function() { 554 | this._closeCurrentConnection(); 555 | var params = extend({}, this.pushstream.extraParams(), currentTimestampParam(), getControlParams(this.pushstream)); 556 | var url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixWebsocket, params, !this.pushstream._useControlArguments()); 557 | this.connection = (window.WebSocket) ? new window.WebSocket(url) : new window.MozWebSocket(url); 558 | this.connection.onerror = linker(onerrorCallback, this); 559 | this.connection.onclose = linker(onerrorCallback, this); 560 | this.connection.onopen = linker(onopenCallback, this); 561 | this.connection.onmessage = linker(onmessageCallback, this); 562 | Log4js.info("[WebSocket] connecting to:", url); 563 | }, 564 | 565 | disconnect: function() { 566 | if (this.connection) { 567 | Log4js.debug("[WebSocket] closing connection to:", this.connection.url); 568 | this.connection.onclose = null; 569 | this._closeCurrentConnection(); 570 | this.pushstream._onclose(); 571 | } 572 | }, 573 | 574 | _closeCurrentConnection: function() { 575 | if (this.connection) { 576 | try { this.connection.close(); } catch (e) { /* ignore error on closing */ } 577 | this.connection = null; 578 | } 579 | }, 580 | 581 | sendMessage: function(message) { 582 | if (this.connection) { this.connection.send(message); } 583 | } 584 | }; 585 | 586 | var EventSourceWrapper = function(pushstream) { 587 | if (!window.EventSource) { throw "EventSource not supported"; } 588 | this.type = EventSourceWrapper.TYPE; 589 | this.pushstream = pushstream; 590 | this.connection = null; 591 | }; 592 | 593 | EventSourceWrapper.TYPE = "EventSource"; 594 | 595 | EventSourceWrapper.prototype = { 596 | connect: function() { 597 | this._closeCurrentConnection(); 598 | var params = extend({}, this.pushstream.extraParams(), currentTimestampParam(), getControlParams(this.pushstream)); 599 | var url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixEventsource, params, !this.pushstream._useControlArguments()); 600 | this.connection = new window.EventSource(url); 601 | this.connection.onerror = linker(onerrorCallback, this); 602 | this.connection.onopen = linker(onopenCallback, this); 603 | this.connection.onmessage = linker(onmessageCallback, this); 604 | Log4js.info("[EventSource] connecting to:", url); 605 | }, 606 | 607 | disconnect: function() { 608 | if (this.connection) { 609 | Log4js.debug("[EventSource] closing connection to:", this.connection.url); 610 | this.connection.onclose = null; 611 | this._closeCurrentConnection(); 612 | this.pushstream._onclose(); 613 | } 614 | }, 615 | 616 | _closeCurrentConnection: function() { 617 | if (this.connection) { 618 | try { this.connection.close(); } catch (e) { /* ignore error on closing */ } 619 | this.connection = null; 620 | } 621 | } 622 | }; 623 | 624 | var StreamWrapper = function(pushstream) { 625 | this.type = StreamWrapper.TYPE; 626 | this.pushstream = pushstream; 627 | this.connection = null; 628 | this.url = null; 629 | this.frameloadtimer = null; 630 | this.pingtimer = null; 631 | this.iframeId = "PushStreamManager_" + pushstream.id; 632 | }; 633 | 634 | StreamWrapper.TYPE = "Stream"; 635 | 636 | StreamWrapper.prototype = { 637 | connect: function() { 638 | this._closeCurrentConnection(); 639 | var domain = Utils.extract_xss_domain(this.pushstream.host); 640 | try { 641 | document.domain = domain; 642 | } catch(e) { 643 | Log4js.error("[Stream] (warning) problem setting document.domain = " + domain + " (OBS: IE8 does not support set IP numbers as domain)"); 644 | } 645 | var params = extend({}, this.pushstream.extraParams(), currentTimestampParam(), {"streamid": this.pushstream.id}, getControlParams(this.pushstream)); 646 | this.url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixStream, params, !this.pushstream._useControlArguments()); 647 | Log4js.debug("[Stream] connecting to:", this.url); 648 | this.loadFrame(this.url); 649 | }, 650 | 651 | disconnect: function() { 652 | if (this.connection) { 653 | Log4js.debug("[Stream] closing connection to:", this.url); 654 | this._closeCurrentConnection(); 655 | this.pushstream._onclose(); 656 | } 657 | }, 658 | 659 | _clear_iframe: function() { 660 | var oldIframe = document.getElementById(this.iframeId); 661 | if (oldIframe) { 662 | oldIframe.onload = null; 663 | oldIframe.src = "about:blank"; 664 | if (oldIframe.parentNode) { oldIframe.parentNode.removeChild(oldIframe); } 665 | } 666 | }, 667 | 668 | _closeCurrentConnection: function() { 669 | this._clear_iframe(); 670 | if (this.connection) { 671 | this.pingtimer = clearTimer(this.pingtimer); 672 | this.frameloadtimer = clearTimer(this.frameloadtimer); 673 | this.connection = null; 674 | this.transferDoc = null; 675 | if (typeof window.CollectGarbage === 'function') { window.CollectGarbage(); } 676 | } 677 | }, 678 | 679 | loadFrame: function(url) { 680 | this._clear_iframe(); 681 | 682 | var ifr = null; 683 | if ("ActiveXObject" in window) { 684 | var transferDoc = new window.ActiveXObject("htmlfile"); 685 | transferDoc.open(); 686 | transferDoc.write("\x3C" + "html" + "\x3E\x3C" + "script" + "\x3E" + "document.domain='" + document.domain + "';\x3C" + "/script" + "\x3E"); 687 | transferDoc.write("\x3C" + "body" + "\x3E\x3C" + "iframe id='" + this.iframeId + "' src='" + url + "'\x3E\x3C" + "/iframe" + "\x3E\x3C" + "/body" + "\x3E\x3C" + "/html" + "\x3E"); 688 | transferDoc.parentWindow.PushStream = PushStream; 689 | transferDoc.close(); 690 | ifr = transferDoc.getElementById(this.iframeId); 691 | this.transferDoc = transferDoc; 692 | } else { 693 | ifr = document.createElement("IFRAME"); 694 | ifr.style.width = "1px"; 695 | ifr.style.height = "1px"; 696 | ifr.style.border = "none"; 697 | ifr.style.position = "absolute"; 698 | ifr.style.top = "-10px"; 699 | ifr.style.marginTop = "-10px"; 700 | ifr.style.zIndex = "-20"; 701 | ifr.PushStream = PushStream; 702 | document.body.appendChild(ifr); 703 | ifr.setAttribute("src", url); 704 | ifr.setAttribute("id", this.iframeId); 705 | } 706 | 707 | ifr.onload = linker(onerrorCallback, this); 708 | this.connection = ifr; 709 | this.frameloadtimer = window.setTimeout(linker(onerrorCallback, this), this.pushstream.timeout); 710 | }, 711 | 712 | register: function(iframeWindow) { 713 | this.frameloadtimer = clearTimer(this.frameloadtimer); 714 | iframeWindow.p = linker(this.process, this); 715 | this.connection.onload = linker(this._onframeloaded, this); 716 | this.pushstream._onopen(); 717 | this.setPingTimer(); 718 | Log4js.info("[Stream] frame registered"); 719 | }, 720 | 721 | process: function(id, channel, text, eventid, time, tag) { 722 | this.pingtimer = clearTimer(this.pingtimer); 723 | Log4js.info("[Stream] message received", arguments); 724 | if (id !== -1) { 725 | if (tag) { this.pushstream._etag = tag; } 726 | if (time) { this.pushstream._lastModified = time; } 727 | if (eventid) { this.pushstream._lastEventId = eventid; } 728 | } 729 | this.pushstream._onmessage(unescapeText(text), id, channel, eventid || "", true, time); 730 | this.setPingTimer(); 731 | }, 732 | 733 | _onframeloaded: function() { 734 | Log4js.info("[Stream] frame loaded (disconnected by server)"); 735 | this.pushstream._onerror({type: "timeout"}); 736 | this.connection.onload = null; 737 | this.disconnect(); 738 | }, 739 | 740 | setPingTimer: function() { 741 | if (this.pingtimer) { clearTimer(this.pingtimer); } 742 | this.pingtimer = window.setTimeout(linker(onerrorCallback, this), this.pushstream.pingtimeout); 743 | } 744 | }; 745 | 746 | var LongPollingWrapper = function(pushstream) { 747 | this.type = LongPollingWrapper.TYPE; 748 | this.pushstream = pushstream; 749 | this.connection = null; 750 | this.opentimer = null; 751 | this.messagesQueue = []; 752 | this._linkedInternalListen = linker(this._internalListen, this); 753 | this.xhrSettings = { 754 | timeout: this.pushstream.timeout, 755 | data: {}, 756 | url: null, 757 | success: linker(this.onmessage, this), 758 | error: linker(this.onerror, this), 759 | load: linker(this.onload, this), 760 | beforeSend: linker(this.beforeSend, this), 761 | afterReceive: linker(this.afterReceive, this) 762 | }; 763 | }; 764 | 765 | LongPollingWrapper.TYPE = "LongPolling"; 766 | 767 | LongPollingWrapper.prototype = { 768 | connect: function() { 769 | this.messagesQueue = []; 770 | this._closeCurrentConnection(); 771 | this.urlWithBacktrack = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixLongpolling, {}, true); 772 | this.urlWithoutBacktrack = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixLongpolling, {}, false); 773 | this.xhrSettings.url = this.urlWithBacktrack; 774 | this.useJSONP = this.pushstream._crossDomain || this.pushstream.useJSONP; 775 | this.xhrSettings.scriptId = "PushStreamManager_" + this.pushstream.id; 776 | if (this.useJSONP) { 777 | this.pushstream.messagesControlByArgument = true; 778 | } 779 | this._listen(); 780 | this.opentimer = window.setTimeout(linker(onopenCallback, this), 150); 781 | Log4js.info("[LongPolling] connecting to:", this.xhrSettings.url); 782 | }, 783 | 784 | _listen: function() { 785 | if (this._internalListenTimeout) { clearTimer(this._internalListenTimeout); } 786 | this._internalListenTimeout = window.setTimeout(this._linkedInternalListen, 100); 787 | }, 788 | 789 | _internalListen: function() { 790 | if (this.pushstream._keepConnected) { 791 | this.xhrSettings.url = this.pushstream._useControlArguments() ? this.urlWithoutBacktrack : this.urlWithBacktrack; 792 | this.xhrSettings.data = extend({}, this.pushstream.extraParams(), this.xhrSettings.data, getControlParams(this.pushstream)); 793 | if (this.useJSONP) { 794 | this.connection = Ajax.jsonp(this.xhrSettings); 795 | } else if (!this.connection) { 796 | this.connection = Ajax.load(this.xhrSettings); 797 | } 798 | } 799 | }, 800 | 801 | disconnect: function() { 802 | if (this.connection) { 803 | Log4js.debug("[LongPolling] closing connection to:", this.xhrSettings.url); 804 | this._closeCurrentConnection(); 805 | this.pushstream._onclose(); 806 | } 807 | }, 808 | 809 | _closeCurrentConnection: function() { 810 | this.opentimer = clearTimer(this.opentimer); 811 | if (this.connection) { 812 | try { this.connection.abort(); } catch (e) { 813 | try { Ajax.clear(this.connection); } catch (e1) { /* ignore error on closing */ } 814 | } 815 | this.connection = null; 816 | this.xhrSettings.url = null; 817 | } 818 | }, 819 | 820 | beforeSend: function(xhr) { 821 | if (!this.pushstream.messagesControlByArgument) { 822 | xhr.setRequestHeader("If-None-Match", this.pushstream._etag); 823 | xhr.setRequestHeader("If-Modified-Since", this.pushstream._lastModified); 824 | } 825 | }, 826 | 827 | afterReceive: function(xhr) { 828 | if (!this.pushstream.messagesControlByArgument) { 829 | this.pushstream._etag = xhr.getResponseHeader('Etag'); 830 | this.pushstream._lastModified = xhr.getResponseHeader('Last-Modified'); 831 | } 832 | this.connection = null; 833 | }, 834 | 835 | onerror: function(status) { 836 | this._closeCurrentConnection(); 837 | if (this.pushstream._keepConnected) { /* abort(), called by disconnect(), call this callback, but should be ignored */ 838 | if (status === 304) { 839 | this._listen(); 840 | } else { 841 | Log4js.info("[LongPolling] error (disconnected by server):", status); 842 | this.pushstream._onerror({type: ((status === 403) || (this.pushstream.readyState === PushStream.CONNECTING)) ? "load" : "timeout"}); 843 | } 844 | } 845 | }, 846 | 847 | onload: function() { 848 | this._listen(); 849 | }, 850 | 851 | onmessage: function(responseText) { 852 | if (this._internalListenTimeout) { clearTimer(this._internalListenTimeout); } 853 | Log4js.info("[LongPolling] message received", responseText); 854 | var lastMessage = null; 855 | var messages = isArray(responseText) ? responseText : responseText.replace(/\}\{/g, "}\r\n{").split("\r\n"); 856 | for (var i = 0; i < messages.length; i++) { 857 | if (messages[i]) { 858 | lastMessage = Utils.parseMessage(messages[i], this.pushstream); 859 | this.messagesQueue.push(lastMessage); 860 | if (this.pushstream.messagesControlByArgument && lastMessage.time) { 861 | this.pushstream._etag = lastMessage.tag; 862 | this.pushstream._lastModified = lastMessage.time; 863 | } 864 | } 865 | } 866 | 867 | this._listen(); 868 | 869 | while (this.messagesQueue.length > 0) { 870 | var message = this.messagesQueue.shift(); 871 | this.pushstream._onmessage(message.text, message.id, message.channel, message.eventid, (this.messagesQueue.length === 0), message.time); 872 | } 873 | } 874 | }; 875 | 876 | /* mains class */ 877 | 878 | var PushStreamManager = []; 879 | 880 | var PushStream = function(settings) { 881 | settings = settings || {}; 882 | 883 | this.id = PushStreamManager.push(this) - 1; 884 | 885 | this.useSSL = settings.useSSL || false; 886 | this.host = settings.host || window.location.hostname; 887 | this.port = Number(settings.port || (this.useSSL ? 443 : 80)); 888 | 889 | this.timeout = settings.timeout || 30000; 890 | this.pingtimeout = settings.pingtimeout || 30000; 891 | this.reconnectOnTimeoutInterval = settings.reconnectOnTimeoutInterval || 3000; 892 | this.reconnectOnChannelUnavailableInterval = settings.reconnectOnChannelUnavailableInterval || 60000; 893 | this.autoReconnect = (settings.autoReconnect !== false); 894 | 895 | this.lastEventId = settings.lastEventId || null; 896 | this.messagesPublishedAfter = settings.messagesPublishedAfter; 897 | this.messagesControlByArgument = settings.messagesControlByArgument || false; 898 | this.tagArgument = settings.tagArgument || 'tag'; 899 | this.timeArgument = settings.timeArgument || 'time'; 900 | this.eventIdArgument = settings.eventIdArgument || 'eventid'; 901 | this.useJSONP = settings.useJSONP || false; 902 | 903 | this._reconnecttimer = null; 904 | this._etag = 0; 905 | this._lastModified = null; 906 | this._lastEventId = null; 907 | 908 | this.urlPrefixPublisher = settings.urlPrefixPublisher || '/pub'; 909 | this.urlPrefixStream = settings.urlPrefixStream || '/sub'; 910 | this.urlPrefixEventsource = settings.urlPrefixEventsource || '/ev'; 911 | this.urlPrefixLongpolling = settings.urlPrefixLongpolling || '/lp'; 912 | this.urlPrefixWebsocket = settings.urlPrefixWebsocket || '/ws'; 913 | 914 | this.jsonIdKey = settings.jsonIdKey || 'id'; 915 | this.jsonChannelKey = settings.jsonChannelKey || 'channel'; 916 | this.jsonTextKey = settings.jsonTextKey || 'text'; 917 | this.jsonTagKey = settings.jsonTagKey || 'tag'; 918 | this.jsonTimeKey = settings.jsonTimeKey || 'time'; 919 | this.jsonEventIdKey = settings.jsonEventIdKey || 'eventid'; 920 | 921 | this.modes = (settings.modes || 'eventsource|websocket|stream|longpolling').split('|'); 922 | this.wrappers = []; 923 | this.wrapper = null; 924 | 925 | this.onchanneldeleted = settings.onchanneldeleted || null; 926 | this.onmessage = settings.onmessage || null; 927 | this.onerror = settings.onerror || null; 928 | this.onstatuschange = settings.onstatuschange || null; 929 | this.extraParams = settings.extraParams || function() { return {}; }; 930 | 931 | this.channels = {}; 932 | this.channelsCount = 0; 933 | this.channelsByArgument = settings.channelsByArgument || false; 934 | this.channelsArgument = settings.channelsArgument || 'channels'; 935 | 936 | this._crossDomain = Utils.isCrossDomainUrl(getPublisherUrl(this)); 937 | 938 | for (var i = 0; i < this.modes.length; i++) { 939 | try { 940 | var wrapper = null; 941 | switch (this.modes[i]) { 942 | case "websocket" : wrapper = new WebSocketWrapper(this); break; 943 | case "eventsource": wrapper = new EventSourceWrapper(this); break; 944 | case "longpolling": wrapper = new LongPollingWrapper(this); break; 945 | case "stream" : wrapper = new StreamWrapper(this); break; 946 | } 947 | this.wrappers[this.wrappers.length] = wrapper; 948 | } catch(e) { Log4js.info(e); } 949 | } 950 | 951 | this.readyState = 0; 952 | }; 953 | 954 | /* constants */ 955 | PushStream.LOG_LEVEL = 'error'; /* debug, info, error */ 956 | PushStream.LOG_OUTPUT_ELEMENT_ID = 'Log4jsLogOutput'; 957 | 958 | /* status codes */ 959 | PushStream.CLOSED = 0; 960 | PushStream.CONNECTING = 1; 961 | PushStream.OPEN = 2; 962 | 963 | /* main code */ 964 | PushStream.prototype = { 965 | addChannel: function(channel, options) { 966 | if (escapeText(channel) !== channel) { 967 | throw "Invalid channel name! Channel has to be a set of [a-zA-Z0-9]"; 968 | } 969 | Log4js.debug("entering addChannel"); 970 | if (typeof(this.channels[channel]) !== "undefined") { throw "Cannot add channel " + channel + ": already subscribed"; } 971 | options = options || {}; 972 | Log4js.info("adding channel", channel, options); 973 | this.channels[channel] = options; 974 | this.channelsCount++; 975 | if (this.readyState !== PushStream.CLOSED) { this.connect(); } 976 | Log4js.debug("leaving addChannel"); 977 | }, 978 | 979 | removeChannel: function(channel) { 980 | if (this.channels[channel]) { 981 | Log4js.info("removing channel", channel); 982 | delete this.channels[channel]; 983 | this.channelsCount--; 984 | } 985 | }, 986 | 987 | removeAllChannels: function() { 988 | Log4js.info("removing all channels"); 989 | this.channels = {}; 990 | this.channelsCount = 0; 991 | }, 992 | 993 | _setState: function(state) { 994 | if (this.readyState !== state) { 995 | Log4js.info("status changed", state); 996 | this.readyState = state; 997 | if (this.onstatuschange) { 998 | this.onstatuschange(this.readyState); 999 | } 1000 | } 1001 | }, 1002 | 1003 | connect: function() { 1004 | Log4js.debug("entering connect"); 1005 | if (!this.host) { throw "PushStream host not specified"; } 1006 | if (isNaN(this.port)) { throw "PushStream port not specified"; } 1007 | if (!this.channelsCount) { throw "No channels specified"; } 1008 | if (this.wrappers.length === 0) { throw "No available support for this browser"; } 1009 | 1010 | this._keepConnected = true; 1011 | this._lastUsedMode = 0; 1012 | this._connect(); 1013 | 1014 | Log4js.debug("leaving connect"); 1015 | }, 1016 | 1017 | disconnect: function() { 1018 | Log4js.debug("entering disconnect"); 1019 | this._keepConnected = false; 1020 | this._disconnect(); 1021 | this._setState(PushStream.CLOSED); 1022 | Log4js.info("disconnected"); 1023 | Log4js.debug("leaving disconnect"); 1024 | }, 1025 | 1026 | _useControlArguments :function() { 1027 | return this.messagesControlByArgument && ((this._lastModified !== null) || (this._lastEventId !== null)); 1028 | }, 1029 | 1030 | _connect: function() { 1031 | if (this._lastEventId === null) { 1032 | this._lastEventId = this.lastEventId; 1033 | } 1034 | if (this._lastModified === null) { 1035 | var date = this.messagesPublishedAfter; 1036 | if (!isDate(date)) { 1037 | var messagesPublishedAfter = Number(this.messagesPublishedAfter); 1038 | if (messagesPublishedAfter > 0) { 1039 | date = new Date(); 1040 | date.setTime(date.getTime() - (messagesPublishedAfter * 1000)); 1041 | } else if (messagesPublishedAfter < 0) { 1042 | date = new Date(0); 1043 | } 1044 | } 1045 | 1046 | if (isDate(date)) { 1047 | this._lastModified = Utils.dateToUTCString(date); 1048 | } 1049 | } 1050 | 1051 | this._disconnect(); 1052 | this._setState(PushStream.CONNECTING); 1053 | this.wrapper = this.wrappers[this._lastUsedMode++ % this.wrappers.length]; 1054 | 1055 | try { 1056 | this.wrapper.connect(); 1057 | } catch (e) { 1058 | //each wrapper has a cleanup routine at disconnect method 1059 | if (this.wrapper) { 1060 | this.wrapper.disconnect(); 1061 | } 1062 | } 1063 | }, 1064 | 1065 | _disconnect: function() { 1066 | this._reconnecttimer = clearTimer(this._reconnecttimer); 1067 | if (this.wrapper) { 1068 | this.wrapper.disconnect(); 1069 | } 1070 | }, 1071 | 1072 | _onopen: function() { 1073 | this._reconnecttimer = clearTimer(this._reconnecttimer); 1074 | this._setState(PushStream.OPEN); 1075 | if (this._lastUsedMode > 0) { 1076 | this._lastUsedMode--; //use same mode on next connection 1077 | } 1078 | }, 1079 | 1080 | _onclose: function() { 1081 | this._reconnecttimer = clearTimer(this._reconnecttimer); 1082 | this._setState(PushStream.CLOSED); 1083 | this._reconnect(this.reconnectOnTimeoutInterval); 1084 | }, 1085 | 1086 | _onmessage: function(text, id, channel, eventid, isLastMessageFromBatch, time) { 1087 | Log4js.debug("message", text, id, channel, eventid, isLastMessageFromBatch, time); 1088 | if (id === -2) { 1089 | if (this.onchanneldeleted) { this.onchanneldeleted(channel); } 1090 | } else if (id > 0) { 1091 | if (this.onmessage) { this.onmessage(text, id, channel, eventid, isLastMessageFromBatch, time); } 1092 | } 1093 | }, 1094 | 1095 | _onerror: function(error) { 1096 | this._setState(PushStream.CLOSED); 1097 | this._reconnect((error.type === "timeout") ? this.reconnectOnTimeoutInterval : this.reconnectOnChannelUnavailableInterval); 1098 | if (this.onerror) { this.onerror(error); } 1099 | }, 1100 | 1101 | _reconnect: function(timeout) { 1102 | if (this.autoReconnect && this._keepConnected && !this._reconnecttimer && (this.readyState !== PushStream.CONNECTING)) { 1103 | Log4js.info("trying to reconnect in", timeout); 1104 | this._reconnecttimer = window.setTimeout(linker(this._connect, this), timeout); 1105 | } 1106 | }, 1107 | 1108 | sendMessage: function(message, successCallback, errorCallback) { 1109 | message = escapeText(message); 1110 | if (this.wrapper.type === WebSocketWrapper.TYPE) { 1111 | this.wrapper.sendMessage(message); 1112 | if (successCallback) { successCallback(); } 1113 | } else { 1114 | Ajax.post({url: getPublisherUrl(this), data: message, success: successCallback, error: errorCallback, crossDomain: this._crossDomain}); 1115 | } 1116 | } 1117 | }; 1118 | 1119 | PushStream.sendMessage = function(url, message, successCallback, errorCallback) { 1120 | Ajax.post({url: url, data: escapeText(message), success: successCallback, error: errorCallback}); 1121 | }; 1122 | 1123 | // to make server header template more clear, it calls register and 1124 | // by a url parameter we find the stream wrapper instance 1125 | PushStream.register = function(iframe) { 1126 | var matcher = iframe.window.location.href.match(/streamid=([0-9]*)&?/); 1127 | if (matcher[1] && PushStreamManager[matcher[1]]) { 1128 | PushStreamManager[matcher[1]].wrapper.register(iframe); 1129 | } 1130 | }; 1131 | 1132 | PushStream.unload = function() { 1133 | for (var i = 0; i < PushStreamManager.length; i++) { 1134 | try { PushStreamManager[i].disconnect(); } catch(e){} 1135 | } 1136 | }; 1137 | 1138 | /* make class public */ 1139 | window.PushStream = PushStream; 1140 | window.PushStreamManager = PushStreamManager; 1141 | 1142 | if (window.attachEvent) { window.attachEvent("onunload", PushStream.unload); } 1143 | if (window.addEventListener) { window.addEventListener.call(window, "unload", PushStream.unload, false); } 1144 | 1145 | })(window, document); 1146 | 1147 | // shhgit 1148 | document.addEventListener('DOMContentLoaded', function(event) { 1149 | window.connection = null; 1150 | window.timeout = null; 1151 | 1152 | var settings = { 1153 | activeSignatures: [], 1154 | burger: document.getElementById('burger'), 1155 | connectionStats: document.getElementById('connection-status'), 1156 | interestingFiles: document.getElementById('setting-interesting-files'), 1157 | highEntropyStrings: document.getElementById('setting-high-entropy-strings'), 1158 | notifications: document.getElementById('setting-notifications'), 1159 | matchesCount: document.getElementById('matches-count').getElementsByTagName('span')[0], 1160 | filtersClear: document.getElementById('filters-clear'), 1161 | filtersCount: document.getElementById('filters-count').getElementsByTagName('span')[0] 1162 | }; 1163 | const slugify = (value) => value.toLowerCase().replace(/[^a-z0-9 -]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-'); 1164 | const getFileUrl = (data) => { 1165 | if (!data.Url.substr(-4) === '.git') return data.Url; 1166 | 1167 | var source = getSource(data.Source); 1168 | var prefix = source.icon == 'bitbucket' ? 'src' : 'blob'; 1169 | 1170 | return `${data.Url.substr(0, data.Url.indexOf('.git'))}/${prefix}/master${data.File}`; 1171 | }; 1172 | const getSource = (source) => { 1173 | switch (source) { 1174 | case 0: return {icon: 'github', name: 'GitHub'}; 1175 | case 1: return {icon: 'github-square', name: 'Gist'}; 1176 | case 2: return {icon: 'bitbucket', name: 'BitBucket'}; 1177 | case 3: return {icon: 'gitlab', name: 'GitLab'}; 1178 | } 1179 | }; 1180 | const getIssueUrl = (data) => { 1181 | var root = data.Url.substr(0, data.Url.indexOf('.git')); 1182 | var title = encodeURIComponent(`Exposed ${data.Signature}`); 1183 | var description = encodeURIComponent(`Potential security breach. See ${data.File}`); 1184 | var source = getSource(data.Source); 1185 | 1186 | switch (source.icon) { 1187 | case 'github': return `${root}/issues/new?title=${title}&body=${description}`; 1188 | case 'gitlab': return `${root}/issues/new?issue[title]=${title}&issue[description]=${description}`; 1189 | } 1190 | }; 1191 | const sort = (list) => { 1192 | signatures = list.getElementsByTagName("li"); 1193 | Array.from(signatures) 1194 | .sort((a, b) => parseInt(b.getElementsByClassName('menu-item')[0].getAttribute('data-badge') || 0) - parseInt(a.getElementsByClassName('menu-item')[0].getAttribute('data-badge') || 0)) 1195 | .forEach(li => list.appendChild(li)); 1196 | }; 1197 | const updateStatus = (text, cls) => { 1198 | settings.connectionStats.classList.remove('is-info', 'is-success', 'is-warning', 'is-danger'); 1199 | settings.connectionStats.classList.add(cls); 1200 | settings.connectionStats.textContent = text; 1201 | }; 1202 | const filterSignature = (signature) => { 1203 | var state = settings.activeSignatures.includes(signature.id); 1204 | signature.classList.toggle('is-active'); 1205 | 1206 | if (!state) settings.activeSignatures.push(signature.id); 1207 | 1208 | Array.from(document.getElementsByClassName('log')).forEach(log => log.style.display = 'none'); 1209 | settings.activeSignatures.forEach((signatureId) => { 1210 | Array.from(document.getElementsByClassName(signatureId)).forEach(log => { 1211 | if (state && signatureId == signature.id) return; 1212 | if (!settings.interestingFiles.checked && log.classList.contains('is-interesting-file')) return; 1213 | if (!settings.highEntropyStrings.checked && log.classList.contains('is-high-entropy-string')) return; 1214 | 1215 | log.style.display = ''; 1216 | }); 1217 | }); 1218 | 1219 | if (state) { 1220 | settings.activeSignatures.splice(settings.activeSignatures.indexOf(signature.id), 1); 1221 | var anyActive = (settings.activeSignatures.length > 0); 1222 | Array.from(document.getElementsByClassName(anyActive ? signature.id : 'log')).forEach(log => { 1223 | if (!settings.interestingFiles.checked && log.classList.contains('is-interesting-file')) return; 1224 | if (!settings.highEntropyStrings.checked && log.classList.contains('is-high-entropy-string')) return; 1225 | 1226 | log.style.display = anyActive ? 'none' : ''; 1227 | }); 1228 | } 1229 | 1230 | settings.filtersCount.textContent = `${settings.activeSignatures.length} filters`; 1231 | }; 1232 | const processEvent = (data) => { 1233 | var eventId = CryptoJS.MD5(data.File + '-' + data.Signature + '-' + (data.Matches ? data.Matches.join('') : '0')).toString(); 1234 | if (document.getElementById(eventId)) return; // duplicate 1235 | 1236 | var sigId = slugify(data.Signature); 1237 | var matchesCount = data.Matches ? data.Matches.length : 1; 1238 | var sigMenuItem = document.getElementById(sigId).getElementsByClassName('menu-item')[0]; 1239 | var source = getSource(data.Source) 1240 | sigMenuItem.setAttribute('data-badge', parseInt(sigMenuItem.getAttribute('data-badge') || 0) + matchesCount); 1241 | sort(document.getElementById('signatures')); 1242 | 1243 | var row = document.getElementById('messages').insertRow(0); 1244 | row.classList.add('log', sigId); 1245 | row.id = eventId; 1246 | row.insertCell(0).innerHTML = ``; 1247 | row.insertCell(1).innerHTML = `${new Date().toLocaleTimeString()}`; 1248 | row.insertCell(2).innerHTML = `${data.Signature}${source.icon != 'bitbucket' ? `` : ''}`; 1249 | row.insertCell(3).innerHTML = `
${data.Matches ? "
" + data.Matches.join('
') + "
" : ''}
`; 1250 | row.insertCell(4).innerHTML = `${data.File}`; 1251 | row.insertCell(5).innerHTML = `${data.Stars}` 1252 | row.addEventListener('click', (event) => { 1253 | event.preventDefault(); 1254 | window.open(getFileUrl(data), '_blank'); 1255 | }); 1256 | settings.matchesCount.textContent = `${document.getElementsByClassName('log').length} matches`; 1257 | 1258 | if (!data.Matches) { 1259 | row.classList.add('is-interesting-file') 1260 | if (!settings.interestingFiles.checked) row.style.display = 'none'; 1261 | } 1262 | 1263 | if (data.Signature === "High entropy string") { 1264 | row.classList.add('is-high-entropy-string') 1265 | if (!settings.highEntropyStrings.checked) row.style.display = 'none'; 1266 | } 1267 | 1268 | if (settings.activeSignatures.length > 0 && !settings.activeSignatures.includes(sigId)) row.style.display = 'none'; 1269 | if (settings.notifications.checked) notifyFinding(data.Signature, data.Matches ? data.Matches.join(', ') : data.File); 1270 | }; 1271 | const listenForEvents = () => { 1272 | window.connection = new PushStream({ 1273 | host: 'localhost', 1274 | port: 8080, 1275 | urlPrefixEventsource: '/events', 1276 | useSSL: false, 1277 | modes: 'eventsource', 1278 | messagesPublishedAfter: 100, 1279 | messagesControlByArgument: true 1280 | }); 1281 | 1282 | window.connection.onerror = (e) => { 1283 | if (confirm("Error connecting to shhgit. Reload to retry?")) window.location.reload(); 1284 | }; 1285 | window.connection.onstatuschange = (e) => { 1286 | if (e == PushStream.OPEN) updateStatus('Connected', 'is-success'); 1287 | else if (e == PushStream.CONNECTING) updateStatus('Syncing...', 'is-info'); 1288 | }; 1289 | window.connection.onmessage = (text, id, channel, eventid, isLast, time) => { 1290 | if (document.getElementById('loading')) document.getElementById('loading').remove(); 1291 | 1292 | processEvent(text); 1293 | }; 1294 | 1295 | window.connection.addChannel('shhgit'); 1296 | window.connection.connect(); 1297 | }; 1298 | const notifyFinding = (title, message) => { 1299 | if (Notification.permission === "granted") { 1300 | new Notification(title, { 1301 | 'icon': '/logo.png', 1302 | 'body': message 1303 | }); 1304 | } 1305 | }; 1306 | 1307 | (() => { 1308 | burger.addEventListener('click', () => { 1309 | const target = burger.dataset.target; 1310 | const $target = document.getElementById(target); 1311 | 1312 | burger.classList.toggle('is-active'); 1313 | $target.classList.toggle('is-active'); 1314 | }); 1315 | 1316 | settings.interestingFiles.addEventListener('change', (event) => { 1317 | Array.from(document.getElementsByClassName('is-interesting-file')) 1318 | .forEach(log => { 1319 | log.style.display = event.target.checked ? '' : 'none' 1320 | }); 1321 | }); 1322 | 1323 | settings.highEntropyStrings.addEventListener('change', (event) => { 1324 | Array.from(document.getElementsByClassName('is-high-entropy-string')) 1325 | .forEach(log => { 1326 | log.style.display = event.target.checked ? '' : 'none' 1327 | }); 1328 | }); 1329 | 1330 | settings.notifications.addEventListener('change', (event) => { 1331 | Notification.requestPermission().then((permission) => { 1332 | if (permission !== "granted") { 1333 | settings.notifications.checked = false; 1334 | settings.notifications.disabled = true; 1335 | } 1336 | }); 1337 | }); 1338 | 1339 | settings.filtersClear.addEventListener('click', (event) => { 1340 | settings.activeSignatures = []; 1341 | settings.filtersCount.textContent = "0 filters"; 1342 | 1343 | Array.from(document.querySelectorAll('#signatures li.is-active')).forEach(log => log.classList.remove('is-active')); 1344 | Array.from(document.getElementsByClassName('log')).forEach(log => log.style.display = ''); 1345 | }); 1346 | 1347 | fetch(`/signatures.json`) 1348 | .then((resp) => resp.json()) 1349 | .then(signatures => { 1350 | signatures.forEach(signature => { 1351 | var li = document.createElement('li'); 1352 | li.id = slugify(signature) 1353 | li.innerHTML = `${signature}`; 1354 | li.addEventListener('click', (event) => { 1355 | event.preventDefault(); 1356 | filterSignature(li); 1357 | }); 1358 | 1359 | document.getElementById('signatures').appendChild(li); 1360 | }); 1361 | 1362 | listenForEvents(); 1363 | }) 1364 | .catch((err) => { 1365 | alert('Failed to retrieve signatures! Reloading...') 1366 | window.location.reload(); 1367 | }); 1368 | })(); 1369 | }); -------------------------------------------------------------------------------- /www/public/signatures.json: -------------------------------------------------------------------------------- 1 | ["1Password password manager database file","Amazon MWS Auth Token","Apache htpasswd file","Apple Keychain database file","Artifactory","AWS Access Key ID","AWS Access Key ID Value","AWS Account ID","AWS CLI credentials file","AWS cred file info","AWS Secret Access Key","AWS Session Token","Azure service configuration schema file","Carrierwave configuration file","Chef Knife configuration file","Chef private key","CodeClimate","Configuration file for auto-login process","Contains a private key","Contains a private key","cPanel backup ProFTPd credentials file","Day One journal file","DBeaver SQL database manager configuration file","DigitalOcean doctl command-line client configuration file","Django configuration file","Docker configuration file","Docker registry authentication file","Environment configuration file","esmtp configuration","Facebook access token","Facebook Client ID","Facebook Secret Key","FileZilla FTP configuration file","FileZilla FTP recent servers file","Firefox saved passwords DB","git-credential-store helper credentials file","Git configuration file","GitHub Hub command-line client configuration file","Github Key","GNOME Keyring database file","GnuCash database file","Google (GCM) Service account","Google Cloud API Key","Google OAuth Access Token","Google OAuth Key","Heroku API key","Heroku config file","Hexchat/XChat IRC client server list configuration file","High entropy string","HockeyApp","Irssi IRC client configuration file","Java keystore file","Jenkins publish over SSH plugin file","Jetbrains IDE Config","KDE Wallet Manager database file","KeePass password manager database file","Linkedin Client ID","LinkedIn Secret Key","Little Snitch firewall configuration file","Log file","MailChimp API Key","MailGun API Key","Microsoft BitLocker recovery key file","Microsoft BitLocker Trusted Platform Module password file","Microsoft SQL database file","Microsoft SQL server compact database file","Mongoid config file","Mutt e-mail client configuration file","MySQL client command history file","MySQL dump w/ bcrypt hashes","netrc with SMTP credentials","Network traffic capture file","NPM configuration file","NuGet API Key","OmniAuth configuration file","OpenVPN client configuration file","Outlook team","Password Safe database file","PayPal/Braintree Access Token","PHP configuration file","Picatic API key","Pidgin chat client account configuration file","Pidgin OTR private key","PostgreSQL client command history file","PostgreSQL password file","Potential cryptographic private key","Potential Jenkins credentials file","Potential jrnl journal file","Potential Linux passwd file","Potential Linux shadow file","Potential MediaWiki configuration file","Potential private key (.asc)","Potential private key (.p21)","Potential private key (.pem)","Potential private key (.pfx)","Potential private key (.pkcs12)","Potential PuTTYgen private key","Potential Ruby On Rails database configuration file","Private SSH key (.dsa)","Private SSH key (.ecdsa)","Private SSH key (.ed25519)","Private SSH key (.rsa)","Public ssh key","Recon-ng web reconnaissance framework API key database","remote-sync for Atom","Remote Desktop connection file","Robomongo MongoDB manager configuration file","Rubygems credentials file","Ruby IRB console history file","Ruby on Rails master key","Ruby on Rails secrets","Ruby On Rails secret token configuration file","S3cmd configuration file","Salesforce credentials","Sauce Token","Sequel Pro MySQL database manager bookmark file","sftp-deployment for Atom","sftp-deployment for Atom","SFTP connection configuration file","Shell command alias configuration file","Shell command history file","Shell configuration file (.bashrc, .zshrc, .cshrc)","Shell configuration file (.exports)","Shell configuration file (.extra)","Shell configuration file (.functions)","Shell profile configuration file","Slack Token","Slack Webhook","SonarQube Docs API Key","SQL Data dump file","SQL dump file","SQLite3 database file","SQLite database file","Square Access Token","Square OAuth Secret","SSH configuration file","SSH Password","Stripe API key","T command-line Twitter client configuration file","Terraform variable config file","Tugboat DigitalOcean management tool configuration","Tunnelblick VPN configuration file","Twilo API Key","Twitter Client ID","Twitter Secret Key","Username and password in URI","Ventrilo server configuration file","vscode-sftp for VSCode","Windows BitLocker full volume encrypted data file","WP-Config"] -------------------------------------------------------------------------------- /www/public/style.css: -------------------------------------------------------------------------------- 1 | html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{ 2 | margin:0; 3 | padding:0 4 | } 5 | h1,h2,h3,h4,h5,h6{ 6 | font-size:100%; 7 | font-weight:normal 8 | } 9 | ul{ 10 | list-style:none 11 | } 12 | button,input,select,textarea{ 13 | margin:0 14 | } 15 | html{ 16 | -webkit-box-sizing:border-box; 17 | box-sizing:border-box 18 | } 19 | *,*::before,*::after{ 20 | -webkit-box-sizing:inherit; 21 | box-sizing:inherit 22 | } 23 | img,audio,video{ 24 | height:auto; 25 | max-width:100% 26 | } 27 | iframe{ 28 | border:0 29 | } 30 | table{ 31 | border-collapse:collapse; 32 | border-spacing:0 33 | } 34 | td,th{ 35 | padding:0; 36 | text-align:left 37 | } 38 | html{ 39 | background-color:#f5f6fa; 40 | font-size:16px; 41 | -moz-osx-font-smoothing:grayscale; 42 | -webkit-font-smoothing:antialiased; 43 | min-width:300px; 44 | overflow-x:hidden; 45 | overflow-y:scroll; 46 | text-rendering:optimizeLegibility; 47 | -webkit-text-size-adjust:100%; 48 | -moz-text-size-adjust:100%; 49 | -ms-text-size-adjust:100%; 50 | text-size-adjust:100% 51 | } 52 | article,aside,figure,footer,header,hgroup,section{ 53 | display:block 54 | } 55 | body,button,input,select,textarea{ 56 | font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif 57 | } 58 | code,pre{ 59 | -moz-osx-font-smoothing:auto; 60 | -webkit-font-smoothing:auto; 61 | font-family:monospace 62 | } 63 | body{ 64 | color:#67757c; 65 | font-size:1rem; 66 | font-weight:400; 67 | line-height:1.5 68 | } 69 | a{ 70 | color:#3273dc; 71 | cursor:pointer; 72 | text-decoration:none 73 | } 74 | a strong{ 75 | color:currentColor 76 | } 77 | a:hover{ 78 | color:#313437 79 | } 80 | 81 | hr{ 82 | background-color:#f5f5f5; 83 | border:none; 84 | display:block; 85 | height:2px; 86 | margin:1.5rem 0 87 | } 88 | img{ 89 | height:auto; 90 | max-width:100% 91 | } 92 | input[type="checkbox"],input[type="radio"]{ 93 | vertical-align:baseline 94 | } 95 | small{ 96 | font-size:0.875em 97 | } 98 | span{ 99 | font-style:inherit; 100 | font-weight:inherit 101 | } 102 | strong{ 103 | color:#313437; 104 | font-weight:700 105 | } 106 | pre{ 107 | background-color: #f5f5f5; 108 | color: #67757c; 109 | font-size: 0.875em; 110 | padding: 1rem; 111 | white-space: pre-wrap; 112 | } 113 | 114 | table td,table th{ 115 | text-align:left; 116 | vertical-align:top 117 | } 118 | table th{ 119 | color:#313437 120 | } 121 | .is-clearfix::after{ 122 | clear:both; 123 | content:" "; 124 | display:table 125 | } 126 | .is-pulled-left{ 127 | float:left !important 128 | } 129 | .is-pulled-right{ 130 | float:right !important 131 | } 132 | .is-clipped{ 133 | overflow:hidden !important 134 | } 135 | .is-size-1{ 136 | font-size:3rem !important 137 | } 138 | .is-size-2{ 139 | font-size:2.5rem !important 140 | } 141 | .is-size-3{ 142 | font-size:2rem !important 143 | } 144 | .is-size-4{ 145 | font-size:1.5rem !important 146 | } 147 | .is-size-5{ 148 | font-size:1.25rem !important 149 | } 150 | .is-size-6{ 151 | font-size:1rem !important 152 | } 153 | .is-size-7{ 154 | font-size:.75rem !important 155 | } 156 | @media screen and (max-width: 768px){ 157 | .is-size-1-mobile{ 158 | font-size:3rem !important 159 | } 160 | .is-size-2-mobile{ 161 | font-size:2.5rem !important 162 | } 163 | .is-size-3-mobile{ 164 | font-size:2rem !important 165 | } 166 | .is-size-4-mobile{ 167 | font-size:1.5rem !important 168 | } 169 | .is-size-5-mobile{ 170 | font-size:1.25rem !important 171 | } 172 | .is-size-6-mobile{ 173 | font-size:1rem !important 174 | } 175 | .is-size-7-mobile{ 176 | font-size:.75rem !important 177 | } 178 | } 179 | @media screen and (min-width: 769px), print{ 180 | .is-size-1-tablet{ 181 | font-size:3rem !important 182 | } 183 | .is-size-2-tablet{ 184 | font-size:2.5rem !important 185 | } 186 | .is-size-3-tablet{ 187 | font-size:2rem !important 188 | } 189 | .is-size-4-tablet{ 190 | font-size:1.5rem !important 191 | } 192 | .is-size-5-tablet{ 193 | font-size:1.25rem !important 194 | } 195 | .is-size-6-tablet{ 196 | font-size:1rem !important 197 | } 198 | .is-size-7-tablet{ 199 | font-size:.75rem !important 200 | } 201 | } 202 | @media screen and (max-width: 1087px){ 203 | .is-size-1-touch{ 204 | font-size:3rem !important 205 | } 206 | .is-size-2-touch{ 207 | font-size:2.5rem !important 208 | } 209 | .is-size-3-touch{ 210 | font-size:2rem !important 211 | } 212 | .is-size-4-touch{ 213 | font-size:1.5rem !important 214 | } 215 | .is-size-5-touch{ 216 | font-size:1.25rem !important 217 | } 218 | .is-size-6-touch{ 219 | font-size:1rem !important 220 | } 221 | .is-size-7-touch{ 222 | font-size:.75rem !important 223 | } 224 | } 225 | @media screen and (min-width: 1088px){ 226 | .is-size-1-desktop{ 227 | font-size:3rem !important 228 | } 229 | .is-size-2-desktop{ 230 | font-size:2.5rem !important 231 | } 232 | .is-size-3-desktop{ 233 | font-size:2rem !important 234 | } 235 | .is-size-4-desktop{ 236 | font-size:1.5rem !important 237 | } 238 | .is-size-5-desktop{ 239 | font-size:1.25rem !important 240 | } 241 | .is-size-6-desktop{ 242 | font-size:1rem !important 243 | } 244 | .is-size-7-desktop{ 245 | font-size:.75rem !important 246 | } 247 | } 248 | @media screen and (min-width: 1280px){ 249 | .is-size-1-widescreen{ 250 | font-size:3rem !important 251 | } 252 | .is-size-2-widescreen{ 253 | font-size:2.5rem !important 254 | } 255 | .is-size-3-widescreen{ 256 | font-size:2rem !important 257 | } 258 | .is-size-4-widescreen{ 259 | font-size:1.5rem !important 260 | } 261 | .is-size-5-widescreen{ 262 | font-size:1.25rem !important 263 | } 264 | .is-size-6-widescreen{ 265 | font-size:1rem !important 266 | } 267 | .is-size-7-widescreen{ 268 | font-size:.75rem !important 269 | } 270 | } 271 | @media screen and (min-width: 1472px){ 272 | .is-size-1-fullhd{ 273 | font-size:3rem !important 274 | } 275 | .is-size-2-fullhd{ 276 | font-size:2.5rem !important 277 | } 278 | .is-size-3-fullhd{ 279 | font-size:2rem !important 280 | } 281 | .is-size-4-fullhd{ 282 | font-size:1.5rem !important 283 | } 284 | .is-size-5-fullhd{ 285 | font-size:1.25rem !important 286 | } 287 | .is-size-6-fullhd{ 288 | font-size:1rem !important 289 | } 290 | .is-size-7-fullhd{ 291 | font-size:.75rem !important 292 | } 293 | } 294 | .has-text-centered{ 295 | text-align:center !important 296 | } 297 | .has-text-justified{ 298 | text-align:justify !important 299 | } 300 | .has-text-left{ 301 | text-align:left !important 302 | } 303 | .has-text-right{ 304 | text-align:right !important 305 | } 306 | @media screen and (max-width: 768px){ 307 | .has-text-centered-mobile{ 308 | text-align:center !important 309 | } 310 | } 311 | @media screen and (min-width: 769px), print{ 312 | .has-text-centered-tablet{ 313 | text-align:center !important 314 | } 315 | } 316 | @media screen and (min-width: 769px) and (max-width: 1087px){ 317 | .has-text-centered-tablet-only{ 318 | text-align:center !important 319 | } 320 | } 321 | @media screen and (max-width: 1087px){ 322 | .has-text-centered-touch{ 323 | text-align:center !important 324 | } 325 | } 326 | @media screen and (min-width: 1088px){ 327 | .has-text-centered-desktop{ 328 | text-align:center !important 329 | } 330 | } 331 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 332 | .has-text-centered-desktop-only{ 333 | text-align:center !important 334 | } 335 | } 336 | @media screen and (min-width: 1280px){ 337 | .has-text-centered-widescreen{ 338 | text-align:center !important 339 | } 340 | } 341 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 342 | .has-text-centered-widescreen-only{ 343 | text-align:center !important 344 | } 345 | } 346 | @media screen and (min-width: 1472px){ 347 | .has-text-centered-fullhd{ 348 | text-align:center !important 349 | } 350 | } 351 | @media screen and (max-width: 768px){ 352 | .has-text-justified-mobile{ 353 | text-align:justify !important 354 | } 355 | } 356 | @media screen and (min-width: 769px), print{ 357 | .has-text-justified-tablet{ 358 | text-align:justify !important 359 | } 360 | } 361 | @media screen and (min-width: 769px) and (max-width: 1087px){ 362 | .has-text-justified-tablet-only{ 363 | text-align:justify !important 364 | } 365 | } 366 | @media screen and (max-width: 1087px){ 367 | .has-text-justified-touch{ 368 | text-align:justify !important 369 | } 370 | } 371 | @media screen and (min-width: 1088px){ 372 | .has-text-justified-desktop{ 373 | text-align:justify !important 374 | } 375 | } 376 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 377 | .has-text-justified-desktop-only{ 378 | text-align:justify !important 379 | } 380 | } 381 | @media screen and (min-width: 1280px){ 382 | .has-text-justified-widescreen{ 383 | text-align:justify !important 384 | } 385 | } 386 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 387 | .has-text-justified-widescreen-only{ 388 | text-align:justify !important 389 | } 390 | } 391 | @media screen and (min-width: 1472px){ 392 | .has-text-justified-fullhd{ 393 | text-align:justify !important 394 | } 395 | } 396 | @media screen and (max-width: 768px){ 397 | .has-text-left-mobile{ 398 | text-align:left !important 399 | } 400 | } 401 | @media screen and (min-width: 769px), print{ 402 | .has-text-left-tablet{ 403 | text-align:left !important 404 | } 405 | } 406 | @media screen and (min-width: 769px) and (max-width: 1087px){ 407 | .has-text-left-tablet-only{ 408 | text-align:left !important 409 | } 410 | } 411 | @media screen and (max-width: 1087px){ 412 | .has-text-left-touch{ 413 | text-align:left !important 414 | } 415 | } 416 | @media screen and (min-width: 1088px){ 417 | .has-text-left-desktop{ 418 | text-align:left !important 419 | } 420 | } 421 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 422 | .has-text-left-desktop-only{ 423 | text-align:left !important 424 | } 425 | } 426 | @media screen and (min-width: 1280px){ 427 | .has-text-left-widescreen{ 428 | text-align:left !important 429 | } 430 | } 431 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 432 | .has-text-left-widescreen-only{ 433 | text-align:left !important 434 | } 435 | } 436 | @media screen and (min-width: 1472px){ 437 | .has-text-left-fullhd{ 438 | text-align:left !important 439 | } 440 | } 441 | @media screen and (max-width: 768px){ 442 | .has-text-right-mobile{ 443 | text-align:right !important 444 | } 445 | } 446 | @media screen and (min-width: 769px), print{ 447 | .has-text-right-tablet{ 448 | text-align:right !important 449 | } 450 | } 451 | @media screen and (min-width: 769px) and (max-width: 1087px){ 452 | .has-text-right-tablet-only{ 453 | text-align:right !important 454 | } 455 | } 456 | @media screen and (max-width: 1087px){ 457 | .has-text-right-touch{ 458 | text-align:right !important 459 | } 460 | } 461 | @media screen and (min-width: 1088px){ 462 | .has-text-right-desktop{ 463 | text-align:right !important 464 | } 465 | } 466 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 467 | .has-text-right-desktop-only{ 468 | text-align:right !important 469 | } 470 | } 471 | @media screen and (min-width: 1280px){ 472 | .has-text-right-widescreen{ 473 | text-align:right !important 474 | } 475 | } 476 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 477 | .has-text-right-widescreen-only{ 478 | text-align:right !important 479 | } 480 | } 481 | @media screen and (min-width: 1472px){ 482 | .has-text-right-fullhd{ 483 | text-align:right !important 484 | } 485 | } 486 | .is-capitalized{ 487 | text-transform:capitalize !important 488 | } 489 | .is-lowercase{ 490 | text-transform:lowercase !important 491 | } 492 | .is-uppercase{ 493 | text-transform:uppercase !important 494 | } 495 | .is-italic{ 496 | font-style:italic !important 497 | } 498 | .has-text-white{ 499 | color:#fff !important 500 | } 501 | a.has-text-white:hover,a.has-text-white:focus{ 502 | color:#e6e5e5 !important 503 | } 504 | .has-background-white{ 505 | background-color:#fff !important 506 | } 507 | .has-text-black{ 508 | color:#0a0a0a !important 509 | } 510 | a.has-text-black:hover,a.has-text-black:focus{ 511 | color:#000 !important 512 | } 513 | .has-background-black{ 514 | background-color:#0a0a0a !important 515 | } 516 | .has-text-light{ 517 | color:#f5f5f5 !important 518 | } 519 | a.has-text-light:hover,a.has-text-light:focus{ 520 | color:#dbdbdb !important 521 | } 522 | .has-background-light{ 523 | background-color:#f5f5f5 !important 524 | } 525 | .has-text-dark{ 526 | color:#2f3d4a !important 527 | } 528 | a.has-text-dark:hover,a.has-text-dark:focus{ 529 | color:#1b232b !important 530 | } 531 | .has-background-dark{ 532 | background-color:#2f3d4a !important 533 | } 534 | .has-text-primary{ 535 | color:#1e88e5 !important 536 | } 537 | a.has-text-primary:hover,a.has-text-primary:focus{ 538 | color:#166dba !important 539 | } 540 | .has-background-primary{ 541 | background-color:#1e88e5 !important 542 | } 543 | .has-text-link{ 544 | color:#3273dc !important 545 | } 546 | a.has-text-link:hover,a.has-text-link:focus{ 547 | color:#205bbc !important 548 | } 549 | .has-background-link{ 550 | background-color:#3273dc !important 551 | } 552 | .has-text-info{ 553 | color:#37aee3 !important 554 | } 555 | a.has-text-info:hover,a.has-text-info:focus{ 556 | color:#1c95cb !important 557 | } 558 | .has-background-info{ 559 | background-color:#37aee3 !important 560 | } 561 | .has-text-success{ 562 | color:#26c6da !important 563 | } 564 | a.has-text-success:hover,a.has-text-success:focus{ 565 | color:#1e9faf !important 566 | } 567 | .has-background-success{ 568 | background-color:#26c6da !important 569 | } 570 | .has-text-warning{ 571 | color:#ffb22b !important 572 | } 573 | a.has-text-warning:hover,a.has-text-warning:focus{ 574 | color:#f79d00 !important 575 | } 576 | .has-background-warning{ 577 | background-color:#ffb22b !important 578 | } 579 | .has-text-danger{ 580 | color:#ef5350 !important 581 | } 582 | a.has-text-danger:hover,a.has-text-danger:focus{ 583 | color:#eb2521 !important 584 | } 585 | .has-background-danger{ 586 | background-color:#ef5350 !important 587 | } 588 | .has-text-black-bis{ 589 | color:#121212 !important 590 | } 591 | .has-background-black-bis{ 592 | background-color:#121212 !important 593 | } 594 | .has-text-black-ter{ 595 | color:#242424 !important 596 | } 597 | .has-background-black-ter{ 598 | background-color:#242424 !important 599 | } 600 | .has-text-grey-darker{ 601 | color:#313437 !important 602 | } 603 | .has-background-grey-darker{ 604 | background-color:#313437 !important 605 | } 606 | .has-text-grey-dark{ 607 | color:#5b748e !important 608 | } 609 | .has-background-grey-dark{ 610 | background-color:#5b748e !important 611 | } 612 | .has-text-grey{ 613 | color:#7a7a7a !important 614 | } 615 | .has-background-grey{ 616 | background-color:#7a7a7a !important 617 | } 618 | .has-text-grey-light{ 619 | color:#b5b5b5 !important 620 | } 621 | .has-background-grey-light{ 622 | background-color:#b5b5b5 !important 623 | } 624 | .has-text-grey-lighter{ 625 | color:rgba(0,0,0,0.08) !important 626 | } 627 | .has-background-grey-lighter{ 628 | background-color:rgba(0,0,0,0.08) !important 629 | } 630 | .has-text-white-ter{ 631 | color:#f5f5f5 !important 632 | } 633 | .has-background-white-ter{ 634 | background-color:#f5f5f5 !important 635 | } 636 | .has-text-white-bis{ 637 | color:#fafafa !important 638 | } 639 | .has-background-white-bis{ 640 | background-color:#fafafa !important 641 | } 642 | .has-text-weight-light{ 643 | font-weight:300 !important 644 | } 645 | .has-text-weight-normal{ 646 | font-weight:400 !important 647 | } 648 | .has-text-weight-semibold{ 649 | font-weight:600 !important 650 | } 651 | .has-text-weight-bold{ 652 | font-weight:700 !important 653 | } 654 | .is-block{ 655 | display:block !important 656 | } 657 | @media screen and (max-width: 768px){ 658 | .is-block-mobile{ 659 | display:block !important 660 | } 661 | } 662 | @media screen and (min-width: 769px), print{ 663 | .is-block-tablet{ 664 | display:block !important 665 | } 666 | } 667 | @media screen and (min-width: 769px) and (max-width: 1087px){ 668 | .is-block-tablet-only{ 669 | display:block !important 670 | } 671 | } 672 | @media screen and (max-width: 1087px){ 673 | .is-block-touch{ 674 | display:block !important 675 | } 676 | } 677 | @media screen and (min-width: 1088px){ 678 | .is-block-desktop{ 679 | display:block !important 680 | } 681 | } 682 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 683 | .is-block-desktop-only{ 684 | display:block !important 685 | } 686 | } 687 | @media screen and (min-width: 1280px){ 688 | .is-block-widescreen{ 689 | display:block !important 690 | } 691 | } 692 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 693 | .is-block-widescreen-only{ 694 | display:block !important 695 | } 696 | } 697 | @media screen and (min-width: 1472px){ 698 | .is-block-fullhd{ 699 | display:block !important 700 | } 701 | } 702 | .is-flex{ 703 | display:-webkit-box !important; 704 | display:-ms-flexbox !important; 705 | display:flex !important 706 | } 707 | @media screen and (max-width: 768px){ 708 | .is-flex-mobile{ 709 | display:-webkit-box !important; 710 | display:-ms-flexbox !important; 711 | display:flex !important 712 | } 713 | } 714 | @media screen and (min-width: 769px), print{ 715 | .is-flex-tablet{ 716 | display:-webkit-box !important; 717 | display:-ms-flexbox !important; 718 | display:flex !important 719 | } 720 | } 721 | @media screen and (min-width: 769px) and (max-width: 1087px){ 722 | .is-flex-tablet-only{ 723 | display:-webkit-box !important; 724 | display:-ms-flexbox !important; 725 | display:flex !important 726 | } 727 | } 728 | @media screen and (max-width: 1087px){ 729 | .is-flex-touch{ 730 | display:-webkit-box !important; 731 | display:-ms-flexbox !important; 732 | display:flex !important 733 | } 734 | } 735 | @media screen and (min-width: 1088px){ 736 | .is-flex-desktop{ 737 | display:-webkit-box !important; 738 | display:-ms-flexbox !important; 739 | display:flex !important 740 | } 741 | } 742 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 743 | .is-flex-desktop-only{ 744 | display:-webkit-box !important; 745 | display:-ms-flexbox !important; 746 | display:flex !important 747 | } 748 | } 749 | @media screen and (min-width: 1280px){ 750 | .is-flex-widescreen{ 751 | display:-webkit-box !important; 752 | display:-ms-flexbox !important; 753 | display:flex !important 754 | } 755 | } 756 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 757 | .is-flex-widescreen-only{ 758 | display:-webkit-box !important; 759 | display:-ms-flexbox !important; 760 | display:flex !important 761 | } 762 | } 763 | @media screen and (min-width: 1472px){ 764 | .is-flex-fullhd{ 765 | display:-webkit-box !important; 766 | display:-ms-flexbox !important; 767 | display:flex !important 768 | } 769 | } 770 | .is-inline{ 771 | display:inline !important 772 | } 773 | @media screen and (max-width: 768px){ 774 | .is-inline-mobile{ 775 | display:inline !important 776 | } 777 | } 778 | @media screen and (min-width: 769px), print{ 779 | .is-inline-tablet{ 780 | display:inline !important 781 | } 782 | } 783 | @media screen and (min-width: 769px) and (max-width: 1087px){ 784 | .is-inline-tablet-only{ 785 | display:inline !important 786 | } 787 | } 788 | @media screen and (max-width: 1087px){ 789 | .is-inline-touch{ 790 | display:inline !important 791 | } 792 | } 793 | @media screen and (min-width: 1088px){ 794 | .is-inline-desktop{ 795 | display:inline !important 796 | } 797 | } 798 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 799 | .is-inline-desktop-only{ 800 | display:inline !important 801 | } 802 | } 803 | @media screen and (min-width: 1280px){ 804 | .is-inline-widescreen{ 805 | display:inline !important 806 | } 807 | } 808 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 809 | .is-inline-widescreen-only{ 810 | display:inline !important 811 | } 812 | } 813 | @media screen and (min-width: 1472px){ 814 | .is-inline-fullhd{ 815 | display:inline !important 816 | } 817 | } 818 | .is-inline-block{ 819 | display:inline-block !important 820 | } 821 | @media screen and (max-width: 768px){ 822 | .is-inline-block-mobile{ 823 | display:inline-block !important 824 | } 825 | } 826 | @media screen and (min-width: 769px), print{ 827 | .is-inline-block-tablet{ 828 | display:inline-block !important 829 | } 830 | } 831 | @media screen and (min-width: 769px) and (max-width: 1087px){ 832 | .is-inline-block-tablet-only{ 833 | display:inline-block !important 834 | } 835 | } 836 | @media screen and (max-width: 1087px){ 837 | .is-inline-block-touch{ 838 | display:inline-block !important 839 | } 840 | } 841 | @media screen and (min-width: 1088px){ 842 | .is-inline-block-desktop{ 843 | display:inline-block !important 844 | } 845 | } 846 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 847 | .is-inline-block-desktop-only{ 848 | display:inline-block !important 849 | } 850 | } 851 | @media screen and (min-width: 1280px){ 852 | .is-inline-block-widescreen{ 853 | display:inline-block !important 854 | } 855 | } 856 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 857 | .is-inline-block-widescreen-only{ 858 | display:inline-block !important 859 | } 860 | } 861 | @media screen and (min-width: 1472px){ 862 | .is-inline-block-fullhd{ 863 | display:inline-block !important 864 | } 865 | } 866 | .is-inline-flex{ 867 | display:-webkit-inline-box !important; 868 | display:-ms-inline-flexbox !important; 869 | display:inline-flex !important 870 | } 871 | @media screen and (max-width: 768px){ 872 | .is-inline-flex-mobile{ 873 | display:-webkit-inline-box !important; 874 | display:-ms-inline-flexbox !important; 875 | display:inline-flex !important 876 | } 877 | } 878 | @media screen and (min-width: 769px), print{ 879 | .is-inline-flex-tablet{ 880 | display:-webkit-inline-box !important; 881 | display:-ms-inline-flexbox !important; 882 | display:inline-flex !important 883 | } 884 | } 885 | @media screen and (min-width: 769px) and (max-width: 1087px){ 886 | .is-inline-flex-tablet-only{ 887 | display:-webkit-inline-box !important; 888 | display:-ms-inline-flexbox !important; 889 | display:inline-flex !important 890 | } 891 | } 892 | @media screen and (max-width: 1087px){ 893 | .is-inline-flex-touch{ 894 | display:-webkit-inline-box !important; 895 | display:-ms-inline-flexbox !important; 896 | display:inline-flex !important 897 | } 898 | } 899 | @media screen and (min-width: 1088px){ 900 | .is-inline-flex-desktop{ 901 | display:-webkit-inline-box !important; 902 | display:-ms-inline-flexbox !important; 903 | display:inline-flex !important 904 | } 905 | } 906 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 907 | .is-inline-flex-desktop-only{ 908 | display:-webkit-inline-box !important; 909 | display:-ms-inline-flexbox !important; 910 | display:inline-flex !important 911 | } 912 | } 913 | @media screen and (min-width: 1280px){ 914 | .is-inline-flex-widescreen{ 915 | display:-webkit-inline-box !important; 916 | display:-ms-inline-flexbox !important; 917 | display:inline-flex !important 918 | } 919 | } 920 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 921 | .is-inline-flex-widescreen-only{ 922 | display:-webkit-inline-box !important; 923 | display:-ms-inline-flexbox !important; 924 | display:inline-flex !important 925 | } 926 | } 927 | @media screen and (min-width: 1472px){ 928 | .is-inline-flex-fullhd{ 929 | display:-webkit-inline-box !important; 930 | display:-ms-inline-flexbox !important; 931 | display:inline-flex !important 932 | } 933 | } 934 | .is-hidden{ 935 | display:none !important 936 | } 937 | @media screen and (max-width: 768px){ 938 | .is-hidden-mobile{ 939 | display:none !important 940 | } 941 | } 942 | @media screen and (min-width: 769px), print{ 943 | .is-hidden-tablet{ 944 | display:none !important 945 | } 946 | } 947 | @media screen and (min-width: 769px) and (max-width: 1087px){ 948 | .is-hidden-tablet-only{ 949 | display:none !important 950 | } 951 | } 952 | @media screen and (max-width: 1087px){ 953 | .is-hidden-touch{ 954 | display:none !important 955 | } 956 | } 957 | @media screen and (min-width: 1088px){ 958 | .is-hidden-desktop{ 959 | display:none !important 960 | } 961 | } 962 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 963 | .is-hidden-desktop-only{ 964 | display:none !important 965 | } 966 | } 967 | @media screen and (min-width: 1280px){ 968 | .is-hidden-widescreen{ 969 | display:none !important 970 | } 971 | } 972 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 973 | .is-hidden-widescreen-only{ 974 | display:none !important 975 | } 976 | } 977 | @media screen and (min-width: 1472px){ 978 | .is-hidden-fullhd{ 979 | display:none !important 980 | } 981 | } 982 | .is-invisible{ 983 | visibility:hidden !important 984 | } 985 | @media screen and (max-width: 768px){ 986 | .is-invisible-mobile{ 987 | visibility:hidden !important 988 | } 989 | } 990 | @media screen and (min-width: 769px), print{ 991 | .is-invisible-tablet{ 992 | visibility:hidden !important 993 | } 994 | } 995 | @media screen and (min-width: 769px) and (max-width: 1087px){ 996 | .is-invisible-tablet-only{ 997 | visibility:hidden !important 998 | } 999 | } 1000 | @media screen and (max-width: 1087px){ 1001 | .is-invisible-touch{ 1002 | visibility:hidden !important 1003 | } 1004 | } 1005 | @media screen and (min-width: 1088px){ 1006 | .is-invisible-desktop{ 1007 | visibility:hidden !important 1008 | } 1009 | } 1010 | @media screen and (min-width: 1088px) and (max-width: 1279px){ 1011 | .is-invisible-desktop-only{ 1012 | visibility:hidden !important 1013 | } 1014 | } 1015 | @media screen and (min-width: 1280px){ 1016 | .is-invisible-widescreen{ 1017 | visibility:hidden !important 1018 | } 1019 | } 1020 | @media screen and (min-width: 1280px) and (max-width: 1471px){ 1021 | .is-invisible-widescreen-only{ 1022 | visibility:hidden !important 1023 | } 1024 | } 1025 | @media screen and (min-width: 1472px){ 1026 | .is-invisible-fullhd{ 1027 | visibility:hidden !important 1028 | } 1029 | } 1030 | .is-marginless{ 1031 | margin:0 !important 1032 | } 1033 | .is-paddingless{ 1034 | padding:0 !important 1035 | } 1036 | .is-radiusless{ 1037 | border-radius:0 !important 1038 | } 1039 | .is-shadowless{ 1040 | -webkit-box-shadow:none !important; 1041 | box-shadow:none !important 1042 | } 1043 | 1044 | html{ 1045 | font-size:15px 1046 | } 1047 | body{ 1048 | height:100vh; 1049 | min-height:100vh; 1050 | overflow:hidden 1051 | } 1052 | body>.columns{ 1053 | display:-webkit-box; 1054 | display:-ms-flexbox; 1055 | display:flex; 1056 | margin:0; 1057 | height:100%; 1058 | min-height:100%; 1059 | background-color:#fff 1060 | } 1061 | body>.columns>.column{ 1062 | height:100%; 1063 | min-height:100%; 1064 | max-width:100%; 1065 | overflow-y:auto 1066 | } 1067 | body>.columns>.column:first-child{ 1068 | display:block; 1069 | -webkit-box-flex:0; 1070 | -ms-flex:none; 1071 | flex:none; 1072 | width:16.6666666667% 1073 | } 1074 | 1075 | .mobile-burger{ 1076 | cursor:pointer; 1077 | display:block; 1078 | height:2rem; 1079 | position:relative; 1080 | width:2rem; 1081 | position:fixed; 1082 | top:1rem; 1083 | right:1rem; 1084 | z-index:50; 1085 | background-color:#f5f5f5; 1086 | padding:1rem; 1087 | border-radius:.2rem 1088 | } 1089 | .mobile-burger span{ 1090 | background-color:currentColor; 1091 | display:block; 1092 | height:1px; 1093 | left:calc(50% - 8px); 1094 | position:absolute; 1095 | -webkit-transform-origin:center; 1096 | transform-origin:center; 1097 | -webkit-transition-duration:86ms; 1098 | transition-duration:86ms; 1099 | -webkit-transition-property:background-color, opacity, -webkit-transform; 1100 | transition-property:background-color, opacity, -webkit-transform; 1101 | transition-property:background-color, opacity, transform; 1102 | transition-property:background-color, opacity, transform, -webkit-transform; 1103 | -webkit-transition-timing-function:ease-out; 1104 | transition-timing-function:ease-out; 1105 | width:16px 1106 | } 1107 | .mobile-burger span:nth-child(1){ 1108 | top:calc(50% - 6px) 1109 | } 1110 | .mobile-burger span:nth-child(2){ 1111 | top:calc(50% - 1px) 1112 | } 1113 | .mobile-burger span:nth-child(3){ 1114 | top:calc(50% + 4px) 1115 | } 1116 | .mobile-burger:hover{ 1117 | background-color:rgba(0,0,0,0.05) 1118 | } 1119 | .mobile-burger.is-active span:nth-child(1){ 1120 | -webkit-transform:translateY(5px) rotate(45deg); 1121 | transform:translateY(5px) rotate(45deg) 1122 | } 1123 | .mobile-burger.is-active span:nth-child(2){ 1124 | opacity:0 1125 | } 1126 | .mobile-burger.is-active span:nth-child(3){ 1127 | -webkit-transform:translateY(-5px) rotate(-45deg); 1128 | transform:translateY(-5px) rotate(-45deg) 1129 | } 1130 | p{ 1131 | margin-bottom:1em 1132 | } 1133 | .has-text-weight-light{ 1134 | font-weight:100 1135 | } 1136 | .menu{ 1137 | font-size:1rem 1138 | } 1139 | .menu.is-small{ 1140 | font-size:.75rem 1141 | } 1142 | .menu.is-medium{ 1143 | font-size:1.25rem 1144 | } 1145 | .menu.is-large{ 1146 | font-size:1.5rem 1147 | } 1148 | .menu-list{ 1149 | line-height:1.25 1150 | } 1151 | .menu-list li { 1152 | border-radius:2px; 1153 | color:#67757c; 1154 | display:block; 1155 | padding:0.3em 0.5em 1156 | } 1157 | .menu-list li:hover{ 1158 | background-color:#f5f5f5; 1159 | color:#313437 1160 | } 1161 | .menu-list li.is-active, .menu-list li.is-active a, .menu-list li.is-active a:hover{ 1162 | background-color:#3273dc; 1163 | color:#fff 1164 | } 1165 | .menu-label{ 1166 | color:#7a7a7a; 1167 | font-size:0.75em; 1168 | letter-spacing:0.1em; 1169 | text-transform:uppercase 1170 | } 1171 | .menu-label:not(:first-child){ 1172 | margin-top:1em 1173 | } 1174 | .menu-label:not(:last-child){ 1175 | margin-bottom:1em 1176 | } 1177 | .menu{ 1178 | background-color:#fff; 1179 | font-size:1rem; 1180 | z-index:2; 1181 | -webkit-transition:all .3s ease; 1182 | transition:all .3s ease; 1183 | -webkit-box-shadow: 5px 0px 30px 0px rgba(204,204,204,1); 1184 | -moz-box-shadow: 5px 0px 30px 0px rgba(204,204,204,1); 1185 | box-shadow: 5px 0px 30px 0px rgba(204,204,204,1); 1186 | -webkit-box-flex:0 !important; 1187 | -ms-flex:0 0 240px !important; 1188 | flex:0 0 240px !important; 1189 | width:240px !important; 1190 | max-width:240px !important 1191 | } 1192 | .menu.is-small{ 1193 | font-size:.75rem 1194 | } 1195 | .menu.is-medium{ 1196 | font-size:1.25rem 1197 | } 1198 | .menu.is-large{ 1199 | font-size:1.5rem 1200 | } 1201 | .menu.is-fullheight{ 1202 | height:100%; 1203 | max-height:100%; 1204 | overflow:hidden; 1205 | overflow-y:auto; 1206 | display:-webkit-box; 1207 | display:-ms-flexbox; 1208 | display:flex; 1209 | -webkit-box-orient:vertical; 1210 | -webkit-box-direction:normal; 1211 | -ms-flex-direction:column; 1212 | flex-direction:column; 1213 | -ms-flex-line-pack:stretch; 1214 | align-content:stretch 1215 | } 1216 | .menu.is-centered{ 1217 | text-align:center 1218 | } 1219 | .menu.is-right{ 1220 | text-align:right 1221 | } 1222 | .menu .menu-item{ 1223 | overflow: hidden; 1224 | border-radius: 0; 1225 | background-color: transparent; 1226 | color: #89969e; 1227 | font-size: 14px; 1228 | white-space: nowrap; 1229 | overflow: hidden; 1230 | text-overflow: ellipsis; 1231 | transform: scale(1); 1232 | text-align: left; 1233 | padding: 5px 10px; 1234 | position: relative; 1235 | } 1236 | .menu .menu-item[data-badge]:after { 1237 | content:attr(data-badge); 1238 | position: absolute; 1239 | z-index: 1; 1240 | right: 5px; 1241 | font-size: .75em; 1242 | font-weight: bold; 1243 | background: red; 1244 | color: white; 1245 | padding: 1.5% 3%; 1246 | text-align: center; 1247 | border-radius: 50%; 1248 | } 1249 | .menu .menu-list { 1250 | line-height:1.25 1251 | } 1252 | 1253 | @media screen and (max-width: 768px){ 1254 | .menu.is-mobile{ 1255 | -webkit-box-shadow:5px 0px 13px 3px rgba(10,10,10,0.1); 1256 | box-shadow:5px 0px 13px 3px rgba(10,10,10,0.1); 1257 | -webkit-transform:translateZ(0); 1258 | transform:translateZ(0); 1259 | -webkit-transition:.3s ease; 1260 | transition:.3s ease; 1261 | -webkit-backface-visibility:hidden; 1262 | backface-visibility:hidden; 1263 | -webkit-perspective:1000; 1264 | perspective:1000; 1265 | will-change:transform; 1266 | left:-100%; 1267 | position:absolute; 1268 | display:block !important; 1269 | z-index:10; 1270 | width:240px; 1271 | max-width:75% 1272 | } 1273 | .menu.is-mobile.is-active{ 1274 | left:0 1275 | } 1276 | } 1277 | @media screen and (min-width: 769px), print{ 1278 | .menu.is-mobile{ 1279 | -webkit-box-shadow:5px 0px 13px 3px rgba(10,10,10,0.1); 1280 | box-shadow:5px 0px 13px 3px rgba(10,10,10,0.1); 1281 | -webkit-transform:translateZ(0); 1282 | transform:translateZ(0); 1283 | -webkit-transition:.3s ease; 1284 | transition:.3s ease; 1285 | -webkit-backface-visibility:hidden; 1286 | backface-visibility:hidden; 1287 | -webkit-perspective:1000; 1288 | perspective:1000; 1289 | will-change:transform; 1290 | left:-100%; 1291 | position:absolute; 1292 | display:block !important; 1293 | z-index:10; 1294 | width:240px; 1295 | max-width:75% 1296 | } 1297 | .menu.is-mobile.is-active{ 1298 | left:0 1299 | } 1300 | } 1301 | @media screen and (min-width: 1088px){ 1302 | .menu.is-mobile{ 1303 | position:relative; 1304 | left:inherit 1305 | } 1306 | } 1307 | .menu-burger{ 1308 | cursor:pointer; 1309 | display:block; 1310 | height:3.25rem; 1311 | position:relative; 1312 | width:3.25rem; 1313 | position:fixed; 1314 | top:1rem; 1315 | right:1rem; 1316 | z-index:50; 1317 | background-color:#f5f5f5; 1318 | padding:1rem; 1319 | border-radius:.2rem 1320 | } 1321 | .menu-burger span{ 1322 | background-color:currentColor; 1323 | display:block; 1324 | height:1px; 1325 | left:calc(50% - 8px); 1326 | position:absolute; 1327 | -webkit-transform-origin:center; 1328 | transform-origin:center; 1329 | -webkit-transition-duration:86ms; 1330 | transition-duration:86ms; 1331 | -webkit-transition-property:background-color, opacity, -webkit-transform; 1332 | transition-property:background-color, opacity, -webkit-transform; 1333 | transition-property:background-color, opacity, transform; 1334 | transition-property:background-color, opacity, transform, -webkit-transform; 1335 | -webkit-transition-timing-function:ease-out; 1336 | transition-timing-function:ease-out; 1337 | width:16px 1338 | } 1339 | .menu-burger span:nth-child(1){ 1340 | top:calc(50% - 6px) 1341 | } 1342 | .menu-burger span:nth-child(2){ 1343 | top:calc(50% - 1px) 1344 | } 1345 | .menu-burger span:nth-child(3){ 1346 | top:calc(50% + 4px) 1347 | } 1348 | .menu-burger:hover{ 1349 | background-color:rgba(0,0,0,0.05) 1350 | } 1351 | .menu-burger.is-active span:nth-child(1){ 1352 | -webkit-transform:translateY(5px) rotate(45deg); 1353 | transform:translateY(5px) rotate(45deg) 1354 | } 1355 | .menu-burger.is-active span:nth-child(2){ 1356 | opacity:0 1357 | } 1358 | .menu-burger.is-active span:nth-child(3){ 1359 | -webkit-transform:translateY(-5px) rotate(-45deg); 1360 | transform:translateY(-5px) rotate(-45deg) 1361 | } 1362 | 1363 | #messages tr:hover { 1364 | background-color: #00d1b2; 1365 | color: white; 1366 | cursor: pointer; 1367 | } 1368 | #messages td { 1369 | font-size: 12px; 1370 | } 1371 | .found { width: 5%; } 1372 | .signature-name { width: 20%; } 1373 | .matches { width: 50%; } 1374 | .file-url { width: 20%; } 1375 | .stars { width: 5%; } 1376 | 1377 | .navbar { 1378 | position: fixed; 1379 | width: 100%; 1380 | } 1381 | --------------------------------------------------------------------------------