├── .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 |  
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 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | shhgit live!
28 | v0.4
29 |
30 |
31 |
32 | 0 matches
33 |
34 |
35 | 0 filters
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
61 |
62 |
63 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | Found
101 | Signature Name
102 | Matches
103 | File
104 |
105 |
106 |
107 |
108 |
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 = ``;
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 |
--------------------------------------------------------------------------------