cloud-platform-repository-checker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 46a88955fe8e5cbfdc0df49317cc0bc5706e37319df3467107696ff40da9167f
4
+ data.tar.gz: dcbd98ac827af616d4ad056d5dca408d8d76d2d476f439b6d230685061fd1eef
5
+ SHA512:
6
+ metadata.gz: fedd1dba7604e13daa9d6db89fc1c3e63f616a4f3cf5fbf8479bf0f77ac137cecc605ec51a8fc5fd5ef9b5e0fdc352bbfbdd9045e040b8c326bd67fde0590d56
7
+ data.tar.gz: 16e38af1b932b204dae2e3e46d52677433e1904b76fedd5b54a1895eb27c381afb66416049ff36f25cc3c42a9cc3d44cff53ad50319ef2d05667eb3ca75552a0
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gem "octokit"
8
+
9
+ group :development do
10
+ gem "pry-byebug"
11
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.7.0)
5
+ public_suffix (>= 2.0.2, < 5.0)
6
+ byebug (11.1.3)
7
+ coderay (1.1.2)
8
+ faraday (1.0.1)
9
+ multipart-post (>= 1.2, < 3)
10
+ method_source (1.0.0)
11
+ multipart-post (2.1.1)
12
+ octokit (4.18.0)
13
+ faraday (>= 0.9)
14
+ sawyer (~> 0.8.0, >= 0.5.3)
15
+ pry (0.13.1)
16
+ coderay (~> 1.1)
17
+ method_source (~> 1.0)
18
+ pry-byebug (3.9.0)
19
+ byebug (~> 11.0)
20
+ pry (~> 0.13.0)
21
+ public_suffix (4.0.5)
22
+ sawyer (0.8.2)
23
+ addressable (>= 2.3.5)
24
+ faraday (> 0.8, < 2.0)
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ octokit
31
+ pry-byebug
32
+
33
+ BUNDLED WITH
34
+ 2.1.2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Ministry of Justice
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # cloud-platform-repository-checker
2
+ Checks all Cloud Platform repositories for compliance
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Script to list repositories in the ministryofjustice organisation whose names
4
+ # match a regular expression, and output a JSON report of how well they
5
+ # do/don't comply with our team-wide standards for how github repositories
6
+ # should be configured.
7
+
8
+ require "bundler/setup"
9
+ require "json"
10
+ require "net/http"
11
+ require "uri"
12
+ require "octokit"
13
+
14
+ libdir = File.join(".", File.dirname(__FILE__), "..", "lib")
15
+ require File.join(libdir, "github_graph_ql_client")
16
+ require File.join(libdir, "repository_lister")
17
+ require File.join(libdir, "repository_report")
18
+
19
+ ############################################################
20
+
21
+ params = {
22
+ organization: ENV.fetch("ORGANIZATION"),
23
+ regexp: Regexp.new(ENV.fetch("REGEXP")),
24
+ team: ENV.fetch("TEAM"),
25
+ github_token: ENV.fetch("GITHUB_TOKEN")
26
+ }
27
+
28
+ repositories = RepositoryLister.new(params)
29
+ .repository_names
30
+ .inject([]) do |arr, repo_name|
31
+ report = RepositoryReport.new(params.merge(repo_name: repo_name)).report
32
+ arr << report
33
+ end
34
+
35
+ puts({
36
+ repositories: repositories,
37
+ updated_at: Time.now
38
+ }.to_json)
data/env.example ADDED
@@ -0,0 +1,4 @@
1
+ export GITHUB_TOKEN="your github personal access token with public_repos scope"
2
+ export ORGANIZATION="ministryofjustice"
3
+ export TEAM="WebOps"
4
+ export REGEXP="^cloud-platform-*"
@@ -0,0 +1,24 @@
1
+ class GithubGraphQlClient
2
+ attr_reader :github_token
3
+
4
+ GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
5
+
6
+ def initialize(params)
7
+ @github_token = params.fetch(:github_token)
8
+ end
9
+
10
+ private
11
+
12
+ def run_query(params)
13
+ body = params.fetch(:body)
14
+ token = params.fetch(:token)
15
+
16
+ json = {query: body}.to_json
17
+ headers = {"Authorization" => "bearer #{token}"}
18
+
19
+ uri = URI.parse(GITHUB_GRAPHQL_URL)
20
+ resp = Net::HTTP.post(uri, json, headers)
21
+
22
+ resp.body
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ class RepositoryLister < GithubGraphQlClient
2
+ attr_reader :organization, :regexp
3
+
4
+ PAGE_SIZE = 100
5
+
6
+ def initialize(params)
7
+ @organization = params.fetch(:organization)
8
+ @regexp = params.fetch(:regexp)
9
+ super(params)
10
+ end
11
+
12
+ # Returns a list of repository names which match `regexp`
13
+ def repository_names
14
+ list_repos
15
+ .filter { |repo| repo["name"] =~ regexp }
16
+ .map { |repo| repo["name"] }
17
+ end
18
+
19
+ private
20
+
21
+ # TODO:
22
+ # * figure out a way to only fetch cloud-platform-* repos
23
+ # * de-duplicate the code
24
+ # * filter out archived repos
25
+ # * filter out disabled repos
26
+ #
27
+ def list_repos
28
+ repos = []
29
+ end_cursor = nil
30
+
31
+ data = get_repos(end_cursor)
32
+ repos = repos + data.fetch("nodes")
33
+ next_page = data.dig("pageInfo", "hasNextPage")
34
+ end_cursor = data.dig("pageInfo", "endCursor")
35
+
36
+ while next_page do
37
+ data = get_repos(end_cursor)
38
+ repos = repos + data.fetch("nodes")
39
+ next_page = data.dig("pageInfo", "hasNextPage")
40
+ end_cursor = data.dig("pageInfo", "endCursor")
41
+ end
42
+
43
+ repos.reject { |r| r.dig("isArchived") || r.dig("isDisabled") }
44
+ end
45
+
46
+ def get_repos(end_cursor = nil)
47
+ json = run_query(
48
+ body: repositories_query(end_cursor),
49
+ token: github_token
50
+ )
51
+
52
+ JSON.parse(json).dig("data", "organization", "repositories")
53
+ end
54
+
55
+ # TODO: it should be possible to exclude disabled/archived repos in this
56
+ # query, but I don't know how to do that yet, so I'm just fetching everything
57
+ # and throwing away the disabled/archived repos later. We should also be able
58
+ # to only fetch repos whose names match the pattern we're interested in, at
59
+ # this stage.
60
+ def repositories_query(end_cursor)
61
+ after = end_cursor.nil? ? "" : %[, after: "#{end_cursor}"]
62
+ %[
63
+ {
64
+ organization(login: "#{organization}") {
65
+ repositories(first: #{PAGE_SIZE} #{after}) {
66
+ nodes {
67
+ id
68
+ name
69
+ isLocked
70
+ isArchived
71
+ isDisabled
72
+ }
73
+ pageInfo {
74
+ hasNextPage
75
+ endCursor
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ]
81
+ end
82
+ end
@@ -0,0 +1,140 @@
1
+ class RepositoryReport < GithubGraphQlClient
2
+ attr_reader :organization, :repo_name, :team
3
+
4
+ MASTER = "master"
5
+ ADMIN = "admin"
6
+ PASS = "PASS"
7
+ FAIL = "FAIL"
8
+
9
+ def initialize(params)
10
+ @organization = params.fetch(:organization)
11
+ @repo_name = params.fetch(:repo_name)
12
+ @team = params.fetch(:team)
13
+ super(params)
14
+ end
15
+
16
+ # TODO: additional checks
17
+ # * has issues enabled
18
+ # * deleteBranchOnMerge
19
+ # * mergeCommitAllowed (do we want this on or off?)
20
+ # * squashMergeAllowed (do we want this on or off?)
21
+
22
+ def report
23
+ {
24
+ organization: organization,
25
+ name: repo_name,
26
+ url: repo_url,
27
+ status: status,
28
+ report: all_checks_result
29
+ }
30
+ end
31
+
32
+ private
33
+
34
+ def repo_data
35
+ @repo_data ||= fetch_repo_data
36
+ end
37
+
38
+ def repo_url
39
+ @url ||= repo_data.dig("data", "repository", "url")
40
+ end
41
+
42
+ def status
43
+ all_checks_result.values.all? ? PASS : FAIL
44
+ end
45
+
46
+ def all_checks_result
47
+ @all_checks_result ||= {
48
+ has_master_branch_protection: has_master_branch_protection?,
49
+ requires_approving_reviews: has_branch_protection_property?("requiresApprovingReviews"),
50
+ requires_code_owner_reviews: has_branch_protection_property?("requiresCodeOwnerReviews"),
51
+ administrators_require_review: has_branch_protection_property?("isAdminEnforced"),
52
+ dismisses_stale_reviews: has_branch_protection_property?("dismissesStaleReviews"),
53
+ requires_strict_status_checks: has_branch_protection_property?("requiresStrictStatusChecks"),
54
+ team_is_admin: is_team_admin?,
55
+ }
56
+ end
57
+
58
+ def fetch_repo_data
59
+ body = repo_settings_query(
60
+ organization: organization,
61
+ repo_name: repo_name,
62
+ )
63
+
64
+ json = run_query(
65
+ body: body,
66
+ token: github_token
67
+ )
68
+
69
+ JSON.parse(json)
70
+ end
71
+
72
+ def repo_settings_query(params)
73
+ owner = params.fetch(:organization)
74
+ repo_name = params.fetch(:repo_name)
75
+
76
+ %[
77
+ {
78
+ repository(owner: "#{owner}", name: "#{repo_name}") {
79
+ name
80
+ url
81
+ owner {
82
+ login
83
+ }
84
+ branchProtectionRules(first: 50) {
85
+ edges {
86
+ node {
87
+ pattern
88
+ requiresApprovingReviews
89
+ requiresCodeOwnerReviews
90
+ isAdminEnforced
91
+ dismissesStaleReviews
92
+ requiresStrictStatusChecks
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ]
99
+ end
100
+
101
+ def is_team_admin?
102
+ client = Octokit::Client.new(access_token: github_token)
103
+
104
+ client.repo_teams([organization, repo_name].join("/")).filter do |team|
105
+ team[:name] == team && team[:permission] == ADMIN
106
+ end.any?
107
+ rescue Octokit::NotFound
108
+ # This happens if our token does not have permission to view repo settings
109
+ false
110
+ end
111
+
112
+ def branch_protection_rules
113
+ @rules ||= repo_data.dig("data", "repository", "branchProtectionRules", "edges")
114
+ end
115
+
116
+ def has_master_branch_protection?
117
+ requiring_branch_protection_rules do |rules|
118
+
119
+ rules
120
+ .filter { |edge| edge.dig("node", "pattern") == MASTER }
121
+ .any?
122
+ end
123
+ end
124
+
125
+ def has_branch_protection_property?(property)
126
+ requiring_branch_protection_rules do |rules|
127
+ rules
128
+ .map { |edge| edge.dig("node", property) }
129
+ .all?
130
+ end
131
+ end
132
+
133
+ def requiring_branch_protection_rules
134
+ rules = branch_protection_rules
135
+ return false unless rules.any?
136
+
137
+ yield rules
138
+ end
139
+
140
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloud-platform-repository-checker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - David Salgado
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: platforms@digital.justice.gov.uk
15
+ executables:
16
+ - cloud-platform-repository-checker
17
+ extensions: []
18
+ extra_rdoc_files:
19
+ - README.md
20
+ files:
21
+ - Gemfile
22
+ - Gemfile.lock
23
+ - LICENSE
24
+ - README.md
25
+ - bin/cloud-platform-repository-checker
26
+ - env.example
27
+ - lib/github_graph_ql_client.rb
28
+ - lib/repository_lister.rb
29
+ - lib/repository_report.rb
30
+ homepage: https://github.com/ministryofjustice/cloud-platform
31
+ licenses: []
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options:
35
+ - "--main"
36
+ - README.md
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.0.3
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: What this thing does
54
+ test_files: []