inspec 1.26.0 → 1.27.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -32
- data/Rakefile +2 -2
- data/docs/resources/crontab.md.erb +17 -1
- data/docs/resources/http.md.erb +6 -3
- data/docs/resources/processes.md.erb +42 -2
- data/examples/inheritance/inspec.yml +1 -1
- data/examples/meta-profile/inspec.yml +1 -1
- data/examples/profile-attribute/inspec.yml +1 -1
- data/examples/profile/inspec.yml +1 -1
- data/lib/bundles/inspec-compliance/api.rb +8 -7
- data/lib/bundles/inspec-compliance/cli.rb +1 -1
- data/lib/bundles/inspec-init/templates/profile/inspec.yml +1 -1
- data/lib/fetchers/local.rb +4 -1
- data/lib/fetchers/url.rb +23 -6
- data/lib/inspec/dependencies/cache.rb +0 -1
- data/lib/inspec/dependencies/requirement.rb +0 -1
- data/lib/inspec/metadata.rb +8 -2
- data/lib/inspec/plugins/fetcher.rb +0 -1
- data/lib/inspec/profile.rb +3 -3
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +0 -1
- data/lib/resources/command.rb +1 -1
- data/lib/resources/crontab.rb +24 -9
- data/lib/resources/host.rb +1 -1
- data/lib/resources/interface.rb +2 -1
- data/lib/resources/postgres.rb +45 -39
- data/lib/resources/processes.rb +17 -4
- data/lib/utils/find_files.rb +1 -1
- data/lib/utils/nginx_parser.rb +74 -0
- data/lib/utils/spdx.rb +13 -0
- data/lib/utils/spdx.txt +344 -0
- metadata +6 -3
data/lib/inspec/metadata.rb
CHANGED
@@ -7,6 +7,7 @@ require 'logger'
|
|
7
7
|
require 'rubygems/version'
|
8
8
|
require 'rubygems/requirement'
|
9
9
|
require 'semverse'
|
10
|
+
require 'utils/spdx'
|
10
11
|
|
11
12
|
module Inspec
|
12
13
|
# Extract metadata.rb information
|
@@ -102,7 +103,7 @@ module Inspec
|
|
102
103
|
end
|
103
104
|
|
104
105
|
# return all warn and errors
|
105
|
-
def valid
|
106
|
+
def valid # rubocop:disable Metrics/AbcSize
|
106
107
|
errors = []
|
107
108
|
warnings = []
|
108
109
|
|
@@ -116,11 +117,16 @@ module Inspec
|
|
116
117
|
errors.push('Version needs to be in SemVer format')
|
117
118
|
end
|
118
119
|
|
119
|
-
%w{ title summary maintainer copyright }.each do |field|
|
120
|
+
%w{ title summary maintainer copyright license }.each do |field|
|
120
121
|
next unless params[field.to_sym].nil?
|
121
122
|
warnings.push("Missing profile #{field} in #{ref}")
|
122
123
|
end
|
123
124
|
|
125
|
+
# if version is set, ensure it is in SPDX format
|
126
|
+
if !params[:license].nil? && !Spdx.valid_license?(params[:license])
|
127
|
+
warnings.push("License '#{params[:license]}' needs to be in SPDX format. See https://spdx.org/licenses/.")
|
128
|
+
end
|
129
|
+
|
124
130
|
[errors, warnings]
|
125
131
|
end
|
126
132
|
|
data/lib/inspec/profile.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# author: Christoph Hartmann
|
5
5
|
|
6
6
|
require 'forwardable'
|
7
|
-
require '
|
7
|
+
require 'openssl'
|
8
8
|
require 'inspec/polyfill'
|
9
9
|
require 'inspec/cached_fetcher'
|
10
10
|
require 'inspec/file_provider'
|
@@ -406,7 +406,7 @@ module Inspec
|
|
406
406
|
# get all dependency checksums
|
407
407
|
deps = Hash[locked_dependencies.list.map { |k, v| [k, v.profile.sha256] }]
|
408
408
|
|
409
|
-
res = Digest::SHA256.new
|
409
|
+
res = OpenSSL::Digest::SHA256.new
|
410
410
|
files = source_reader.tests.to_a + source_reader.libraries.to_a +
|
411
411
|
source_reader.data_files.to_a +
|
412
412
|
[['inspec.yml', source_reader.metadata.content]] +
|
@@ -415,7 +415,7 @@ module Inspec
|
|
415
415
|
files.sort { |a, b| a[0] <=> b[0] }
|
416
416
|
.map { |f| res << f[0] << "\0" << f[1] << "\0" }
|
417
417
|
|
418
|
-
res.
|
418
|
+
res.digest.unpack('H*')[0]
|
419
419
|
end
|
420
420
|
|
421
421
|
private
|
data/lib/inspec/version.rb
CHANGED
data/lib/matchers/matchers.rb
CHANGED
@@ -88,7 +88,6 @@ end
|
|
88
88
|
|
89
89
|
RSpec::Matchers.define :contain_duplicates do
|
90
90
|
match do |arr|
|
91
|
-
warn '[DEPRECATION] `contain_duplicates` is deprecated and will be removed in the next major version. See https://github.com/chef/inspec/issues/738 for more details'
|
92
91
|
dup = arr.select { |element| arr.count(element) > 1 }
|
93
92
|
!dup.uniq.empty?
|
94
93
|
end
|
data/lib/resources/command.rb
CHANGED
@@ -50,7 +50,7 @@ module Inspec::Resources
|
|
50
50
|
if inspec.os.linux?
|
51
51
|
res = inspec.backend.run_command("bash -c 'type \"#{@command}\"'")
|
52
52
|
elsif inspec.os.windows?
|
53
|
-
res = inspec.backend.run_command("
|
53
|
+
res = inspec.backend.run_command("Get-Command \"#{@command}\"")
|
54
54
|
elsif inspec.os.unix?
|
55
55
|
res = inspec.backend.run_command("type \"#{@command}\"")
|
56
56
|
else
|
data/lib/resources/crontab.rb
CHANGED
@@ -46,15 +46,30 @@ module Inspec::Resources
|
|
46
46
|
data, = parse_comment_line(l, comment_char: '#', standalone_comments: false)
|
47
47
|
return nil if data.nil? || data.empty?
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
'minute'
|
52
|
-
|
53
|
-
'day'
|
54
|
-
|
55
|
-
'weekday' =>
|
56
|
-
|
57
|
-
|
49
|
+
case data
|
50
|
+
when /@hourly .*/
|
51
|
+
{ 'minute' => '0', 'hour' => '*', 'day' => '*', 'month' => '*', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
52
|
+
when /@(midnight|daily) .*/
|
53
|
+
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
54
|
+
when /@weekly .*/
|
55
|
+
{ 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'weekday' => '0', 'command' => data.split(/\s+/, 2).at(1) }
|
56
|
+
when /@monthly ./
|
57
|
+
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '*', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
58
|
+
when /@(annually|yearly) .*/
|
59
|
+
{ 'minute' => '0', 'hour' => '0', 'day' => '1', 'month' => '1', 'weekday' => '*', 'command' => data.split(/\s+/, 2).at(1) }
|
60
|
+
when /@reboot .*/
|
61
|
+
{ 'minute' => '-1', 'hour' => '-1', 'day' => '-1', 'month' => '-1', 'weekday' => '-1', 'command' => data.split(/\s+/, 2).at(1) }
|
62
|
+
else
|
63
|
+
elements = data.split(/\s+/, 6)
|
64
|
+
{
|
65
|
+
'minute' => elements.at(0),
|
66
|
+
'hour' => elements.at(1),
|
67
|
+
'day' => elements.at(2),
|
68
|
+
'month' => elements.at(3),
|
69
|
+
'weekday' => elements.at(4),
|
70
|
+
'command' => elements.at(5),
|
71
|
+
}
|
72
|
+
end
|
58
73
|
end
|
59
74
|
|
60
75
|
def crontab_cmd
|
data/lib/resources/host.rb
CHANGED
@@ -148,7 +148,7 @@ module Inspec::Resources
|
|
148
148
|
def ping(hostname, port = nil, _proto = nil)
|
149
149
|
# ICMP: Test-NetConnection www.microsoft.com
|
150
150
|
# TCP and port: Test-NetConnection -ComputerName www.microsoft.com -RemotePort 80
|
151
|
-
request = "Test-NetConnection -ComputerName #{hostname}"
|
151
|
+
request = "Test-NetConnection -ComputerName #{hostname} -WarningAction SilentlyContinue"
|
152
152
|
request += " -RemotePort #{port}" unless port.nil?
|
153
153
|
request += '| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json'
|
154
154
|
cmd = inspec.command(request)
|
data/lib/resources/interface.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
# author: Christoph Hartmann
|
3
3
|
# author: Dominik Richter
|
4
|
+
# author: Aaron Lippold
|
4
5
|
|
5
6
|
require 'utils/convert'
|
6
7
|
|
@@ -64,7 +65,7 @@ module Inspec::Resources
|
|
64
65
|
class LinuxInterface < InterfaceInfo
|
65
66
|
def interface_info(iface)
|
66
67
|
# will return "[mtu]\n1500\n[type]\n1"
|
67
|
-
cmd = inspec.command("find /sys/class/net/#{iface}/ -
|
68
|
+
cmd = inspec.command("find /sys/class/net/#{iface}/ -maxdepth 1 -type f -exec sh -c 'echo \"[$(basename {})]\"; cat {} || echo -n' \\;")
|
68
69
|
return nil if cmd.exit_status.to_i != 0
|
69
70
|
|
70
71
|
# parse values, we only recieve values, therefore we threat them as keys
|
data/lib/resources/postgres.rb
CHANGED
@@ -2,13 +2,14 @@
|
|
2
2
|
# copyright: 2015, Vulcano Security GmbH
|
3
3
|
# author: Dominik Richter
|
4
4
|
# author: Christoph Hartmann
|
5
|
+
# author: Aaron Lippold
|
5
6
|
# license: All rights reserved
|
6
7
|
|
7
8
|
module Inspec::Resources
|
8
9
|
class Postgres < Inspec.resource(1)
|
9
10
|
name 'postgres'
|
10
11
|
|
11
|
-
attr_reader :service, :data_dir, :conf_dir, :conf_path
|
12
|
+
attr_reader :service, :data_dir, :conf_dir, :conf_path, :version, :cluster
|
12
13
|
def initialize
|
13
14
|
os = inspec.os
|
14
15
|
if os.debian?
|
@@ -18,48 +19,26 @@ module Inspec::Resources
|
|
18
19
|
# Debian allows multiple versions of postgresql to be
|
19
20
|
# installed as well as multiple "clusters" to be configured.
|
20
21
|
#
|
21
|
-
version = version_from_dir('/etc/postgresql')
|
22
|
-
cluster = cluster_from_dir("/etc/postgresql/#{version}")
|
23
|
-
@conf_dir = "/etc/postgresql/#{version}/#{cluster}"
|
24
|
-
@data_dir = "/var/lib/postgresql/#{version}/#{cluster}"
|
25
|
-
elsif os.redhat?
|
26
|
-
#
|
27
|
-
# /var/lib/pgsql/data is the default data directory on RHEL6
|
28
|
-
# and RHEL7. However, PR #824 explicitly added version-based
|
29
|
-
# directories. Thus, we call #version_from_dir unless it looks
|
30
|
-
# like we are using unversioned directories.
|
31
|
-
#
|
32
|
-
# TODO(ssd): This has the potential to be noisy because of the
|
33
|
-
# warning in version_from_dir. We should determine which case
|
34
|
-
# is more common and only warn in the less common case.
|
35
|
-
#
|
36
|
-
version = if inspec.directory('/var/lib/pgsql/data').exist?
|
37
|
-
warn 'Found /var/lib/pgsql/data. Assuming postgresql install uses un-versioned directories.'
|
38
|
-
nil
|
39
|
-
else
|
40
|
-
version_from_dir('/var/lib/pgsql/')
|
41
|
-
end
|
42
|
-
|
43
|
-
@data_dir = File.join('/var/lib/pgsql/', version.to_s, 'data')
|
44
|
-
elsif os[:name] == 'arch'
|
45
|
-
#
|
46
|
-
# https://wiki.archlinux.org/index.php/PostgreSQL
|
47
|
-
#
|
48
|
-
# The archlinux wiki points to /var/lib/postgresql/data as the
|
49
|
-
# main data directory.
|
50
|
-
#
|
51
|
-
@data_dir = '/var/lib/postgres/data'
|
22
|
+
@version = version_from_psql || version_from_dir('/etc/postgresql')
|
23
|
+
@cluster = cluster_from_dir("/etc/postgresql/#{@version}")
|
24
|
+
@conf_dir = "/etc/postgresql/#{@version}/#{@cluster}"
|
25
|
+
@data_dir = "/var/lib/postgresql/#{@version}/#{@cluster}"
|
52
26
|
else
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
27
|
+
@version = version_from_psql
|
28
|
+
if @version.nil?
|
29
|
+
if inspec.directory('/var/lib/pgsql/data').exist?
|
30
|
+
warn 'Unable to determine PostgreSQL version: psql did not return
|
31
|
+
a version number and unversioned data directories were found.'
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
@version = version_from_dir('/var/lib/pgsql/')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
@data_dir = locate_data_dir_location_by_version(@version)
|
60
38
|
end
|
61
39
|
|
62
40
|
@service = 'postgresql'
|
41
|
+
@service += "-#{@version}" if @version.to_f >= 9.4
|
63
42
|
@conf_dir ||= @data_dir
|
64
43
|
verify_dirs
|
65
44
|
@conf_path = File.join @conf_dir, 'postgresql.conf'
|
@@ -81,6 +60,33 @@ module Inspec::Resources
|
|
81
60
|
end
|
82
61
|
end
|
83
62
|
|
63
|
+
def version_from_psql
|
64
|
+
return unless inspec.command('psql').exist?
|
65
|
+
inspec.command("psql --version | awk '{ print $NF }' | awk -F. '{ print $1\".\"$2 }'").stdout.strip
|
66
|
+
end
|
67
|
+
|
68
|
+
def locate_data_dir_location_by_version(ver = @version)
|
69
|
+
data_dir_loc = nil
|
70
|
+
dir_list = [
|
71
|
+
"/var/lib/pgsql/#{ver}/data",
|
72
|
+
'/var/lib/pgsql/data',
|
73
|
+
'/var/lib/postgres/data',
|
74
|
+
'/var/lib/postgresql/data',
|
75
|
+
]
|
76
|
+
|
77
|
+
dir_list.each do |dir|
|
78
|
+
data_dir_loc if inspec.directory(dir).exists?
|
79
|
+
break
|
80
|
+
end
|
81
|
+
|
82
|
+
if data_dir_loc.nil?
|
83
|
+
warn 'Unable to find the PostgreSQL data_dir in expected location(s), please
|
84
|
+
execute "psql -t -A -p <port> -h <host> -c "show hba_file";" as the PostgreSQL
|
85
|
+
DBA to find the non-starndard data_dir location.'
|
86
|
+
end
|
87
|
+
data_dir_loc
|
88
|
+
end
|
89
|
+
|
84
90
|
def version_from_dir(dir)
|
85
91
|
dirs = inspec.command("ls -d #{dir}/*/").stdout
|
86
92
|
entries = dirs.lines.count
|
data/lib/resources/processes.rb
CHANGED
@@ -25,15 +25,24 @@ module Inspec::Resources
|
|
25
25
|
@grep = grep
|
26
26
|
# turn into a regexp if it isn't one yet
|
27
27
|
if grep.class == String
|
28
|
-
|
29
|
-
|
28
|
+
# if windows ignore case as we can't make up our minds
|
29
|
+
if inspec.os.windows?
|
30
|
+
grep = '(?i)' + grep
|
31
|
+
else
|
32
|
+
grep = '(/[^/]*)*' + grep unless grep[0] == '/'
|
33
|
+
grep = '^' + grep + '(\s|$)'
|
34
|
+
end
|
35
|
+
grep = Regexp.new(grep)
|
30
36
|
end
|
37
|
+
|
31
38
|
all_cmds = ps_axo
|
32
39
|
@list = all_cmds.find_all do |hm|
|
33
40
|
hm[:command] =~ grep
|
34
41
|
end
|
42
|
+
end
|
35
43
|
|
36
|
-
|
44
|
+
def exists?
|
45
|
+
!@list.empty?
|
37
46
|
end
|
38
47
|
|
39
48
|
def to_s
|
@@ -74,6 +83,10 @@ module Inspec::Resources
|
|
74
83
|
if os.linux?
|
75
84
|
command = 'ps axo label,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user:32,command'
|
76
85
|
regex = /^([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(\w{3} \d{2}|\d{2}:\d{2}:\d{2})\s+([^ ]+)\s+([^ ]+)\s+(.*)$/
|
86
|
+
elsif os.windows?
|
87
|
+
command = '$Proc = Get-Process -IncludeUserName | Where-Object {$_.Path -ne $null } | Select-Object PriorityClass,Id,CPU,PM,VirtualMemorySize,NPM,SessionId,Responding,StartTime,TotalProcessorTime,UserName,Path | ConvertTo-Csv -NoTypeInformation;$Proc.Replace("""","").Replace("`r`n","`n")'
|
88
|
+
# Wanted to use /(?:^|,)([^,]*)/; works on rubular.com not sure why here?
|
89
|
+
regex = /^(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+)$/
|
77
90
|
else
|
78
91
|
command = 'ps axo pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user,command'
|
79
92
|
regex = /^\s*([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(.*)$/
|
@@ -95,7 +108,7 @@ module Inspec::Resources
|
|
95
108
|
end.compact
|
96
109
|
lines.map do |m|
|
97
110
|
a = m.to_a[1..-1] # grab all matching groups
|
98
|
-
a.unshift(nil) unless os.linux?
|
111
|
+
a.unshift(nil) unless os.linux? || os.windows?
|
99
112
|
a[1] = a[1].to_i
|
100
113
|
a[4] = a[4].to_i
|
101
114
|
a[5] = a[5].to_i
|
data/lib/utils/find_files.rb
CHANGED
@@ -26,8 +26,8 @@ module FindFiles
|
|
26
26
|
type = TYPES[opts[:type].to_sym] if opts[:type]
|
27
27
|
|
28
28
|
cmd = "find #{path}"
|
29
|
-
cmd += " -maxdepth #{depth.to_i}" if depth.to_i > 0
|
30
29
|
cmd += " -type #{type}" unless type.nil?
|
30
|
+
cmd += " -maxdepth #{depth.to_i}" if depth.to_i > 0
|
31
31
|
|
32
32
|
result = inspec.command(cmd)
|
33
33
|
exit_status = result.exit_status
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'parslet'
|
6
|
+
|
7
|
+
class NginxParser < Parslet::Parser
|
8
|
+
root :outermost
|
9
|
+
# only designed for rabbitmq config files for now:
|
10
|
+
rule(:outermost) { filler? >> exp.repeat }
|
11
|
+
|
12
|
+
rule(:filler?) { one_filler.repeat }
|
13
|
+
rule(:one_filler) { match('\s+') | match["\n"] | comment }
|
14
|
+
rule(:space) { match('\s+') }
|
15
|
+
rule(:comment) { str('#') >> (match["\n\r"].absent? >> any).repeat }
|
16
|
+
|
17
|
+
rule(:exp) {
|
18
|
+
section | assignment
|
19
|
+
}
|
20
|
+
rule(:assignment) {
|
21
|
+
(identifier >> values.maybe.as(:args)).as(:assignment) >> str(';') >> filler?
|
22
|
+
}
|
23
|
+
|
24
|
+
rule(:identifier) {
|
25
|
+
(match('[a-zA-Z]') >> match('[a-zA-Z0-9_]').repeat).as(:identifier) >> space >> space.repeat
|
26
|
+
}
|
27
|
+
|
28
|
+
rule(:value) {
|
29
|
+
((match('[#;{]').absent? >> any) >> (
|
30
|
+
str('\\') >> any | match('[#;{]|\s').absent? >> any
|
31
|
+
).repeat).as(:value) >> space.repeat
|
32
|
+
}
|
33
|
+
rule(:values) {
|
34
|
+
value.repeat >> space.maybe
|
35
|
+
}
|
36
|
+
|
37
|
+
rule(:section) {
|
38
|
+
identifier.as(:section) >> values.maybe.as(:args) >> str('{') >> filler? >> exp.repeat.as(:expressions) >> str('}') >> filler?
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
class NginxTransform < Parslet::Transform
|
43
|
+
Group = Struct.new(:id, :args, :body)
|
44
|
+
Exp = Struct.new(:key, :vals)
|
45
|
+
|
46
|
+
def self.assemble_binary(seq)
|
47
|
+
b = ErlangBitstream.new
|
48
|
+
seq.each { |i| b.add(i) }
|
49
|
+
b.value
|
50
|
+
end
|
51
|
+
|
52
|
+
rule(section: { identifier: simple(:x) }, args: subtree(:y), expressions: subtree(:z)) { Group.new(x.to_s, y, z) }
|
53
|
+
rule(assignment: { identifier: simple(:x), args: subtree(:y) }) { Exp.new(x.to_s, y) }
|
54
|
+
rule(value: simple(:x)) { x.to_s }
|
55
|
+
end
|
56
|
+
|
57
|
+
class NginxConfig
|
58
|
+
def self.parse(content)
|
59
|
+
lex = NginxParser.new.parse(content)
|
60
|
+
tree = NginxTransform.new.apply(lex)
|
61
|
+
gtree = NginxTransform::Group.new(nil, '', tree)
|
62
|
+
read_nginx_group(gtree)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.read_nginx_group(t)
|
66
|
+
agg_conf = Hash.new([])
|
67
|
+
agg_conf['_'] = t.args unless t.args == ''
|
68
|
+
|
69
|
+
groups, conf = t.body.partition { |i| i.is_a? NginxTransform::Group }
|
70
|
+
conf.each { |x| agg_conf[x.key] += [x.vals.join(' ')] }
|
71
|
+
groups.each { |x| agg_conf[x.id] += [read_nginx_group(x)] }
|
72
|
+
agg_conf
|
73
|
+
end
|
74
|
+
end
|
data/lib/utils/spdx.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Christoph Hartmann
|
3
|
+
# author: Dominik Richter
|
4
|
+
class Spdx
|
5
|
+
def self.licenses
|
6
|
+
spdx_file = File.join(File.dirname(__FILE__), 'spdx.txt').freeze
|
7
|
+
File.read(spdx_file).split("\n")
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.valid_license?(license)
|
11
|
+
licenses.include?(license)
|
12
|
+
end
|
13
|
+
end
|