├── .editorconfig
├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE.txt
├── README.md
├── build-static.sh
├── build.sh
├── common
├── banner.go
├── browser.go
├── interfaces.go
├── io.go
├── log.go
├── sourcecontrol.go
└── strings.go
├── contentsignatures.json
├── core
├── analysis.go
├── bindata.go
├── options.go
├── router.go
└── session.go
├── docker-compose.yml
├── docker_compose_entrypoint.sh
├── filesignatures.json
├── github
├── apiClient.go
└── git.go
├── gitlab
├── apiClient.go
└── git.go
├── go.mod
├── go.sum
├── main.go
├── matching
├── contentsignatures.go
├── filesignatures.go
├── findings.go
├── matchfile.go
└── signatures.go
├── release.sh
├── scripts
└── 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
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig
2 | # editorconfig.org
3 |
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 4
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.go]
14 | indent_style = tab
15 |
16 | [*.{toml,yml,yaml}]
17 | indent_size = 2
18 |
19 | [{Makefile, makefile, GNUmakefile}]
20 | indent_style = tab
21 |
22 | [*.md]
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Gitrob
2 |
3 | on:
4 | push:
5 | branches: "*"
6 | pull_request:
7 | branches: "*"
8 |
9 | jobs:
10 |
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Build the Docker image
18 | run: docker build . --tag gitrob:$(date +%s)
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | gitrob*
8 | gitrob.exe
9 | coverage.txt
10 |
11 | bin/
12 | vendor/
13 | build/
14 | bin/
15 | .vscode
16 | .idea
17 | __debug_bin
18 | go_build_gitrob_
19 | gitrob-script.sh
20 |
21 | # Test binary, build with `go test -c`
22 | *.test
23 |
24 | # Output of the go coverage tool, specifically when used with LiteIDE
25 | *.out
26 |
27 | # Dropbox settings and caches
28 | .dropbox
29 | .dropbox.attr
30 | .dropbox.cache
31 |
32 | # temporary files which can be created if a process still has a handle open of a deleted file
33 | .fuse_hidden*
34 |
35 | # KDE directory preferences
36 | .directory
37 |
38 | # Linux trash folder which might appear on any partition or disk
39 | .Trash-*
40 |
41 | # .nfs files are created when an open file is removed but is still being accessed
42 | .nfs*
43 |
44 |
45 | # TextMate
46 | *.tmproj
47 | *.tmproject
48 | tmtags
49 |
50 | # Swap
51 | [._]*.s[a-v][a-z]
52 | [._]*.sw[a-p]
53 | [._]s[a-v][a-z]
54 | [._]sw[a-p]
55 |
56 | # Session
57 | Session.vim
58 |
59 | # Temporary
60 | .netrwhist
61 | *~
62 | # Auto-generated tag files
63 | tags
64 |
65 | # General
66 | .DS_Store
67 | .AppleDouble
68 | .LSOverride
69 |
70 | # Icon must end with two \r
71 | Icon
72 |
73 |
74 | # Thumbnails
75 | ._*
76 |
77 | # Files that might appear in the root of a volume
78 | .DocumentRevisions-V100
79 | .fseventsd
80 | .Spotlight-V100
81 | .TemporaryItems
82 | .Trashes
83 | .VolumeIcon.icns
84 | .com.apple.timemachine.donotpresent
85 |
86 | # Directories potentially created on remote AFP share
87 | .AppleDB
88 | .AppleDesktop
89 | Network Trash Folder
90 | Temporary Items
91 | .apdisk
92 |
93 | # Taregt list
94 | targets.txt
95 |
--------------------------------------------------------------------------------
/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 | ## 3.4.5-beta 2022-02-14
9 |
10 | - Use prefix when looking for GitLab personal access tokens
11 |
12 | ## 3.4.4-beta 2021-07-19
13 |
14 | Improvement for slack token regex
15 |
16 | ## 3.4.3-beta 2020-02-02
17 | - When a GitLab group is specified, include projects from subgroups by default
18 |
19 | ## 3.4.2-beta 2020-12-04
20 | - Merged PR that [fixed a bug](https://gitlab.com/gitlab-com/gl-security/security-operations/gl-redteam/gitrob/-/issues/3) causing private repos not to be scanned even if the token provided had access
21 | - Improved implementation of IClient interface for GitLab as a result of previous merge
22 |
23 | ## 3.4.1-beta 2020-12-04
24 | - Add a `-exit-on-finish` option for better support of automation scenarios.
25 |
26 | ## 3.4.0-beta 2020-06-18
27 | - Update/fix file and content signatures
28 | - Fix bug where repo clones weren't properly deleted from the temp directory
29 | - Add new signatures for zoom meeting links, google meet links, and ngrok reverse tunnels
30 |
31 | ## 3.3.2-beta 2020-06-18
32 | ### Changed
33 | - Re-add build and release scripts after merge from phantomSecrets
34 |
35 | ## 3.3.1-beta 2020-06-18
36 | ### Changed
37 | - Bring in changes from @mattyjones for go modules support.
38 |
39 | ## 3.2.1-beta 2020-05-18
40 | ### Changed
41 | - Improve matching for GitLab PATs
42 | - Fix escaping in content signatures.
43 |
44 | ## 3.2.0-beta 2020-05-18
45 | ### Changed
46 | - Improve matching for file signatures in general via regex improvements
47 |
48 | ### Added
49 | - Add file signatures for common GitLab configuration files
50 |
51 | ## 3.1.4-alpha 2020-05-18
52 | ### Added
53 | - Improve regexes for GitLab PAT
54 |
55 | ## 3.1.3-alpha 2020-05-04
56 | ### Added
57 | - Bug fixes for content scans that hit really large commits. This bug is due to an issue the go-diff depenency used by go-git: https://github.com/sergi/go-diff/issues/89
58 |
59 | ## 3.1.2-alpha 2020-05-03
60 | ### Added
61 | - Bug fixes for UI: results should now load in the modal properly
62 | - Added GitHub action for branch and master builds
63 |
64 | ## 3.1.1-alpha 2020-04-08
65 | ### Changed
66 | - Resolved a dependency problem where the locked version of github.com/xanzy/go-gitlab was incorrect.
67 | - Removed rate limit handling for GitLab API requests from gitrob directly in leu of go-gitlab's new implementation with the newly locked version.
68 |
69 | ## 3.1.0-alpha 2020-03-30
70 | ### Added
71 | - Docker support
72 | - Bug fix: include go-gitlab in dep dependency .toml and .lock files.
73 |
74 | ### Changed
75 | - Windows releases have been removed temporarily due to a platform build issue introduced with github.com/xanzy/go-gitlab
76 |
77 | ## 3.0.0-alpha - 2020-03-27
78 | ### Added
79 | - Support for GitLab users and groups
80 | - Support for multiple modes of execution including content search
81 | - Mode 1 - Default mode to match on [file signatures](./filesignatures.json)
82 | - Mode 2 - Match on [file signatures](./filesignatures.json) then [content signatures](./contentsignatures.json) to constitute a result.
83 | - Mode 3 - Match on [content signatures](./contentsignatures.json) only without file signature matches.
84 | - Support for in-memory repository clones, which can result in significantly faster analysis times depending on your hardware.
85 | - File signatures for Google Cloud Platform credentials
86 | - Content signatures similar to [trufflehog](https://github.com/dxa4481/truffleHogRegexes/blob/master/truffleHogRegexes/regexes.json).
87 | - Dependency management with dep
88 |
89 | ### Changed
90 | - Skip expensive signature checking for image extensions and files in `node_modules` and other package directories
91 |
92 | ## 2.0.0-beta - 2018-06-08
93 | ### Added
94 | - Total rewrite of Gitrob in [Golang](https://golang.org/)
95 | - Find interesting files in history down to a default (and configurable) depth of 500 commits
96 | - Hexdump view for binary files
97 | - Saving and loading of session files for easy sharing
98 |
99 | ### Removed
100 | - All the stupid Rubygems with native extensions
101 | - PostgreSQL dependency
102 | - Messy assessment comparison feature
103 | - User overview
104 | - Repository overview
105 |
106 | [Unreleased]: https://github.com/michenriksen/gitrob/compare/v2.0.0-beta...HEAD
107 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine as build
2 |
3 | RUN apk add --no-cache git perl-utils zip
4 |
5 | WORKDIR /go/src/github.com/gitrob
6 |
7 | COPY . .
8 | RUN go build
9 |
10 | FROM golang:alpine as deploy
11 |
12 | COPY --from=build /go/src/github.com/gitrob \
13 | /go/src/github.com/gitrob/filesignatures.json \
14 | /go/src/github.com/gitrob/contentsignatures.json \
15 | ./
16 | ENTRYPOINT ["./gitrob"]
17 |
--------------------------------------------------------------------------------
/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 | # Gitrob: Putting the Open Source in OSINT
6 |
7 | 
8 |
9 | Gitrob is a tool to help find potentially sensitive information pushed to repositories on GitLab or Github. Gitrob will clone repositories belonging to a user or group/organization down to a configurable depth and iterate through the commit history and flag files and/or commit content that match signatures for potentially sensitive information. The findings will be presented through a web interface for easy browsing and analysis.
10 |
11 | ## Usage
12 |
13 | gitrob [options] target [target2] ... [targetN]
14 |
15 | **IMPORTANT** If you are targeting a GitLab group, please give the **group ID** as the target argument. You can find the group ID just below the group name in the GitLab UI. Otherwise, names with suffice for the target arguments.
16 |
17 | ### Options
18 |
19 | ```
20 | -bind-address string
21 | Address to bind web server to (default "127.0.0.1")
22 | -commit-depth int
23 | Number of repository commits to process (default 500)
24 | -debug
25 | Print debugging information
26 | -exit-on-finish
27 | Let the program exit on finish. Useful for automated scans.
28 | -github-access-token string
29 | Github access token to use for API requests (set one)
30 | -gitlab-access-token string
31 | GitLab access token to use for API requests (set one)
32 | -in-mem-clone
33 | Clone repositories into memory for faster analysis depending on your hardware
34 | -load string
35 | Load session file from specified path
36 | -mode int {1, 2, or 3}
37 | Designate a mode for execution. Mode 1 (default) searches for file signature matches. Mode 2 (-mode 2) searches for file signature matches. Given a file signature match, mode 2 then attempts to match on content in order to produce a result. Mode 3 (-mode 3) searches by content matches only. In mode 3, no file signature matches are performed.
38 | -no-expand-orgs
39 | Don't add members to targets when processing organizations
40 | -port int
41 | Port to run web server on (default 9393)
42 | -save string
43 | Save session to a file at the given path
44 | -silent
45 | Suppress all output except for errors
46 | -threads int
47 | Number of concurrent threads (default number of logical CPUs)
48 | ```
49 |
50 | ## Examples
51 |
52 | Scan a GitLab group assuming your access token has been added to the environment variable with name GITROB_GITLAB_ACCESS_TOKEN. Look for file signature matches only:
53 |
54 | gitrob
55 |
56 | Scan a multiple GitLab groups assuming your access token has been added to the environment variable with name GITROB_GITLAB_ACCESS_TOKEN. Clone repositories into memory for faster analysis. Set the scan mode to 2 to scan each file match for a content match before creating a result. Save the results to `./output.json`:
57 |
58 | gitrob -in-mem-clone -mode 2 -save "./output.json"
59 |
60 | Scan a GitLab groups assuming your access token has been added to the environment variable with name GITROB_GITLAB_ACCESS_TOKEN. Clone repositories into memory for faster analysis. Set the scan mode to 3 to scan each commit for content matches only. Save the results to `./output.json`:
61 |
62 | gitrob -in-mem-clone -mode 3 -save "./output.json"
63 |
64 | Scan a Github user setting your Github access token as a parameter. Clone repositories into memory for faster analysis.
65 |
66 | gitrob -github-access-token -in-mem-clone
67 |
68 | ### Editing File and Content Regular Expressions
69 |
70 | Regular expressions are included in the [filesignatures.json](./filesignatures.json) and [contentsignatures.json](./contentsignatures.json) files respectively. Edit these files to adjust your scope and fine-tune your results.
71 |
72 | ### Loading session from a file
73 |
74 | A session stored in a file can be loaded with the `-load` option:
75 |
76 | gitrob -load ./output.json
77 |
78 | Gitrob will start its web interface and serve the results for analysis.
79 |
80 | ## Installation
81 |
82 | A [precompiled version is available](https://github.com/codeEmitter/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.
83 |
84 | To install from source, make sure you have a correctly configured **Go >= 1.8** environment and that `$GOPATH/bin` is in your `$PATH`. Also, make sure you have installed [dep](https://github.com/golang/dep) locally.
85 |
86 | $ go get github.com/codeEmitter/gitrob
87 | $ cd ~/go/src/github.com/codeEmitter/gitrob
88 | $ dep ensure
89 | $ go build
90 |
91 | *Note that installing with `go install` will not work due to the static json file dependencies. However, it was deemed more useful to have the files be adjustable without recompiling the binary than to have everything bundled into the binary itself.*
92 |
93 | ## Using docker
94 |
95 | The [included Dockerfile](./Dockerfile) can be used to build images needed to run gitrob. You can build a basic image with:
96 |
97 | docker build . -t gitrob:latest
98 |
99 | You can then run the container, optionally specifying how many logical CPUs to allocate for concurrency with:
100 |
101 | docker run -p 9393:9393 --cpus gitrob:latest -bind-address 0.0.0.0 -github-access-token -in-mem-clone -mode 2 ...
102 |
103 | With this container running, use your browser to hit the UI with: http://localhost:9393.
104 |
105 |
106 | Alternatively, the [included docker-compose.yml](./docker-compose.yml) can be used with `docker-compose`. Make sure to set either `GITROB_GITHUB_ACCESS_TOKEN` or `GITROB_GITLAB_ACCESS_TOKEN` in the `docker-compose.yml` file. Do not set both the environment variables as Gitrob only supports one at a time. After that, you can create a file `targets.txt` in the repo directory with targets in every line:
107 |
108 | target1
109 | target2
110 |
111 | And then execute the following command to run gitrob on the targets specified:
112 | docker-compose up --build
113 |
114 | The UI can be accessed at http://localhost:9393.
115 |
116 | ## Access Tokens
117 |
118 | Gitrob will need either a GitLab or Github access token in order to interact with the appropriate API. You can create a [GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html), or [a Github 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:
119 |
120 | export GITROB_GITLAB_ACCESS_TOKEN=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
121 | export GITROB_GITHUB_ACCESS_TOKEN=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
122 |
123 | Alternatively you can specify the access token with the `-gitlab-access-token` or `-github-access-token` option on the command line, but watch out for your command history!
124 |
--------------------------------------------------------------------------------
/build-static.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #Script to generate code for ./core/bindata.go
4 |
5 | #install dependencies using the directions here: https://github.com/elazarl/go-bindata-assetfs
6 | go-bindata-assetfs -o ./core/bindata.go -pkg "core" ./static/*
7 | go build
8 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | BUILD_FOLDER=build
4 | VERSION=$(cat common/banner.go | grep Version | cut -d '"' -f 2)
5 |
6 | bin_dep() {
7 | BIN=$1
8 | which $BIN >/dev/null || {
9 | echo "[-] Dependency $BIN not found !"
10 | exit 1
11 | }
12 | }
13 |
14 | create_exe_archive() {
15 | bin_dep 'zip'
16 |
17 | OUTPUT=$1
18 |
19 | echo "[*] Creating archive $OUTPUT ..."
20 | zip -j "$OUTPUT" gitrob.exe ../README.md ../LICENSE.txt ../contentsignatures.json ../filesignatures.json >/dev/null
21 | rm -rf gitrob gitrob.exe
22 | }
23 |
24 | create_archive() {
25 | bin_dep 'zip'
26 |
27 | OUTPUT=$1
28 |
29 | echo "[*] Creating archive $OUTPUT ..."
30 | zip -j "$OUTPUT" gitrob ../README.md ../LICENSE.md ../contentsignatures.json ../filesignatures.json >/dev/null
31 | rm -rf gitrob gitrob.exe
32 | }
33 |
34 | build_linux_amd64() {
35 | echo "[*] Building linux/amd64 ..."
36 | GOOS=linux GOARCH=amd64 go build -o gitrob ..
37 | }
38 |
39 | build_macos_amd64() {
40 | echo "[*] Building darwin/amd64 ..."
41 | GOOS=darwin GOARCH=amd64 go build -o gitrob ..
42 | }
43 |
44 | build_windows_amd64() {
45 | echo "[*] Building windows/amd64 ..."
46 | GOOS=windows GOARCH=amd64 go build -o gitrob.exe ..
47 | }
48 |
49 | rm -rf $BUILD_FOLDER
50 | mkdir $BUILD_FOLDER
51 | cd $BUILD_FOLDER
52 |
53 | build_linux_amd64 && create_archive gitrob_linux_amd64_$VERSION.zip
54 | build_macos_amd64 && create_archive gitrob_macos_amd64_$VERSION.zip
55 | #windows builds are broken with the addition of go-gitlab
56 | #build_windows_amd64 && create_exe_archive gitrob_windows_amd64_$VERSION.zip
57 | shasum -a 256 * >checksums.txt
58 |
59 | echo
60 | echo
61 | du -sh *
62 |
63 | cd --
64 |
65 |
--------------------------------------------------------------------------------
/common/banner.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | const (
4 | Name = "gitrob"
5 | Version = "3.4.4-beta"
6 | ASCIIBanner = " _ __ __\n" +
7 | " ___ _(_) /________ / /\n" +
8 | " / _ `/ / __/ __/ _ \\/ _ \\\n" +
9 | " \\_, /_/\\__/_/ \\___/_.__/\n" +
10 | "/___/"
11 | )
12 |
13 | const GitLabTanuki = "\n" +
14 | " // // \n" +
15 | " //// //// \n" +
16 | " ////// ////// \n" +
17 | " ((((((((/////////(((((((( \n" +
18 | " ((((((((////////((((((((( \n" +
19 | " ((((((((((///////(((((((((( \n" +
20 | " ((((((((/////(((((((( \n" +
21 | " (((((///((((( \n" +
22 | " (((/((( \n" +
23 | " * \n" +
24 | " GitLab Red Team \n" +
25 | "\n"
26 |
--------------------------------------------------------------------------------
/common/browser.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | var UserAgent = fmt.Sprintf("%s v%s", Name, Version)
9 |
10 | func CleanUrlSpaces(dirtyStrings ...string) []string {
11 | var result []string
12 | for _, s := range dirtyStrings {
13 | result = append(result, strings.ReplaceAll(s, " ", "-"))
14 | }
15 | return result
16 | }
17 |
--------------------------------------------------------------------------------
/common/interfaces.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | type IClient interface {
4 | GetUserOrOrganization(login string) (*Owner, error)
5 | GetRepositoriesFromOwner(target Owner) ([]*Repository, error)
6 | GetRepositoriesFromOrganization(target Owner) ([]*Repository, error)
7 | GetOrganizationMembers(target Owner) ([]*Owner, error)
8 | }
9 |
--------------------------------------------------------------------------------
/common/io.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import "os"
4 |
5 | func FileExists(path string) bool {
6 | if _, err := os.Stat(path); os.IsNotExist(err) {
7 | return false
8 | }
9 | return true
10 | }
11 |
--------------------------------------------------------------------------------
/common/log.go:
--------------------------------------------------------------------------------
1 | package common
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 |
--------------------------------------------------------------------------------
/common/sourcecontrol.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "gopkg.in/src-d/go-git.v4"
7 | "gopkg.in/src-d/go-git.v4/plumbing"
8 | "gopkg.in/src-d/go-git.v4/plumbing/object"
9 | "gopkg.in/src-d/go-git.v4/utils/merkletrie"
10 | )
11 |
12 | const (
13 | TargetTypeUser = "User"
14 | TargetTypeOrganization = "Organization"
15 | )
16 |
17 | type CloneConfiguration struct {
18 | InMemClone *bool
19 | Url *string
20 | Username *string
21 | Token *string
22 | Branch *string
23 | Depth *int
24 | }
25 |
26 | type Owner struct {
27 | Login *string
28 | ID *int64
29 | Type *string
30 | Name *string
31 | AvatarURL *string
32 | URL *string
33 | Company *string
34 | Blog *string
35 | Location *string
36 | Email *string
37 | Bio *string
38 | }
39 |
40 | type Repository struct {
41 | Owner *string
42 | ID *int64
43 | Name *string
44 | FullName *string
45 | CloneURL *string
46 | URL *string
47 | DefaultBranch *string
48 | Description *string
49 | Homepage *string
50 | }
51 |
52 | const (
53 | EmptyTreeCommitId = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
54 | )
55 |
56 | func getParentCommit(commit *object.Commit, repo *git.Repository) (*object.Commit, error) {
57 | if commit.NumParents() == 0 {
58 | parentCommit, err := repo.CommitObject(plumbing.NewHash(EmptyTreeCommitId))
59 | if err != nil {
60 | return nil, err
61 | }
62 | return parentCommit, nil
63 | }
64 | parentCommit, err := commit.Parents().Next()
65 | if err != nil {
66 | return nil, err
67 | }
68 | return parentCommit, nil
69 | }
70 |
71 | func GetRepositoryHistory(repository *git.Repository) ([]*object.Commit, error) {
72 | var commits []*object.Commit
73 | ref, err := repository.Head()
74 | if err != nil {
75 | return nil, err
76 | }
77 | cIter, err := repository.Log(&git.LogOptions{From: ref.Hash()})
78 | if err != nil {
79 | return nil, err
80 | }
81 | cIter.ForEach(func(c *object.Commit) error {
82 | commits = append(commits, c)
83 | return nil
84 | })
85 | return commits, nil
86 | }
87 |
88 | func GetChanges(commit *object.Commit, repo *git.Repository) (object.Changes, error) {
89 | parentCommit, err := getParentCommit(commit, repo)
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | commitTree, err := commit.Tree()
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | parentCommitTree, err := parentCommit.Tree()
100 | if err != nil {
101 | return nil, err
102 | }
103 |
104 | changes, err := object.DiffTree(parentCommitTree, commitTree)
105 | if err != nil {
106 | return nil, err
107 | }
108 | return changes, nil
109 | }
110 |
111 | func GetChangeAction(change *object.Change) string {
112 | action, err := change.Action()
113 | if err != nil {
114 | return "Unknown"
115 | }
116 | switch action {
117 | case merkletrie.Insert:
118 | return "Insert"
119 | case merkletrie.Modify:
120 | return "Modify"
121 | case merkletrie.Delete:
122 | return "Delete"
123 | default:
124 | return "Unknown"
125 | }
126 | }
127 |
128 | func GetChangePath(change *object.Change) string {
129 | action, err := change.Action()
130 | if err != nil {
131 | return change.To.Name
132 | }
133 |
134 | if action == merkletrie.Delete {
135 | return change.From.Name
136 | } else {
137 | return change.To.Name
138 | }
139 | }
140 |
141 | func GetChangeContent(change *object.Change) (result string, contentError error) {
142 | //temporary response to: https://github.com/sergi/go-diff/issues/89
143 | defer func() {
144 | if err := recover(); err != nil {
145 | contentError = errors.New(fmt.Sprintf("Panic occurred while retrieving change content: %s", err))
146 | }
147 | }()
148 | patch, err := change.Patch()
149 | if err != nil {
150 | return "", err
151 | }
152 | for _, filePatch := range patch.FilePatches() {
153 | if filePatch.IsBinary() {
154 | continue
155 | }
156 | for _, chunk := range filePatch.Chunks() {
157 | result += chunk.Content()
158 | }
159 | }
160 | return result, nil
161 | }
162 |
--------------------------------------------------------------------------------
/common/strings.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 | )
8 |
9 | var NewlineRegex = regexp.MustCompile(`\r?\n`)
10 |
11 | func Pluralize(count int, singular string, plural string) string {
12 | if count == 1 {
13 | return singular
14 | }
15 | return plural
16 | }
17 |
18 | func TruncateString(str string, maxLength int) string {
19 | str = NewlineRegex.ReplaceAllString(str, " ")
20 | str = strings.TrimSpace(str)
21 | if len(str) > maxLength {
22 | str = fmt.Sprintf("%s...", str[0:maxLength])
23 | }
24 | return str
25 | }
26 |
--------------------------------------------------------------------------------
/contentsignatures.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "ContentSignatures": [
4 | {
5 | "MatchOn": "((\\\"|'|`)?((?i)aws)?_?((?i)account)_?((?i)id)?(\\\"|'|`)?\\\\s{0,50}(:|=>|=)\\\\s{0,50}(\\\"|'|`)?[0-9]{4}-?[0-9]{4}-?[0-9]{4}(\\\"|'|`)?)",
6 | "Description": "AWS Access Key ID",
7 | "Comment": "An AWS access key ID needs a secret access key as well."
8 | },
9 | {
10 | "MatchOn": "[\\s][a-zA-Z0-9]{40}[\\s]",
11 | "Description": "AWS Secret Access Key",
12 | "Comment": "An AWS secret access key needs a access key ID as well."
13 | },
14 | {
15 | "MatchOn": "aws_secret_access_key.*?[a-zA-Z0-9/\\+]{40}",
16 | "Description": "AWS Secret Key",
17 | "Comment": ""
18 | },
19 | {
20 | "MatchOn": "(?i)(aws_access_key_id|aws_secret_access_key)(.{0,20})?=.[0-9a-zA-Z/+]{20,40}",
21 | "Description": "AWS cred file info",
22 | "Comment": ""
23 | },
24 | {
25 | "MatchOn": "amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
26 | "Description": "Amazon MWS Auth Token",
27 | "Comment": ""
28 | },
29 | {
30 | "MatchOn": "EAACEdEose0cBA[0-9A-Za-z]+",
31 | "Description": "Facebook Access Token",
32 | "Comment": ""
33 | },
34 | {
35 | "MatchOn": "[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K].*['|\"][0-9a-f]{32}['|\"]",
36 | "Description": "Facebook OAuth",
37 | "Comment": ""
38 | },
39 | {
40 | "MatchOn": "[a|A][p|P][i|I][_]?[k|K][e|E][y|Y].*['|\"][0-9a-zA-Z]{32,45}['|\"]",
41 | "Description": "Generic API Key",
42 | "Comment": ""
43 | },
44 | {
45 | "MatchOn": "[s|S][e|E][c|C][r|R][e|E][t|T].*['|\"][0-9a-zA-Z]{32,45}['|\"]",
46 | "Description": "Generic Secret",
47 | "Comment": ""
48 | },
49 | {
50 | "MatchOn": "glpat-[0-9a-zA-Z\\-\\_]{20}",
51 | "Description": "GitLab PAT",
52 | "Comment": ""
53 | },
54 | {
55 | "MatchOn": "[g|G][i|I][t|T][h|H][u|U][b|B].*['|\"][0-9a-zA-Z]{35,40}['|\"]",
56 | "Description": "Github Token",
57 | "Comment": ""
58 | },
59 | {
60 | "MatchOn": "\"type\": \"service_account\"",
61 | "Description": "Google (GCP) Service-account",
62 | "Comment": ""
63 | },
64 | {
65 | "MatchOn": "meet.google.com/[a-z]{3}-[a-z]{4}-[a-z]{3}",
66 | "Description": "Google Meet Meeting Link",
67 | "Comment": ""
68 | },
69 | {
70 | "MatchOn": "[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com",
71 | "Description": "Google OAuth",
72 | "Comment": "Could be a Google Drive, Gmail, Cloud (GCP), or YouTube OAuth token"
73 | },
74 | {
75 | "MatchOn": "ya29\\.[0-9A-Za-z\\-_]+",
76 | "Description": "Google OAuth Access Token",
77 | "Comment": ""
78 | },
79 | {
80 | "MatchOn": "AIza[0-9A-Za-z\\-_]{35}",
81 | "Description": "Google Token",
82 | "Comment": "Could be a Google Drive, Gmail, Cloud, or YouTube API key"
83 | },
84 | {
85 | "MatchOn": "[h|H][e|E][r|R][o|O][k|K][u|U].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}",
86 | "Description": "Heroku API Key",
87 | "Comment": ""
88 | },
89 | {
90 | "MatchOn": "[0-9a-f]{32}-us[0-9]{1,2}",
91 | "Description": "MailChimp API Key",
92 | "Comment": ""
93 | },
94 | {
95 | "MatchOn": "key-[0-9a-zA-Z]{32}",
96 | "Description": "Mailgun API Key",
97 | "Comment": ""
98 | },
99 | {
100 | "MatchOn": "[a-f0-9]+\\.ngrok\\.io",
101 | "Description": "Ngrok reverse tunnels",
102 | "Comment": ""
103 | },
104 | {
105 | "MatchOn": "[a-zA-Z]{3,10}://[^/\\s:@]{3,20}:[^/\\s:@]{3,20}@.{1,100}[\"'\\s]",
106 | "Description": "Password in URL",
107 | "Comment": ""
108 | },
109 | {
110 | "MatchOn": "access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}",
111 | "Description": "PayPal Braintree Access Token",
112 | "Comment": ""
113 | },
114 | {
115 | "MatchOn": "sk_live_[0-9a-z]{32}",
116 | "Description": "Picatic API Key",
117 | "Comment": ""
118 | },
119 | {
120 | "MatchOn": "(-*)BEGIN [\\s\\S]{2,} PRIVATE KEY(-*)",
121 | "Description": "SSH Private Key",
122 | "Comment": ""
123 | },
124 | {
125 | "MatchOn": "SG\\.[a-zA-Z0-9]{22}\\.[a-zA-Z0-9]{43}",
126 | "Description": "Send Grid API",
127 | "Comment": ""
128 | },
129 | {
130 | "MatchOn": "xox[baprs]-([0-9a-zA-Z]{10,48})?",
131 | "Description": "Slack Token",
132 | "Comment": ""
133 | },
134 | {
135 | "MatchOn": "https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}",
136 | "Description": "Slack Webhook",
137 | "Comment": ""
138 | },
139 | {
140 | "MatchOn": "sq0atp-[0-9A-Za-z\\-_]{22}",
141 | "Description": "Square Access Token",
142 | "Comment": ""
143 | },
144 | {
145 | "MatchOn": "sq0csp-[0-9A-Za-z\\-_]{43}",
146 | "Description": "Square OAuth Secret",
147 | "Comment": ""
148 | },
149 | {
150 | "MatchOn": "sk_live_[0-9a-zA-Z]{24}",
151 | "Description": "Stripe API Key",
152 | "Comment": ""
153 | },
154 | {
155 | "MatchOn": "rk_live_[0-9a-zA-Z]{24}",
156 | "Description": "Stripe Restricted API Key",
157 | "Comment": ""
158 | },
159 | {
160 | "MatchOn": "SK[0-9a-fA-F]{32}",
161 | "Description": "Twilio API Key",
162 | "Comment": ""
163 | },
164 | {
165 | "MatchOn": "[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*[1-9][0-9]+-[0-9a-zA-Z]{40}",
166 | "Description": "Twitter Access Token",
167 | "Comment": ""
168 | },
169 | {
170 | "MatchOn": "[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*['|\"][0-9a-zA-Z]{35,44}['|\"]",
171 | "Description": "Twitter OAuth",
172 | "Comment": ""
173 | },
174 | {
175 | "MatchOn": "[a-zA-Z0-9._-]*zoom.us/(?:j|my)/[a-zA-Z0-9.?=]+",
176 | "Description": "Zoom Meeting Link",
177 | "Comment": ""
178 | }
179 | ]
180 | }
181 |
--------------------------------------------------------------------------------
/core/analysis.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "gitrob/common"
6 | "gitrob/github"
7 | "gitrob/gitlab"
8 | "gitrob/matching"
9 | "gopkg.in/src-d/go-git.v4"
10 | "gopkg.in/src-d/go-git.v4/plumbing/object"
11 | "os"
12 | "strings"
13 | "sync"
14 | )
15 |
16 | func PrintSessionStats(sess *Session) {
17 | sess.Out.Info("\nFindings....: %d\n", sess.Stats.Findings)
18 | sess.Out.Info("Files.......: %d\n", sess.Stats.Files)
19 | sess.Out.Info("Commits.....: %d\n", sess.Stats.Commits)
20 | sess.Out.Info("Repositories: %d\n", sess.Stats.Repositories)
21 | sess.Out.Info("Targets.....: %d\n\n", sess.Stats.Targets)
22 | }
23 |
24 | func GatherTargets(sess *Session) {
25 | sess.Stats.Status = StatusGathering
26 | sess.Out.Important("Gathering targets...\n")
27 |
28 | for _, loginOption := range sess.Options.Logins {
29 | target, err := sess.Client.GetUserOrOrganization(loginOption)
30 | if err != nil || target == nil {
31 | sess.Out.Error(" Error retrieving information on %s: %s\n", loginOption, err)
32 | continue
33 | }
34 | sess.Out.Debug("%s (ID: %d) type: %s\n", *target.Login, *target.ID, *target.Type)
35 | sess.AddTarget(target)
36 | if *sess.Options.NoExpandOrgs == false && *target.Type == common.TargetTypeOrganization {
37 | sess.Out.Debug("Gathering members of %s (ID: %d)...\n", *target.Login, *target.ID)
38 | members, err := sess.Client.GetOrganizationMembers(*target)
39 | if err != nil {
40 | sess.Out.Error(" Error retrieving members of %s: %s\n", *target.Login, err)
41 | continue
42 | }
43 | for _, member := range members {
44 | sess.Out.Debug("Adding organization member %s (ID: %d) to targets\n", *member.Login, *member.ID)
45 | sess.AddTarget(member)
46 | }
47 | }
48 | }
49 | }
50 |
51 | func GatherRepositories(sess *Session) {
52 | var ch = make(chan *common.Owner, len(sess.Targets))
53 | var wg sync.WaitGroup
54 | var threadNum int
55 | if len(sess.Targets) == 1 {
56 | threadNum = 1
57 | } else if len(sess.Targets) <= *sess.Options.Threads {
58 | threadNum = len(sess.Targets) - 1
59 | } else {
60 | threadNum = *sess.Options.Threads
61 | }
62 | wg.Add(threadNum)
63 | sess.Out.Debug("Threads for repository gathering: %d\n", threadNum)
64 | for i := 0; i < threadNum; i++ {
65 | go func() {
66 | for {
67 | var repos []*common.Repository
68 | var err error
69 | target, ok := <-ch
70 | if !ok {
71 | wg.Done()
72 | return
73 | }
74 | if *target.Type == "Organization" {
75 | repos, err = sess.Client.GetRepositoriesFromOrganization(*target)
76 | } else {
77 | repos, err = sess.Client.GetRepositoriesFromOwner(*target)
78 | }
79 | if err != nil {
80 | sess.Out.Error(" Failed to retrieve repositories from %s: %s\n", *target.Login, err)
81 | }
82 | if len(repos) == 0 {
83 | continue
84 | }
85 | for _, repo := range repos {
86 | sess.Out.Debug(" Retrieved repository: %s\n", *repo.CloneURL)
87 | sess.AddRepository(repo)
88 | }
89 | sess.Stats.IncrementTargets()
90 | sess.Out.Info(" Retrieved %d %s from %s\n", len(repos), common.Pluralize(len(repos), "repository", "repositories"), *target.Login)
91 | }
92 | }()
93 | }
94 |
95 | for _, target := range sess.Targets {
96 | ch <- target
97 | }
98 | close(ch)
99 | wg.Wait()
100 | }
101 |
102 | func deletePath(path string, cloneUrl string, threadId int, sess *Session) {
103 | if path != "" {
104 | err := os.RemoveAll(path)
105 | if err != nil {
106 | sess.Out.Error("[THREAD #%d][%s] Unable to delete path %s\n", threadId, cloneUrl, path)
107 | } else {
108 | sess.Out.Debug("[THREAD #%d][%s] Deleted clone path %s\n", threadId, cloneUrl, path)
109 | }
110 | }
111 | }
112 |
113 | func createFinding(repo common.Repository,
114 | commit object.Commit,
115 | change *object.Change,
116 | fileSignature matching.FileSignature,
117 | contentSignature matching.ContentSignature,
118 | isGitHubSession bool) *matching.Finding {
119 |
120 | finding := &matching.Finding{
121 | FilePath: common.GetChangePath(change),
122 | Action: common.GetChangeAction(change),
123 | FileSignatureDescription: fileSignature.GetDescription(),
124 | FileSignatureComment: fileSignature.GetComment(),
125 | ContentSignatureDescription: contentSignature.GetDescription(),
126 | ContentSignatureComment: contentSignature.GetComment(),
127 | RepositoryOwner: *repo.Owner,
128 | RepositoryName: *repo.Name,
129 | CommitHash: commit.Hash.String(),
130 | CommitMessage: strings.TrimSpace(commit.Message),
131 | CommitAuthor: commit.Author.String(),
132 | CloneUrl: *repo.CloneURL,
133 | }
134 | finding.Initialize(isGitHubSession)
135 | return finding
136 |
137 | }
138 |
139 | func matchContent(sess *Session,
140 | matchTarget matching.MatchTarget,
141 | repo common.Repository,
142 | change *object.Change,
143 | commit object.Commit,
144 | fileSignature matching.FileSignature,
145 | threadId int) {
146 |
147 | content, err := common.GetChangeContent(change)
148 | if err != nil {
149 | sess.Out.Error("Error retrieving content in commit %s, change %s: %s", commit.String(), change.String(), err)
150 | }
151 | matchTarget.Content = content
152 | sess.Out.Debug("[THREAD #%d][%s] Matching content in %s...\n", threadId, *repo.CloneURL, commit.Hash)
153 | for _, contentSignature := range sess.Signatures.ContentSignatures {
154 | matched, err := contentSignature.Match(matchTarget)
155 | if err != nil {
156 | sess.Out.Error("Error while performing content match with '%s': %s\n", contentSignature.Description, err)
157 | }
158 | if !matched {
159 | continue
160 | }
161 | finding := createFinding(repo, commit, change, fileSignature, contentSignature, sess.IsGithubSession)
162 | sess.AddFinding(finding)
163 | }
164 | }
165 |
166 | /*func saveChangeToJson(c *object.Change) error {
167 | sessionJson, err := json.Marshal(c)
168 | if err != nil {
169 | return err
170 | }
171 | path := fmt.Sprintf("change-%s.json", strconv.FormatInt(int64(rand.Intn(10000)), 16))
172 | file, err := os.Create(path)
173 | defer file.Close()
174 | file.Write(sessionJson)
175 | if err != nil {
176 | return err
177 | }
178 | return nil
179 | }*/
180 |
181 | func findSecrets(sess *Session, repo *common.Repository, commit *object.Commit, changes object.Changes, threadId int) {
182 | for _, change := range changes {
183 | /*err1 := saveChangeToJson(change)
184 | if err1 != nil {
185 | panic(err1)
186 | }*/
187 | path := common.GetChangePath(change)
188 | matchTarget := matching.NewMatchTarget(path)
189 | if matchTarget.IsSkippable() {
190 | sess.Out.Debug("[THREAD #%d][%s] Skipping %s\n", threadId, *repo.CloneURL, matchTarget.Path)
191 | continue
192 | }
193 | sess.Out.Debug("[THREAD #%d][%s] Inspecting file: %s...\n", threadId, *repo.CloneURL, matchTarget.Path)
194 |
195 | if *sess.Options.Mode != 3 {
196 | for _, fileSignature := range sess.Signatures.FileSignatures {
197 | matched, err := fileSignature.Match(matchTarget)
198 | if err != nil {
199 | sess.Out.Error(fmt.Sprintf("Error while performing file match: %s\n", err))
200 | }
201 | if !matched {
202 | continue
203 | }
204 | if *sess.Options.Mode == 1 {
205 | finding := createFinding(*repo, *commit, change, fileSignature,
206 | matching.ContentSignature{Description: "NA"}, sess.IsGithubSession)
207 | sess.AddFinding(finding)
208 | }
209 | if *sess.Options.Mode == 2 {
210 | matchContent(sess, matchTarget, *repo, change, *commit, fileSignature, threadId)
211 | }
212 | break
213 | }
214 | sess.Stats.IncrementFiles()
215 | } else {
216 | matchContent(sess, matchTarget, *repo, change, *commit, matching.FileSignature{Description: "NA"}, threadId)
217 | sess.Stats.IncrementFiles()
218 | }
219 | }
220 | }
221 |
222 | func cloneRepository(sess *Session, repo *common.Repository, threadId int) (*git.Repository, string, error) {
223 | sess.Out.Debug("[THREAD #%d][%s] Cloning repository...\n", threadId, *repo.CloneURL)
224 |
225 | userName := "oauth2"
226 |
227 | cloneConfig := common.CloneConfiguration{
228 | Url: repo.CloneURL,
229 | Branch: repo.DefaultBranch,
230 | Depth: sess.Options.CommitDepth,
231 | InMemClone: sess.Options.InMemClone,
232 | Username: &userName,
233 | }
234 |
235 | var clone *git.Repository
236 | var path string
237 | var err error
238 |
239 | if sess.IsGithubSession {
240 | cloneConfig.Token = &sess.Github.AccessToken
241 | clone, path, err = github.CloneRepository(&cloneConfig)
242 | } else {
243 | cloneConfig.Token = &sess.GitLab.AccessToken
244 | clone, path, err = gitlab.CloneRepository(&cloneConfig)
245 | }
246 | if err != nil {
247 | if err.Error() != "remote repository is empty" {
248 | sess.Out.Error("Error cloning repository %s: %s\n", *repo.CloneURL, err)
249 | }
250 | sess.Stats.IncrementRepositories()
251 | sess.Stats.UpdateProgress(sess.Stats.Repositories, len(sess.Repositories))
252 | return nil, "", err
253 | }
254 | sess.Out.Debug("[THREAD #%d][%s] Cloned repository to: %s\n", threadId, *repo.CloneURL, path)
255 | return clone, path, err
256 | }
257 |
258 | func getRepositoryHistory(sess *Session, clone *git.Repository, repo *common.Repository, path string, threadId int) ([]*object.Commit, error) {
259 | history, err := common.GetRepositoryHistory(clone)
260 | if err != nil {
261 | sess.Out.Error("[THREAD #%d][%s] Error getting commit history: %s\n", threadId, *repo.CloneURL, err)
262 | deletePath(path, *repo.CloneURL, threadId, sess)
263 | sess.Stats.IncrementRepositories()
264 | sess.Stats.UpdateProgress(sess.Stats.Repositories, len(sess.Repositories))
265 | return nil, err
266 | }
267 | sess.Out.Debug("[THREAD #%d][%s] Number of commits: %d\n", threadId, *repo.CloneURL, len(history))
268 | return history, err
269 | }
270 |
271 | func AnalyzeRepositories(sess *Session) {
272 | sess.Stats.Status = StatusAnalyzing
273 | var ch = make(chan *common.Repository, len(sess.Repositories))
274 | var wg sync.WaitGroup
275 | var threadNum int
276 | if len(sess.Repositories) <= 1 {
277 | threadNum = 1
278 | } else if len(sess.Repositories) <= *sess.Options.Threads {
279 | threadNum = len(sess.Repositories) - 1
280 | } else {
281 | threadNum = *sess.Options.Threads
282 | }
283 | wg.Add(threadNum)
284 | sess.Out.Debug("Threads for repository analysis: %d\n", threadNum)
285 |
286 | sess.Out.Important("Analyzing %d %s...\n", len(sess.Repositories), common.Pluralize(len(sess.Repositories), "repository", "repositories"))
287 |
288 | for i := 0; i < threadNum; i++ {
289 | go func(tid int) {
290 | for {
291 | sess.Out.Debug("[THREAD #%d] Requesting new repository to analyze...\n", tid)
292 | repo, ok := <-ch
293 | if !ok {
294 | sess.Out.Debug("[THREAD #%d] No more tasks, marking WaitGroup as done\n", tid)
295 | wg.Done()
296 | return
297 | }
298 |
299 | clone, path, err := cloneRepository(sess, repo, tid)
300 | if err != nil {
301 | continue
302 | }
303 |
304 | history, err := getRepositoryHistory(sess, clone, repo, path, tid)
305 | if err != nil {
306 | continue
307 | }
308 |
309 | for _, commit := range history {
310 | sess.Out.Debug("[THREAD #%d][%s] Analyzing commit: %s\n", tid, *repo.CloneURL, commit.Hash)
311 | changes, _ := common.GetChanges(commit, clone)
312 | sess.Out.Debug("[THREAD #%d][%s] %s changes in %d\n", tid, *repo.CloneURL, commit.Hash, len(changes))
313 |
314 | findSecrets(sess, repo, commit, changes, tid)
315 |
316 | sess.Stats.IncrementCommits()
317 | sess.Out.Debug("[THREAD #%d][%s] Done analyzing changes in %s\n", tid, *repo.CloneURL, commit.Hash)
318 | }
319 |
320 | sess.Out.Debug("[THREAD #%d][%s] Done analyzing commits\n", tid, *repo.CloneURL)
321 | deletePath(path, *repo.CloneURL, tid, sess)
322 | sess.Out.Debug("[THREAD #%d][%s] Deleted %s\n", tid, *repo.CloneURL, path)
323 | sess.Stats.IncrementRepositories()
324 | sess.Stats.UpdateProgress(sess.Stats.Repositories, len(sess.Repositories))
325 | }
326 | }(i)
327 | }
328 | for _, repo := range sess.Repositories {
329 | ch <- repo
330 | }
331 | close(ch)
332 | wg.Wait()
333 | }
334 |
--------------------------------------------------------------------------------
/core/options.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "flag"
5 | )
6 |
7 | type Options struct {
8 | BindAddress *string `json:"-"`
9 | CommitDepth *int
10 | Debug *bool `json:"-"`
11 | ExitOnFinish *bool
12 | GitLabAccessToken *string `json:"-"`
13 | GithubAccessToken *string `json:"-"`
14 | InMemClone *bool
15 | Load *string `json:"-"`
16 | Logins []string
17 | Mode *int
18 | NoExpandOrgs *bool
19 | Port *int
20 | Save *string `json:"-"`
21 | Silent *bool `json:"-"`
22 | Threads *int
23 | }
24 |
25 | func ParseOptions() (Options, error) {
26 | options := Options{
27 | BindAddress: flag.String("bind-address", "127.0.0.1", "Address to bind web server to"),
28 | CommitDepth: flag.Int("commit-depth", 500, "Number of repository commits to process"),
29 | Debug: flag.Bool("debug", false, "Print debugging information"),
30 | ExitOnFinish: flag.Bool("exit-on-finish", false, "Let the program exit on finish. Useful for automated scans."),
31 | GitLabAccessToken: flag.String("gitlab-access-token", "", "GitLab access token to use for API requests"),
32 | GithubAccessToken: flag.String("github-access-token", "", "GitHub access token to use for API requests"),
33 | InMemClone: flag.Bool("in-mem-clone", false, "Clone repositories into memory"),
34 | Load: flag.String("load", "", "Load session file"),
35 | Mode: flag.Int("mode", 1, "Secrets matching mode (see documentation)."),
36 | NoExpandOrgs: flag.Bool("no-expand-orgs", false, "Don't add members to targets when processing organizations"),
37 | Port: flag.Int("port", 9393, "Port to run web server on"),
38 | Save: flag.String("save", "", "Save session to file"),
39 | Silent: flag.Bool("silent", false, "Suppress all output except for errors"),
40 | Threads: flag.Int("threads", 0, "Number of concurrent threads (default number of logical CPUs)"),
41 | }
42 |
43 | flag.Parse()
44 | options.Logins = flag.Args()
45 |
46 | return options, nil
47 | }
48 |
--------------------------------------------------------------------------------
/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 | "gitrob/common"
14 | )
15 |
16 | const (
17 | GithubBaseUri = "https://raw.githubusercontent.com"
18 | MaximumFileSize = 153600
19 | GitLabBaseUri = "https://gitlab.com"
20 | CspPolicy = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
21 | ReferrerPolicy = "no-referrer"
22 | )
23 |
24 | var IsGithub bool
25 |
26 | type binaryFileSystem struct {
27 | fs http.FileSystem
28 | }
29 |
30 | func (b *binaryFileSystem) Open(name string) (http.File, error) {
31 | return b.fs.Open(name)
32 | }
33 |
34 | func (b *binaryFileSystem) Exists(prefix string, filepath string) bool {
35 | if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
36 | if _, err := b.fs.Open(p); err != nil {
37 | return false
38 | }
39 | return true
40 | }
41 | return false
42 | }
43 |
44 | func BinaryFileSystem(root string) *binaryFileSystem {
45 | fs := &assetfs.AssetFS{Asset, AssetDir, AssetInfo, root}
46 | return &binaryFileSystem{
47 | fs,
48 | }
49 | }
50 |
51 | func NewRouter(s *Session) *gin.Engine {
52 |
53 | IsGithub = s.IsGithubSession
54 |
55 | if *s.Options.Debug == true {
56 | gin.SetMode(gin.DebugMode)
57 | } else {
58 | gin.SetMode(gin.ReleaseMode)
59 | }
60 |
61 | router := gin.New()
62 | router.Use(static.Serve("/", BinaryFileSystem("static")))
63 | router.Use(secure.New(secure.Config{
64 | SSLRedirect: false,
65 | IsDevelopment: false,
66 | FrameDeny: true,
67 | ContentTypeNosniff: true,
68 | BrowserXssFilter: true,
69 | ContentSecurityPolicy: CspPolicy,
70 | ReferrerPolicy: ReferrerPolicy,
71 | }))
72 | router.GET("/stats", func(c *gin.Context) {
73 | c.JSON(200, s.Stats)
74 | })
75 | router.GET("/findings", func(c *gin.Context) {
76 | c.JSON(200, s.Findings)
77 | })
78 | router.GET("/targets", func(c *gin.Context) {
79 | c.JSON(200, s.Targets)
80 | })
81 | router.GET("/repositories", func(c *gin.Context) {
82 | c.JSON(200, s.Repositories)
83 | })
84 | router.GET("/files/:owner/:repo/:commit/*path", fetchFile)
85 |
86 | return router
87 | }
88 |
89 | func fetchFile(c *gin.Context) {
90 | fileUrl := func() string {
91 | if IsGithub {
92 | return fmt.Sprintf("%s/%s/%s/%s%s", GithubBaseUri, c.Param("owner"), c.Param("repo"), c.Param("commit"), c.Param("path"))
93 | } else {
94 | results := common.CleanUrlSpaces(c.Param("owner"), c.Param("repo"), c.Param("commit"), c.Param("path"))
95 | return fmt.Sprintf("%s/%s/%s/%s/%s%s", GitLabBaseUri, results[0], results[1], "/-/raw/", results[2], results[3])
96 | }
97 | }()
98 | resp, err := http.Head(fileUrl)
99 | if err != nil {
100 | c.JSON(http.StatusInternalServerError, gin.H{
101 | "message": err,
102 | })
103 | return
104 | }
105 |
106 | if resp.StatusCode == http.StatusNotFound {
107 | c.JSON(http.StatusNotFound, gin.H{
108 | "message": "No content",
109 | })
110 | return
111 | }
112 |
113 | if resp.ContentLength > MaximumFileSize {
114 | c.JSON(http.StatusUnprocessableEntity, gin.H{
115 | "message": fmt.Sprintf("File size exceeds maximum of %d bytes", MaximumFileSize),
116 | })
117 | return
118 | }
119 |
120 | resp, err = http.Get(fileUrl)
121 | if err != nil {
122 | c.JSON(http.StatusInternalServerError, gin.H{
123 | "message": err,
124 | })
125 | return
126 | }
127 |
128 | defer resp.Body.Close()
129 | body, err := ioutil.ReadAll(resp.Body)
130 | if err != nil {
131 | c.JSON(http.StatusInternalServerError, gin.H{
132 | "message": err,
133 | })
134 | return
135 | }
136 |
137 | c.String(http.StatusOK, string(body[:]))
138 | }
139 |
--------------------------------------------------------------------------------
/core/session.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "os"
9 | "gitrob/matching"
10 | "runtime"
11 | "strings"
12 | "sync"
13 | "time"
14 |
15 | "gitrob/common"
16 | gh "gitrob/github"
17 | gl "gitrob/gitlab"
18 |
19 | "github.com/gin-gonic/gin"
20 | )
21 |
22 | const (
23 | GitHubAccessTokenEnvVariable = "GITROB_GITHUB_ACCESS_TOKEN"
24 | GitLabAccessTokenEnvVariable = "GITROB_GITLAB_ACCESS_TOKEN"
25 | StatusInitializing = "initializing"
26 | StatusGathering = "gathering"
27 | StatusAnalyzing = "analyzing"
28 | StatusFinished = "finished"
29 | )
30 |
31 | type Stats struct {
32 | sync.Mutex
33 |
34 | StartedAt time.Time
35 | FinishedAt time.Time
36 | Status string
37 | Progress float64
38 | Targets int
39 | Repositories int
40 | Commits int
41 | Files int
42 | Findings int
43 | }
44 |
45 | type Github struct {
46 | AccessToken string `json:"-"`
47 | }
48 |
49 | type GitLab struct {
50 | AccessToken string `json:"-"`
51 | }
52 |
53 | type Session struct {
54 | sync.Mutex
55 |
56 | Version string
57 | Options Options `json:"-"` //do not unmarshal to json on save
58 | Out *common.Logger `json:"-"` //do not unmarshal to json on save
59 | Stats *Stats
60 | Github Github `json:"-"` //do not unmarshal to json on save
61 | GitLab GitLab `json:"-"` //do not unmarshal to json on save
62 | Client common.IClient `json:"-"` //do not unmarshal to json on save
63 | Router *gin.Engine `json:"-"` //do not unmarshal to json on save
64 | Targets []*common.Owner
65 | Repositories []*common.Repository
66 | Findings []*matching.Finding
67 | IsGithubSession bool `json:"-"` //do not unmarshal to json on save
68 | Signatures matching.Signatures `json:"-"` //do not unmarshal to json on save
69 | }
70 |
71 | func (s *Session) Initialize() {
72 | s.InitStats()
73 | s.InitLogger()
74 | s.InitThreads()
75 | s.InitAccessToken()
76 | s.InitSignatures()
77 | s.ValidateTokenConfig()
78 | s.InitAPIClient()
79 | s.InitRouter()
80 | }
81 |
82 | func (s *Session) InitSignatures() {
83 | s.Signatures = matching.Signatures{}
84 | err := s.Signatures.Load(*s.Options.Mode)
85 | if err != nil {
86 | s.Out.Fatal("Error loading signatures: %s\n", err)
87 | }
88 | }
89 |
90 | func (s *Session) Finish() {
91 | s.Stats.FinishedAt = time.Now()
92 | s.Stats.Status = StatusFinished
93 | }
94 |
95 | func (s *Session) AddTarget(target *common.Owner) {
96 | s.Lock()
97 | defer s.Unlock()
98 | for _, t := range s.Targets {
99 | if *target.ID == *t.ID {
100 | return
101 | }
102 | }
103 | s.Targets = append(s.Targets, target)
104 | }
105 |
106 | func (s *Session) AddRepository(repository *common.Repository) {
107 | s.Lock()
108 | defer s.Unlock()
109 | for _, r := range s.Repositories {
110 | if *repository.ID == *r.ID {
111 | return
112 | }
113 | }
114 | s.Repositories = append(s.Repositories, repository)
115 | }
116 |
117 | func (s *Session) AddFinding(finding *matching.Finding) {
118 | s.Lock()
119 | defer s.Unlock()
120 | const MaxStrLen = 100
121 | s.Findings = append(s.Findings, finding)
122 | s.Out.Warn(" %s: %s, %s\n", strings.ToUpper(finding.Action), "File Match: "+finding.FileSignatureDescription, "Content Match: "+finding.ContentSignatureDescription)
123 | s.Out.Info(" Path......................: %s\n", finding.FilePath)
124 | s.Out.Info(" Repo......................: %s\n", finding.CloneUrl)
125 | s.Out.Info(" Message...................: %s\n", common.TruncateString(finding.CommitMessage, MaxStrLen))
126 | s.Out.Info(" Author....................: %s\n", finding.CommitAuthor)
127 | if finding.FileSignatureComment != "" {
128 | s.Out.Info(" FileSignatureComment......: %s\n", common.TruncateString(finding.FileSignatureComment, MaxStrLen))
129 | }
130 | if finding.ContentSignatureComment != "" {
131 | s.Out.Info(" ContentSignatureComment...:%s\n", common.TruncateString(finding.ContentSignatureComment, MaxStrLen))
132 | }
133 | s.Out.Info(" File URL...: %s\n", finding.FileUrl)
134 | s.Out.Info(" Commit URL.: %s\n", finding.CommitUrl)
135 | s.Out.Info(" ------------------------------------------------\n\n")
136 | s.Stats.IncrementFindings()
137 | }
138 |
139 | func (s *Session) InitStats() {
140 | if s.Stats != nil {
141 | return
142 | }
143 | s.Stats = &Stats{
144 | StartedAt: time.Now(),
145 | Status: StatusInitializing,
146 | Progress: 0.0,
147 | Targets: 0,
148 | Repositories: 0,
149 | Commits: 0,
150 | Files: 0,
151 | Findings: 0,
152 | }
153 | }
154 |
155 | func (s *Session) InitLogger() {
156 | s.Out = &common.Logger{}
157 | s.Out.SetDebug(*s.Options.Debug)
158 | s.Out.SetSilent(*s.Options.Silent)
159 | }
160 |
161 | func (s *Session) InitAccessToken() {
162 | if *s.Options.GithubAccessToken == "" {
163 | s.Github.AccessToken = os.Getenv(GitHubAccessTokenEnvVariable)
164 | } else {
165 | s.Github.AccessToken = *s.Options.GithubAccessToken
166 | }
167 | if *s.Options.GitLabAccessToken == "" {
168 | s.GitLab.AccessToken = os.Getenv(GitLabAccessTokenEnvVariable)
169 | } else {
170 | s.GitLab.AccessToken = *s.Options.GitLabAccessToken
171 | }
172 | }
173 |
174 | func (s *Session) ValidateTokenConfig() {
175 | if *s.Options.Load == "" {
176 | if s.GitLab.AccessToken != "" && s.Github.AccessToken != "" {
177 | s.Out.Fatal("Both a GitLab and Github token are present. Only one may be set.\n")
178 | }
179 | if s.GitLab.AccessToken == "" && s.Github.AccessToken == "" {
180 | s.Out.Fatal("No valid API token was found.\n")
181 | }
182 | }
183 | s.IsGithubSession = s.Github.AccessToken != ""
184 | }
185 |
186 | func (s *Session) InitAPIClient() {
187 | if s.IsGithubSession {
188 | s.Client = gh.Client.NewClient(gh.Client{}, s.Github.AccessToken)
189 | } else {
190 | var err error
191 | s.Client, err = gl.Client.NewClient(gl.Client{}, s.GitLab.AccessToken, s.Out)
192 | if err != nil {
193 | s.Out.Fatal("Error initializing GitLab client: %s", err)
194 | }
195 | }
196 | }
197 |
198 | func (s *Session) InitThreads() {
199 | if *s.Options.Threads == 0 {
200 | numCPUs := runtime.NumCPU()
201 | s.Options.Threads = &numCPUs
202 | }
203 | runtime.GOMAXPROCS(*s.Options.Threads + 2) // thread count + main + web server
204 | }
205 |
206 | func (s *Session) InitRouter() {
207 | bind := fmt.Sprintf("%s:%d", *s.Options.BindAddress, *s.Options.Port)
208 | s.Router = NewRouter(s)
209 | go func(sess *Session) {
210 | if err := sess.Router.Run(bind); err != nil {
211 | sess.Out.Fatal("Error when starting web server: %s\n", err)
212 | }
213 | }(s)
214 | }
215 |
216 | func (s *Session) SaveToFile(location string) error {
217 | sessionJson, err := json.Marshal(s)
218 | if err != nil {
219 | return err
220 | }
221 | err = ioutil.WriteFile(location, sessionJson, 0644)
222 | if err != nil {
223 | return err
224 | }
225 | return nil
226 | }
227 |
228 | func (s *Stats) IncrementTargets() {
229 | s.Lock()
230 | defer s.Unlock()
231 | s.Targets++
232 | }
233 |
234 | func (s *Stats) IncrementRepositories() {
235 | s.Lock()
236 | defer s.Unlock()
237 | s.Repositories++
238 | }
239 |
240 | func (s *Stats) IncrementCommits() {
241 | s.Lock()
242 | defer s.Unlock()
243 | s.Commits++
244 | }
245 |
246 | func (s *Stats) IncrementFiles() {
247 | s.Lock()
248 | defer s.Unlock()
249 | s.Files++
250 | }
251 |
252 | func (s *Stats) IncrementFindings() {
253 | s.Lock()
254 | defer s.Unlock()
255 | s.Findings++
256 | }
257 |
258 | func (s *Stats) UpdateProgress(current int, total int) {
259 | s.Lock()
260 | defer s.Unlock()
261 | if current >= total {
262 | s.Progress = 100.0
263 | } else {
264 | s.Progress = (float64(current) * float64(100)) / float64(total)
265 | }
266 | }
267 |
268 | func NewSession() (*Session, error) {
269 | var err error
270 | var session Session
271 |
272 | if session.Options, err = ParseOptions(); err != nil {
273 | return nil, err
274 | }
275 |
276 | if *session.Options.Save != "" && common.FileExists(*session.Options.Save) {
277 | return nil, errors.New(fmt.Sprintf("File: %s already exists.", *session.Options.Save))
278 | }
279 |
280 | if *session.Options.Load != "" {
281 | if !common.FileExists(*session.Options.Load) {
282 | return nil, errors.New(fmt.Sprintf("Session file %s does not exist or is not readable.", *session.Options.Load))
283 | }
284 | data, err := ioutil.ReadFile(*session.Options.Load)
285 | if err != nil {
286 | return nil, err
287 | }
288 | if err := json.Unmarshal(data, &session); err != nil {
289 | return nil, errors.New(fmt.Sprintf("Session file %s is corrupt or generated by an old version of Gitrob.", *session.Options.Load))
290 | }
291 | }
292 |
293 | session.Version = common.Version
294 | session.Initialize()
295 |
296 | return &session, nil
297 | }
298 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 | services:
3 | gitrob:
4 | build: .
5 | container_name: "gitrob"
6 | ports:
7 | - 0.0.0.0:9393:9393/tcp
8 | environment:
9 | - GITROB_GITLAB_ACCESS_TOKEN="deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
10 | entrypoint: "/bin/sh"
11 | command: "./docker_compose_entrypoint.sh"
12 | volumes:
13 | - ./targets.txt:/tmp/targets.txt
14 |
--------------------------------------------------------------------------------
/docker_compose_entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cat /tmp/targets.txt | xargs ./gitrob -bind-address 0.0.0.0 -in-mem-clone -mode 2
3 |
--------------------------------------------------------------------------------
/filesignatures.json:
--------------------------------------------------------------------------------
1 | {
2 | "FileSignatures": [
3 | {
4 | "Part": "extension",
5 | "MatchOn": "\\.agilekeychain$",
6 | "Description": "1Password password manager database file",
7 | "Comment": "Feed it to Hashcat and see if you're lucky"
8 | },
9 | {
10 | "Part": "path",
11 | "MatchOn": "\\.?aws/credentials$",
12 | "Description": "AWS CLI credentials file",
13 | "Comment": ""
14 | },
15 | {
16 | "Part": "filename",
17 | "MatchOn": "^\\.?htpasswd$",
18 | "Description": "Apache htpasswd file",
19 | "Comment": ""
20 | },
21 | {
22 | "Part": "extension",
23 | "MatchOn": "\\.keychain$",
24 | "Description": "Apple Keychain database file",
25 | "Comment": ""
26 | },
27 | {
28 | "Part": "extension",
29 | "MatchOn": "\\.cscfg$",
30 | "Description": "Azure service configuration schema file",
31 | "Comment": ""
32 | },
33 | {
34 | "Part": "filename",
35 | "MatchOn": "carrierwave\\.rb$",
36 | "Description": "Carrierwave configuration file",
37 | "Comment": "Can contain credentials for cloud storage systems such as Amazon S3 and Google Storage"
38 | },
39 | {
40 | "Part": "filename",
41 | "MatchOn": "knife\\.rb$",
42 | "Description": "Chef Knife configuration file",
43 | "Comment": "Can contain references to Chef servers"
44 | },
45 | {
46 | "Part": "path",
47 | "MatchOn": "\\.?chef/(.*)\\.pem$",
48 | "Description": "Chef private key",
49 | "Comment": "Can be used to authenticate against Chef servers"
50 | },
51 | {
52 | "Part": "path",
53 | "MatchOn": "gitlab/gitlab\\.rb$",
54 | "Description": "GitLab Omnibus configuration file",
55 | "Comment": ""
56 | },
57 | {
58 | "Part": "path",
59 | "MatchOn": "gitaly/config\\.toml$",
60 | "Description": "GitLab gitaly configuration file",
61 | "Comment": ""
62 | },
63 | {
64 | "Part": "path",
65 | "MatchOn": "config/gitlab\\.yml$",
66 | "Description": "GitLab configuration file",
67 | "Comment": ""
68 | },
69 | {
70 | "Part": "path",
71 | "MatchOn": "config/secrets\\.yml$",
72 | "Description": "GitLab secrets file",
73 | "Comment": ""
74 | },
75 | {
76 | "Part": "path",
77 | "MatchOn": "config/resque\\.yml$",
78 | "Description": "GitLab redis config file",
79 | "Comment": ""
80 | },
81 | {
82 | "Part": "path",
83 | "MatchOn": "config/database\\.yml$",
84 | "Description": "GitLab database configuration file",
85 | "Comment": ""
86 | },
87 | {
88 | "Part": "filename",
89 | "MatchOn": "^(\\.|_)?netrc$",
90 | "Description": "Configuration file for auto-login process",
91 | "Comment": "Can contain username and password"
92 | },
93 | {
94 | "Part": "path",
95 | "MatchOn": "credential",
96 | "Description": "Contains word: credential",
97 | "Comment": ""
98 | },
99 | {
100 | "Part": "path",
101 | "MatchOn": "password",
102 | "Description": "Contains word: password",
103 | "Comment": ""
104 | },
105 | {
106 | "Part": "filename",
107 | "MatchOn": "^\\.?dbeaver-data-sources\\.xml$",
108 | "Description": "DBeaver SQL database manager configuration file",
109 | "Comment": ""
110 | },
111 | {
112 | "Part": "extension",
113 | "MatchOn": "\\.dayone$",
114 | "Description": "Day One journal file",
115 | "Comment": "Now it's getting creepy..."
116 | },
117 | {
118 | "Part": "path",
119 | "MatchOn": "doctl/config\\.yaml$",
120 | "Description": "DigitalOcean doctl command-line client configuration file",
121 | "Comment": "Contains DigitalOcean API key and other information"
122 | },
123 | {
124 | "Part": "filename",
125 | "MatchOn": "settings\\.py$",
126 | "Description": "Django configuration file",
127 | "Comment": "Can contain database credentials, cloud storage system credentials, and other secrets"
128 | },
129 | {
130 | "Part": "filename",
131 | "MatchOn": "^\\.?dockercfg$",
132 | "Description": "Docker configuration file",
133 | "Comment": "Can contain credentials for public or private Docker registries"
134 | },
135 | {
136 | "Part": "filename",
137 | "MatchOn": "^\\.?env$",
138 | "Description": "Environment configuration file",
139 | "Comment": ""
140 | },
141 | {
142 | "Part": "filename",
143 | "MatchOn": "filezilla\\.xml$",
144 | "Description": "FileZilla FTP configuration file",
145 | "Comment": "Can contain credentials for FTP servers"
146 | },
147 | {
148 | "Part": "filename",
149 | "MatchOn": "recentservers\\.xml$",
150 | "Description": "FileZilla FTP recent servers file",
151 | "Comment": "Can contain credentials for FTP servers"
152 | },
153 | {
154 | "Part": "extension",
155 | "MatchOn": "^key(store|ring)$",
156 | "Description": "GNOME Keyring database file",
157 | "Comment": ""
158 | },
159 | {
160 | "Part": "filename",
161 | "MatchOn": "^\\.?gitconfig$",
162 | "Description": "Git configuration file",
163 | "Comment": ""
164 | },
165 | {
166 | "Part": "path",
167 | "MatchOn": "config/hub$",
168 | "Description": "GitHub Hub command-line client configuration file",
169 | "Comment": "Can contain GitHub API access token"
170 | },
171 | {
172 | "Part": "extension",
173 | "MatchOn": "\\.gnucash$",
174 | "Description": "GnuCash database file",
175 | "Comment": ""
176 | },
177 | {
178 | "Part": "filename",
179 | "MatchOn": "credentials\\.db$",
180 | "Description": "Google Cloud Platform gcloud credential database",
181 | "Comment": "sqlite database containing credentials used by the gcloud command from Google's Cloud SDK"
182 | },
183 | {
184 | "Part": "filename",
185 | "MatchOn": "credentials\\.json$",
186 | "Description": "Google Cloud Platform service account credentials keyfile",
187 | "Comment": "GCP service account credentials can be activated using the gcloud command from Google's Cloud SDK (https://cloud.google.com/sdk/gcloud/reference/auth/activate-service-account)"
188 | },
189 | {
190 | "Part": "filename",
191 | "MatchOn": "^.*-[a-f0-9]{12}\\.json$",
192 | "Description": "Google Cloud Platform service account credentials keyfile",
193 | "Comment": "GCP service account credentials can be activated using the gcloud command from Google's Cloud SDK (https://cloud.google.com/sdk/gcloud/reference/auth/activate-service-account)"
194 | },
195 | {
196 | "Part": "path",
197 | "MatchOn": "\\.?xchat2?/servlist_?\\.conf$",
198 | "Description": "Hexchat/XChat IRC client server list configuration file",
199 | "Comment": ""
200 | },
201 | {
202 | "Part": "path",
203 | "MatchOn": "\\.?irssi/config$",
204 | "Description": "Irssi IRC client configuration file",
205 | "Comment": ""
206 | },
207 | {
208 | "Part": "extension",
209 | "MatchOn": "\\.jks$",
210 | "Description": "Java keystore file",
211 | "Comment": ""
212 | },
213 | {
214 | "Part": "filename",
215 | "MatchOn": "jenkins\\.plugins\\.publish_over_ssh\\.BapSshPublisherPlugin\\.xml",
216 | "Description": "Jenkins publish over SSH plugin file",
217 | "Comment": ""
218 | },
219 | {
220 | "Part": "extension",
221 | "MatchOn": "\\.kwallet$",
222 | "Description": "KDE Wallet Manager database file",
223 | "Comment": ""
224 | },
225 | {
226 | "Part": "extension",
227 | "MatchOn": "^kdbx?$",
228 | "Description": "KeePass password manager database file",
229 | "Comment": "Feed it to Hashcat and see if you're lucky"
230 | },
231 | {
232 | "Part": "filename",
233 | "MatchOn": "\\.boto$",
234 | "Description": "Legacy Google Cloud Platform gcloud credential database",
235 | "Comment": "File containing credentials used by the gcloud command from Google's Cloud SDK"
236 | },
237 | {
238 | "Part": "filename",
239 | "MatchOn": "adc\\.json$",
240 | "Description": "Legacy Google Cloud Platform service account credentials keyfile",
241 | "Comment": "GCP service account credentials can be activated using the gcloud command from Google's Cloud SDK (https://cloud.google.com/sdk/gcloud/reference/auth/activate-service-account)"
242 | },
243 | {
244 | "Part": "filename",
245 | "MatchOn": "configuration\\.user\\.xpl$",
246 | "Description": "Little Snitch firewall configuration file",
247 | "Comment": "Contains traffic rules for applications"
248 | },
249 | {
250 | "Part": "extension",
251 | "MatchOn": "\\.log$",
252 | "Description": "Log file",
253 | "Comment": "Log files can contain secret HTTP endpoints, session IDs, API keys and other goodies"
254 | },
255 | {
256 | "Part": "extension",
257 | "MatchOn": "\\.tpm$",
258 | "Description": "Microsoft BitLocker Trusted Platform Module password file",
259 | "Comment": ""
260 | },
261 | {
262 | "Part": "extension",
263 | "MatchOn": "\\.bek$",
264 | "Description": "Microsoft BitLocker recovery key file",
265 | "Comment": ""
266 | },
267 | {
268 | "Part": "extension",
269 | "MatchOn": "\\.mdf$",
270 | "Description": "Microsoft SQL database file",
271 | "Comment": ""
272 | },
273 | {
274 | "Part": "extension",
275 | "MatchOn": "\\.sdf$",
276 | "Description": "Microsoft SQL server compact database file",
277 | "Comment": ""
278 | },
279 | {
280 | "Part": "filename",
281 | "MatchOn": "^\\.?muttrc$",
282 | "Description": "Mutt e-mail client configuration file",
283 | "Comment": ""
284 | },
285 | {
286 | "Part": "filename",
287 | "MatchOn": "^\\.?mysql_history$",
288 | "Description": "MySQL client command history file",
289 | "Comment": ""
290 | },
291 | {
292 | "Part": "filename",
293 | "MatchOn": "^\\.?npmrc$",
294 | "Description": "NPM configuration file",
295 | "Comment": "Can contain credentials for NPM registries"
296 | },
297 | {
298 | "Part": "extension",
299 | "MatchOn": "\\.pcap$",
300 | "Description": "Network traffic capture file",
301 | "Comment": ""
302 | },
303 | {
304 | "Part": "filename",
305 | "MatchOn": "omniauth\\.rb$",
306 | "Description": "OmniAuth configuration file",
307 | "Comment": "The OmniAuth configuration file can contain client application secrets"
308 | },
309 | {
310 | "Part": "extension",
311 | "MatchOn": "\\.ovpn$",
312 | "Description": "OpenVPN client configuration file",
313 | "Comment": ""
314 | },
315 | {
316 | "Part": "filename",
317 | "MatchOn": "config(\\.inc)?\\.php$",
318 | "Description": "PHP configuration file",
319 | "Comment": ""
320 | },
321 | {
322 | "Part": "extension",
323 | "MatchOn": "\\.psafe3$",
324 | "Description": "Password Safe database file",
325 | "Comment": ""
326 | },
327 | {
328 | "Part": "filename",
329 | "MatchOn": "otr\\.private_key",
330 | "Description": "Pidgin OTR private key",
331 | "Comment": ""
332 | },
333 | {
334 | "Part": "path",
335 | "MatchOn": "\\.?purple/accounts\\.xml$",
336 | "Description": "Pidgin chat client account configuration file",
337 | "Comment": ""
338 | },
339 | {
340 | "Part": "filename",
341 | "MatchOn": "^\\.?psql_history$",
342 | "Description": "PostgreSQL client command history file",
343 | "Comment": ""
344 | },
345 | {
346 | "Part": "filename",
347 | "MatchOn": "^\\.?pgpass$",
348 | "Description": "PostgreSQL password file",
349 | "Comment": ""
350 | },
351 | {
352 | "Part": "filename",
353 | "MatchOn": "credentials\\.xml$",
354 | "Description": "Potential Jenkins credentials file",
355 | "Comment": ""
356 | },
357 | {
358 | "Part": "path",
359 | "MatchOn": "etc/passwd$",
360 | "Description": "Potential Linux passwd file",
361 | "Comment": "Contains system user information"
362 | },
363 | {
364 | "Part": "path",
365 | "MatchOn": "etc/shadow$",
366 | "Description": "Potential Linux shadow file",
367 | "Comment": "Contains hashed passwords for system users"
368 | },
369 | {
370 | "Part": "filename",
371 | "MatchOn": "LocalSettings\\.php$",
372 | "Description": "Potential MediaWiki configuration file",
373 | "Comment": ""
374 | },
375 | {
376 | "Part": "filename",
377 | "MatchOn": "database\\.yml$",
378 | "Description": "Potential Ruby On Rails database configuration file",
379 | "Comment": "Can contain database credentials"
380 | },
381 | {
382 | "Part": "path",
383 | "MatchOn": "web[\\\/]ruby[\\\/]secrets\\.yml",
384 | "Description": "Ruby on rails secrets.yml file (contains passwords)",
385 | "Comment": ""
386 | },
387 | {
388 | "Part": "extension",
389 | "MatchOn": "\\.pkcs12$",
390 | "Description": "Potential cryptographic key bundle",
391 | "Comment": ""
392 | },
393 | {
394 | "Part": "extension",
395 | "MatchOn": "\\.p12$",
396 | "Description": "Potential cryptographic key bundle",
397 | "Comment": ""
398 | },
399 | {
400 | "Part": "extension",
401 | "MatchOn": "\\.pfx$",
402 | "Description": "Potential cryptographic key bundle",
403 | "Comment": ""
404 | },
405 | {
406 | "Part": "extension",
407 | "MatchOn": "\\.asc$",
408 | "Description": "Potential cryptographic key bundle",
409 | "Comment": ""
410 | },
411 | {
412 | "Part": "extension",
413 | "MatchOn": "^key(pair)?$",
414 | "Description": "Potential cryptographic private key",
415 | "Comment": ""
416 | },
417 | {
418 | "Part": "extension",
419 | "MatchOn": "\\.pem$",
420 | "Description": "Potential cryptographic private key",
421 | "Comment": ""
422 | },
423 | {
424 | "Part": "filename",
425 | "MatchOn": "journal\\.txt$",
426 | "Description": "Potential jrnl journal file",
427 | "Comment": "Now it's getting creepy..."
428 | },
429 | {
430 | "Part": "filename",
431 | "MatchOn": "^.*_rsa$",
432 | "Description": "Private SSH key",
433 | "Comment": ""
434 | },
435 | {
436 | "Part": "filename",
437 | "MatchOn": "^.*_dsa$",
438 | "Description": "Private SSH key",
439 | "Comment": ""
440 | },
441 | {
442 | "Part": "filename",
443 | "MatchOn": "^.*_ed25519$",
444 | "Description": "Private SSH key",
445 | "Comment": ""
446 | },
447 | {
448 | "Part": "filename",
449 | "MatchOn": "^.*_ecdsa$",
450 | "Description": "Private SSH key",
451 | "Comment": ""
452 | },
453 | {
454 | "Part": "path",
455 | "MatchOn": "\\.?recon-ng/keys\\.db$",
456 | "Description": "Recon-ng web reconnaissance framework API key database",
457 | "Comment": ""
458 | },
459 | {
460 | "Part": "extension",
461 | "MatchOn": "\\.rdp$",
462 | "Description": "Remote Desktop connection file",
463 | "Comment": ""
464 | },
465 | {
466 | "Part": "filename",
467 | "MatchOn": "robomongo\\.json$",
468 | "Description": "Robomongo MongoDB manager configuration file",
469 | "Comment": "Can contain credentials for MongoDB databases"
470 | },
471 | {
472 | "Part": "filename",
473 | "MatchOn": "^\\.?irb_history$",
474 | "Description": "Ruby IRB console history file",
475 | "Comment": ""
476 | },
477 | {
478 | "Part": "filename",
479 | "MatchOn": "secret_token\\.rb$",
480 | "Description": "Ruby On Rails secret token configuration file",
481 | "Comment": "If the Rails secret token is known, it can allow for remote code execution (http://www.exploit-db.com/exploits/27527/)"
482 | },
483 | {
484 | "Part": "path",
485 | "MatchOn": "\\.?gem/credentials$",
486 | "Description": "Rubygems credentials file",
487 | "Comment": "Can contain API key for a rubygems.org account"
488 | },
489 | {
490 | "Part": "filename",
491 | "MatchOn": "^\\.?s3cfg$",
492 | "Description": "S3cmd configuration file",
493 | "Comment": ""
494 | },
495 | {
496 | "Part": "filename",
497 | "MatchOn": "^sftp-config(\\.json)?$",
498 | "Description": "SFTP connection configuration file",
499 | "Comment": ""
500 | },
501 | {
502 | "Part": "extension",
503 | "MatchOn": "^sql(dump)?$",
504 | "Description": "SQL dump file",
505 | "Comment": ""
506 | },
507 | {
508 | "Part": "extension",
509 | "MatchOn": "\\.sqlite$",
510 | "Description": "SQLite database file",
511 | "Comment": ""
512 | },
513 | {
514 | "Part": "path",
515 | "MatchOn": "\\.?ssh/config$",
516 | "Description": "SSH configuration file",
517 | "Comment": ""
518 | },
519 | {
520 | "Part": "filename",
521 | "MatchOn": "Favorites\\.plist$",
522 | "Description": "Sequel Pro MySQL database manager bookmark file",
523 | "Comment": ""
524 | },
525 | {
526 | "Part": "filename",
527 | "MatchOn": "`^\\.?(bash_|zsh_)?aliases$",
528 | "Description": "Shell command alias configuration file",
529 | "Comment": "Shell configuration files can contain passwords, API keys, hostnames and other goodies"
530 | },
531 | {
532 | "Part": "filename",
533 | "MatchOn": "^\\.?(bash_|zsh_|sh_|z)?history$",
534 | "Description": "Shell command history file",
535 | "Comment": ""
536 | },
537 | {
538 | "Part": "filename",
539 | "MatchOn": "^\\.?(bash|zsh|csh)rc$",
540 | "Description": "Shell configuration file",
541 | "Comment": "Shell configuration files can contain passwords, API keys, hostnames and other goodies"
542 | },
543 | {
544 | "Part": "filename",
545 | "MatchOn": "\\.exports$",
546 | "Description": "Shell configuration file",
547 | "Comment": "Shell configuration files can contain passwords, API keys, hostnames and other goodies"
548 | },
549 | {
550 | "Part": "filename",
551 | "MatchOn": "\\.functions$",
552 | "Description": "Shell configuration file",
553 | "Comment": "Shell configuration files can contain passwords, API keys, hostnames and other goodies"
554 | },
555 | {
556 | "Part": "filename",
557 | "MatchOn": "\\.extra$",
558 | "Description": "Shell configuration file",
559 | "Comment": "Shell configuration files can contain passwords, API keys, hostnames and other goodies"
560 | },
561 | {
562 | "Part": "filename",
563 | "MatchOn": "^\\.?(bash_|zsh_)?profile$",
564 | "Description": "Shell profile configuration file",
565 | "Comment": "Shell configuration files can contain passwords, API keys, hostnames and other goodies"
566 | },
567 | {
568 | "Part": "filename",
569 | "MatchOn": "^\\.?trc$",
570 | "Description": "T command-line Twitter client configuration file",
571 | "Comment": ""
572 | },
573 | {
574 | "Part": "filename",
575 | "MatchOn": "terraform\\.tfvars$",
576 | "Description": "Terraform variable config file",
577 | "Comment": "Can contain credentials for terraform providers"
578 | },
579 | {
580 | "Part": "filename",
581 | "MatchOn": "^\\.?tugboat$",
582 | "Description": "Tugboat DigitalOcean management tool configuration",
583 | "Comment": ""
584 | },
585 | {
586 | "Part": "extension",
587 | "MatchOn": "\\.tblk$",
588 | "Description": "Tunnelblick VPN configuration file",
589 | "Comment": ""
590 | },
591 | {
592 | "Part": "filename",
593 | "MatchOn": "ventrilo_srv\\.ini",
594 | "Description": "Ventrilo server configuration file",
595 | "Comment": "Can contain passwords"
596 | },
597 | {
598 | "Part": "filename",
599 | "MatchOn": "^\\.?gitrobrc$",
600 | "Description": "Well, this is awkward... Gitrob configuration file",
601 | "Comment": ""
602 | },
603 | {
604 | "Part": "extension",
605 | "MatchOn": "\\.fve$",
606 | "Description": "Windows BitLocker full volume encrypted data file",
607 | "Comment": ""
608 | },
609 | {
610 | "Part": "filename",
611 | "MatchOn": "proftpdpasswd$",
612 | "Description": "cPanel backup ProFTPd credentials file",
613 | "Comment": "Contains usernames and password hashes for FTP accounts"
614 | },
615 | {
616 | "Part": "filename",
617 | "MatchOn": "^\\.?git-credentials$",
618 | "Description": "git-credential-store helper credentials file",
619 | "Comment": ""
620 | }
621 | ]
622 | }
--------------------------------------------------------------------------------
/github/apiClient.go:
--------------------------------------------------------------------------------
1 | package github
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/google/go-github/github"
7 | "golang.org/x/oauth2"
8 | "gitrob/common"
9 | )
10 |
11 | type Client struct {
12 | apiClient *github.Client
13 | }
14 |
15 | func (c Client) NewClient(token string) (apiClient Client) {
16 | ctx := context.Background()
17 | ts := oauth2.StaticTokenSource(
18 | &oauth2.Token{AccessToken: token},
19 | )
20 | tc := oauth2.NewClient(ctx, ts)
21 | c.apiClient = github.NewClient(tc)
22 | c.apiClient.UserAgent = common.UserAgent
23 | return c
24 | }
25 |
26 | func (c Client) GetUserOrOrganization(login string) (*common.Owner, error) {
27 | ctx := context.Background()
28 | user, _, err := c.apiClient.Users.Get(ctx, login)
29 | if err != nil {
30 | return nil, err
31 | }
32 | return &common.Owner{
33 | Login: user.Login,
34 | ID: user.ID,
35 | Type: user.Type,
36 | Name: user.Name,
37 | AvatarURL: user.AvatarURL,
38 | URL: user.HTMLURL,
39 | Company: user.Company,
40 | Blog: user.Blog,
41 | Location: user.Location,
42 | Email: user.Email,
43 | Bio: user.Bio,
44 | }, nil
45 | }
46 |
47 | func (c Client) GetRepositoriesFromOwner(target common.Owner) ([]*common.Repository, error) {
48 | var allRepos []*common.Repository
49 | ctx := context.Background()
50 | opt := &github.RepositoryListOptions{
51 | Type: "sources",
52 | }
53 |
54 | for {
55 | repos, resp, err := c.apiClient.Repositories.List(ctx, *target.Login, opt)
56 | if err != nil {
57 | return allRepos, err
58 | }
59 | for _, repo := range repos {
60 | if !*repo.Fork {
61 | r := common.Repository{
62 | Owner: repo.Owner.Login,
63 | ID: repo.ID,
64 | Name: repo.Name,
65 | FullName: repo.FullName,
66 | CloneURL: repo.CloneURL,
67 | URL: repo.HTMLURL,
68 | DefaultBranch: repo.DefaultBranch,
69 | Description: repo.Description,
70 | Homepage: repo.Homepage,
71 | }
72 | allRepos = append(allRepos, &r)
73 | }
74 | }
75 | if resp.NextPage == 0 {
76 | break
77 | }
78 | opt.Page = resp.NextPage
79 | }
80 |
81 | return allRepos, nil
82 | }
83 |
84 | func (c Client) GetRepositoriesFromOrganization(target common.Owner) ([]*common.Repository, error) {
85 | var allRepos []*common.Repository
86 | ctx := context.Background()
87 | opt := &github.RepositoryListByOrgOptions{
88 | Type: "sources",
89 | }
90 |
91 | for {
92 | repos, resp, err := c.apiClient.Repositories.ListByOrg(ctx, *target.Login, opt)
93 | if err != nil {
94 | return allRepos, err
95 | }
96 | for _, repo := range repos {
97 | if !*repo.Fork {
98 | r := common.Repository{
99 | Owner: repo.Owner.Login,
100 | ID: repo.ID,
101 | Name: repo.Name,
102 | FullName: repo.FullName,
103 | CloneURL: repo.SSHURL,
104 | URL: repo.HTMLURL,
105 | DefaultBranch: repo.DefaultBranch,
106 | Description: repo.Description,
107 | Homepage: repo.Homepage,
108 | }
109 | allRepos = append(allRepos, &r)
110 | }
111 | }
112 | if resp.NextPage == 0 {
113 | break
114 | }
115 | opt.Page = resp.NextPage
116 | }
117 |
118 | return allRepos, nil
119 | }
120 |
121 | func (c Client) GetOrganizationMembers(target common.Owner) ([]*common.Owner, error) {
122 | var allMembers []*common.Owner
123 | ctx := context.Background()
124 | opt := &github.ListMembersOptions{}
125 | for {
126 | members, resp, err := c.apiClient.Organizations.ListMembers(ctx, *target.Login, opt)
127 | if err != nil {
128 | return allMembers, err
129 | }
130 | for _, member := range members {
131 | allMembers = append(allMembers, &common.Owner{Login: member.Login, ID: member.ID, Type: member.Type})
132 | }
133 | if resp.NextPage == 0 {
134 | break
135 | }
136 | opt.Page = resp.NextPage
137 | }
138 | return allMembers, nil
139 | }
140 |
--------------------------------------------------------------------------------
/github/git.go:
--------------------------------------------------------------------------------
1 | package github
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "gopkg.in/src-d/go-git.v4/storage/memory"
7 | "io/ioutil"
8 |
9 | "gitrob/common"
10 |
11 | "gopkg.in/src-d/go-git.v4"
12 | "gopkg.in/src-d/go-git.v4/plumbing"
13 | "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
14 | )
15 |
16 | func CloneRepository(cloneConfig *common.CloneConfiguration) (*git.Repository, string, error) {
17 |
18 | cloneOptions := &git.CloneOptions{
19 | URL: *cloneConfig.Url,
20 | Depth: *cloneConfig.Depth,
21 | ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", *cloneConfig.Branch)),
22 | SingleBranch: true,
23 | Tags: git.NoTags,
24 | Auth: &http.BasicAuth{
25 | Username: *cloneConfig.Username,
26 | Password: *cloneConfig.Token,
27 | },
28 | }
29 |
30 | cloneOptions.URL = strings.Replace(cloneOptions.URL, "git@github.com:", "https://github.com/", -1)
31 |
32 |
33 | var repository *git.Repository
34 | var err error
35 | var dir string
36 | if !*cloneConfig.InMemClone {
37 | dir, err := ioutil.TempDir("", "gitrob")
38 | if err != nil {
39 | return nil, "", err
40 | }
41 | repository, err = git.PlainClone(dir, false, cloneOptions)
42 | } else {
43 | repository, err = git.Clone(memory.NewStorage(), nil, cloneOptions)
44 | }
45 | if err != nil {
46 | return nil, dir, err
47 | }
48 | return repository, dir, nil
49 | }
50 |
--------------------------------------------------------------------------------
/gitlab/apiClient.go:
--------------------------------------------------------------------------------
1 | package gitlab
2 |
3 | import (
4 | "fmt"
5 | "github.com/xanzy/go-gitlab"
6 | "gitrob/common"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | type Client struct {
12 | apiClient *gitlab.Client
13 | logger *common.Logger
14 | }
15 |
16 | func (c Client) NewClient(token string, logger *common.Logger) (Client, error) {
17 | var err error
18 | c.apiClient, err = gitlab.NewClient(token)
19 | if err != nil {
20 | return Client{}, err
21 | }
22 | c.apiClient.UserAgent = common.UserAgent
23 | c.logger = logger
24 | return c, nil
25 | }
26 |
27 | func (c Client) GetUserOrOrganization(login string) (*common.Owner, error) {
28 | emptyString := gitlab.String("")
29 | org, orgErr := c.getOrganization(login)
30 | if orgErr != nil {
31 | user, userErr := c.getUser(login)
32 | if userErr != nil {
33 | return nil, userErr
34 | }
35 | id := int64(user.ID)
36 | return &common.Owner{
37 | Login: gitlab.String(user.Username),
38 | ID: &id,
39 | Type: gitlab.String(common.TargetTypeUser),
40 | Name: gitlab.String(user.Name),
41 | AvatarURL: gitlab.String(user.AvatarURL),
42 | URL: gitlab.String(user.WebsiteURL),
43 | Company: gitlab.String(user.Organization),
44 | Blog: emptyString,
45 | Location: emptyString,
46 | Email: gitlab.String(user.PublicEmail),
47 | Bio: gitlab.String(user.Bio),
48 | }, nil
49 | } else {
50 | id := int64(org.ID)
51 | return &common.Owner{
52 | Login: gitlab.String(org.Name),
53 | ID: &id,
54 | Type: gitlab.String(common.TargetTypeOrganization),
55 | Name: gitlab.String(org.Name),
56 | AvatarURL: gitlab.String(org.AvatarURL),
57 | URL: gitlab.String(org.WebURL),
58 | Company: gitlab.String(org.FullName),
59 | Blog: emptyString,
60 | Location: emptyString,
61 | Email: emptyString,
62 | Bio: gitlab.String(org.Description),
63 | }, nil
64 | }
65 | }
66 |
67 | func (c Client) GetOrganizationMembers(target common.Owner) ([]*common.Owner, error) {
68 | var allMembers []*common.Owner
69 | opt := &gitlab.ListGroupMembersOptions{}
70 | sID := strconv.FormatInt(*target.ID, 10) //safely downcast an int64 to an int
71 | for {
72 | members, resp, err := c.apiClient.Groups.ListAllGroupMembers(sID, opt)
73 | if err != nil {
74 | return nil, err
75 | }
76 | for _, member := range members {
77 | id := int64(member.ID)
78 | allMembers = append(allMembers,
79 | &common.Owner{
80 | Login: gitlab.String(member.Username),
81 | ID: &id,
82 | Type: gitlab.String(common.TargetTypeUser)})
83 | }
84 | if resp.NextPage == 0 {
85 | break
86 | }
87 | opt.Page = resp.NextPage
88 | }
89 | return allMembers, nil
90 | }
91 |
92 | func (c Client) GetRepositoriesFromOwner(target common.Owner) ([]*common.Repository, error) {
93 | var allProjects []*common.Repository
94 | id := int(*target.ID)
95 | userProjects, err := c.getUserProjects(id)
96 | if err != nil {
97 | return nil, err
98 | }
99 | for _, project := range userProjects {
100 | allProjects = append(allProjects, project)
101 | }
102 | return allProjects, nil
103 | }
104 |
105 | func (c Client) GetRepositoriesFromOrganization(target common.Owner) ([]*common.Repository, error) {
106 | var allProjects []*common.Repository
107 | groupProjects, err := c.getGroupProjects(target)
108 | if err != nil {
109 | return nil, err
110 | }
111 | for _, project := range groupProjects {
112 | allProjects = append(allProjects, project)
113 | }
114 | return allProjects, nil
115 | }
116 |
117 | func (c Client) getUser(login string) (*gitlab.User, error) {
118 | users, _, err := c.apiClient.Users.ListUsers(&gitlab.ListUsersOptions{Username: gitlab.String(login)})
119 | if err != nil {
120 | return nil, err
121 | }
122 | if len(users) == 0 {
123 | return nil, fmt.Errorf("No GitLab %s or %s %s was found. If you are targeting a GitLab group, be sure to"+
124 | " use an ID in place of a name.",
125 | strings.ToLower(common.TargetTypeUser),
126 | strings.ToLower(common.TargetTypeOrganization),
127 | login)
128 | }
129 | return users[0], err
130 | }
131 |
132 | func (c Client) getOrganization(login string) (*gitlab.Group, error) {
133 | id, err := strconv.Atoi(login)
134 | if err != nil {
135 | return nil, err
136 | }
137 | org, _, err := c.apiClient.Groups.GetGroup(id)
138 | if err != nil {
139 | return nil, err
140 | }
141 | return org, err
142 | }
143 |
144 | func (c Client) getUserProjects(id int) ([]*common.Repository, error) {
145 | var allUserProjects []*common.Repository
146 | listUserProjectsOps := &gitlab.ListProjectsOptions{}
147 | for {
148 | projects, response, err := c.apiClient.Projects.ListUserProjects(id, listUserProjectsOps)
149 | if err != nil {
150 | return nil, err
151 | }
152 | for _, project := range projects {
153 | //don't capture forks
154 | if project.ForkedFromProject == nil {
155 | id := int64(project.ID)
156 | p := common.Repository{
157 | Owner: gitlab.String(project.Owner.Username),
158 | ID: &id,
159 | Name: gitlab.String(project.Name),
160 | FullName: gitlab.String(project.NameWithNamespace),
161 | CloneURL: gitlab.String(project.HTTPURLToRepo),
162 | URL: gitlab.String(project.WebURL),
163 | DefaultBranch: gitlab.String(project.DefaultBranch),
164 | Description: gitlab.String(project.Description),
165 | Homepage: gitlab.String(project.WebURL),
166 | }
167 | allUserProjects = append(allUserProjects, &p)
168 | }
169 | }
170 | if response.NextPage == 0 {
171 | break
172 | }
173 | listUserProjectsOps.Page = response.NextPage
174 | }
175 | return allUserProjects, nil
176 | }
177 |
178 | func (c Client) getGroupProjects(target common.Owner) ([]*common.Repository, error) {
179 | var allGroupProjects []*common.Repository
180 | includeSubGroups := true
181 | listGroupProjectsOps := &gitlab.ListGroupProjectsOptions{IncludeSubgroups: &includeSubGroups}
182 | id := strconv.FormatInt(*target.ID, 10)
183 | for {
184 | projects, response, err := c.apiClient.Groups.ListGroupProjects(id, listGroupProjectsOps)
185 | if err != nil {
186 | return nil, err
187 | }
188 | for _, project := range projects {
189 | //don't capture forks
190 | if project.ForkedFromProject == nil {
191 | id := int64(project.ID)
192 | p := common.Repository{
193 | Owner: gitlab.String(project.Namespace.FullPath),
194 | ID: &id,
195 | Name: gitlab.String(project.Name),
196 | FullName: gitlab.String(project.NameWithNamespace),
197 | CloneURL: gitlab.String(project.HTTPURLToRepo),
198 | URL: gitlab.String(project.WebURL),
199 | DefaultBranch: gitlab.String(project.DefaultBranch),
200 | Description: gitlab.String(project.Description),
201 | Homepage: gitlab.String(project.WebURL),
202 | }
203 | allGroupProjects = append(allGroupProjects, &p)
204 | }
205 | }
206 | if response.NextPage == 0 {
207 | break
208 | }
209 | listGroupProjectsOps.Page = response.NextPage
210 | }
211 | return allGroupProjects, nil
212 | }
213 |
--------------------------------------------------------------------------------
/gitlab/git.go:
--------------------------------------------------------------------------------
1 | package gitlab
2 |
3 | import (
4 | "fmt"
5 | "gopkg.in/src-d/go-git.v4"
6 | "gopkg.in/src-d/go-git.v4/plumbing"
7 | "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
8 | "gopkg.in/src-d/go-git.v4/storage/memory"
9 | "io/ioutil"
10 | "gitrob/common"
11 | )
12 |
13 | func CloneRepository(cloneConfig *common.CloneConfiguration) (*git.Repository, string, error) {
14 |
15 | cloneOptions := &git.CloneOptions{
16 | URL: *cloneConfig.Url,
17 | Depth: *cloneConfig.Depth,
18 | ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", *cloneConfig.Branch)),
19 | SingleBranch: true,
20 | Tags: git.NoTags,
21 | Auth: &http.BasicAuth{
22 | Username: *cloneConfig.Username,
23 | Password: *cloneConfig.Token,
24 | },
25 | }
26 |
27 | var repository *git.Repository
28 | var err error
29 | var dir string
30 | if !*cloneConfig.InMemClone {
31 | dir, err = ioutil.TempDir("", "gitrob")
32 | if err != nil {
33 | return nil, "", err
34 | }
35 | repository, err = git.PlainClone(dir, false, cloneOptions)
36 | } else {
37 | repository, err = git.Clone(memory.NewStorage(), nil, cloneOptions)
38 | }
39 | if err != nil {
40 | return nil, dir, err
41 | }
42 | return repository, dir, nil
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module gitrob
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/elazarl/go-bindata-assetfs v1.0.0
7 | github.com/fatih/color v1.9.0
8 | github.com/gin-contrib/secure v0.0.1
9 | github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
10 | github.com/gin-gonic/gin v1.6.3
11 | github.com/go-playground/validator/v10 v10.3.0 // indirect
12 | github.com/golang/protobuf v1.4.2 // indirect
13 | github.com/google/go-github v17.0.0+incompatible
14 | github.com/hashicorp/go-retryablehttp v0.6.6 // indirect
15 | github.com/mattn/go-colorable v0.1.6 // indirect
16 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
17 | github.com/modern-go/reflect2 v1.0.1 // indirect
18 | github.com/sergi/go-diff v1.1.0 // indirect
19 | github.com/xanzy/go-gitlab v0.32.1
20 | golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
21 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
22 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
23 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
24 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
25 | google.golang.org/appengine v1.6.6 // indirect
26 | google.golang.org/protobuf v1.24.0 // indirect
27 | gopkg.in/src-d/go-git.v4 v4.13.1
28 | gopkg.in/yaml.v2 v2.3.0 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "time"
7 |
8 | "gitrob/common"
9 | "gitrob/core"
10 | )
11 |
12 | var (
13 | sess *core.Session
14 | err error
15 | )
16 |
17 | func main() {
18 | if sess, err = core.NewSession(); err != nil {
19 | fmt.Println(err)
20 | os.Exit(1)
21 | }
22 |
23 | sess.Out.Info("%s\n\n", common.ASCIIBanner)
24 | sess.Out.Important("%s v%s started at %s\n", common.Name, common.Version, sess.Stats.StartedAt.Format(time.RFC3339))
25 | sess.Out.Important("Loaded %d file signatures and %d content signatures.\n", len(sess.Signatures.FileSignatures), len(sess.Signatures.ContentSignatures))
26 | sess.Out.Important("Web interface available at http://%s:%d\n", *sess.Options.BindAddress, *sess.Options.Port)
27 |
28 | if sess.Stats.Status == "finished" {
29 | sess.Out.Important("Loaded session file: %s\n", *sess.Options.Load)
30 | } else {
31 | if len(sess.Options.Logins) == 0 {
32 | host := func() string {
33 | if sess.IsGithubSession {
34 | return "Github organization"
35 | } else {
36 | return "GitLab group"
37 | }
38 | }()
39 | sess.Out.Fatal("Please provide at least one %s or user\n", host)
40 | }
41 |
42 | core.GatherTargets(sess)
43 | core.GatherRepositories(sess)
44 | core.AnalyzeRepositories(sess)
45 | sess.Finish()
46 |
47 | if *sess.Options.Save != "" {
48 | err := sess.SaveToFile(*sess.Options.Save)
49 | if err != nil {
50 | sess.Out.Error("Error saving session to %s: %s\n", *sess.Options.Save, err)
51 | }
52 | sess.Out.Important("Saved session to: %s\n\n", *sess.Options.Save)
53 | }
54 | }
55 |
56 | core.PrintSessionStats(sess)
57 | if !sess.IsGithubSession {
58 | sess.Out.Error("%s", common.GitLabTanuki)
59 | }
60 | if !*sess.Options.ExitOnFinish {
61 | sess.Out.Important("Press Ctrl+C to stop web server and exit.\n\n")
62 | select {}
63 | } else {
64 | sess.Out.Important("Scan complete. Exiting.\n")
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/matching/contentsignatures.go:
--------------------------------------------------------------------------------
1 | package matching
2 |
3 | import "regexp"
4 |
5 | type ContentSignature struct {
6 | MatchOn string
7 | Description string
8 | Comment string
9 | }
10 |
11 | func (c ContentSignature) Match(target MatchTarget) (bool, error) {
12 | return regexp.MatchString(c.MatchOn, target.Content)
13 | }
14 |
15 | func (c ContentSignature) GetDescription() string {
16 | return c.Description
17 | }
18 |
19 | func (c ContentSignature) GetComment() string {
20 | return c.Comment
21 | }
22 |
--------------------------------------------------------------------------------
/matching/filesignatures.go:
--------------------------------------------------------------------------------
1 | package matching
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "regexp"
7 | )
8 |
9 | type FileSignatureType struct {
10 | Extension string
11 | Filename string
12 | Path string
13 | }
14 |
15 | var fileSignatureTypes = FileSignatureType{
16 | Extension: "extension",
17 | Filename: "filename",
18 | Path: "path",
19 | }
20 |
21 | type FileSignature struct {
22 | Part string
23 | MatchOn string
24 | Description string
25 | Comment string
26 | }
27 |
28 | func (f FileSignature) Match(target MatchTarget) (bool, error) {
29 | var haystack *string
30 | switch f.Part {
31 | case fileSignatureTypes.Path:
32 | haystack = &target.Path
33 | case fileSignatureTypes.Filename:
34 | haystack = &target.Filename
35 | case fileSignatureTypes.Extension:
36 | haystack = &target.Extension
37 | default:
38 | return false , errors.New(fmt.Sprintf("Unrecognized 'Part' parameter: %s\n", f.Part))
39 | }
40 | return regexp.MatchString(f.MatchOn, *haystack)
41 | }
42 |
43 | func (f FileSignature) GetDescription() string {
44 | return f.Description
45 | }
46 |
47 | func (f FileSignature) GetComment() string {
48 | return f.Comment
49 | }
50 |
--------------------------------------------------------------------------------
/matching/findings.go:
--------------------------------------------------------------------------------
1 | package matching
2 |
3 | import (
4 | "crypto/sha1"
5 | "fmt"
6 | "io"
7 | "gitrob/common"
8 | )
9 |
10 | type Finding struct {
11 | Id string
12 | FilePath string
13 | Action string
14 | FileSignatureDescription string
15 | FileSignatureComment string
16 | ContentSignatureDescription string
17 | ContentSignatureComment string
18 | RepositoryOwner string
19 | RepositoryName string
20 | CommitHash string
21 | CommitMessage string
22 | CommitAuthor string
23 | FileUrl string
24 | CommitUrl string
25 | RepositoryUrl string
26 | CloneUrl string
27 | }
28 |
29 | func (f *Finding) setupUrls(isGithubSession bool) {
30 | if isGithubSession {
31 | f.RepositoryUrl = fmt.Sprintf("https://github.com/%s/%s", f.RepositoryOwner, f.RepositoryName)
32 | f.FileUrl = fmt.Sprintf("%s/blob/%s/%s", f.RepositoryUrl, f.CommitHash, f.FilePath)
33 | f.CommitUrl = fmt.Sprintf("%s/commit/%s", f.RepositoryUrl, f.CommitHash)
34 | } else {
35 | results := common.CleanUrlSpaces(f.RepositoryOwner, f.RepositoryName)
36 | f.RepositoryUrl = fmt.Sprintf("https://gitlab.com/%s/%s", results[0], results[1])
37 | f.FileUrl = fmt.Sprintf("%s/blob/%s/%s", f.RepositoryUrl, f.CommitHash, f.FilePath)
38 | f.CommitUrl = fmt.Sprintf("%s/commit/%s", f.RepositoryUrl, f.CommitHash)
39 | }
40 | }
41 |
42 | func (f *Finding) generateID() {
43 | h := sha1.New()
44 | io.WriteString(h, f.FilePath)
45 | io.WriteString(h, f.Action)
46 | io.WriteString(h, f.RepositoryOwner)
47 | io.WriteString(h, f.RepositoryName)
48 | io.WriteString(h, f.CommitHash)
49 | io.WriteString(h, f.CommitMessage)
50 | io.WriteString(h, f.CommitAuthor)
51 | f.Id = fmt.Sprintf("%x", h.Sum(nil))
52 | }
53 |
54 | func (f *Finding) Initialize(isGithubSession bool) {
55 | f.setupUrls(isGithubSession)
56 | f.generateID()
57 | }
58 |
--------------------------------------------------------------------------------
/matching/matchfile.go:
--------------------------------------------------------------------------------
1 | package matching
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 | )
7 |
8 | type MatchTarget struct {
9 | Path string
10 | Filename string
11 | Extension string
12 | Content string
13 | }
14 |
15 | var skippableExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".psd", ".xcf"}
16 | var skippablePathIndicators = []string{"node_modules/", "vendor/bundle", "vendor/cache"}
17 |
18 | func (f *MatchTarget) IsSkippable() bool {
19 | ext := strings.ToLower(f.Extension)
20 | path := strings.ToLower(f.Path)
21 | for _, skippableExt := range skippableExtensions {
22 | if ext == skippableExt {
23 | return true
24 | }
25 | }
26 | for _, skippablePathIndicator := range skippablePathIndicators {
27 | if strings.Contains(path, skippablePathIndicator) {
28 | return true
29 | }
30 | }
31 | return false
32 | }
33 |
34 | func NewMatchTarget(path string) MatchTarget {
35 | _, filename := filepath.Split(path)
36 | extension := filepath.Ext(path)
37 | return MatchTarget{
38 | Path: path,
39 | Filename: filename,
40 | Extension: extension,
41 | Content: "",
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/matching/signatures.go:
--------------------------------------------------------------------------------
1 | package matching
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "gitrob/common"
9 | )
10 |
11 | type Signatures struct {
12 | FileSignatures []FileSignature
13 | ContentSignatures []ContentSignature
14 | }
15 |
16 | func (s *Signatures) loadSignatures(path string) error {
17 | if !common.FileExists(path) {
18 | return errors.New(fmt.Sprintf("Missing signature file: %s.\n", path))
19 | }
20 | data, readError := ioutil.ReadFile(path)
21 | if readError != nil {
22 | return readError
23 | }
24 | if unmarshalError := json.Unmarshal(data, &s); unmarshalError != nil {
25 | return unmarshalError
26 | }
27 | return nil
28 | }
29 |
30 | func (s *Signatures) Load(mode int) error {
31 | var e error
32 | if mode != 3 {
33 | e = s.loadSignatures("./filesignatures.json")
34 | if e != nil {
35 | return e
36 | }
37 | }
38 | if mode != 1 {
39 | //source: https://github.com/dxa4481/truffleHogRegexes/blob/master/truffleHogRegexes/regexes.json
40 | e = s.loadSignatures("./contentsignatures.json")
41 | if e != nil {
42 | return e
43 | }
44 | }
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CURRENT_VERSION=$(cat common/banner.go | grep Version | cut -d '"' -f 2)
4 | TO_UPDATE=(
5 | common/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[@]}"; do
17 | echo "[*] Patching $file ..."
18 | sed -i "s/$CURRENT_VERSION/$NEW_VERSION/g" $file
19 | git add $file
20 | done
21 |
22 | git add CHANGELOG.md
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 |
32 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CURRENT_VERSION=$(cat common/banner.go | grep Version | cut -d '"' -f 2)
4 | TO_UPDATE=(
5 | common/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[@]}"; do
17 | echo "[*] Patching $file ..."
18 | sed -i "s/$CURRENT_VERSION/$NEW_VERSION/g" $file
19 | git add $file
20 | done
21 |
22 | git add CHANGELOG.md
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/GitLab-Red-Team/gitrob/8d7799af2fecf32a9d7dcb313790a5fa3935bf58/static/fonts/open-iconic.eot
--------------------------------------------------------------------------------
/static/fonts/open-iconic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitLab-Red-Team/gitrob/8d7799af2fecf32a9d7dcb313790a5fa3935bf58/static/fonts/open-iconic.otf
--------------------------------------------------------------------------------
/static/fonts/open-iconic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitLab-Red-Team/gitrob/8d7799af2fecf32a9d7dcb313790a5fa3935bf58/static/fonts/open-iconic.ttf
--------------------------------------------------------------------------------
/static/fonts/open-iconic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitLab-Red-Team/gitrob/8d7799af2fecf32a9d7dcb313790a5fa3935bf58/static/fonts/open-iconic.woff
--------------------------------------------------------------------------------
/static/images/gopher_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitLab-Red-Team/gitrob/8d7799af2fecf32a9d7dcb313790a5fa3935bf58/static/images/gopher_full.png
--------------------------------------------------------------------------------
/static/images/gopher_head.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitLab-Red-Team/gitrob/8d7799af2fecf32a9d7dcb313790a5fa3935bf58/static/images/gopher_head.png
--------------------------------------------------------------------------------
/static/images/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitLab-Red-Team/gitrob/8d7799af2fecf32a9d7dcb313790a5fa3935bf58/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 |
20 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Initializing...
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
0
45 |
Findings
46 |
47 |
48 |
49 |
57 |
65 |
66 |
67 |
68 |
0
69 |
Repositories
70 |
71 |
72 |
73 |
81 |
82 |
83 |
84 |
00:00:00
85 |
Duration
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
114 |
115 |
116 |
123 |
124 |
142 |
143 |
202 |
203 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
--------------------------------------------------------------------------------
/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 | getHostName: function () {
339 | if (this.model.get("CommitUrl").indexOf("github") !== -1) return "Github";
340 | return "GitLab";
341 | },
342 | truncatedCommitMessage: function () {
343 | var message = this.model.trimmedCommitMessage();
344 | if (message.length <= 150) {
345 | return _.escape(message);
346 | }
347 | return _.escape(message.substr(0, 150)) + "…";
348 | },
349 | isTestRelated: function () {
350 | return this.model.isTestRelated();
351 | },
352 | isBinary: function (data) {
353 | return /[\x00-\x08\x0E-\x1F]/.test(data);
354 | },
355 | highlightInterestingStrings: function (haystack) {
356 | this.interestingStringPatterns.forEach(function (pattern) {
357 | haystack = haystack.replace(pattern, "$1 ");
358 | });
359 | return haystack;
360 | },
361 | fetchFileContents: function () {
362 | if (this.model.get("Action") == "Delete") {
363 | var content = "View commit on %s to see contents of deleted files.
";
364 | var host = this.getHostName();
365 | var fadeInFunc = function () {
366 | $("#modal_file_contents_container").html(content.replace("%s", host)).fadeIn("fast");
367 | $('.modal-content #view-file, #finding_view_raw, #finding_view_hexdump').addClass('disabled');
368 | };
369 | $("#modal_file_spinner_container").fadeOut("fast", fadeInFunc());
370 | return;
371 | }
372 | var context = this;
373 | this.model.fileContents(function (data) {
374 | var worker = new Worker("/javascripts/highlight_worker.js");
375 | worker.onmessage = function (event) {
376 | $("#modal_file_spinner_container").fadeOut("fast", _.bind(function () {
377 | var content = this.highlightInterestingStrings(event.data);
378 | $("#modal_file_contents").html(content);
379 | new Hexdump(data, {
380 | container: "modal_file_hexdump",
381 | base: "hex",
382 | width: 8,
383 | byteGrouping: 1,
384 | html: true,
385 | ascii: true,
386 | lineNumbers: true,
387 | style: {
388 | lineNumberLeft: '',
389 | lineNumberRight: ':',
390 | stringLeft: '|',
391 | stringRight: '|',
392 | hexLeft: '',
393 | hexRight: '',
394 | hexNull: '.',
395 | nonPrintable: '.',
396 | stringNull: '.',
397 | }
398 | });
399 | $("#modal_file_contents_container").fadeIn("fast");
400 | if (this.isBinary(data)) {
401 | this.showHexDumpContents();
402 | } else {
403 | this.showRawContents();
404 | }
405 | }, context));
406 | };
407 | worker.postMessage(data);
408 | }, function () {
409 | $("#modal_file_spinner_container").fadeOut("fast", function () {
410 | $("#modal_file_contents_container").html("File size too large to display inline. View file on GitHub.
").fadeIn("fast");
411 | });
412 | });
413 | }
414 | });
415 |
--------------------------------------------------------------------------------
/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'}
--------------------------------------------------------------------------------