gitrob 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|