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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +47 -0
  7. data/Rakefile +2 -0
  8. data/bin/gitrob +258 -0
  9. data/gitrob.gemspec +36 -0
  10. data/lib/gitrob.rb +116 -0
  11. data/lib/gitrob/github/blob.rb +41 -0
  12. data/lib/gitrob/github/http_client.rb +127 -0
  13. data/lib/gitrob/github/organization.rb +93 -0
  14. data/lib/gitrob/github/repository.rb +72 -0
  15. data/lib/gitrob/github/user.rb +78 -0
  16. data/lib/gitrob/observers/sensitive_files.rb +82 -0
  17. data/lib/gitrob/progressbar.rb +52 -0
  18. data/lib/gitrob/util.rb +11 -0
  19. data/lib/gitrob/version.rb +3 -0
  20. data/lib/gitrob/webapp.rb +76 -0
  21. data/models/blob.rb +35 -0
  22. data/models/finding.rb +14 -0
  23. data/models/organization.rb +32 -0
  24. data/models/repo.rb +22 -0
  25. data/models/user.rb +28 -0
  26. data/patterns.json +303 -0
  27. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  28. data/public/fonts/glyphicons-halflings-regular.svg +229 -0
  29. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  30. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  31. data/public/javascripts/bootstrap.min.js +7 -0
  32. data/public/javascripts/gitrob.js +75 -0
  33. data/public/javascripts/jquery-2.1.1.min.js +4 -0
  34. data/public/javascripts/lang-apollo.js +2 -0
  35. data/public/javascripts/lang-basic.js +3 -0
  36. data/public/javascripts/lang-clj.js +18 -0
  37. data/public/javascripts/lang-css.js +2 -0
  38. data/public/javascripts/lang-dart.js +3 -0
  39. data/public/javascripts/lang-erlang.js +2 -0
  40. data/public/javascripts/lang-go.js +1 -0
  41. data/public/javascripts/lang-hs.js +2 -0
  42. data/public/javascripts/lang-lisp.js +3 -0
  43. data/public/javascripts/lang-llvm.js +1 -0
  44. data/public/javascripts/lang-lua.js +2 -0
  45. data/public/javascripts/lang-matlab.js +6 -0
  46. data/public/javascripts/lang-ml.js +2 -0
  47. data/public/javascripts/lang-mumps.js +2 -0
  48. data/public/javascripts/lang-n.js +4 -0
  49. data/public/javascripts/lang-pascal.js +3 -0
  50. data/public/javascripts/lang-proto.js +1 -0
  51. data/public/javascripts/lang-r.js +2 -0
  52. data/public/javascripts/lang-rd.js +1 -0
  53. data/public/javascripts/lang-scala.js +2 -0
  54. data/public/javascripts/lang-sql.js +2 -0
  55. data/public/javascripts/lang-tcl.js +3 -0
  56. data/public/javascripts/lang-tex.js +1 -0
  57. data/public/javascripts/lang-vb.js +2 -0
  58. data/public/javascripts/lang-vhdl.js +3 -0
  59. data/public/javascripts/lang-wiki.js +2 -0
  60. data/public/javascripts/lang-xq.js +3 -0
  61. data/public/javascripts/lang-yaml.js +2 -0
  62. data/public/javascripts/prettify.js +30 -0
  63. data/public/javascripts/run_prettify.js +34 -0
  64. data/public/stylesheets/bootstrap.min.css +7 -0
  65. data/public/stylesheets/bootstrap.min.css.vanilla +5 -0
  66. data/public/stylesheets/gitrob.css +88 -0
  67. data/public/stylesheets/prettify.css +51 -0
  68. data/spec/lib/gitrob/observers/sensitive_files_spec.rb +558 -0
  69. data/spec/spec_helper.rb +127 -0
  70. data/views/blob.erb +22 -0
  71. data/views/index.erb +32 -0
  72. data/views/layout.erb +30 -0
  73. data/views/organization.erb +126 -0
  74. data/views/repository.erb +51 -0
  75. data/views/user.erb +51 -0
  76. 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