denmark 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73d9e39f18a874e61b889ce6adae3f7793e40af5f0077aede656c47c02305c14
4
- data.tar.gz: d3b7d990966b04a22a66fb44ccc53202bb2046eecbd5f696f0e7496e6b07be03
3
+ metadata.gz: f9f7e41d628d1cb52bd1af3055c8f34ada3edcc8092e2d1c779fd30a37dd72ad
4
+ data.tar.gz: 76829cb396c0115321efeccbe91c51adaf8f77cac48657b75399579c1c6e94ea
5
5
  SHA512:
6
- metadata.gz: e7bfbddb66940d3433376f357ccd41f9fd34e5cb077a2b5274492f078636a4c5cc65d68740c397f384375dbfd072137a8cd80e2962009dfdf805c0a403384f2e
7
- data.tar.gz: c90ba1f9cf0af6a71301cd0592e0ae3322cbf68c9d1630e4ff22ee9574252f8265f968a2d39b956b9c7754a396f75dfb86b1c2e84e231c3cc22769d45e5b0b1f
6
+ metadata.gz: f3ae368e8233da41ac9926f0163184ee5f580750245e8c7af82554efc21bf2a1593202a9d05163c3b1238ef0dcd1064cfe8be89be353faeb5d8e05742b5dd628
7
+ data.tar.gz: e320a6024a23c1c707627352b7f7ec4ded0bad2bbf397b5f2f2c65d83ea10ad31d8d92f6cb936f7d7d10998345cfa8fde5435f5d57c5a31f3c2352bd4bf80bf1
data/README.md CHANGED
@@ -6,9 +6,12 @@
6
6
  I'm sure you've had the experience of evaluating modules on the Puppet Forge. Maybe
7
7
  you were comparing a handful that all claimed to meet your needs, or maybe you were
8
8
  just determining whether a specific module met your standards for deploying into
9
- your production environment.
9
+ your production environment. With tools like `puppet-lint` and the PDK, it's fairly
10
+ straightforward to evaluate code quality. But what about the health of the project
11
+ itself? Is it actively maintained? Are Forge releases kept up to date? What are the
12
+ chances that the latest release was compromised with a hidden bitcoin miner?
10
13
 
11
- How did you go about it? You probably
14
+ How do you answer those sorts of questions? You probably
12
15
 
13
16
  * Skimmed the module's README for signs of the author's diligence.
14
17
  * Poked through the issue list and pull requests on the repository hosting the module
data/bin/denmark CHANGED
@@ -6,8 +6,9 @@ require 'denmark/version'
6
6
  class Denmark
7
7
  extend GLI::App
8
8
 
9
- program_desc 'A simple tool for checking Puppet Forge modules for maintenance smells'
10
- version Denmark::VERSION
9
+ program_desc 'A simple tool for checking Puppet Forge modules for maintenance smells'
10
+ version Denmark::VERSION
11
+ wrap_help_text :verbatim
11
12
 
12
13
  pre do |global, command, options, args|
13
14
  Denmark.config = YAML.load_file("#{Dir.home}/.config/denmark.yaml") rescue {}
@@ -30,8 +31,18 @@ class Denmark
30
31
  end
31
32
  end
32
33
 
33
- desc 'Smell test a module using all enabled tests'
34
- command :smell do |c|
34
+ desc 'Smell test a module using all enabled tests.'
35
+ long_desc <<~DESC
36
+ Pass this command the name of a module, the path to a module or its
37
+ `metadata.json`, or simply execute it within the root directory of a module.
38
+
39
+ Examples:
40
+ $ denmark binford2k-node_encrypt
41
+ $ denmark /Users/ben/Projects/binford2k-node_encrypt
42
+ $ denmark /Users/ben/Projects/binford2k-node_encrypt/metadata.json
43
+ $ cd Projects/binford2k-node_encrypt && denmark
44
+ DESC
45
+ command [:smell,:check] do |c|
35
46
  c.desc 'Lists of tests to enable'
36
47
  c.flag [:enable, :e], :type => Array
37
48
 
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ require 'paint'
3
+ require 'paint/shortcuts'
2
4
 
3
5
  class Array
4
6
  def percent_of(digits = nil)
5
7
  raise "Select the items you want to count using a block that returns a boolean" unless block_given?
8
+ return 0 if self.empty?
6
9
 
7
10
  count = self.size
8
11
  match = 0
@@ -17,3 +20,11 @@ class Array
17
20
  end
18
21
  end
19
22
  end
23
+
24
+ Paint::SHORTCUTS[:color] = {
25
+ :red => Paint.color(:red),
26
+ :orange => Paint.color('orange', :bright),
27
+ :yellow => Paint.color(:yellow),
28
+ :green => Paint.color(:green),
29
+ }
30
+ include Paint::Color::Prefix::ColorName
@@ -29,13 +29,21 @@ class Denmark::Plugins::Metadata
29
29
 
30
30
  if (Date.today - release_date) > 365
31
31
  response << {
32
- severity: :yellow,
32
+ severity: :green,
33
33
  message: "The most current module release is more than a year old.",
34
- explanation: "Sometimes when issues are not responded to, it means that the project is no longer being maintained. You might consider contacting the maintainer to determine the status of the project.",
34
+ explanation: "Sometimes a module not seeing regular updates is a sign that it's no longer being maintained. You might consider contacting the maintainer to determine the status of the project.",
35
+ }
36
+ end
37
+
38
+ if (Date.today - release_date) < 15
39
+ response << {
40
+ severity: :green,
41
+ message: "The latest module release is less than two weeks old.",
42
+ explanation: "Sometimes it's a good idea to let the early adopters shake out the bugs with a new release.",
35
43
  }
36
44
  end
37
45
 
38
- if version != repo_metadata[:version]
46
+ if version != repo_metadata['version']
39
47
  response << {
40
48
  severity: :red,
41
49
  message: "The version released on the Forge does not match the version in the repository.",
@@ -51,7 +59,7 @@ class Denmark::Plugins::Metadata
51
59
  }
52
60
  end
53
61
 
54
- if version != latest_tag
62
+ unless [version, "v#{version}"].include? latest_tag
55
63
  response << {
56
64
  severity: :yellow,
57
65
  message: "The version released on the Forge does not match the latest tag in the repo.",
@@ -71,7 +79,7 @@ class Denmark::Plugins::Metadata
71
79
  response << {
72
80
  severity: :green,
73
81
  message: "There was a gap of at least a year between the last two releases.",
74
- explanation: "A large gap between releases often shows sporadic maintenance. This is not always bad.",
82
+ explanation: "A large gap between releases often shows sporadic maintenance. This doesn't necessarily indicate anything wrong, but attackers do sometimes target stagnant projects in the hope that they'll be undetected for a longer time period.",
75
83
  }
76
84
  end
77
85
 
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # environments plugin
4
+ class Denmark::Plugins::Timeline
5
+ def self.description
6
+ # This is a Ruby squiggle heredoc; just a multi-line string with indentation removed
7
+ <<~DESCRIPTION
8
+ This smell test infers trends a module base on its timeline of issues and commits and whatnot.
9
+ DESCRIPTION
10
+ end
11
+ def self.setup
12
+ # run just before evaluating this plugin
13
+ end
14
+
15
+ def self.run(mod, repo)
16
+ # return an array of hashes representing any smells discovered
17
+ response = Array.new
18
+
19
+ unreleased = repo.commits_since_tag.size
20
+ new_issues = repo.issues_since_tag.size
21
+ taggers = repo.committers(repo.tags)
22
+ last_tagger = taggers.shift
23
+
24
+ unsigned_commits = repo.commits.percent_of {|i| not repo.verified(i) }
25
+ unsigned_tags = repo.tags.percent_of {|i| not repo.verified(i) }
26
+
27
+ unless taggers.include? last_tagger
28
+ response << {
29
+ severity: :yellow,
30
+ message: "The last tag was pushed by #{last_tagger}, who has not tagged any other release.",
31
+ explanation: "This often indicates that a project has recently changed owners. Check to ensure you still know who's maintaining the project.",
32
+ }
33
+ end
34
+
35
+ unless repo.verified(repo.tags.first)
36
+ response << {
37
+ severity: :yellow,
38
+ message: "The last tag was not verified.",
39
+ explanation: "Many authors don't bother to sign their tags. This means you have no way to ensure who creates them.",
40
+ }
41
+ end
42
+
43
+ # this smell would be more accurate if we weighted more recent commits
44
+ if (25..75).include? unsigned_commits
45
+ response << {
46
+ severity: :green,
47
+ message: "#{unsigned_commits}% of the commits in this repo are not signed.",
48
+ explanation: "The repository is using signed commits, but some of the contributions are unverified.",
49
+ }
50
+ end
51
+
52
+ # this smell would be more accurate if we weighted more recent tags
53
+ if (15..85).include? unsigned_tags
54
+ response << {
55
+ severity: :green,
56
+ message: "#{unsigned_tags}% of the tags in this repo are not signed.",
57
+ explanation: "The repository is using signed tags, but a significant number are unverified.",
58
+ }
59
+ end
60
+
61
+ if unsigned_tags > 85 and not repo.verified(repo.tags.first)
62
+ response << {
63
+ severity: :red,
64
+ message: "Most tags in this repo are signed, but not the latest one.",
65
+ explanation: "At best, this means a sloppy release. But it could also mean a compromised release.",
66
+ }
67
+ end
68
+
69
+ if unreleased > 10
70
+ response << {
71
+ severity: :yellow,
72
+ message: "There are #{unreleased} commits since the last release.",
73
+ explanation: "Sometimes maintainers forget to make a release. Maybe you should remind them?",
74
+ }
75
+ end
76
+
77
+ if new_issues > 5
78
+ response << {
79
+ severity: :yellow,
80
+ message: "There have been #{new_issues} issues since the last tagged release.",
81
+ explanation: "Many issues on a release might indicate that there's a problem with it.",
82
+ }
83
+ end
84
+
85
+ response
86
+ end
87
+
88
+ def self.cleanup
89
+ # run just after evaluating this plugin
90
+ end
91
+ end
@@ -54,6 +54,45 @@ class Denmark::Repository
54
54
  end
55
55
  end
56
56
 
57
+ def issues_since_tag(tag = nil)
58
+ tag ||= tags[0]
59
+ case @flavor
60
+ when :github
61
+ issues_since(commit_date(tag.commit.sha))
62
+ when :gitlab
63
+ issues_since(tag.commit.created_at)
64
+ else
65
+ Array.new
66
+ end
67
+ end
68
+
69
+ def issues_since(date)
70
+ case @flavor
71
+ when :github
72
+ @client.issues(@repo, {:state => 'open', :since=> date}).reject {|i| i[:pull_request] }
73
+ when :gitlab
74
+ @client.issues(@repo, updated_after: date, scope: 'all')
75
+ else
76
+ Array.new
77
+ end
78
+ end
79
+
80
+ def committers(list)
81
+ list = Array(list)
82
+ case @flavor
83
+ when :github
84
+ list.reduce(Array.new) do |acc, item|
85
+ acc << (item.author&.login || commit(item.commit.sha).author.login)
86
+ end
87
+ when :gitlab
88
+ list.reduce(Array.new) do |acc, item|
89
+ acc << item.commit.author_name
90
+ end
91
+ else
92
+ Array.new
93
+ end
94
+ end
95
+
57
96
  def tags
58
97
  case @flavor
59
98
  when :github, :gitlab
@@ -74,6 +113,22 @@ class Denmark::Repository
74
113
  end
75
114
  end
76
115
 
116
+ def verified(item)
117
+ case @flavor
118
+ when :github
119
+ if item.commit.verification.nil?
120
+ commit(item.commit.sha).commit.verification.verified
121
+ else
122
+ item.commit.verification&.verified
123
+ end
124
+ when :gitlab
125
+ commit(tag.commit.id).verification.verification_status == 'verified'
126
+ else
127
+ false
128
+ end
129
+
130
+ end
131
+
77
132
  def commit(sha)
78
133
  case @flavor
79
134
  when :github, :gitlab
@@ -99,10 +154,8 @@ class Denmark::Repository
99
154
  when :gitlab
100
155
  @client.commit(@repo, sha).commit.created_at.to_date
101
156
  else
102
- Array.new
157
+ nil
103
158
  end
104
-
105
-
106
159
  end
107
160
 
108
161
  def commits_since_tag(tag = nil)
@@ -110,7 +163,7 @@ class Denmark::Repository
110
163
 
111
164
  case @flavor
112
165
  when :github
113
- @client.commits_since(@repo, tag.commit.committer.date)
166
+ @client.commits_since(@repo, commit_date(tag.commit.sha))
114
167
  when :gitlab
115
168
  @client.commits(@repo, since: tag.commit.created_at)
116
169
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Denmark
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
data/lib/denmark.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require 'colorize'
5
4
  require 'httpclient'
6
5
  require 'puppet_forge'
7
6
  require 'denmark/plugins'
@@ -31,7 +30,8 @@ class Denmark
31
30
 
32
31
  def self.evaluate(slug, options)
33
32
  @options = options
34
- slug.sub!('/', '-')
33
+ slug = resolve_slug(slug)
34
+
35
35
  begin
36
36
  mod = PuppetForge::Module.find(slug)
37
37
  rescue Faraday::BadRequestError, Faraday::ResourceNotFound
@@ -51,6 +51,23 @@ class Denmark
51
51
  end
52
52
  end
53
53
 
54
+ def self.resolve_slug(path)
55
+ begin
56
+ if path.nil?
57
+ path = JSON.parse(File.read('metadata.json'))['name']
58
+ elsif File.directory?(path)
59
+ path = JSON.parse(File.read("#{path}/metadata.json"))['name']
60
+ elsif path.end_with?('metadata.json')
61
+ path = JSON.parse(File.read(path))['name']
62
+ end
63
+ rescue Errno::ENOENT => e
64
+ raise "Cannot load metadata from '#{path}'. Pass this tool the name of a module, or the local path to a module."
65
+ end
66
+
67
+ # if we get this far, assume it's the name of a module and normalize it
68
+ path.sub('/', '-')
69
+ end
70
+
54
71
  def self.generate_report(data)
55
72
  if data.empty?
56
73
  puts "Congrats, no smells discovered"
@@ -60,7 +77,7 @@ class Denmark
60
77
  alerts = data.select {|i| i[:severity] == severity}
61
78
  next unless alerts.size > 0
62
79
 
63
- puts "[#{severity.upcase}] alerts:".colorize(severity)
80
+ puts "[#{severity.upcase}] alerts:".color_name(severity)
64
81
  alerts.each do |alert|
65
82
  puts " #{alert[:message]}"
66
83
  puts " > #{alert[:explanation]}" if @options[:detail]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: denmark
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Ford
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-02 00:00:00.000000000 Z
11
+ date: 2022-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -123,19 +123,33 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '4.0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: colorize
126
+ name: paint
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0.8'
131
+ version: '2.0'
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0.8'
138
+ version: '2.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: paint-shortcuts
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.0'
139
153
  description: |
140
154
  Denmark will check a Puppet module for things you should be concerned about, like signs of an
141
155
  unmaintained module. It uses the Puppet Forge API and GitHub/GitLab APIs to discover information
@@ -155,6 +169,7 @@ files:
155
169
  - lib/denmark/plugins/issues.rb
156
170
  - lib/denmark/plugins/metadata.rb
157
171
  - lib/denmark/plugins/pull_requests.rb
172
+ - lib/denmark/plugins/timeline.rb
158
173
  - lib/denmark/repository.rb
159
174
  - lib/denmark/version.rb
160
175
  homepage: https://github.com/binford2k/denmark