gitrob 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +2 -0
- data/bin/gitrob +258 -0
- data/gitrob.gemspec +36 -0
- data/lib/gitrob.rb +116 -0
- data/lib/gitrob/github/blob.rb +41 -0
- data/lib/gitrob/github/http_client.rb +127 -0
- data/lib/gitrob/github/organization.rb +93 -0
- data/lib/gitrob/github/repository.rb +72 -0
- data/lib/gitrob/github/user.rb +78 -0
- data/lib/gitrob/observers/sensitive_files.rb +82 -0
- data/lib/gitrob/progressbar.rb +52 -0
- data/lib/gitrob/util.rb +11 -0
- data/lib/gitrob/version.rb +3 -0
- data/lib/gitrob/webapp.rb +76 -0
- data/models/blob.rb +35 -0
- data/models/finding.rb +14 -0
- data/models/organization.rb +32 -0
- data/models/repo.rb +22 -0
- data/models/user.rb +28 -0
- data/patterns.json +303 -0
- data/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/public/javascripts/bootstrap.min.js +7 -0
- data/public/javascripts/gitrob.js +75 -0
- data/public/javascripts/jquery-2.1.1.min.js +4 -0
- data/public/javascripts/lang-apollo.js +2 -0
- data/public/javascripts/lang-basic.js +3 -0
- data/public/javascripts/lang-clj.js +18 -0
- data/public/javascripts/lang-css.js +2 -0
- data/public/javascripts/lang-dart.js +3 -0
- data/public/javascripts/lang-erlang.js +2 -0
- data/public/javascripts/lang-go.js +1 -0
- data/public/javascripts/lang-hs.js +2 -0
- data/public/javascripts/lang-lisp.js +3 -0
- data/public/javascripts/lang-llvm.js +1 -0
- data/public/javascripts/lang-lua.js +2 -0
- data/public/javascripts/lang-matlab.js +6 -0
- data/public/javascripts/lang-ml.js +2 -0
- data/public/javascripts/lang-mumps.js +2 -0
- data/public/javascripts/lang-n.js +4 -0
- data/public/javascripts/lang-pascal.js +3 -0
- data/public/javascripts/lang-proto.js +1 -0
- data/public/javascripts/lang-r.js +2 -0
- data/public/javascripts/lang-rd.js +1 -0
- data/public/javascripts/lang-scala.js +2 -0
- data/public/javascripts/lang-sql.js +2 -0
- data/public/javascripts/lang-tcl.js +3 -0
- data/public/javascripts/lang-tex.js +1 -0
- data/public/javascripts/lang-vb.js +2 -0
- data/public/javascripts/lang-vhdl.js +3 -0
- data/public/javascripts/lang-wiki.js +2 -0
- data/public/javascripts/lang-xq.js +3 -0
- data/public/javascripts/lang-yaml.js +2 -0
- data/public/javascripts/prettify.js +30 -0
- data/public/javascripts/run_prettify.js +34 -0
- data/public/stylesheets/bootstrap.min.css +7 -0
- data/public/stylesheets/bootstrap.min.css.vanilla +5 -0
- data/public/stylesheets/gitrob.css +88 -0
- data/public/stylesheets/prettify.css +51 -0
- data/spec/lib/gitrob/observers/sensitive_files_spec.rb +558 -0
- data/spec/spec_helper.rb +127 -0
- data/views/blob.erb +22 -0
- data/views/index.erb +32 -0
- data/views/layout.erb +30 -0
- data/views/organization.erb +126 -0
- data/views/repository.erb +51 -0
- data/views/user.erb +51 -0
- metadata +317 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module Gitrob
|
2
|
+
module Github
|
3
|
+
class Blob
|
4
|
+
attr_reader :path, :size, :repository
|
5
|
+
|
6
|
+
def initialize(path, size, repository)
|
7
|
+
@path, @size, @repository = path, size, repository
|
8
|
+
end
|
9
|
+
|
10
|
+
def extension
|
11
|
+
File.extname(path)[1..-1]
|
12
|
+
end
|
13
|
+
|
14
|
+
def filename
|
15
|
+
File.basename(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def dirname
|
19
|
+
File.dirname(path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def url
|
23
|
+
"https://github.com/#{URI.escape(repository.owner)}/#{URI.escape(repository.name)}/blob/master/#{URI.escape(path)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_model(organization, repository)
|
27
|
+
repository.blobs.new(
|
28
|
+
:path => self.path,
|
29
|
+
:filename => self.filename,
|
30
|
+
:extension => self.extension,
|
31
|
+
:size => self.size,
|
32
|
+
:organization => organization
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def save_to_database!(organization, repository)
|
37
|
+
self.to_model(organization, repository).tap { |m| m.save }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Gitrob
|
2
|
+
module Github
|
3
|
+
class HttpClient
|
4
|
+
include HTTParty
|
5
|
+
base_uri 'https://api.github.com'
|
6
|
+
|
7
|
+
class HttpError < StandardError; end
|
8
|
+
class ConnectionError < HttpError; end
|
9
|
+
|
10
|
+
class RequestError < HttpError
|
11
|
+
attr_reader :status, :body
|
12
|
+
def initialize(method, path, status, body, options)
|
13
|
+
@status = status
|
14
|
+
@body = body
|
15
|
+
super("#{method} to #{path} returned status #{status} - options: #{options.inspect}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ClientError < RequestError; end
|
20
|
+
class ServerError < RequestError; end
|
21
|
+
|
22
|
+
class UnhandledError < StandardError; end
|
23
|
+
|
24
|
+
class AccessTokenError < StandardError; end
|
25
|
+
class MissingAccessTokensError < AccessTokenError; end
|
26
|
+
class AccessTokensDepletedError < AccessTokenError; end
|
27
|
+
|
28
|
+
DEFAULT_TIMEOUT = 0.5 #seconds
|
29
|
+
DEFAULT_RETRIES = 3
|
30
|
+
|
31
|
+
Response = Struct.new(:status, :headers, :body)
|
32
|
+
|
33
|
+
RETRIABLE_EXCEPTIONS = [
|
34
|
+
ServerError,
|
35
|
+
AccessTokenError,
|
36
|
+
Timeout::Error,
|
37
|
+
Errno::ETIMEDOUT,
|
38
|
+
Errno::ECONNRESET,
|
39
|
+
Errno::ECONNREFUSED,
|
40
|
+
Errno::ENETUNREACH,
|
41
|
+
Errno::EHOSTUNREACH,
|
42
|
+
EOFError
|
43
|
+
]
|
44
|
+
|
45
|
+
def initialize(options)
|
46
|
+
@config = {
|
47
|
+
:timeout => DEFAULT_TIMEOUT,
|
48
|
+
:retries => DEFAULT_RETRIES
|
49
|
+
}.merge(options)
|
50
|
+
raise MissingAccessTokensErrors.new("No access tokens given") unless @config[:access_tokens]
|
51
|
+
default_timeout = @config[:timeout]
|
52
|
+
end
|
53
|
+
|
54
|
+
def do_get(path, params=nil, options={})
|
55
|
+
do_request(:get, path, {:query => params}.merge(options))
|
56
|
+
end
|
57
|
+
|
58
|
+
def do_post(path, params=nil, options={})
|
59
|
+
do_request(:post, path, {:query => params}.merge(options))
|
60
|
+
end
|
61
|
+
|
62
|
+
def do_put(path, params=nil, options={})
|
63
|
+
do_request(:put, path, {:query => params}.merge(options))
|
64
|
+
end
|
65
|
+
|
66
|
+
def do_delete(path, params=nil, options={})
|
67
|
+
do_request(:delete, path, {:query => params}.merge(options))
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def do_request(method, path, options)
|
73
|
+
with_retries do
|
74
|
+
access_token = get_access_token!
|
75
|
+
response = self.class.send(method, path, {
|
76
|
+
:headers => {
|
77
|
+
'Authorization' => "token #{access_token}",
|
78
|
+
'User-Agent' => "Gitrob v#{Gitrob::VERSION}"
|
79
|
+
}
|
80
|
+
}.merge(options))
|
81
|
+
handle_possible_error!(method, path, response, options, access_token)
|
82
|
+
Response.new(response.code, response.headers, response.body)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def with_retries(&block)
|
87
|
+
tries = @config[:retries]
|
88
|
+
yield
|
89
|
+
rescue *RETRIABLE_EXCEPTIONS => ex
|
90
|
+
if (tries -= 1) > 0
|
91
|
+
sleep 0.2
|
92
|
+
retry
|
93
|
+
else
|
94
|
+
raise ex
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_possible_error!(method, path, response, options, access_token)
|
99
|
+
if access_token_rate_limited?(response) || access_token_unauthorized?(response)
|
100
|
+
access_tokens.delete(access_token)
|
101
|
+
raise AccessTokenError
|
102
|
+
elsif response.code >= 500
|
103
|
+
raise ServerError.new(method, path, response.code, response.body, options)
|
104
|
+
elsif response.code >= 400
|
105
|
+
raise ClientError.new(method, path, response.code, response.body, options)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def access_token_rate_limited?(response)
|
110
|
+
response.code == 403 && response.headers['X-RateLimit-Remaining'].to_i.zero?
|
111
|
+
end
|
112
|
+
|
113
|
+
def access_token_unauthorized?(response)
|
114
|
+
response.code == 401
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_access_token!
|
118
|
+
raise AccessTokensDepletedError.new("Rate limit on all access tokens has been used up") if access_tokens.count.zero?
|
119
|
+
access_tokens.sample
|
120
|
+
end
|
121
|
+
|
122
|
+
def access_tokens
|
123
|
+
@config[:access_tokens]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Gitrob
|
2
|
+
module Github
|
3
|
+
class Organization
|
4
|
+
attr_reader :name, :http_client
|
5
|
+
|
6
|
+
def initialize(name, http_client)
|
7
|
+
@name, @http_client = name, http_client
|
8
|
+
end
|
9
|
+
|
10
|
+
def display_name
|
11
|
+
info['name'].to_s.empty? ? info['login'] : info['name']
|
12
|
+
end
|
13
|
+
|
14
|
+
def login
|
15
|
+
info['login']
|
16
|
+
end
|
17
|
+
|
18
|
+
def website
|
19
|
+
info['blog']
|
20
|
+
end
|
21
|
+
|
22
|
+
def location
|
23
|
+
info['location']
|
24
|
+
end
|
25
|
+
|
26
|
+
def email
|
27
|
+
info['email']
|
28
|
+
end
|
29
|
+
|
30
|
+
def url
|
31
|
+
"https://github.com/#{name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def avatar_url
|
35
|
+
info['avatar_url']
|
36
|
+
end
|
37
|
+
|
38
|
+
def repositories
|
39
|
+
if !@repositories
|
40
|
+
@repositories = []
|
41
|
+
response = JSON.parse(http_client.do_get("/orgs/#{name}/repos").body)
|
42
|
+
response.each do |repo|
|
43
|
+
next if repo['fork']
|
44
|
+
@repositories << Repository.new(name, repo['name'], http_client)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@repositories
|
48
|
+
end
|
49
|
+
|
50
|
+
def members()
|
51
|
+
@members ||= recursive_members
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_model
|
55
|
+
Gitrob::Organization.new(
|
56
|
+
:name => self.display_name,
|
57
|
+
:login => self.login,
|
58
|
+
:website => self.website,
|
59
|
+
:location => self.location,
|
60
|
+
:email => self.email,
|
61
|
+
:avatar_url => self.avatar_url,
|
62
|
+
:url => self.url
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def save_to_database!
|
67
|
+
self.to_model.tap { |m| m.save }
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def recursive_members(page = 1)
|
73
|
+
members = Array.new
|
74
|
+
response = http_client.do_get("/orgs/#{name}/members?page=#{page.to_i}")
|
75
|
+
JSON.parse(response.body).each do |member|
|
76
|
+
members << User.new(member['login'], http_client)
|
77
|
+
end
|
78
|
+
|
79
|
+
if response.headers.include?('link') && response.headers['link'].include?('rel="next"')
|
80
|
+
members += recursive_members(page + 1)
|
81
|
+
end
|
82
|
+
members
|
83
|
+
end
|
84
|
+
|
85
|
+
def info
|
86
|
+
if !@info
|
87
|
+
@info = JSON.parse(http_client.do_get("/orgs/#{name}").body)
|
88
|
+
end
|
89
|
+
@info
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Gitrob
|
2
|
+
module Github
|
3
|
+
class Repository
|
4
|
+
|
5
|
+
attr_reader :owner, :name, :http_client
|
6
|
+
def initialize(owner, name, http_client)
|
7
|
+
@owner, @name, @http_client = owner, name, http_client
|
8
|
+
end
|
9
|
+
|
10
|
+
def contents
|
11
|
+
if !@contents
|
12
|
+
@contents = []
|
13
|
+
response = JSON.parse(http_client.do_get("/repos/#{owner}/#{name}/git/trees/master?recursive=1").body)
|
14
|
+
response['tree'].each do |object|
|
15
|
+
next unless object['type'] == 'blob'
|
16
|
+
@contents << Blob.new(object['path'], object['size'], self)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@contents
|
20
|
+
rescue HttpClient::ClientError => ex
|
21
|
+
if ex.status == 409 || ex.status == 404
|
22
|
+
@contents = []
|
23
|
+
else
|
24
|
+
raise ex
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def full_name
|
29
|
+
[owner, name].join('/')
|
30
|
+
end
|
31
|
+
|
32
|
+
def url
|
33
|
+
info['html_url']
|
34
|
+
end
|
35
|
+
|
36
|
+
def description
|
37
|
+
info['description']
|
38
|
+
end
|
39
|
+
|
40
|
+
def website
|
41
|
+
info['homepage']
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_model(organization, user = nil)
|
45
|
+
Gitrob::Repo.new(
|
46
|
+
:name => self.name,
|
47
|
+
:owner_name => self.owner,
|
48
|
+
:description => self.description,
|
49
|
+
:website => self.website,
|
50
|
+
:url => self.url,
|
51
|
+
:organization => organization,
|
52
|
+
:user => user
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def save_to_database!(organization, user = nil)
|
57
|
+
self.to_model(organization, user).tap { |m| m.save }
|
58
|
+
rescue DataMapper::SaveFailureError => e
|
59
|
+
puts e.resource.errors.inspect
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def info
|
65
|
+
if !@info
|
66
|
+
@info = JSON.parse(http_client.do_get("/repos/#{owner}/#{name}").body)
|
67
|
+
end
|
68
|
+
@info
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Gitrob
|
2
|
+
module Github
|
3
|
+
class User
|
4
|
+
|
5
|
+
attr_reader :username, :http_client
|
6
|
+
|
7
|
+
def initialize(username, http_client)
|
8
|
+
@username, @http_client = username, http_client
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
info['name'] || username
|
13
|
+
end
|
14
|
+
|
15
|
+
def email
|
16
|
+
info['email']
|
17
|
+
end
|
18
|
+
|
19
|
+
def website
|
20
|
+
info['blog']
|
21
|
+
end
|
22
|
+
|
23
|
+
def location
|
24
|
+
info['location']
|
25
|
+
end
|
26
|
+
|
27
|
+
def bio
|
28
|
+
info['bio']
|
29
|
+
end
|
30
|
+
|
31
|
+
def url
|
32
|
+
info['html_url']
|
33
|
+
end
|
34
|
+
|
35
|
+
def avatar_url
|
36
|
+
info['avatar_url']
|
37
|
+
end
|
38
|
+
|
39
|
+
def repositories
|
40
|
+
if !@repositories
|
41
|
+
@repositories = []
|
42
|
+
response = JSON.parse(http_client.do_get("/users/#{username}/repos").body)
|
43
|
+
response.each do |repo|
|
44
|
+
next if repo['fork']
|
45
|
+
@repositories << Repository.new(username, repo['name'], http_client)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
@repositories
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_model(organization)
|
52
|
+
organization.users.new(
|
53
|
+
:username => self.username,
|
54
|
+
:name => self.name,
|
55
|
+
:website => self.website,
|
56
|
+
:location => self.location,
|
57
|
+
:email => self.email,
|
58
|
+
:bio => self.bio,
|
59
|
+
:url => self.url,
|
60
|
+
:avatar_url => self.avatar_url
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def save_to_database!(organization)
|
65
|
+
self.to_model(organization).tap { |m| m.save }
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def info
|
71
|
+
if !@info
|
72
|
+
@info = JSON.parse(http_client.do_get("/users/#{username}").body)
|
73
|
+
end
|
74
|
+
@info
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Gitrob
|
2
|
+
module Observers
|
3
|
+
class SensitiveFiles
|
4
|
+
|
5
|
+
class InvalidPatternFileError < StandardError; end
|
6
|
+
class InvalidPatternError < StandardError; end
|
7
|
+
|
8
|
+
VALID_KEYS = %w(part type pattern caption description)
|
9
|
+
VALID_PARTS = %w(path filename extension)
|
10
|
+
VALID_TYPES = %w(match regex)
|
11
|
+
|
12
|
+
def self.observe(blob)
|
13
|
+
patterns.each do |pattern|
|
14
|
+
check_blob(blob, pattern)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.load_patterns!
|
19
|
+
patterns = read_pattern_file!
|
20
|
+
validate_patterns!(patterns)
|
21
|
+
@patterns = patterns
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.patterns
|
25
|
+
@patterns
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def self.read_pattern_file!
|
31
|
+
JSON.parse(File.read("#{File.dirname(__FILE__)}/../../../patterns.json"))
|
32
|
+
rescue JSON::ParserError => e
|
33
|
+
raise InvalidPatternFileError.new("Cannot parse pattern file: #{e.message}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.validate_patterns!(patterns)
|
37
|
+
if !patterns.is_a?(Array) || patterns.empty?
|
38
|
+
raise InvalidPatternFileError.new("Pattern file contains no patterns")
|
39
|
+
end
|
40
|
+
patterns.each do |pattern|
|
41
|
+
validate_pattern!(pattern)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.validate_pattern!(pattern)
|
46
|
+
pattern.keys.each do |key|
|
47
|
+
if !VALID_KEYS.include?(key)
|
48
|
+
raise InvalidPatternError.new("Pattern contains unknown key: #{key}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if !VALID_PARTS.include?(pattern['part'])
|
53
|
+
raise InvalidPatternError.new("Pattern has unknown part: #{pattern['part']}")
|
54
|
+
end
|
55
|
+
|
56
|
+
if !VALID_TYPES.include?(pattern['type'])
|
57
|
+
raise InvalidPatternError.new("Pattern has unknown type: #{pattern['type']}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.check_blob(blob, pattern)
|
62
|
+
haystack = blob.send(pattern['part'].to_sym)
|
63
|
+
if pattern['type'] == 'match'
|
64
|
+
if haystack == pattern['pattern']
|
65
|
+
blob.findings.new(
|
66
|
+
:caption => pattern['caption'],
|
67
|
+
:description => pattern['description']
|
68
|
+
)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
regex = Regexp.new(pattern['pattern'], Regexp::IGNORECASE)
|
72
|
+
if !regex.match(haystack).nil?
|
73
|
+
blob.findings.new(
|
74
|
+
:caption => pattern['caption'],
|
75
|
+
:description => pattern['description']
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|