denmark 0.0.2 → 0.0.3
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 +4 -4
- data/README.md +5 -2
- data/bin/denmark +15 -4
- data/lib/denmark/monkeypatches.rb +11 -0
- data/lib/denmark/plugins/metadata.rb +13 -5
- data/lib/denmark/plugins/timeline.rb +91 -0
- data/lib/denmark/repository.rb +57 -4
- data/lib/denmark/version.rb +1 -1
- data/lib/denmark.rb +20 -3
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9f7e41d628d1cb52bd1af3055c8f34ada3edcc8092e2d1c779fd30a37dd72ad
|
4
|
+
data.tar.gz: 76829cb396c0115321efeccbe91c51adaf8f77cac48657b75399579c1c6e94ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
10
|
-
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
|
-
|
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: :
|
32
|
+
severity: :green,
|
33
33
|
message: "The most current module release is more than a year old.",
|
34
|
-
explanation: "Sometimes
|
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[
|
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
|
-
|
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
|
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
|
data/lib/denmark/repository.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
data/lib/denmark/version.rb
CHANGED
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
|
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:".
|
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.
|
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-
|
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:
|
126
|
+
name: paint
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: '0
|
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
|
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
|