├── .github
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CHANGELOG.md
├── Gopkg.lock
├── Gopkg.toml
├── LICENSE.txt
├── README.md
├── build.sh
├── core
├── banner.go
├── bindata.go
├── core.go
├── git.go
├── github.go
├── log.go
├── options.go
├── router.go
├── session.go
└── signatures.go
├── main.go
├── release.sh
└── static
├── fonts
├── open-iconic.eot
├── open-iconic.otf
├── open-iconic.svg
├── open-iconic.ttf
└── open-iconic.woff
├── images
├── gopher_full.png
├── gopher_head.png
└── spinner.gif
├── index.html
├── javascripts
├── application.js
├── backbone.js
├── bootstrap.js
├── clipboard.js
├── hexdump.js
├── highlight.js
├── highlight_worker.js
├── jquery-3.3.1.js
├── popper.js
└── underscore.js
└── stylesheets
├── application.css
├── bootstrap.css
├── highlight.css
└── openiconic.css
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Hey there and thank you for using the issue tracker!
2 |
3 | ## Checklist before filing an issue:
4 |
5 | - [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
6 | - [ ] Have a usage question? Ask your question on [StackOverflow](http://stackoverflow.com), [StackExchange Security](https://security.stackexchange.com) or similar platform.
7 | - [ ] Have an idea for a feature? Make sure that it hasn't been suggested before and describe your idea in detail.
8 |
9 | ## None of the above? create a bug report
10 |
11 | Make sure to add **all the information needed to understand the bug** so that someone can help. If information is missing, the issue will be labeled with 'Needs more information' and closed until there is enough information.
12 |
13 | ## Expected Behavior
14 |
15 |
16 | ## Actual Behavior
17 |
18 |
19 | ## Steps to Reproduce the Problem
20 |
21 | 1.
22 | 2.
23 | 3.
24 |
25 | ## Specifications
26 |
27 | - Gitrob version:
28 | - Operating system:
29 | - Go version:
30 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **IMPORTANT: Please do not create a Pull Request without creating an issue first.**
2 |
3 | *Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.*
4 |
5 | Please provide enough information so that others can review your pull request:
6 |
7 |
8 |
9 | Explain the **details** for making this change. What existing problem does the pull request solve?
10 |
11 |
12 |
13 | **Closing issues**
14 |
15 | Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such).
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | gitrob
8 | gitrob.exe
9 |
10 | vendor/
11 | build/
12 |
13 | # Test binary, build with `go test -c`
14 | *.test
15 |
16 | # Output of the go coverage tool, specifically when used with LiteIDE
17 | *.out
18 |
19 | # Dropbox settings and caches
20 | .dropbox
21 | .dropbox.attr
22 | .dropbox.cache
23 |
24 | # temporary files which can be created if a process still has a handle open of a deleted file
25 | .fuse_hidden*
26 |
27 | # KDE directory preferences
28 | .directory
29 |
30 | # Linux trash folder which might appear on any partition or disk
31 | .Trash-*
32 |
33 | # .nfs files are created when an open file is removed but is still being accessed
34 | .nfs*
35 |
36 |
37 | # TextMate
38 | *.tmproj
39 | *.tmproject
40 | tmtags
41 |
42 | # Swap
43 | [._]*.s[a-v][a-z]
44 | [._]*.sw[a-p]
45 | [._]s[a-v][a-z]
46 | [._]sw[a-p]
47 |
48 | # Session
49 | Session.vim
50 |
51 | # Temporary
52 | .netrwhist
53 | *~
54 | # Auto-generated tag files
55 | tags
56 |
57 | # General
58 | .DS_Store
59 | .AppleDouble
60 | .LSOverride
61 |
62 | # Icon must end with two \r
63 | Icon
64 |
65 |
66 | # Thumbnails
67 | ._*
68 |
69 | # Files that might appear in the root of a volume
70 | .DocumentRevisions-V100
71 | .fseventsd
72 | .Spotlight-V100
73 | .TemporaryItems
74 | .Trashes
75 | .VolumeIcon.icns
76 | .com.apple.timemachine.donotpresent
77 |
78 | # Directories potentially created on remote AFP share
79 | .AppleDB
80 | .AppleDesktop
81 | Network Trash Folder
82 | Temporary Items
83 | .apdisk
84 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # Changelog
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 | ### Added
10 | - Dependency management with dep
11 |
12 | ### Changed
13 | - Skip expensive signature checking for image extensions and files in `node_modules` and other package directories
14 |
15 | ## 2.0.0-beta - 2018-06-08
16 | ### Added
17 | - Total rewrite of Gitrob in [Golang](https://golang.org/)
18 | - Find interesting files in history down to a default (and configurable) depth of 500 commits
19 | - Hexdump view for binary files
20 | - Saving and loading of session files for easy sharing
21 |
22 | ### Removed
23 | - All the stupid Rubygems with native extensions
24 | - PostgreSQL dependency
25 | - Messy assessment comparison feature
26 | - User overview
27 | - Repository overview
28 |
29 | [Unreleased]: https://github.com/michenriksen/gitrob/compare/v2.0.0-beta...HEAD
30 |
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | name = "github.com/elazarl/go-bindata-assetfs"
6 | packages = ["."]
7 | revision = "30f82fa23fd844bd5bb1e5f216db87fd77b5eb43"
8 | version = "v1.0.0"
9 |
10 | [[projects]]
11 | name = "github.com/emirpasic/gods"
12 | packages = [
13 | "containers",
14 | "lists",
15 | "lists/arraylist",
16 | "trees",
17 | "trees/binaryheap",
18 | "utils"
19 | ]
20 | revision = "f6c17b524822278a87e3b3bd809fec33b51f5b46"
21 | version = "v1.9.0"
22 |
23 | [[projects]]
24 | name = "github.com/fatih/color"
25 | packages = ["."]
26 | revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
27 | version = "v1.7.0"
28 |
29 | [[projects]]
30 | branch = "master"
31 | name = "github.com/gin-contrib/secure"
32 | packages = ["."]
33 | revision = "624341e112805e689f488495c9880bb97026b47c"
34 |
35 | [[projects]]
36 | branch = "master"
37 | name = "github.com/gin-contrib/sse"
38 | packages = ["."]
39 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
40 |
41 | [[projects]]
42 | branch = "master"
43 | name = "github.com/gin-contrib/static"
44 | packages = ["."]
45 | revision = "73da7037e716e63aa2b0ffceb630dfa7be299086"
46 |
47 | [[projects]]
48 | name = "github.com/gin-gonic/gin"
49 | packages = [
50 | ".",
51 | "binding",
52 | "render"
53 | ]
54 | revision = "d459835d2b077e44f7c9b453505ee29881d5d12d"
55 | version = "v1.2"
56 |
57 | [[projects]]
58 | name = "github.com/golang/protobuf"
59 | packages = ["proto"]
60 | revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
61 | version = "v1.1.0"
62 |
63 | [[projects]]
64 | name = "github.com/google/go-github"
65 | packages = ["github"]
66 | revision = "e48060a28fac52d0f1cb758bc8b87c07bac4a87d"
67 | version = "v15.0.0"
68 |
69 | [[projects]]
70 | branch = "master"
71 | name = "github.com/google/go-querystring"
72 | packages = ["query"]
73 | revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
74 |
75 | [[projects]]
76 | branch = "master"
77 | name = "github.com/jbenet/go-context"
78 | packages = ["io"]
79 | revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4"
80 |
81 | [[projects]]
82 | name = "github.com/kevinburke/ssh_config"
83 | packages = ["."]
84 | revision = "9fc7bb800b555d63157c65a904c86a2cc7b4e795"
85 | version = "0.4"
86 |
87 | [[projects]]
88 | name = "github.com/mattn/go-colorable"
89 | packages = ["."]
90 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
91 | version = "v0.0.9"
92 |
93 | [[projects]]
94 | name = "github.com/mattn/go-isatty"
95 | packages = ["."]
96 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
97 | version = "v0.0.3"
98 |
99 | [[projects]]
100 | branch = "master"
101 | name = "github.com/mitchellh/go-homedir"
102 | packages = ["."]
103 | revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
104 |
105 | [[projects]]
106 | name = "github.com/pelletier/go-buffruneio"
107 | packages = ["."]
108 | revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
109 | version = "v0.2.0"
110 |
111 | [[projects]]
112 | name = "github.com/sergi/go-diff"
113 | packages = ["diffmatchpatch"]
114 | revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
115 | version = "v1.0.0"
116 |
117 | [[projects]]
118 | name = "github.com/src-d/gcfg"
119 | packages = [
120 | ".",
121 | "scanner",
122 | "token",
123 | "types"
124 | ]
125 | revision = "f187355171c936ac84a82793659ebb4936bc1c23"
126 | version = "v1.3.0"
127 |
128 | [[projects]]
129 | name = "github.com/ugorji/go"
130 | packages = ["codec"]
131 | revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
132 | version = "v1.1.1"
133 |
134 | [[projects]]
135 | branch = "master"
136 | name = "github.com/xanzy/ssh-agent"
137 | packages = ["."]
138 | revision = "ba9c9e33906f58169366275e3450db66139a31a9"
139 |
140 | [[projects]]
141 | branch = "master"
142 | name = "golang.org/x/crypto"
143 | packages = [
144 | "cast5",
145 | "curve25519",
146 | "ed25519",
147 | "ed25519/internal/edwards25519",
148 | "internal/chacha20",
149 | "openpgp",
150 | "openpgp/armor",
151 | "openpgp/elgamal",
152 | "openpgp/errors",
153 | "openpgp/packet",
154 | "openpgp/s2k",
155 | "poly1305",
156 | "ssh",
157 | "ssh/agent",
158 | "ssh/knownhosts"
159 | ]
160 | revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9"
161 |
162 | [[projects]]
163 | branch = "master"
164 | name = "golang.org/x/net"
165 | packages = [
166 | "context",
167 | "context/ctxhttp"
168 | ]
169 | revision = "db08ff08e8622530d9ed3a0e8ac279f6d4c02196"
170 |
171 | [[projects]]
172 | branch = "master"
173 | name = "golang.org/x/oauth2"
174 | packages = [
175 | ".",
176 | "internal"
177 | ]
178 | revision = "1e0a3fa8ba9a5c9eb35c271780101fdaf1b205d7"
179 |
180 | [[projects]]
181 | branch = "master"
182 | name = "golang.org/x/sys"
183 | packages = [
184 | "unix",
185 | "windows"
186 | ]
187 | revision = "a9e25c09b96b8870693763211309e213c6ef299d"
188 |
189 | [[projects]]
190 | name = "golang.org/x/text"
191 | packages = [
192 | "internal/gen",
193 | "internal/triegen",
194 | "internal/ucd",
195 | "transform",
196 | "unicode/cldr",
197 | "unicode/norm"
198 | ]
199 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
200 | version = "v0.3.0"
201 |
202 | [[projects]]
203 | name = "google.golang.org/appengine"
204 | packages = [
205 | "internal",
206 | "internal/base",
207 | "internal/datastore",
208 | "internal/log",
209 | "internal/remote_api",
210 | "internal/urlfetch",
211 | "urlfetch"
212 | ]
213 | revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
214 | version = "v1.0.0"
215 |
216 | [[projects]]
217 | name = "gopkg.in/go-playground/validator.v8"
218 | packages = ["."]
219 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
220 | version = "v8.18.2"
221 |
222 | [[projects]]
223 | name = "gopkg.in/src-d/go-billy.v4"
224 | packages = [
225 | ".",
226 | "helper/chroot",
227 | "helper/polyfill",
228 | "osfs",
229 | "util"
230 | ]
231 | revision = "df053870ae7070b0350624ba5a22161ba3796cc0"
232 | version = "v4.1.1"
233 |
234 | [[projects]]
235 | name = "gopkg.in/src-d/go-git.v4"
236 | packages = [
237 | ".",
238 | "config",
239 | "internal/revision",
240 | "plumbing",
241 | "plumbing/cache",
242 | "plumbing/filemode",
243 | "plumbing/format/config",
244 | "plumbing/format/diff",
245 | "plumbing/format/gitignore",
246 | "plumbing/format/idxfile",
247 | "plumbing/format/index",
248 | "plumbing/format/objfile",
249 | "plumbing/format/packfile",
250 | "plumbing/format/pktline",
251 | "plumbing/object",
252 | "plumbing/protocol/packp",
253 | "plumbing/protocol/packp/capability",
254 | "plumbing/protocol/packp/sideband",
255 | "plumbing/revlist",
256 | "plumbing/storer",
257 | "plumbing/transport",
258 | "plumbing/transport/client",
259 | "plumbing/transport/file",
260 | "plumbing/transport/git",
261 | "plumbing/transport/http",
262 | "plumbing/transport/internal/common",
263 | "plumbing/transport/server",
264 | "plumbing/transport/ssh",
265 | "storage",
266 | "storage/filesystem",
267 | "storage/filesystem/dotgit",
268 | "storage/memory",
269 | "utils/binary",
270 | "utils/diff",
271 | "utils/ioutil",
272 | "utils/merkletrie",
273 | "utils/merkletrie/filesystem",
274 | "utils/merkletrie/index",
275 | "utils/merkletrie/internal/frame",
276 | "utils/merkletrie/noder"
277 | ]
278 | revision = "b23570073eaee3489e5e3d666f22ba5cbeb53243"
279 | version = "v4.4.1"
280 |
281 | [[projects]]
282 | name = "gopkg.in/warnings.v0"
283 | packages = ["."]
284 | revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b"
285 | version = "v0.1.2"
286 |
287 | [[projects]]
288 | name = "gopkg.in/yaml.v2"
289 | packages = ["."]
290 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
291 | version = "v2.2.1"
292 |
293 | [solve-meta]
294 | analyzer-name = "dep"
295 | analyzer-version = 1
296 | inputs-digest = "e17d65bb62cd54df865e20c85c022cb54e74ef937e6e2520c9d1c38fc6f5d090"
297 | solver-name = "gps-cdcl"
298 | solver-version = 1
299 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 | #
22 | # [prune]
23 | # non-go = false
24 | # go-tests = true
25 | # unused-packages = true
26 |
27 |
28 | [[constraint]]
29 | name = "github.com/elazarl/go-bindata-assetfs"
30 | version = "1.0.0"
31 |
32 | [[constraint]]
33 | name = "github.com/fatih/color"
34 | version = "1.7.0"
35 |
36 | [[constraint]]
37 | branch = "master"
38 | name = "github.com/gin-contrib/secure"
39 |
40 | [[constraint]]
41 | branch = "master"
42 | name = "github.com/gin-contrib/static"
43 |
44 | [[constraint]]
45 | name = "github.com/gin-gonic/gin"
46 | version = "1.1.4"
47 |
48 | [[constraint]]
49 | name = "github.com/google/go-github"
50 | version = "15.0.0"
51 |
52 | [[constraint]]
53 | branch = "master"
54 | name = "golang.org/x/oauth2"
55 |
56 | [[constraint]]
57 | name = "gopkg.in/src-d/go-git.v4"
58 | version = "4.4.1"
59 |
60 | [prune]
61 | go-tests = true
62 | unused-packages = true
63 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Michael Henriksen
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # Gitrob: Putting the Open Source in OSINT
9 |
10 | Gitrob is a tool to help find potentially sensitive files pushed to public repositories on Github. Gitrob will clone repositories belonging to a user or organization down to a configurable depth and iterate through the commit history and flag files that match signatures for potentially sensitive files. The findings will be presented through a web interface for easy browsing and analysis.
11 |
12 | ## Usage
13 |
14 | gitrob [options] target [target2] ... [targetN]
15 |
16 | ### Options
17 |
18 | ```
19 | -bind-address string
20 | Address to bind web server to (default "127.0.0.1")
21 | -commit-depth int
22 | Number of repository commits to process (default 500)
23 | -debug
24 | Print debugging information
25 | -github-access-token string
26 | GitHub access token to use for API requests
27 | -load string
28 | Load session file
29 | -no-expand-orgs
30 | Don't add members to targets when processing organizations
31 | -port int
32 | Port to run web server on (default 9393)
33 | -save string
34 | Save session to file
35 | -silent
36 | Suppress all output except for errors
37 | -threads int
38 | Number of concurrent threads (default number of logical CPUs)
39 | ```
40 |
41 | ### Saving session to a file
42 |
43 | By default, gitrob will store its state for an assessment in memory. This means that the results of an assessment is lost when Gitrob is closed. You can save the session to a file by using the `-save` option:
44 |
45 | gitrob -save ~/gitrob-session.json acmecorp
46 |
47 | Gitrob will save all the gathered information to the specified file path as a special JSON document. The file can be loaded again for browsing at another point in time, shared with other analysts or parsed for custom integrations with other tools and systems.
48 |
49 | ### Loading session from a file
50 |
51 | A session stored in a file can be loaded with the `-load` option:
52 |
53 | gitrob -load ~/gitrob-session.json
54 |
55 | Gitrob will start its web interface and serve the results for analysis.
56 |
57 | ## Installation
58 |
59 | A [precompiled version is available](https://github.com/michenriksen/gitrob/releases) for each release, alternatively you can use the latest version of the source code from this repository in order to build your own binary.
60 |
61 | Make sure you have a correctly configured **Go >= 1.8** environment and that `$GOPATH/bin` is in your `$PATH`
62 |
63 | $ go get github.com/michenriksen/gitrob
64 |
65 | This command will download gitrob, install its dependencies, compile it and move the `gitrob` executable to `$GOPATH/bin`.
66 |
67 | ### Github access token
68 |
69 | Gitrob will need a Github access token in order to interact with the Github API. [Create a personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) and save it in an environment variable in your `.bashrc` or similar shell configuration file:
70 |
71 | export GITROB_ACCESS_TOKEN=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
72 |
73 | Alternatively you can specify the access token with the `-github-access-token` option, but watch out for your command history!
74 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | BUILD_FOLDER=build
4 | VERSION=$(cat core/banner.go | grep Version | cut -d '"' -f 2)
5 |
6 | bin_dep() {
7 | BIN=$1
8 | which $BIN > /dev/null || { echo "[-] Dependency $BIN not found !"; exit 1; }
9 | }
10 |
11 | create_exe_archive() {
12 | bin_dep 'zip'
13 |
14 | OUTPUT=$1
15 |
16 | echo "[*] Creating archive $OUTPUT ..."
17 | zip -j "$OUTPUT" gitrob.exe ../README.md ../LICENSE.txt > /dev/null
18 | rm -rf gitrob gitrob.exe
19 | }
20 |
21 | create_archive() {
22 | bin_dep 'zip'
23 |
24 | OUTPUT=$1
25 |
26 | echo "[*] Creating archive $OUTPUT ..."
27 | zip -j "$OUTPUT" gitrob ../README.md ../LICENSE.md > /dev/null
28 | rm -rf gitrob gitrob.exe
29 | }
30 |
31 | build_linux_amd64() {
32 | echo "[*] Building linux/amd64 ..."
33 | GOOS=linux GOARCH=amd64 go build -o gitrob ..
34 | }
35 |
36 | build_macos_amd64() {
37 | echo "[*] Building darwin/amd64 ..."
38 | GOOS=darwin GOARCH=amd64 go build -o gitrob ..
39 | }
40 |
41 | build_windows_amd64() {
42 | echo "[*] Building windows/amd64 ..."
43 | GOOS=windows GOARCH=amd64 go build -o gitrob.exe ..
44 | }
45 |
46 | rm -rf $BUILD_FOLDER
47 | mkdir $BUILD_FOLDER
48 | cd $BUILD_FOLDER
49 |
50 | build_linux_amd64 && create_archive gitrob_linux_amd64_$VERSION.zip
51 | build_macos_amd64 && create_archive gitrob_macos_amd64_$VERSION.zip
52 | build_windows_amd64 && create_exe_archive gitrob_windows_amd64_$VERSION.zip
53 | shasum -a 256 * > checksums.txt
54 |
55 | echo
56 | echo
57 | du -sh *
58 |
59 | cd --
60 |
--------------------------------------------------------------------------------
/core/banner.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | const (
4 | Name = "gitrob"
5 | Version = "2.0.0-beta"
6 | Author = "Michael Henriksen"
7 | Website = "https://github.com/michenriksen/gitrob"
8 | ASCIIBanner = " _ __ __\n" +
9 | " ___ _(_) /________ / /\n" +
10 | " / _ `/ / __/ __/ _ \\/ _ \\\n" +
11 | " \\_, /_/\\__/_/ \\___/_.__/\n" +
12 | "/___/ by @michenriksen"
13 | )
14 |
--------------------------------------------------------------------------------
/core/core.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "regexp"
7 | "strings"
8 | )
9 |
10 | var NewlineRegex = regexp.MustCompile(`\r?\n`)
11 |
12 | func FileExists(path string) bool {
13 | if _, err := os.Stat(path); os.IsNotExist(err) {
14 | return false
15 | }
16 | return true
17 | }
18 |
19 | func Pluralize(count int, singular string, plural string) string {
20 | if count == 1 {
21 | return singular
22 | }
23 | return plural
24 | }
25 |
26 | func TruncateString(str string, maxLength int) string {
27 | str = NewlineRegex.ReplaceAllString(str, " ")
28 | str = strings.TrimSpace(str)
29 | if len(str) > maxLength {
30 | str = fmt.Sprintf("%s...", str[0:maxLength])
31 | }
32 | return str
33 | }
34 |
--------------------------------------------------------------------------------
/core/git.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 |
7 | "gopkg.in/src-d/go-git.v4"
8 | "gopkg.in/src-d/go-git.v4/plumbing"
9 | "gopkg.in/src-d/go-git.v4/plumbing/object"
10 | "gopkg.in/src-d/go-git.v4/utils/merkletrie"
11 | )
12 |
13 | const (
14 | EmptyTreeCommitId = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
15 | )
16 |
17 | func CloneRepository(url *string, branch *string, depth int) (*git.Repository, string, error) {
18 | urlVal := *url
19 | branchVal := *branch
20 | dir, err := ioutil.TempDir("", "gitrob")
21 | if err != nil {
22 | return nil, "", err
23 | }
24 | repository, err := git.PlainClone(dir, false, &git.CloneOptions{
25 | URL: urlVal,
26 | Depth: depth,
27 | ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branchVal)),
28 | SingleBranch: true,
29 | Tags: git.NoTags,
30 | })
31 | if err != nil {
32 | return nil, dir, err
33 | }
34 | return repository, dir, nil
35 | }
36 |
37 | func GetRepositoryHistory(repository *git.Repository) ([]*object.Commit, error) {
38 | var commits []*object.Commit
39 | ref, err := repository.Head()
40 | if err != nil {
41 | return nil, err
42 | }
43 | cIter, err := repository.Log(&git.LogOptions{From: ref.Hash()})
44 | if err != nil {
45 | return nil, err
46 | }
47 | cIter.ForEach(func(c *object.Commit) error {
48 | commits = append(commits, c)
49 | return nil
50 | })
51 | return commits, nil
52 | }
53 |
54 | func GetChanges(commit *object.Commit, repo *git.Repository) (object.Changes, error) {
55 | parentCommit, err := GetParentCommit(commit, repo)
56 | if err != nil {
57 | return nil, err
58 | }
59 |
60 | commitTree, err := commit.Tree()
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | parentCommitTree, err := parentCommit.Tree()
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | changes, err := object.DiffTree(parentCommitTree, commitTree)
71 | if err != nil {
72 | return nil, err
73 | }
74 | return changes, nil
75 | }
76 |
77 | func GetParentCommit(commit *object.Commit, repo *git.Repository) (*object.Commit, error) {
78 | if commit.NumParents() == 0 {
79 | parentCommit, err := repo.CommitObject(plumbing.NewHash(EmptyTreeCommitId))
80 | if err != nil {
81 | return nil, err
82 | }
83 | return parentCommit, nil
84 | }
85 | parentCommit, err := commit.Parents().Next()
86 | if err != nil {
87 | return nil, err
88 | }
89 | return parentCommit, nil
90 | }
91 |
92 | func GetChangeAction(change *object.Change) string {
93 | action, err := change.Action()
94 | if err != nil {
95 | return "Unknown"
96 | }
97 | switch action {
98 | case merkletrie.Insert:
99 | return "Insert"
100 | case merkletrie.Modify:
101 | return "Modify"
102 | case merkletrie.Delete:
103 | return "Delete"
104 | default:
105 | return "Unknown"
106 | }
107 | }
108 |
109 | func GetChangePath(change *object.Change) string {
110 | action, err := change.Action()
111 | if err != nil {
112 | return change.To.Name
113 | }
114 |
115 | if action == merkletrie.Delete {
116 | return change.From.Name
117 | } else {
118 | return change.To.Name
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/core/github.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/google/go-github/github"
7 | )
8 |
9 | type GithubOwner struct {
10 | Login *string
11 | ID *int64
12 | Type *string
13 | Name *string
14 | AvatarURL *string
15 | URL *string
16 | Company *string
17 | Blog *string
18 | Location *string
19 | Email *string
20 | Bio *string
21 | }
22 |
23 | type GithubRepository struct {
24 | Owner *string
25 | ID *int64
26 | Name *string
27 | FullName *string
28 | CloneURL *string
29 | URL *string
30 | DefaultBranch *string
31 | Description *string
32 | Homepage *string
33 | }
34 |
35 | func GetUserOrOrganization(login string, client *github.Client) (*GithubOwner, error) {
36 | ctx := context.Background()
37 | user, _, err := client.Users.Get(ctx, login)
38 | if err != nil {
39 | return nil, err
40 | }
41 | return &GithubOwner{
42 | Login: user.Login,
43 | ID: user.ID,
44 | Type: user.Type,
45 | Name: user.Name,
46 | AvatarURL: user.AvatarURL,
47 | URL: user.HTMLURL,
48 | Company: user.Company,
49 | Blog: user.Blog,
50 | Location: user.Location,
51 | Email: user.Email,
52 | Bio: user.Bio,
53 | }, nil
54 | }
55 |
56 | func GetRepositoriesFromOwner(login *string, client *github.Client) ([]*GithubRepository, error) {
57 | var allRepos []*GithubRepository
58 | loginVal := *login
59 | ctx := context.Background()
60 | opt := &github.RepositoryListOptions{
61 | Type: "sources",
62 | }
63 |
64 | for {
65 | repos, resp, err := client.Repositories.List(ctx, loginVal, opt)
66 | if err != nil {
67 | return allRepos, err
68 | }
69 | for _, repo := range repos {
70 | if !*repo.Fork {
71 | r := GithubRepository{
72 | Owner: repo.Owner.Login,
73 | ID: repo.ID,
74 | Name: repo.Name,
75 | FullName: repo.FullName,
76 | CloneURL: repo.CloneURL,
77 | URL: repo.HTMLURL,
78 | DefaultBranch: repo.DefaultBranch,
79 | Description: repo.Description,
80 | Homepage: repo.Homepage,
81 | }
82 | allRepos = append(allRepos, &r)
83 | }
84 | }
85 | if resp.NextPage == 0 {
86 | break
87 | }
88 | opt.Page = resp.NextPage
89 | }
90 |
91 | return allRepos, nil
92 | }
93 |
94 | func GetOrganizationMembers(login *string, client *github.Client) ([]*GithubOwner, error) {
95 | var allMembers []*GithubOwner
96 | loginVal := *login
97 | ctx := context.Background()
98 | opt := &github.ListMembersOptions{}
99 | for {
100 | members, resp, err := client.Organizations.ListMembers(ctx, loginVal, opt)
101 | if err != nil {
102 | return allMembers, err
103 | }
104 | for _, member := range members {
105 | allMembers = append(allMembers, &GithubOwner{Login: member.Login, ID: member.ID, Type: member.Type})
106 | }
107 | if resp.NextPage == 0 {
108 | break
109 | }
110 | opt.Page = resp.NextPage
111 | }
112 | return allMembers, nil
113 | }
114 |
--------------------------------------------------------------------------------
/core/log.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "sync"
7 |
8 | "github.com/fatih/color"
9 | )
10 |
11 | const (
12 | FATAL = 5
13 | ERROR = 4
14 | WARN = 3
15 | IMPORTANT = 2
16 | INFO = 1
17 | DEBUG = 0
18 | )
19 |
20 | var LogColors = map[int]*color.Color{
21 | FATAL: color.New(color.FgRed).Add(color.Bold),
22 | ERROR: color.New(color.FgRed),
23 | WARN: color.New(color.FgYellow),
24 | IMPORTANT: color.New(color.Bold),
25 | DEBUG: color.New(color.FgCyan).Add(color.Faint),
26 | }
27 |
28 | type Logger struct {
29 | sync.Mutex
30 |
31 | debug bool
32 | silent bool
33 | }
34 |
35 | func (l *Logger) SetSilent(s bool) {
36 | l.silent = s
37 | }
38 |
39 | func (l *Logger) SetDebug(d bool) {
40 | l.debug = d
41 | }
42 |
43 | func (l *Logger) Log(level int, format string, args ...interface{}) {
44 | l.Lock()
45 | defer l.Unlock()
46 | if level == DEBUG && l.debug == false {
47 | return
48 | } else if level < ERROR && l.silent == true {
49 | return
50 | }
51 |
52 | if c, ok := LogColors[level]; ok {
53 | c.Printf(format, args...)
54 | } else {
55 | fmt.Printf(format, args...)
56 | }
57 |
58 | if level == FATAL {
59 | os.Exit(1)
60 | }
61 | }
62 |
63 | func (l *Logger) Fatal(format string, args ...interface{}) {
64 | l.Log(FATAL, format, args...)
65 | }
66 |
67 | func (l *Logger) Error(format string, args ...interface{}) {
68 | l.Log(ERROR, format, args...)
69 | }
70 |
71 | func (l *Logger) Warn(format string, args ...interface{}) {
72 | l.Log(WARN, format, args...)
73 | }
74 |
75 | func (l *Logger) Important(format string, args ...interface{}) {
76 | l.Log(IMPORTANT, format, args...)
77 | }
78 |
79 | func (l *Logger) Info(format string, args ...interface{}) {
80 | l.Log(INFO, format, args...)
81 | }
82 |
83 | func (l *Logger) Debug(format string, args ...interface{}) {
84 | l.Log(DEBUG, format, args...)
85 | }
86 |
--------------------------------------------------------------------------------
/core/options.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "flag"
5 | )
6 |
7 | type Options struct {
8 | CommitDepth *int
9 | GithubAccessToken *string `json:"-"`
10 | NoExpandOrgs *bool
11 | Threads *int
12 | Save *string `json:"-"`
13 | Load *string `json:"-"`
14 | BindAddress *string
15 | Port *int
16 | Silent *bool
17 | Debug *bool
18 | Logins []string
19 | }
20 |
21 | func ParseOptions() (Options, error) {
22 | options := Options{
23 | CommitDepth: flag.Int("commit-depth", 500, "Number of repository commits to process"),
24 | GithubAccessToken: flag.String("github-access-token", "", "GitHub access token to use for API requests"),
25 | NoExpandOrgs: flag.Bool("no-expand-orgs", false, "Don't add members to targets when processing organizations"),
26 | Threads: flag.Int("threads", 0, "Number of concurrent threads (default number of logical CPUs)"),
27 | Save: flag.String("save", "", "Save session to file"),
28 | Load: flag.String("load", "", "Load session file"),
29 | BindAddress: flag.String("bind-address", "127.0.0.1", "Address to bind web server to"),
30 | Port: flag.Int("port", 9393, "Port to run web server on"),
31 | Silent: flag.Bool("silent", false, "Suppress all output except for errors"),
32 | Debug: flag.Bool("debug", false, "Print debugging information"),
33 | }
34 |
35 | flag.Parse()
36 | options.Logins = flag.Args()
37 |
38 | return options, nil
39 | }
40 |
--------------------------------------------------------------------------------
/core/router.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "strings"
8 |
9 | assetfs "github.com/elazarl/go-bindata-assetfs"
10 | "github.com/gin-contrib/secure"
11 | "github.com/gin-contrib/static"
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | const (
16 | GithubBaseUri = "https://raw.githubusercontent.com"
17 | MaximumFileSize = 102400
18 | CspPolicy = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
19 | ReferrerPolicy = "no-referrer"
20 | )
21 |
22 | type binaryFileSystem struct {
23 | fs http.FileSystem
24 | }
25 |
26 | func (b *binaryFileSystem) Open(name string) (http.File, error) {
27 | return b.fs.Open(name)
28 | }
29 |
30 | func (b *binaryFileSystem) Exists(prefix string, filepath string) bool {
31 | if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
32 | if _, err := b.fs.Open(p); err != nil {
33 | return false
34 | }
35 | return true
36 | }
37 | return false
38 | }
39 |
40 | func BinaryFileSystem(root string) *binaryFileSystem {
41 | fs := &assetfs.AssetFS{Asset, AssetDir, AssetInfo, root}
42 | return &binaryFileSystem{
43 | fs,
44 | }
45 | }
46 |
47 | func NewRouter(s *Session) *gin.Engine {
48 | if *s.Options.Debug == true {
49 | gin.SetMode(gin.DebugMode)
50 | } else {
51 | gin.SetMode(gin.ReleaseMode)
52 | }
53 |
54 | router := gin.New()
55 | router.Use(static.Serve("/", BinaryFileSystem("static")))
56 | router.Use(secure.New(secure.Config{
57 | SSLRedirect: false,
58 | IsDevelopment: false,
59 | FrameDeny: true,
60 | ContentTypeNosniff: true,
61 | BrowserXssFilter: true,
62 | ContentSecurityPolicy: CspPolicy,
63 | ReferrerPolicy: ReferrerPolicy,
64 | }))
65 | router.GET("/stats", func(c *gin.Context) {
66 | c.JSON(200, s.Stats)
67 | })
68 | router.GET("/findings", func(c *gin.Context) {
69 | c.JSON(200, s.Findings)
70 | })
71 | router.GET("/targets", func(c *gin.Context) {
72 | c.JSON(200, s.Targets)
73 | })
74 | router.GET("/repositories", func(c *gin.Context) {
75 | c.JSON(200, s.Repositories)
76 | })
77 | router.GET("/files/:owner/:repo/:commit/*path", fetchFile)
78 |
79 | return router
80 | }
81 |
82 | func fetchFile(c *gin.Context) {
83 | fileUrl := fmt.Sprintf("%s/%s/%s/%s%s", GithubBaseUri, c.Param("owner"), c.Param("repo"), c.Param("commit"), c.Param("path"))
84 | resp, err := http.Head(fileUrl)
85 | if err != nil {
86 | c.JSON(http.StatusInternalServerError, gin.H{
87 | "message": err,
88 | })
89 | return
90 | }
91 |
92 | if resp.StatusCode == http.StatusNotFound {
93 | c.JSON(http.StatusNotFound, gin.H{
94 | "message": "No content",
95 | })
96 | return
97 | }
98 |
99 | if resp.ContentLength > MaximumFileSize {
100 | c.JSON(http.StatusUnprocessableEntity, gin.H{
101 | "message": fmt.Sprintf("File size exceeds maximum of %d bytes", MaximumFileSize),
102 | })
103 | return
104 | }
105 |
106 | resp, err = http.Get(fileUrl)
107 | if err != nil {
108 | c.JSON(http.StatusInternalServerError, gin.H{
109 | "message": err,
110 | })
111 | return
112 | }
113 |
114 | defer resp.Body.Close()
115 | body, err := ioutil.ReadAll(resp.Body)
116 | if err != nil {
117 | c.JSON(http.StatusInternalServerError, gin.H{
118 | "message": err,
119 | })
120 | return
121 | }
122 |
123 | c.String(http.StatusOK, string(body[:]))
124 | }
125 |
--------------------------------------------------------------------------------
/core/session.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io/ioutil"
9 | "os"
10 | "runtime"
11 | "sync"
12 | "time"
13 |
14 | "github.com/gin-gonic/gin"
15 | "github.com/google/go-github/github"
16 | "golang.org/x/oauth2"
17 | )
18 |
19 | const (
20 | AccessTokenEnvVariable = "GITROB_ACCESS_TOKEN"
21 |
22 | StatusInitializing = "initializing"
23 | StatusGathering = "gathering"
24 | StatusAnalyzing = "analyzing"
25 | StatusFinished = "finished"
26 | )
27 |
28 | type Stats struct {
29 | sync.Mutex
30 |
31 | StartedAt time.Time
32 | FinishedAt time.Time
33 | Status string
34 | Progress float64
35 | Targets int
36 | Repositories int
37 | Commits int
38 | Files int
39 | Findings int
40 | }
41 |
42 | type Session struct {
43 | sync.Mutex
44 |
45 | Version string
46 | Options Options `json:"-"`
47 | Out *Logger `json:"-"`
48 | Stats *Stats
49 | GithubAccessToken string `json:"-"`
50 | GithubClient *github.Client `json:"-"`
51 | Router *gin.Engine `json:"-"`
52 | Targets []*GithubOwner
53 | Repositories []*GithubRepository
54 | Findings []*Finding
55 | }
56 |
57 | func (s *Session) Start() {
58 | s.InitStats()
59 | s.InitLogger()
60 | s.InitThreads()
61 | s.InitGithubAccessToken()
62 | s.InitGithubClient()
63 | s.InitRouter()
64 | }
65 |
66 | func (s *Session) Finish() {
67 | s.Stats.FinishedAt = time.Now()
68 | s.Stats.Status = StatusFinished
69 | }
70 |
71 | func (s *Session) AddTarget(target *GithubOwner) {
72 | s.Lock()
73 | defer s.Unlock()
74 | for _, t := range s.Targets {
75 | if *target.ID == *t.ID {
76 | return
77 | }
78 | }
79 | s.Targets = append(s.Targets, target)
80 | }
81 |
82 | func (s *Session) AddRepository(repository *GithubRepository) {
83 | s.Lock()
84 | defer s.Unlock()
85 | for _, r := range s.Repositories {
86 | if *repository.ID == *r.ID {
87 | return
88 | }
89 | }
90 | s.Repositories = append(s.Repositories, repository)
91 | }
92 |
93 | func (s *Session) AddFinding(finding *Finding) {
94 | s.Lock()
95 | defer s.Unlock()
96 | s.Findings = append(s.Findings, finding)
97 | }
98 |
99 | func (s *Session) InitStats() {
100 | if s.Stats != nil {
101 | return
102 | }
103 | s.Stats = &Stats{
104 | StartedAt: time.Now(),
105 | Status: StatusInitializing,
106 | Progress: 0.0,
107 | Targets: 0,
108 | Repositories: 0,
109 | Commits: 0,
110 | Files: 0,
111 | Findings: 0,
112 | }
113 | }
114 |
115 | func (s *Session) InitLogger() {
116 | s.Out = &Logger{}
117 | s.Out.SetDebug(*s.Options.Debug)
118 | s.Out.SetSilent(*s.Options.Silent)
119 | }
120 |
121 | func (s *Session) InitGithubAccessToken() {
122 | if *s.Options.GithubAccessToken == "" {
123 | accessToken := os.Getenv(AccessTokenEnvVariable)
124 | if accessToken == "" {
125 | s.Out.Fatal("No GitHub access token given. Please provide via command line option or in the %s environment variable.\n", AccessTokenEnvVariable)
126 | }
127 | s.GithubAccessToken = accessToken
128 | } else {
129 | s.GithubAccessToken = *s.Options.GithubAccessToken
130 | }
131 | }
132 |
133 | func (s *Session) InitGithubClient() {
134 | ctx := context.Background()
135 | ts := oauth2.StaticTokenSource(
136 | &oauth2.Token{AccessToken: s.GithubAccessToken},
137 | )
138 | tc := oauth2.NewClient(ctx, ts)
139 | s.GithubClient = github.NewClient(tc)
140 | s.GithubClient.UserAgent = fmt.Sprintf("%s v%s", Name, Version)
141 | }
142 |
143 | func (s *Session) InitThreads() {
144 | if *s.Options.Threads == 0 {
145 | numCPUs := runtime.NumCPU()
146 | s.Options.Threads = &numCPUs
147 | }
148 | runtime.GOMAXPROCS(*s.Options.Threads + 2) // thread count + main + web server
149 | }
150 |
151 | func (s *Session) InitRouter() {
152 | bind := fmt.Sprintf("%s:%d", *s.Options.BindAddress, *s.Options.Port)
153 | s.Router = NewRouter(s)
154 | go func(sess *Session) {
155 | if err := sess.Router.Run(bind); err != nil {
156 | sess.Out.Fatal("Error when starting web server: %s\n", err)
157 | }
158 | }(s)
159 | }
160 |
161 | func (s *Session) SaveToFile(location string) error {
162 | sessionJson, err := json.Marshal(s)
163 | if err != nil {
164 | return err
165 | }
166 | err = ioutil.WriteFile(location, sessionJson, 0644)
167 | if err != nil {
168 | return err
169 | }
170 | return nil
171 | }
172 |
173 | func (s *Stats) IncrementTargets() {
174 | s.Lock()
175 | defer s.Unlock()
176 | s.Targets++
177 | }
178 |
179 | func (s *Stats) IncrementRepositories() {
180 | s.Lock()
181 | defer s.Unlock()
182 | s.Repositories++
183 | }
184 |
185 | func (s *Stats) IncrementCommits() {
186 | s.Lock()
187 | defer s.Unlock()
188 | s.Commits++
189 | }
190 |
191 | func (s *Stats) IncrementFiles() {
192 | s.Lock()
193 | defer s.Unlock()
194 | s.Files++
195 | }
196 |
197 | func (s *Stats) IncrementFindings() {
198 | s.Lock()
199 | defer s.Unlock()
200 | s.Findings++
201 | }
202 |
203 | func (s *Stats) UpdateProgress(current int, total int) {
204 | s.Lock()
205 | defer s.Unlock()
206 | if current >= total {
207 | s.Progress = 100.0
208 | } else {
209 | s.Progress = (float64(current) * float64(100)) / float64(total)
210 | }
211 | }
212 |
213 | func NewSession() (*Session, error) {
214 | var err error
215 | var session Session
216 |
217 | if session.Options, err = ParseOptions(); err != nil {
218 | return nil, err
219 | }
220 |
221 | if *session.Options.Save != "" && FileExists(*session.Options.Save) {
222 | return nil, errors.New(fmt.Sprintf("File: %s already exists.", *session.Options.Save))
223 | }
224 |
225 | if *session.Options.Load != "" {
226 | if !FileExists(*session.Options.Load) {
227 | return nil, errors.New(fmt.Sprintf("Session file %s does not exist or is not readable.", *session.Options.Load))
228 | }
229 | data, err := ioutil.ReadFile(*session.Options.Load)
230 | if err != nil {
231 | return nil, err
232 | }
233 | if err := json.Unmarshal(data, &session); err != nil {
234 | return nil, errors.New(fmt.Sprintf("Session file %s is corrupt or generated by an old version of Gitrob.", *session.Options.Load))
235 | }
236 | }
237 |
238 | session.Version = Version
239 | session.Start()
240 |
241 | return &session, nil
242 | }
243 |
--------------------------------------------------------------------------------
/core/signatures.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "crypto/sha1"
5 | "fmt"
6 | "io"
7 | "path/filepath"
8 | "regexp"
9 | "strings"
10 | )
11 |
12 | const (
13 | TypeSimple = "simple"
14 | TypePattern = "pattern"
15 |
16 | PartExtension = "extension"
17 | PartFilename = "filename"
18 | PartPath = "path"
19 | )
20 |
21 | var skippableExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".psd", ".xcf"}
22 | var skippablePathIndicators = []string{"node_modules/", "vendor/bundle", "vendor/cache"}
23 |
24 | type MatchFile struct {
25 | Path string
26 | Filename string
27 | Extension string
28 | }
29 |
30 | func (f *MatchFile) IsSkippable() bool {
31 | ext := strings.ToLower(f.Extension)
32 | path := strings.ToLower(f.Path)
33 | for _, skippableExt := range skippableExtensions {
34 | if ext == skippableExt {
35 | return true
36 | }
37 | }
38 | for _, skippablePathIndicator := range skippablePathIndicators {
39 | if strings.Contains(path, skippablePathIndicator) {
40 | return true
41 | }
42 | }
43 | return false
44 | }
45 |
46 | type Finding struct {
47 | Id string
48 | FilePath string
49 | Action string
50 | Description string
51 | Comment string
52 | RepositoryOwner string
53 | RepositoryName string
54 | CommitHash string
55 | CommitMessage string
56 | CommitAuthor string
57 | FileUrl string
58 | CommitUrl string
59 | RepositoryUrl string
60 | }
61 |
62 | func (f *Finding) setupUrls() {
63 | f.RepositoryUrl = fmt.Sprintf("https://github.com/%s/%s", f.RepositoryOwner, f.RepositoryName)
64 | f.FileUrl = fmt.Sprintf("%s/blob/%s/%s", f.RepositoryUrl, f.CommitHash, f.FilePath)
65 | f.CommitUrl = fmt.Sprintf("%s/commit/%s", f.RepositoryUrl, f.CommitHash)
66 | }
67 |
68 | func (f *Finding) generateID() {
69 | h := sha1.New()
70 | io.WriteString(h, f.FilePath)
71 | io.WriteString(h, f.Action)
72 | io.WriteString(h, f.RepositoryOwner)
73 | io.WriteString(h, f.RepositoryName)
74 | io.WriteString(h, f.CommitHash)
75 | io.WriteString(h, f.CommitMessage)
76 | io.WriteString(h, f.CommitAuthor)
77 | f.Id = fmt.Sprintf("%x", h.Sum(nil))
78 | }
79 |
80 | func (f *Finding) Initialize() {
81 | f.setupUrls()
82 | f.generateID()
83 | }
84 |
85 | type Signature interface {
86 | Match(file MatchFile) bool
87 | Description() string
88 | Comment() string
89 | }
90 |
91 | type SimpleSignature struct {
92 | part string
93 | match string
94 | description string
95 | comment string
96 | }
97 |
98 | type PatternSignature struct {
99 | part string
100 | match *regexp.Regexp
101 | description string
102 | comment string
103 | }
104 |
105 | func (s SimpleSignature) Match(file MatchFile) bool {
106 | var haystack *string
107 | switch s.part {
108 | case PartPath:
109 | haystack = &file.Path
110 | case PartFilename:
111 | haystack = &file.Filename
112 | case PartExtension:
113 | haystack = &file.Extension
114 | default:
115 | return false
116 | }
117 |
118 | return (s.match == *haystack)
119 | }
120 |
121 | func (s SimpleSignature) Description() string {
122 | return s.description
123 | }
124 |
125 | func (s SimpleSignature) Comment() string {
126 | return s.comment
127 | }
128 |
129 | func (s PatternSignature) Match(file MatchFile) bool {
130 | var haystack *string
131 | switch s.part {
132 | case PartPath:
133 | haystack = &file.Path
134 | case PartFilename:
135 | haystack = &file.Filename
136 | case PartExtension:
137 | haystack = &file.Extension
138 | default:
139 | return false
140 | }
141 |
142 | return s.match.MatchString(*haystack)
143 | }
144 |
145 | func (s PatternSignature) Description() string {
146 | return s.description
147 | }
148 |
149 | func (s PatternSignature) Comment() string {
150 | return s.comment
151 | }
152 |
153 | func NewMatchFile(path string) MatchFile {
154 | _, filename := filepath.Split(path)
155 | extension := filepath.Ext(path)
156 | return MatchFile{
157 | Path: path,
158 | Filename: filename,
159 | Extension: extension,
160 | }
161 | }
162 |
163 | var Signatures = []Signature{
164 | SimpleSignature{
165 | part: PartExtension,
166 | match: ".pem",
167 | description: "Potential cryptographic private key",
168 | comment: "",
169 | },
170 | SimpleSignature{
171 | part: PartExtension,
172 | match: ".log",
173 | description: "Log file",
174 | comment: "Log files can contain secret HTTP endpoints, session IDs, API keys and other goodies",
175 | },
176 | SimpleSignature{
177 | part: PartExtension,
178 | match: ".pkcs12",
179 | description: "Potential cryptographic key bundle",
180 | comment: "",
181 | },
182 | SimpleSignature{
183 | part: PartExtension,
184 | match: ".p12",
185 | description: "Potential cryptographic key bundle",
186 | comment: "",
187 | },
188 | SimpleSignature{
189 | part: PartExtension,
190 | match: ".pfx",
191 | description: "Potential cryptographic key bundle",
192 | comment: "",
193 | },
194 | SimpleSignature{
195 | part: PartExtension,
196 | match: ".asc",
197 | description: "Potential cryptographic key bundle",
198 | comment: "",
199 | },
200 | SimpleSignature{
201 | part: PartFilename,
202 | match: "otr.private_key",
203 | description: "Pidgin OTR private key",
204 | comment: "",
205 | },
206 | SimpleSignature{
207 | part: PartExtension,
208 | match: ".ovpn",
209 | description: "OpenVPN client configuration file",
210 | comment: "",
211 | },
212 | SimpleSignature{
213 | part: PartExtension,
214 | match: ".cscfg",
215 | description: "Azure service configuration schema file",
216 | comment: "",
217 | },
218 | SimpleSignature{
219 | part: PartExtension,
220 | match: ".rdp",
221 | description: "Remote Desktop connection file",
222 | comment: "",
223 | },
224 | SimpleSignature{
225 | part: PartExtension,
226 | match: ".mdf",
227 | description: "Microsoft SQL database file",
228 | comment: "",
229 | },
230 | SimpleSignature{
231 | part: PartExtension,
232 | match: ".sdf",
233 | description: "Microsoft SQL server compact database file",
234 | comment: "",
235 | },
236 | SimpleSignature{
237 | part: PartExtension,
238 | match: ".sqlite",
239 | description: "SQLite database file",
240 | comment: "",
241 | },
242 | SimpleSignature{
243 | part: PartExtension,
244 | match: ".bek",
245 | description: "Microsoft BitLocker recovery key file",
246 | comment: "",
247 | },
248 | SimpleSignature{
249 | part: PartExtension,
250 | match: ".tpm",
251 | description: "Microsoft BitLocker Trusted Platform Module password file",
252 | comment: "",
253 | },
254 | SimpleSignature{
255 | part: PartExtension,
256 | match: ".fve",
257 | description: "Windows BitLocker full volume encrypted data file",
258 | comment: "",
259 | },
260 | SimpleSignature{
261 | part: PartExtension,
262 | match: ".jks",
263 | description: "Java keystore file",
264 | comment: "",
265 | },
266 | SimpleSignature{
267 | part: PartExtension,
268 | match: ".psafe3",
269 | description: "Password Safe database file",
270 | comment: "",
271 | },
272 | SimpleSignature{
273 | part: PartFilename,
274 | match: "secret_token.rb",
275 | description: "Ruby On Rails secret token configuration file",
276 | comment: "If the Rails secret token is known, it can allow for remote code execution (http://www.exploit-db.com/exploits/27527/)",
277 | },
278 | SimpleSignature{
279 | part: PartFilename,
280 | match: "carrierwave.rb",
281 | description: "Carrierwave configuration file",
282 | comment: "Can contain credentials for cloud storage systems such as Amazon S3 and Google Storage",
283 | },
284 | SimpleSignature{
285 | part: PartFilename,
286 | match: "database.yml",
287 | description: "Potential Ruby On Rails database configuration file",
288 | comment: "Can contain database credentials",
289 | },
290 | SimpleSignature{
291 | part: PartFilename,
292 | match: "omniauth.rb",
293 | description: "OmniAuth configuration file",
294 | comment: "The OmniAuth configuration file can contain client application secrets",
295 | },
296 | SimpleSignature{
297 | part: PartFilename,
298 | match: "settings.py",
299 | description: "Django configuration file",
300 | comment: "Can contain database credentials, cloud storage system credentials, and other secrets",
301 | },
302 | SimpleSignature{
303 | part: PartExtension,
304 | match: ".agilekeychain",
305 | description: "1Password password manager database file",
306 | comment: "Feed it to Hashcat and see if you're lucky",
307 | },
308 | SimpleSignature{
309 | part: PartExtension,
310 | match: ".keychain",
311 | description: "Apple Keychain database file",
312 | comment: "",
313 | },
314 | SimpleSignature{
315 | part: PartExtension,
316 | match: ".pcap",
317 | description: "Network traffic capture file",
318 | comment: "",
319 | },
320 | SimpleSignature{
321 | part: PartExtension,
322 | match: ".gnucash",
323 | description: "GnuCash database file",
324 | comment: "",
325 | },
326 | SimpleSignature{
327 | part: PartFilename,
328 | match: "jenkins.plugins.publish_over_ssh.BapSshPublisherPlugin.xml",
329 | description: "Jenkins publish over SSH plugin file",
330 | comment: "",
331 | },
332 | SimpleSignature{
333 | part: PartFilename,
334 | match: "credentials.xml",
335 | description: "Potential Jenkins credentials file",
336 | comment: "",
337 | },
338 | SimpleSignature{
339 | part: PartExtension,
340 | match: ".kwallet",
341 | description: "KDE Wallet Manager database file",
342 | comment: "",
343 | },
344 | SimpleSignature{
345 | part: PartFilename,
346 | match: "LocalSettings.php",
347 | description: "Potential MediaWiki configuration file",
348 | comment: "",
349 | },
350 | SimpleSignature{
351 | part: PartExtension,
352 | match: ".tblk",
353 | description: "Tunnelblick VPN configuration file",
354 | comment: "",
355 | },
356 | SimpleSignature{
357 | part: PartFilename,
358 | match: "Favorites.plist",
359 | description: "Sequel Pro MySQL database manager bookmark file",
360 | comment: "",
361 | },
362 | SimpleSignature{
363 | part: PartFilename,
364 | match: "configuration.user.xpl",
365 | description: "Little Snitch firewall configuration file",
366 | comment: "Contains traffic rules for applications",
367 | },
368 | SimpleSignature{
369 | part: PartExtension,
370 | match: ".dayone",
371 | description: "Day One journal file",
372 | comment: "Now it's getting creepy...",
373 | },
374 | SimpleSignature{
375 | part: PartFilename,
376 | match: "journal.txt",
377 | description: "Potential jrnl journal file",
378 | comment: "Now it's getting creepy...",
379 | },
380 | SimpleSignature{
381 | part: PartFilename,
382 | match: "knife.rb",
383 | description: "Chef Knife configuration file",
384 | comment: "Can contain references to Chef servers",
385 | },
386 | SimpleSignature{
387 | part: PartFilename,
388 | match: "proftpdpasswd",
389 | description: "cPanel backup ProFTPd credentials file",
390 | comment: "Contains usernames and password hashes for FTP accounts",
391 | },
392 | SimpleSignature{
393 | part: PartFilename,
394 | match: "robomongo.json",
395 | description: "Robomongo MongoDB manager configuration file",
396 | comment: "Can contain credentials for MongoDB databases",
397 | },
398 | SimpleSignature{
399 | part: PartFilename,
400 | match: "filezilla.xml",
401 | description: "FileZilla FTP configuration file",
402 | comment: "Can contain credentials for FTP servers",
403 | },
404 | SimpleSignature{
405 | part: PartFilename,
406 | match: "recentservers.xml",
407 | description: "FileZilla FTP recent servers file",
408 | comment: "Can contain credentials for FTP servers",
409 | },
410 | SimpleSignature{
411 | part: PartFilename,
412 | match: "ventrilo_srv.ini",
413 | description: "Ventrilo server configuration file",
414 | comment: "Can contain passwords",
415 | },
416 | SimpleSignature{
417 | part: PartFilename,
418 | match: "terraform.tfvars",
419 | description: "Terraform variable config file",
420 | comment: "Can contain credentials for terraform providers",
421 | },
422 | SimpleSignature{
423 | part: PartFilename,
424 | match: ".exports",
425 | description: "Shell configuration file",
426 | comment: "Shell configuration files can contain passwords, API keys, hostnames and other goodies",
427 | },
428 | SimpleSignature{
429 | part: PartFilename,
430 | match: ".functions",
431 | description: "Shell configuration file",
432 | comment: "Shell configuration files can contain passwords, API keys, hostnames and other goodies",
433 | },
434 | SimpleSignature{
435 | part: PartFilename,
436 | match: ".extra",
437 | description: "Shell configuration file",
438 | comment: "Shell configuration files can contain passwords, API keys, hostnames and other goodies",
439 | },
440 | PatternSignature{
441 | part: PartFilename,
442 | match: regexp.MustCompile(`^.*_rsa$`),
443 | description: "Private SSH key",
444 | comment: "",
445 | },
446 | PatternSignature{
447 | part: PartFilename,
448 | match: regexp.MustCompile(`^.*_dsa$`),
449 | description: "Private SSH key",
450 | comment: "",
451 | },
452 | PatternSignature{
453 | part: PartFilename,
454 | match: regexp.MustCompile(`^.*_ed25519$`),
455 | description: "Private SSH key",
456 | comment: "",
457 | },
458 | PatternSignature{
459 | part: PartFilename,
460 | match: regexp.MustCompile(`^.*_ecdsa$`),
461 | description: "Private SSH key",
462 | comment: "",
463 | },
464 | PatternSignature{
465 | part: PartPath,
466 | match: regexp.MustCompile(`\.?ssh/config$`),
467 | description: "SSH configuration file",
468 | comment: "",
469 | },
470 | PatternSignature{
471 | part: PartExtension,
472 | match: regexp.MustCompile(`^key(pair)?$`),
473 | description: "Potential cryptographic private key",
474 | comment: "",
475 | },
476 | PatternSignature{
477 | part: PartFilename,
478 | match: regexp.MustCompile(`^\.?(bash_|zsh_|sh_|z)?history$`),
479 | description: "Shell command history file",
480 | comment: "",
481 | },
482 | PatternSignature{
483 | part: PartFilename,
484 | match: regexp.MustCompile(`^\.?mysql_history$`),
485 | description: "MySQL client command history file",
486 | comment: "",
487 | },
488 | PatternSignature{
489 | part: PartFilename,
490 | match: regexp.MustCompile(`^\.?psql_history$`),
491 | description: "PostgreSQL client command history file",
492 | comment: "",
493 | },
494 | PatternSignature{
495 | part: PartFilename,
496 | match: regexp.MustCompile(`^\.?pgpass$`),
497 | description: "PostgreSQL password file",
498 | comment: "",
499 | },
500 | PatternSignature{
501 | part: PartFilename,
502 | match: regexp.MustCompile(`^\.?irb_history$`),
503 | description: "Ruby IRB console history file",
504 | comment: "",
505 | },
506 | PatternSignature{
507 | part: PartPath,
508 | match: regexp.MustCompile(`\.?purple/accounts\.xml$`),
509 | description: "Pidgin chat client account configuration file",
510 | comment: "",
511 | },
512 | PatternSignature{
513 | part: PartPath,
514 | match: regexp.MustCompile(`\.?xchat2?/servlist_?\.conf$`),
515 | description: "Hexchat/XChat IRC client server list configuration file",
516 | comment: "",
517 | },
518 | PatternSignature{
519 | part: PartPath,
520 | match: regexp.MustCompile(`\.?irssi/config$`),
521 | description: "Irssi IRC client configuration file",
522 | comment: "",
523 | },
524 | PatternSignature{
525 | part: PartPath,
526 | match: regexp.MustCompile(`\.?recon-ng/keys\.db$`),
527 | description: "Recon-ng web reconnaissance framework API key database",
528 | comment: "",
529 | },
530 | PatternSignature{
531 | part: PartFilename,
532 | match: regexp.MustCompile(`^\.?dbeaver-data-sources.xml$`),
533 | description: "DBeaver SQL database manager configuration file",
534 | comment: "",
535 | },
536 | PatternSignature{
537 | part: PartFilename,
538 | match: regexp.MustCompile(`^\.?muttrc$`),
539 | description: "Mutt e-mail client configuration file",
540 | comment: "",
541 | },
542 | PatternSignature{
543 | part: PartFilename,
544 | match: regexp.MustCompile(`^\.?s3cfg$`),
545 | description: "S3cmd configuration file",
546 | comment: "",
547 | },
548 | PatternSignature{
549 | part: PartPath,
550 | match: regexp.MustCompile(`\.?aws/credentials$`),
551 | description: "AWS CLI credentials file",
552 | comment: "",
553 | },
554 | PatternSignature{
555 | part: PartFilename,
556 | match: regexp.MustCompile(`^sftp-config(\.json)?$`),
557 | description: "SFTP connection configuration file",
558 | comment: "",
559 | },
560 | PatternSignature{
561 | part: PartFilename,
562 | match: regexp.MustCompile(`^\.?trc$`),
563 | description: "T command-line Twitter client configuration file",
564 | comment: "",
565 | },
566 | PatternSignature{
567 | part: PartFilename,
568 | match: regexp.MustCompile(`^\.?gitrobrc$`),
569 | description: "Well, this is awkward... Gitrob configuration file",
570 | comment: "",
571 | },
572 | PatternSignature{
573 | part: PartFilename,
574 | match: regexp.MustCompile(`^\.?(bash|zsh|csh)rc$`),
575 | description: "Shell configuration file",
576 | comment: "Shell configuration files can contain passwords, API keys, hostnames and other goodies",
577 | },
578 | PatternSignature{
579 | part: PartFilename,
580 | match: regexp.MustCompile(`^\.?(bash_|zsh_)?profile$`),
581 | description: "Shell profile configuration file",
582 | comment: "Shell configuration files can contain passwords, API keys, hostnames and other goodies",
583 | },
584 | PatternSignature{
585 | part: PartFilename,
586 | match: regexp.MustCompile(`^\.?(bash_|zsh_)?aliases$`),
587 | description: "Shell command alias configuration file",
588 | comment: "Shell configuration files can contain passwords, API keys, hostnames and other goodies",
589 | },
590 | PatternSignature{
591 | part: PartFilename,
592 | match: regexp.MustCompile(`config(\.inc)?\.php$`),
593 | description: "PHP configuration file",
594 | comment: "",
595 | },
596 | PatternSignature{
597 | part: PartExtension,
598 | match: regexp.MustCompile(`^key(store|ring)$`),
599 | description: "GNOME Keyring database file",
600 | comment: "",
601 | },
602 | PatternSignature{
603 | part: PartExtension,
604 | match: regexp.MustCompile(`^kdbx?$`),
605 | description: "KeePass password manager database file",
606 | comment: "Feed it to Hashcat and see if you're lucky",
607 | },
608 | PatternSignature{
609 | part: PartExtension,
610 | match: regexp.MustCompile(`^sql(dump)?$`),
611 | description: "SQL dump file",
612 | comment: "",
613 | },
614 | PatternSignature{
615 | part: PartFilename,
616 | match: regexp.MustCompile(`^\.?htpasswd$`),
617 | description: "Apache htpasswd file",
618 | comment: "",
619 | },
620 | PatternSignature{
621 | part: PartFilename,
622 | match: regexp.MustCompile(`^(\.|_)?netrc$`),
623 | description: "Configuration file for auto-login process",
624 | comment: "Can contain username and password",
625 | },
626 | PatternSignature{
627 | part: PartPath,
628 | match: regexp.MustCompile(`\.?gem/credentials$`),
629 | description: "Rubygems credentials file",
630 | comment: "Can contain API key for a rubygems.org account",
631 | },
632 | PatternSignature{
633 | part: PartFilename,
634 | match: regexp.MustCompile(`^\.?tugboat$`),
635 | description: "Tugboat DigitalOcean management tool configuration",
636 | comment: "",
637 | },
638 | PatternSignature{
639 | part: PartPath,
640 | match: regexp.MustCompile(`doctl/config.yaml$`),
641 | description: "DigitalOcean doctl command-line client configuration file",
642 | comment: "Contains DigitalOcean API key and other information",
643 | },
644 | PatternSignature{
645 | part: PartFilename,
646 | match: regexp.MustCompile(`^\.?git-credentials$`),
647 | description: "git-credential-store helper credentials file",
648 | comment: "",
649 | },
650 | PatternSignature{
651 | part: PartPath,
652 | match: regexp.MustCompile(`config/hub$`),
653 | description: "GitHub Hub command-line client configuration file",
654 | comment: "Can contain GitHub API access token",
655 | },
656 | PatternSignature{
657 | part: PartFilename,
658 | match: regexp.MustCompile(`^\.?gitconfig$`),
659 | description: "Git configuration file",
660 | comment: "",
661 | },
662 | PatternSignature{
663 | part: PartPath,
664 | match: regexp.MustCompile(`\.?chef/(.*)\.pem$`),
665 | description: "Chef private key",
666 | comment: "Can be used to authenticate against Chef servers",
667 | },
668 | PatternSignature{
669 | part: PartPath,
670 | match: regexp.MustCompile(`etc/shadow$`),
671 | description: "Potential Linux shadow file",
672 | comment: "Contains hashed passwords for system users",
673 | },
674 | PatternSignature{
675 | part: PartPath,
676 | match: regexp.MustCompile(`etc/passwd$`),
677 | description: "Potential Linux passwd file",
678 | comment: "Contains system user information",
679 | },
680 | PatternSignature{
681 | part: PartFilename,
682 | match: regexp.MustCompile(`^\.?dockercfg$`),
683 | description: "Docker configuration file",
684 | comment: "Can contain credentials for public or private Docker registries",
685 | },
686 | PatternSignature{
687 | part: PartFilename,
688 | match: regexp.MustCompile(`^\.?npmrc$`),
689 | description: "NPM configuration file",
690 | comment: "Can contain credentials for NPM registries",
691 | },
692 | PatternSignature{
693 | part: PartFilename,
694 | match: regexp.MustCompile(`^\.?env$`),
695 | description: "Environment configuration file",
696 | comment: "",
697 | },
698 | PatternSignature{
699 | part: PartPath,
700 | match: regexp.MustCompile(`credential`),
701 | description: "Contains word: credential",
702 | comment: "",
703 | },
704 | PatternSignature{
705 | part: PartPath,
706 | match: regexp.MustCompile(`password`),
707 | description: "Contains word: password",
708 | comment: "",
709 | },
710 | }
711 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | "github.com/michenriksen/gitrob/core"
11 | )
12 |
13 | var (
14 | sess *core.Session
15 | err error
16 | )
17 |
18 | func GatherTargets(sess *core.Session) {
19 | sess.Stats.Status = core.StatusGathering
20 | sess.Out.Important("Gathering targets...\n")
21 | for _, login := range sess.Options.Logins {
22 | target, err := core.GetUserOrOrganization(login, sess.GithubClient)
23 | if err != nil {
24 | sess.Out.Error(" Error retrieving information on %s: %s\n", login, err)
25 | continue
26 | }
27 | sess.Out.Debug("%s (ID: %d) type: %s\n", *target.Login, *target.ID, *target.Type)
28 | sess.AddTarget(target)
29 | if *sess.Options.NoExpandOrgs == false && *target.Type == "Organization" {
30 | sess.Out.Debug("Gathering members of %s (ID: %d)...\n", *target.Login, *target.ID)
31 | members, err := core.GetOrganizationMembers(target.Login, sess.GithubClient)
32 | if err != nil {
33 | sess.Out.Error(" Error retrieving members of %s: %s\n", *target.Login, err)
34 | continue
35 | }
36 | for _, member := range members {
37 | sess.Out.Debug("Adding organization member %s (ID: %d) to targets\n", *member.Login, *member.ID)
38 | sess.AddTarget(member)
39 | }
40 | }
41 | }
42 | }
43 |
44 | func GatherRepositories(sess *core.Session) {
45 | var ch = make(chan *core.GithubOwner, len(sess.Targets))
46 | var wg sync.WaitGroup
47 | var threadNum int
48 | if len(sess.Targets) == 1 {
49 | threadNum = 1
50 | } else if len(sess.Targets) <= *sess.Options.Threads {
51 | threadNum = len(sess.Targets) - 1
52 | } else {
53 | threadNum = *sess.Options.Threads
54 | }
55 | wg.Add(threadNum)
56 | sess.Out.Debug("Threads for repository gathering: %d\n", threadNum)
57 | for i := 0; i < threadNum; i++ {
58 | go func() {
59 | for {
60 | target, ok := <-ch
61 | if !ok {
62 | wg.Done()
63 | return
64 | }
65 | repos, err := core.GetRepositoriesFromOwner(target.Login, sess.GithubClient)
66 | if err != nil {
67 | sess.Out.Error(" Failed to retrieve repositories from %s: %s\n", *target.Login, err)
68 | }
69 | if len(repos) == 0 {
70 | continue
71 | }
72 | for _, repo := range repos {
73 | sess.Out.Debug(" Retrieved repository: %s\n", *repo.FullName)
74 | sess.AddRepository(repo)
75 | }
76 | sess.Stats.IncrementTargets()
77 | sess.Out.Info(" Retrieved %d %s from %s\n", len(repos), core.Pluralize(len(repos), "repository", "repositories"), *target.Login)
78 | }
79 | }()
80 | }
81 |
82 | for _, target := range sess.Targets {
83 | ch <- target
84 | }
85 | close(ch)
86 | wg.Wait()
87 | }
88 |
89 | func AnalyzeRepositories(sess *core.Session) {
90 | sess.Stats.Status = core.StatusAnalyzing
91 | var ch = make(chan *core.GithubRepository, len(sess.Repositories))
92 | var wg sync.WaitGroup
93 | var threadNum int
94 | if len(sess.Repositories) <= 1 {
95 | threadNum = 1
96 | } else if len(sess.Repositories) <= *sess.Options.Threads {
97 | threadNum = len(sess.Repositories) - 1
98 | } else {
99 | threadNum = *sess.Options.Threads
100 | }
101 | wg.Add(threadNum)
102 | sess.Out.Debug("Threads for repository analysis: %d\n", threadNum)
103 |
104 | sess.Out.Important("Analyzing %d %s...\n", len(sess.Repositories), core.Pluralize(len(sess.Repositories), "repository", "repositories"))
105 |
106 | for i := 0; i < threadNum; i++ {
107 | go func(tid int) {
108 | for {
109 | sess.Out.Debug("[THREAD #%d] Requesting new repository to analyze...\n", tid)
110 | repo, ok := <-ch
111 | if !ok {
112 | sess.Out.Debug("[THREAD #%d] No more tasks, marking WaitGroup as done\n", tid)
113 | wg.Done()
114 | return
115 | }
116 |
117 | sess.Out.Debug("[THREAD #%d][%s] Cloning repository...\n", tid, *repo.FullName)
118 | clone, path, err := core.CloneRepository(repo.CloneURL, repo.DefaultBranch, *sess.Options.CommitDepth)
119 | if err != nil {
120 | if err.Error() != "remote repository is empty" {
121 | sess.Out.Error("Error cloning repository %s: %s\n", *repo.FullName, err)
122 | }
123 | sess.Stats.IncrementRepositories()
124 | sess.Stats.UpdateProgress(sess.Stats.Repositories, len(sess.Repositories))
125 | continue
126 | }
127 | sess.Out.Debug("[THREAD #%d][%s] Cloned repository to: %s\n", tid, *repo.FullName, path)
128 |
129 | history, err := core.GetRepositoryHistory(clone)
130 | if err != nil {
131 | sess.Out.Error("[THREAD #%d][%s] Error getting commit history: %s\n", tid, *repo.FullName, err)
132 | os.RemoveAll(path)
133 | sess.Stats.IncrementRepositories()
134 | sess.Stats.UpdateProgress(sess.Stats.Repositories, len(sess.Repositories))
135 | continue
136 | }
137 | sess.Out.Debug("[THREAD #%d][%s] Number of commits: %d\n", tid, *repo.FullName, len(history))
138 |
139 | for _, commit := range history {
140 | sess.Out.Debug("[THREAD #%d][%s] Analyzing commit: %s\n", tid, *repo.FullName, commit.Hash)
141 | changes, _ := core.GetChanges(commit, clone)
142 | sess.Out.Debug("[THREAD #%d][%s] Changes in %s: %d\n", tid, *repo.FullName, commit.Hash, len(changes))
143 | for _, change := range changes {
144 | changeAction := core.GetChangeAction(change)
145 | path := core.GetChangePath(change)
146 | matchFile := core.NewMatchFile(path)
147 | if matchFile.IsSkippable() {
148 | sess.Out.Debug("[THREAD #%d][%s] Skipping %s\n", tid, *repo.FullName, matchFile.Path)
149 | continue
150 | }
151 | sess.Out.Debug("[THREAD #%d][%s] Matching: %s...\n", tid, *repo.FullName, matchFile.Path)
152 | for _, signature := range core.Signatures {
153 | if signature.Match(matchFile) {
154 |
155 | finding := &core.Finding{
156 | FilePath: path,
157 | Action: changeAction,
158 | Description: signature.Description(),
159 | Comment: signature.Comment(),
160 | RepositoryOwner: *repo.Owner,
161 | RepositoryName: *repo.Name,
162 | CommitHash: commit.Hash.String(),
163 | CommitMessage: strings.TrimSpace(commit.Message),
164 | CommitAuthor: commit.Author.String(),
165 | }
166 | finding.Initialize()
167 | sess.AddFinding(finding)
168 |
169 | sess.Out.Warn(" %s: %s\n", strings.ToUpper(changeAction), finding.Description)
170 | sess.Out.Info(" Path.......: %s\n", finding.FilePath)
171 | sess.Out.Info(" Repo.......: %s\n", *repo.FullName)
172 | sess.Out.Info(" Message....: %s\n", core.TruncateString(finding.CommitMessage, 100))
173 | sess.Out.Info(" Author.....: %s\n", finding.CommitAuthor)
174 | if finding.Comment != "" {
175 | sess.Out.Info(" Comment....: %s\n", finding.Comment)
176 | }
177 | sess.Out.Info(" File URL...: %s\n", finding.FileUrl)
178 | sess.Out.Info(" Commit URL.: %s\n", finding.CommitUrl)
179 | sess.Out.Info(" ------------------------------------------------\n\n")
180 | sess.Stats.IncrementFindings()
181 | break
182 | }
183 | }
184 | sess.Stats.IncrementFiles()
185 | }
186 | sess.Stats.IncrementCommits()
187 | sess.Out.Debug("[THREAD #%d][%s] Done analyzing changes in %s\n", tid, *repo.FullName, commit.Hash)
188 | }
189 | sess.Out.Debug("[THREAD #%d][%s] Done analyzing commits\n", tid, *repo.FullName)
190 | os.RemoveAll(path)
191 | sess.Out.Debug("[THREAD #%d][%s] Deleted %s\n", tid, *repo.FullName, path)
192 | sess.Stats.IncrementRepositories()
193 | sess.Stats.UpdateProgress(sess.Stats.Repositories, len(sess.Repositories))
194 | }
195 | }(i)
196 | }
197 | for _, repo := range sess.Repositories {
198 | ch <- repo
199 | }
200 | close(ch)
201 | wg.Wait()
202 | }
203 |
204 | func PrintSessionStats(sess *core.Session) {
205 | sess.Out.Info("\nFindings....: %d\n", sess.Stats.Findings)
206 | sess.Out.Info("Files.......: %d\n", sess.Stats.Files)
207 | sess.Out.Info("Commits.....: %d\n", sess.Stats.Commits)
208 | sess.Out.Info("Repositories: %d\n", sess.Stats.Repositories)
209 | sess.Out.Info("Targets.....: %d\n\n", sess.Stats.Targets)
210 | }
211 |
212 | func main() {
213 | if sess, err = core.NewSession(); err != nil {
214 | fmt.Println(err)
215 | os.Exit(1)
216 | }
217 |
218 | sess.Out.Info("%s\n\n", core.ASCIIBanner)
219 | sess.Out.Important("%s v%s started at %s\n", core.Name, core.Version, sess.Stats.StartedAt.Format(time.RFC3339))
220 | sess.Out.Important("Loaded %d signatures\n", len(core.Signatures))
221 | sess.Out.Important("Web interface available at http://%s:%d\n", *sess.Options.BindAddress, *sess.Options.Port)
222 |
223 | if sess.Stats.Status == "finished" {
224 | sess.Out.Important("Loaded session file: %s\n", *sess.Options.Load)
225 | } else {
226 | if len(sess.Options.Logins) == 0 {
227 | sess.Out.Fatal("Please provide at least one GitHub organization or user\n")
228 | }
229 |
230 | GatherTargets(sess)
231 | GatherRepositories(sess)
232 | AnalyzeRepositories(sess)
233 | sess.Finish()
234 |
235 | if *sess.Options.Save != "" {
236 | err := sess.SaveToFile(*sess.Options.Save)
237 | if err != nil {
238 | sess.Out.Error("Error saving session to %s: %s\n", *sess.Options.Save, err)
239 | }
240 | sess.Out.Important("Saved session to: %s\n\n", *sess.Options.Save)
241 | }
242 | }
243 |
244 | PrintSessionStats(sess)
245 | sess.Out.Important("Press Ctrl+C to stop web server and exit.\n\n")
246 | select {}
247 | }
248 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CURRENT_VERSION=$(cat core/banner.go | grep Version | cut -d '"' -f 2)
4 | TO_UPDATE=(
5 | core/banner.go
6 | )
7 |
8 | read -p "[?] Did you remember to update CHANGELOG.md? "
9 | read -p "[?] Did you remember to update README.md with new features/changes? "
10 |
11 | echo -n "[*] Current version is $CURRENT_VERSION. Enter new version: "
12 | read NEW_VERSION
13 | echo "[*] Pushing and tagging version $NEW_VERSION in 5 seconds..."
14 | sleep 5
15 |
16 | for file in "${TO_UPDATE[@]}"
17 | do
18 | echo "[*] Patching $file ..."
19 | sed -i "s/$CURRENT_VERSION/$NEW_VERSION/g" $file
20 | git add $file
21 | done
22 |
23 | git commit -m "Releasing v$NEW_VERSION"
24 | git push
25 |
26 | git tag -a v$NEW_VERSION -m "Release v$NEW_VERSION"
27 | git push origin v$NEW_VERSION
28 |
29 | echo
30 | echo "[*] All done, v$NEW_VERSION released."
31 |
--------------------------------------------------------------------------------
/static/fonts/open-iconic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michenriksen/gitrob/7be4c5306a61383a3ba16777b520b3c2a8956a1e/static/fonts/open-iconic.eot
--------------------------------------------------------------------------------
/static/fonts/open-iconic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michenriksen/gitrob/7be4c5306a61383a3ba16777b520b3c2a8956a1e/static/fonts/open-iconic.otf
--------------------------------------------------------------------------------
/static/fonts/open-iconic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michenriksen/gitrob/7be4c5306a61383a3ba16777b520b3c2a8956a1e/static/fonts/open-iconic.ttf
--------------------------------------------------------------------------------
/static/fonts/open-iconic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michenriksen/gitrob/7be4c5306a61383a3ba16777b520b3c2a8956a1e/static/fonts/open-iconic.woff
--------------------------------------------------------------------------------
/static/images/gopher_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michenriksen/gitrob/7be4c5306a61383a3ba16777b520b3c2a8956a1e/static/images/gopher_full.png
--------------------------------------------------------------------------------
/static/images/gopher_head.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michenriksen/gitrob/7be4c5306a61383a3ba16777b520b3c2a8956a1e/static/images/gopher_head.png
--------------------------------------------------------------------------------
/static/images/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michenriksen/gitrob/7be4c5306a61383a3ba16777b520b3c2a8956a1e/static/images/spinner.gif
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Gitrob
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
29 |
30 |
31 |
32 |
33 |
34 |
Initializing...
35 |
36 |
37 |
38 |
39 |
40 |
41 |
0
42 |
Findings
43 |
44 |
45 |
46 |
54 |
62 |
63 |
64 |
65 |
0
66 |
Repositories
67 |
68 |
69 |
70 |
78 |
79 |
80 |
81 |
00:00:00
82 |
Duration
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
110 |
111 |
112 |
117 |
118 |
134 |
135 |
187 |
188 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
--------------------------------------------------------------------------------
/static/javascripts/application.js:
--------------------------------------------------------------------------------
1 | var Stats = Backbone.Model.extend({
2 | url: "/stats",
3 | defaults: {
4 | "Status": "initializing",
5 | "StartedAt": null,
6 | "FinishedAt": null,
7 | "Progress": 0,
8 | "Targets": 0,
9 | "Repositories": 0,
10 | "Commits": 0,
11 | "Files": 0,
12 | "Findings": 0,
13 | },
14 | isFinished: function() {
15 | return this.get("Status") === "finished";
16 | },
17 | duration: function() {
18 | if (this.get("StartedAt") === null) {
19 | return "00:00:00";
20 | }
21 | var end;
22 | var start = Date.parse(this.get("StartedAt"));
23 | if (this.isFinished()) {
24 | end = Date.parse(this.get("FinishedAt"));
25 | } else {
26 | end = Date.now();
27 | }
28 | var millis = end - start;
29 | var seconds = Math.floor(millis / 1000);
30 | var nullDate = new Date(null);
31 | nullDate.setSeconds(seconds);
32 | return nullDate.toISOString().substr(11, 8);
33 | },
34 | });
35 | window.stats = new Stats;
36 |
37 | var Finding = Backbone.Model.extend({
38 | idAttribute: "Id",
39 | testFileIndicators: ["test", "_spec", "fixture", "mock", "stub", "fake", "demo", "sample"],
40 | shortCommitHash: function() {
41 | return this.get("CommitHash").substr(0, 7);
42 | },
43 | trimmedCommitMessage: function() {
44 | var message = this.get("CommitMessage").split("-----END PGP SIGNATURE-----", 2).pop();
45 | return message.replace(/^\s\s*/, "").replace(/\s\s*$/, "")
46 | },
47 | isTestRelated: function() {
48 | var path = this.get("FilePath").toLowerCase();
49 | for (var i = 0; i < this.testFileIndicators.length; i++) {
50 | if (path.indexOf(this.testFileIndicators[i]) > -1) {
51 | return true;
52 | }
53 | }
54 | return false;
55 | },
56 | fileContentsUrl: function() {
57 | return ["/files", this.get("RepositoryOwner"), this.get("RepositoryName"), this.get("CommitHash"), this.get("FilePath")].join("/");
58 | },
59 | fileContents: function(callback, error) {
60 | $.ajax({
61 | url: this.fileContentsUrl(),
62 | success: callback,
63 | error: error
64 | });
65 | },
66 | });
67 |
68 | var Findings = Backbone.Collection.extend({
69 | url: "/findings",
70 | model: Finding,
71 | });
72 |
73 | window.findings = new Findings();
74 |
75 | var StatsView = Backbone.View.extend({
76 | id: "stats_container",
77 | model: stats,
78 | pollingTicker: null,
79 | durationTicker: null,
80 | pollingInterval: 500,
81 | initialize: function() {
82 | this.listenTo(this.model, "change", this.render)
83 | this.startDurationTicker();
84 | this.startPolling();
85 | },
86 | render: function() {
87 | if (this.model.isFinished()) {
88 | this.stopPolling();
89 | this.stopDurationTicker();
90 | }
91 | if (this.model.hasChanged("Progress")) {
92 | this.updateProgress();
93 | }
94 | if (this.model.hasChanged("Findings")) {
95 | this.updateFindings();
96 | }
97 | if (this.model.hasChanged("Files")) {
98 | this.updateFiles();
99 | }
100 | if (this.model.hasChanged("Commits")) {
101 | this.updateCommits();
102 | }
103 | if (this.model.hasChanged("Repositories")) {
104 | this.updateRepositories();
105 | }
106 | if (this.model.hasChanged("Targets")) {
107 | this.updateTargets();
108 | }
109 | },
110 | startPolling: function() {
111 | this.pollingTicker = setInterval(function() {
112 | statsView.model.fetch();
113 | }, this.pollingInterval);
114 | },
115 | stopPolling: function() {
116 | if (this.pollingTicker !== null) {
117 | clearInterval(this.pollingTicker);
118 | }
119 | },
120 | startDurationTicker: function() {
121 | this.DurationTicker = setInterval(function() {
122 | statsView.updateDuration()
123 | }, 1000);
124 | },
125 | stopDurationTicker: function() {
126 | this.updateDuration();
127 | if (this.durationTicker !== null) {
128 | clearInterval(this.durationTicker);
129 | }
130 | },
131 | updateDuration: function() {
132 | $("#card_duration_value").text(this.model.duration());
133 | },
134 | updateProgress: function() {
135 | var status = this.statusToHuman();
136 | $("title").text("Gitrob: " + status);
137 | $("#progress_bar").text(status).css("width", this.model.get("Progress") + "%");
138 | if (this.model.isFinished()) {
139 | $("#progress_bar").removeClass("progress-bar-animated progress-bar-striped").css("width", "100%");
140 | }
141 | },
142 | updateFindings: function() {
143 | $("#card_findings_value").hide().text(this.model.get("Findings").toLocaleString()).fadeIn("fast");
144 | },
145 | updateFiles: function() {
146 | $("#card_files_value").hide().text(this.model.get("Files").toLocaleString()).fadeIn("fast");
147 | },
148 | updateCommits: function() {
149 | $("#card_commits_value").hide().text(this.model.get("Commits").toLocaleString()).fadeIn("fast");
150 | },
151 | updateRepositories: function() {
152 | $("#card_repositories_value").hide().text(this.model.get("Repositories").toLocaleString()).fadeIn("fast");
153 | },
154 | updateTargets: function() {
155 | $("#card_targets_value").hide().text(this.model.get("Targets").toLocaleString()).fadeIn("fast");
156 | },
157 | statusToHuman: function() {
158 | var status;
159 | switch(this.model.get("Status")) {
160 | case "initializing":
161 | status = "Initializing";
162 | break;
163 | case "gathering":
164 | status = "Gathering repositories";
165 | break;
166 | case "analyzing":
167 | status = "Analyzing repositories"
168 | break;
169 | case "finished":
170 | status = "Finished";
171 | break;
172 | default:
173 | status = "Unknown";
174 | break;
175 | }
176 | return status + " (" + parseInt(this.model.get("Progress")) + "%)";
177 | }
178 | });
179 | window.statsView = new StatsView({el: $("#stats_container")});
180 |
181 | var FindingView = Backbone.View.extend({
182 | tagName: "tr",
183 | events: {
184 | "click td.col-path a": "showFinding",
185 | },
186 | template: _.template($("#template_finding").html()),
187 | render: function() {
188 | this.$el.html(this.template(this.model.attributes)).data("finding", this.model);
189 | if (this.model.isTestRelated()) {
190 | this.$el.addClass("test-related");
191 | }
192 | return this;
193 | },
194 | formattedFilePath: function() {
195 | var splits = this.model.get("FilePath").split("/");
196 | var filename = splits.pop();
197 | var directory = this.ellipsisize(splits.join("/"), 60, 25);
198 | if (directory === "") {
199 | return "" + _.escape(filename) + "";
200 | }
201 | return _.escape(directory) + "/" + "" + _.escape(filename) + "";
202 | },
203 | ellipsisize: function(str, minLength, edgeLength) {
204 | str = String(str);
205 | if (str.length < minLength || str.length <= (edgeLength * 2)) {
206 | return str;
207 | }
208 | var edge = Array(edgeLength + 1).join(".");
209 | var midLength = str.length - edgeLength * 2;
210 | var pattern = "(" + edge + ").{" + midLength + "}(" + edge + ")";
211 | return str.replace(new RegExp(pattern), "$1…$2");
212 | },
213 | showFinding: function(e) {
214 | e.preventDefault();
215 | this.markAsSelected();
216 | var modalView = new FindingModal({
217 | model: this.model,
218 | el: "#finding_modal .modal-content"
219 | });
220 | modalView.render();
221 | $("#finding_modal").modal();
222 | modalView.fetchFileContents();
223 | },
224 | markAsSelected: function() {
225 | this.$el.closest("tbody").find("tr.table-selected").removeClass("table-selected");
226 | this.$el.addClass("table-selected");
227 | },
228 | });
229 |
230 | var FindingsView = Backbone.View.extend({
231 | collection: findings,
232 | initialize: function() {
233 | this.listenTo(this.collection, "add", this.renderFinding);
234 | this.listenTo(stats, "change:Findings", _.debounce(this.update, 500));
235 | $("#findings_search").on("keyup", _.debounce(this.searchFindings, 200));
236 | $("#finding_modal").on("show.bs.modal", function(event) {
237 | $(document).on("keydown", function(e) {
238 | switch(e.keyCode) {
239 | case 37:
240 | var finding = findingsView.previousFinding();
241 | break;
242 | case 39:
243 | var finding = findingsView.nextFinding();
244 | break;
245 | default:
246 | return;
247 | }
248 | if (finding.length === 0) {
249 | return;
250 | }
251 | findingsView.activeFinding().removeClass("table-selected");
252 | finding.addClass("table-selected");
253 | var modalView = new FindingModal({
254 | model: finding.data("finding"),
255 | el: "#finding_modal .modal-content"
256 | });
257 | modalView.render();
258 | $("#finding_modal").modal();
259 | modalView.fetchFileContents();
260 | });
261 | })
262 | .on("hidden.bs.modal", function(event) {
263 | $(document).unbind("keydown");
264 | });
265 | },
266 | update: function() {
267 | this.collection.fetch();
268 | },
269 | renderFinding: function(finding) {
270 | var findingEl = new FindingView({model: finding}).render().el;
271 | $(findingEl).appendTo(this.$el);
272 | },
273 | activeFinding: function() {
274 | return this.$el.find("tr.table-selected");
275 | },
276 | nextFinding: function() {
277 | return this.activeFinding().nextAll("tr").not(".d-none").first();
278 | },
279 | previousFinding: function() {
280 | return this.activeFinding().prevAll("tr").not(".d-none").first();
281 | },
282 | searchFindings: function() {
283 | var needle = $.trim($("#findings_search").val()).toLowerCase();
284 | if (needle == "") {
285 | $("#table_findings tbody tr").removeClass("d-none");
286 | return;
287 | }
288 | $("#table_findings tbody tr").each(function() {
289 | var path = $(this).find("td.col-path").text().toLowerCase();
290 | var commit = $(this).find("td.col-commit").text().toLowerCase();
291 | var repository = $(this).find("td.col-repository").text().toLowerCase();
292 | if (path.indexOf(needle) > -1 || commit.indexOf(needle) > -1 || repository.indexOf(needle) > -1) {
293 | $(this).removeClass("d-none");
294 | } else {
295 | $(this).addClass("d-none");
296 | }
297 | });
298 | }
299 | });
300 | window.findingsView = new FindingsView({el: "#table_findings tbody"});
301 |
302 | var FindingModal = Backbone.View.extend({
303 | template: _.template($("#template_finding_modal").html()),
304 | interestingStringPatterns: [
305 | /((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))/gmi,
306 | /([a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)/gmi,
307 | /((\w*:\/\/)([\da-z\.-]+)\.([a-z\.]{2,6}))/gmi,
308 | /([a-f0-9\-\$\/]{20,})/gmi,
309 | /(username)/gmi,
310 | /(secret)/gmi,
311 | /(passw(or)?d)/gmi,
312 | /(cred(s|ential))/gmi,
313 | /(access(_|-|.)?token)/gmi,
314 | ],
315 | events: {
316 | "click #finding_view_raw": "showRawContents",
317 | "click #finding_view_hexdump": "showHexDumpContents",
318 | },
319 | render: function() {
320 | this.$el.html(this.template(this.model.attributes));
321 | new ClipboardJS('.btn', {
322 | container: document.getElementById('finding_modal')
323 | });
324 | return this;
325 | },
326 | showRawContents: function() {
327 | $("#finding_view_raw").addClass("active");
328 | $("#finding_view_hexdump").removeClass("active");
329 | $("#modal_file_hexdump").hide();
330 | $("#modal_file_contents").show();
331 | },
332 | showHexDumpContents: function() {
333 | $("#finding_view_raw").removeClass("active");
334 | $("#finding_view_hexdump").addClass("active");
335 | $("#modal_file_contents").hide();
336 | $("#modal_file_hexdump").show();
337 | },
338 | truncatedCommitMessage: function() {
339 | var message = this.model.trimmedCommitMessage();
340 | if (message.length <= 150) {
341 | return _.escape(message);
342 | }
343 | return _.escape(message.substr(0, 150)) + "…";
344 | },
345 | isTestRelated: function() {
346 | return this.model.isTestRelated();
347 | },
348 | isBinary: function(data) {
349 | return /[\x00-\x08\x0E-\x1F]/.test(data);
350 | },
351 | highlightInterestingStrings: function(haystack) {
352 | this.interestingStringPatterns.forEach(function(pattern) {
353 | haystack = haystack.replace(pattern, "$1");
354 | });
355 | return haystack;
356 | },
357 | fetchFileContents: function() {
358 | if (this.model.get("Action") == "Delete") {
359 | $("#modal_file_spinner_container").fadeOut("fast", function() {
360 | $("#modal_file_contents_container").html("View commit on GitHub to see contents of deleted files.
").fadeIn("fast");
361 | });
362 | return;
363 | }
364 | var context = this;
365 | this.model.fileContents(function(data) {
366 | var worker = new Worker("/javascripts/highlight_worker.js");
367 | worker.onmessage = function(event) {
368 | $("#modal_file_spinner_container").fadeOut("fast", _.bind(function() {
369 | var content = this.highlightInterestingStrings(event.data);
370 | $("#modal_file_contents").html(content);
371 | new Hexdump(data, {
372 | container: "modal_file_hexdump",
373 | base: "hex",
374 | width: 8,
375 | byteGrouping: 1,
376 | html: true,
377 | ascii: true,
378 | lineNumbers: true,
379 | style: {
380 | lineNumberLeft: '',
381 | lineNumberRight: ':',
382 | stringLeft: '|',
383 | stringRight: '|',
384 | hexLeft: '',
385 | hexRight: '',
386 | hexNull: '.',
387 | nonPrintable: '.',
388 | stringNull: '.',
389 | }
390 | });
391 | $("#modal_file_contents_container").fadeIn("fast");
392 | if (this.isBinary(data)) {
393 | this.showHexDumpContents();
394 | } else {
395 | this.showRawContents();
396 | }
397 | }, context));
398 | }
399 | worker.postMessage(data);
400 | }, function() {
401 | $("#modal_file_spinner_container").fadeOut("fast", function() {
402 | $("#modal_file_contents_container").html("File size too large to display inline. View file on GitHub.
").fadeIn("fast");
403 | });
404 | });
405 | }
406 | });
407 |
--------------------------------------------------------------------------------
/static/javascripts/backbone.js:
--------------------------------------------------------------------------------
1 | (function(t){var e=typeof self=="object"&&self.self===self&&self||typeof global=="object"&&global.global===global&&global;if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,n){e.Backbone=t(e,n,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore"),r;try{r=require("jquery")}catch(n){}t(e,exports,i,r)}else{e.Backbone=t(e,{},e._,e.jQuery||e.Zepto||e.ender||e.$)}})(function(t,e,i,r){var n=t.Backbone;var s=Array.prototype.slice;e.VERSION="1.3.3";e.$=r;e.noConflict=function(){t.Backbone=n;return this};e.emulateHTTP=false;e.emulateJSON=false;var a=function(t,e,r){switch(t){case 1:return function(){return i[e](this[r])};case 2:return function(t){return i[e](this[r],t)};case 3:return function(t,n){return i[e](this[r],o(t,this),n)};case 4:return function(t,n,s){return i[e](this[r],o(t,this),n,s)};default:return function(){var t=s.call(arguments);t.unshift(this[r]);return i[e].apply(i,t)}}};var h=function(t,e,r){i.each(e,function(e,n){if(i[n])t.prototype[n]=a(e,n,r)})};var o=function(t,e){if(i.isFunction(t))return t;if(i.isObject(t)&&!e._isModel(t))return l(t);if(i.isString(t))return function(e){return e.get(t)};return t};var l=function(t){var e=i.matches(t);return function(t){return e(t.attributes)}};var u=e.Events={};var c=/\s+/;var f=function(t,e,r,n,s){var a=0,h;if(r&&typeof r==="object"){if(n!==void 0&&"context"in s&&s.context===void 0)s.context=n;for(h=i.keys(r);athis.length)n=this.length;if(n<0)n+=this.length+1;var s=[];var a=[];var h=[];var o=[];var l={};var u=e.add;var c=e.merge;var f=e.remove;var d=false;var v=this.comparator&&n==null&&e.sort!==false;var g=i.isString(this.comparator)?this.comparator:null;var p,m;for(m=0;m7);this._useHashChange=this._wantsHashChange&&this._hasHashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.history&&this.history.pushState);this._usePushState=this._wantsPushState&&this._hasPushState;this.fragment=this.getFragment();this.root=("/"+this.root+"/").replace(O,"/");if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){var e=this.root.slice(0,-1)||"/";this.location.replace(e+"#"+this.getPath());return true}else if(this._hasPushState&&this.atRoot()){this.navigate(this.getHash(),{replace:true})}}if(!this._hasHashChange&&this._wantsHashChange&&!this._usePushState){this.iframe=document.createElement("iframe");this.iframe.src="javascript:0";this.iframe.style.display="none";this.iframe.tabIndex=-1;var r=document.body;var n=r.insertBefore(this.iframe,r.firstChild).contentWindow;n.document.open();n.document.close();n.location.hash="#"+this.fragment}var s=window.addEventListener||function(t,e){return attachEvent("on"+t,e)};if(this._usePushState){s("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){s("hashchange",this.checkUrl,false)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}if(!this.options.silent)return this.loadUrl()},stop:function(){var t=window.removeEventListener||function(t,e){return detachEvent("on"+t,e)};if(this._usePushState){t("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){t("hashchange",this.checkUrl,false)}if(this.iframe){document.body.removeChild(this.iframe);this.iframe=null}if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);N.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getHash(this.iframe.contentWindow)}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(t){if(!this.matchRoot())return false;t=this.fragment=this.getFragment(t);return i.some(this.handlers,function(e){if(e.route.test(t)){e.callback(t);return true}})},navigate:function(t,e){if(!N.started)return false;if(!e||e===true)e={trigger:!!e};t=this.getFragment(t||"");var i=this.root;if(t===""||t.charAt(0)==="?"){i=i.slice(0,-1)||"/"}var r=i+t;t=this.decodeFragment(t.replace(U,""));if(this.fragment===t)return;this.fragment=t;if(this._usePushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,r)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getHash(this.iframe.contentWindow)){var n=this.iframe.contentWindow;if(!e.replace){n.document.open();n.document.close()}this._updateHash(n.location,t,e.replace)}}else{return this.location.assign(r)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});e.history=new N;var q=function(t,e){var r=this;var n;if(t&&i.has(t,"constructor")){n=t.constructor}else{n=function(){return r.apply(this,arguments)}}i.extend(n,r,e);n.prototype=i.create(r.prototype,t);n.prototype.constructor=n;n.__super__=r.prototype;return n};y.extend=x.extend=$.extend=k.extend=N.extend=q;var F=function(){throw new Error('A "url" property or function must be specified')};var B=function(t,e){var i=e.error;e.error=function(r){if(i)i.call(e.context,t,r,e);t.trigger("error",t,r,e)}};return e});
2 | //# sourceMappingURL=backbone-min.map
--------------------------------------------------------------------------------
/static/javascripts/clipboard.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * clipboard.js v2.0.1
3 | * https://zenorocha.github.io/clipboard.js
4 | *
5 | * Licensed MIT © Zeno Rocha
6 | */
7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e,n){var o,r,i;!function(a,c){r=[t,n(7)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=function(t){return t&&t.__esModule?t:{default:t}}(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,o.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,o.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=a})},function(t,e,n){function o(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return r(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function r(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return u(document.body,t,e,n)}var c=n(6),u=n(5);t.exports=o},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function o(){r.off(t,o),e.apply(n,arguments)}var r=this;return o._=e,this.on(t,o,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;for(o;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,f.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(s.default);t.exports=p})},function(t,e){function n(t,e){for(;t&&t.nodeType!==o;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}var o=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var r=Element.prototype;r.matches=r.matchesSelector||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector}t.exports=n},function(t,e,n){function o(t,e,n,o,r){var a=i.apply(this,arguments);return t.addEventListener(n,a,r),{destroy:function(){t.removeEventListener(n,a,r)}}}function r(t,e,n,r,i){return"function"==typeof t.addEventListener?o.apply(null,arguments):"function"==typeof n?o.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return o(t,e,n,r,i)}))}function i(t,e,n,o){return function(n){n.delegateTarget=a(n.target,e),n.delegateTarget&&o.call(t,n)}}var a=n(4);t.exports=r},function(t,e){e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))},e.string=function(t){return"string"==typeof t||t instanceof String},e.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e){function n(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}t.exports=n}])});
--------------------------------------------------------------------------------
/static/javascripts/hexdump.js:
--------------------------------------------------------------------------------
1 | // Hexdump.js 0.1.1
2 | // (c) 2011 Dustin Willis Webber
3 | // Hexdump is freely distributable under the MIT license.
4 | // For all details and documentation:
5 | // http://github.com/mephux/hexdump.js
6 | var Hexdump;
7 | Hexdump=function(){function f(c,b){var a=this;a.hexdump=[];a.hex=!1;a.options={container:b.container||"",width:b.width||16,byteGrouping:b.byteGrouping||0,ascii:b.ascii,lineNumber:b.lineNumber,endian:b.endian||"big",html:b.html,base:b.base||"hexadecimal",nonPrintable:b.nonPrintable||".",style:{lineNumberLeft:b.style.lineNumberLeft||"",lineNumberRight:b.style.lineNumberRight||":",stringLeft:b.style.stringLeft||"|",stringRight:b.style.stringRight||"|",hexLeft:b.style.hexLeft||"",hexRight:b.style.hexRight||"",
8 | hexNull:b.style.hexNull||".",stringNull:b.style.stringNull||" "}};if(a.options.base=="hex")a.hex=!0;else if(a.options.base=="hexadecimal")a.hex=!0;var d=a.options.lineNumber;if(typeof d=="undefined"||d==null)a.options.lineNumber=!0;d=a.options.ascii;if(typeof d=="undefined"||d==null)a.options.ascii=!1;d=a.options.html;if(typeof d=="undefined"||d==null)a.options.html=!0;if(a.endian!="little")a.endian="big";if(a.options.byteGrouping>c.length)a.options.byteGrouping=c.length;a.options.byteGrouping--;
9 | if(a.options.width>c.length)a.options.width=c.length;a.padding={hex:4,dec:5,bin:8};switch(a.options.base){case "hexadecimal":case "hex":case 16:a.setNullPadding(a.padding.hex);a.baseConvert=function(b){for(;0'+b+"":b}b=0;this.output+=
12 | this.options.style.hexLeft;for(a=0;a2)for(var d=0;d'+e[f]+"")}else a.push(''+e+"");b.push(''+this.checkForNonPrintable(c[d])+"")}else{e=this.baseConvert(c[d]);if(this.hex){e=this.splitNulls(e);for(f=0;f'+this.options.style.hexNull+"":this.options.style.hexNull,
15 | a.push(e)}if(b.length'+this.options.style.stringNull+"":this.options.style.stringNull,b.push(e)}return{data:a,string:b.join("")}};f.prototype.setNullPadding=function(c){var b=this.options.style.hexNull[0];this.options.style.hexNull="";this.hex&&(c/=2);for(var a=0;a2&&this.options.ascii?".":c};return f}();
17 |
--------------------------------------------------------------------------------
/static/javascripts/highlight_worker.js:
--------------------------------------------------------------------------------
1 | onmessage = function(event) {
2 | importScripts("/javascripts/highlight.js");
3 | var result = self.hljs.highlightAuto(event.data);
4 | postMessage(result.value);
5 | }
6 |
--------------------------------------------------------------------------------
/static/javascripts/popper.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Federico Zivolo 2017
3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll)/.test(r+s+p)?e:n(o(e))}function r(e){var o=e&&e.offsetParent,i=o&&o.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(o.nodeName)&&'static'===t(o,'position')?r(o):o:e?e.ownerDocument.documentElement:document.documentElement}function p(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||r(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function d(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=o?e:t,n=o?t:e,a=document.createRange();a.setStart(i,0),a.setEnd(n,0);var l=a.commonAncestorContainer;if(e!==l&&t!==l||i.contains(n))return p(l)?l:r(l);var f=s(e);return f.host?d(f.host,t):d(e,s(t).host)}function a(e){var t=1=o.clientWidth&&i>=o.clientHeight}),l=0i[e]&&!t.escapeWithReference&&(n=_(p[o],i[e]-('right'===e?p.width:p.height))),pe({},o,n)}};return n.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';p=se({},p,s[t](e))}),e.offsets.popper=p,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,i=t.reference,n=e.placement.split('-')[0],r=X,p=-1!==['top','bottom'].indexOf(n),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(i[s])&&(e.offsets.popper[d]=r(i[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var i;if(!F(e.instance.modifiers,'arrow','keepTogether'))return e;var n=o.element;if('string'==typeof n){if(n=e.instance.popper.querySelector(n),!n)return e;}else if(!e.instance.popper.contains(n))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',g=a?'bottom':'right',u=L(n)[l];d[g]-us[g]&&(e.offsets.popper[m]+=d[m]+u-s[g]),e.offsets.popper=c(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=J(_(s[l]-u,v),0),e.arrowElement=n,e.offsets.arrow=(i={},pe(i,m,Math.round(v)),pe(i,h,''),i),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(k(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=y(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement),i=e.placement.split('-')[0],n=x(i),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case le.FLIP:p=[i,n];break;case le.CLOCKWISE:p=q(i);break;case le.COUNTERCLOCKWISE:p=q(i,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(i!==s||p.length===d+1)return e;i=e.placement.split('-')[0],n=x(i);var a=e.offsets.popper,l=e.offsets.reference,f=X,m='left'===i&&f(a.right)>f(l.left)||'right'===i&&f(a.left)f(l.top)||'bottom'===i&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===i&&h||'right'===i&&c||'top'===i&&g||'bottom'===i&&u,w=-1!==['top','bottom'].indexOf(i),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u);(m||b||y)&&(e.flipped=!0,(m||b)&&(i=p[d+1]),y&&(r=K(r)),e.placement=i+(r?'-'+r:''),e.offsets.popper=se({},e.offsets.popper,S(e.instance.popper,e.offsets.reference,e.placement)),e=C(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],i=e.offsets,n=i.popper,r=i.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return n[p?'left':'top']=r[o]-(s?n[p?'width':'height']:0),e.placement=x(t),e.offsets.popper=c(n),e}},hide:{order:800,enabled:!0,fn:function(e){if(!F(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=T(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this);
6 | //# sourceMappingURL=underscore-min.map
--------------------------------------------------------------------------------
/static/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | a {
2 | color: inherit;
3 | text-decoration: underline;
4 | }
5 |
6 | a:hover {
7 | color: inherit;
8 | }
9 |
10 | a.btn {
11 | text-decoration: none;
12 | }
13 |
14 | code {
15 | color: inherit;
16 | }
17 |
18 | footer {
19 | border-top: 1px solid #303030;
20 | margin-top: 50px;
21 | padding: 20px 0px 100px 0px;
22 | font-size: 11px;
23 | }
24 |
25 | .navbar a {
26 | text-decoration: none;
27 | }
28 |
29 | .navbar a:hover {
30 | text-decoration: underline;
31 | }
32 |
33 | .progress-bar {
34 | text-overflow: ellipsis;
35 | white-space: nowrap;
36 | overflow: hidden;
37 | }
38 |
39 | #findings_search {
40 | width: 260px;
41 | }
42 |
43 | #table_findings td.col-path {
44 | color: #ccc;
45 | }
46 |
47 | #table_findings td.col-path strong {
48 | color: #fff;
49 | }
50 |
51 | #table_findings .col-action {
52 | width: 50px;
53 | }
54 |
55 | #table_findings .col-action .badge {
56 | width: 100%;
57 | }
58 |
59 | #table_findings .col-commit {
60 | width: 70px;
61 | text-align: right;
62 | }
63 |
64 | #table_findings .col-repository {
65 | width: 200px;
66 | text-align: right;
67 | }
68 |
69 | #table_findings tr.test-related {
70 | opacity: 0.4;
71 | }
72 |
73 | .spinner {
74 | display: block;
75 | margin: 25px auto 10px auto;
76 | }
77 |
78 | tr.table-selected {
79 | background-color: #375a7f !important;
80 | }
81 |
82 | #modal_file .alert-secondary {
83 | color: #ccc
84 | }
85 |
86 | #modal_file .alert-secondary strong {
87 | color: #fff;
88 | }
89 |
90 | .finding-meta-table {
91 | font-size: 13px;
92 | }
93 |
94 | .finding-meta-table th {
95 | padding-right: 10px;
96 | }
97 |
98 | #finding_id_clipboard {
99 | transform: scale(0.7);
100 | }
101 |
102 | #modal_file_contents_container {
103 | display: none;
104 | }
105 |
106 | #modal_file_contents, #modal_file_hexdump {
107 | display: none;
108 | }
109 |
110 | #modal_file_hexdump {
111 | max-height: 400px;
112 | }
113 |
114 | #modal_file_hexdump #line-number {
115 | color: #00bc8c;
116 | }
117 |
118 | #modal_file_hexdump span[data-string-id], #modal_file_hexdump span[data-string-null] {
119 | color: #F39C12;
120 | vertical-align: middle;
121 | position: relative;
122 | display: inline-block;
123 | overflow: hidden;
124 | height: 15px;
125 | width: 14px;
126 | text-align: center;
127 | }
128 |
--------------------------------------------------------------------------------
/static/stylesheets/highlight.css:
--------------------------------------------------------------------------------
1 | /* Tomorrow Night Theme */
2 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
3 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */
4 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
5 |
6 | /* Tomorrow Comment */
7 | .hljs-comment,
8 | .hljs-quote {
9 | color: #969896;
10 | }
11 |
12 | /* Tomorrow Red */
13 | .hljs-variable,
14 | .hljs-template-variable,
15 | .hljs-tag,
16 | .hljs-name,
17 | .hljs-selector-id,
18 | .hljs-selector-class,
19 | .hljs-regexp,
20 | .hljs-deletion {
21 | color: #cc6666;
22 | }
23 |
24 | /* Tomorrow Orange */
25 | .hljs-number,
26 | .hljs-built_in,
27 | .hljs-builtin-name,
28 | .hljs-literal,
29 | .hljs-type,
30 | .hljs-params,
31 | .hljs-meta,
32 | .hljs-link {
33 | color: #de935f;
34 | }
35 |
36 | /* Tomorrow Yellow */
37 | .hljs-attribute {
38 | color: #f0c674;
39 | }
40 |
41 | /* Tomorrow Green */
42 | .hljs-string,
43 | .hljs-symbol,
44 | .hljs-bullet,
45 | .hljs-addition {
46 | color: #b5bd68;
47 | }
48 |
49 | /* Tomorrow Blue */
50 | .hljs-title,
51 | .hljs-section {
52 | color: #81a2be;
53 | }
54 |
55 | /* Tomorrow Purple */
56 | .hljs-keyword,
57 | .hljs-selector-tag {
58 | color: #b294bb;
59 | }
60 |
61 | .hljs {
62 | display: block;
63 | overflow-x: auto;
64 | background: #1d1f21;
65 | color: #c5c8c6;
66 | padding: 0.5em;
67 | }
68 |
69 | .hljs-emphasis {
70 | font-style: italic;
71 | }
72 |
73 | .hljs-strong {
74 | font-weight: bold;
75 | }
76 |
--------------------------------------------------------------------------------
/static/stylesheets/openiconic.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'}
--------------------------------------------------------------------------------