inspec 1.13.0 → 1.14.0
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/CHANGELOG.md +20 -2
- data/Gemfile +1 -1
- data/examples/meta-profile/inspec.lock +18 -0
- data/examples/meta-profile/vendor/3d473e72d8b70018386a53e0a105e92ccbb4115dc268cadc16ff53d550d2898e.tar.gz +0 -0
- data/examples/meta-profile/vendor/793adcbb91cfc2da0044bb9cbf0863773ae2cf89ce9b8343b4295b137f70897b.tar.gz +0 -0
- data/examples/meta-profile/vendor/e25d521fb1093b4c23b31a7dc8f41b5540236f4a433960b151bc427523662ab6.tar.gz +0 -0
- data/lib/bundles/inspec-artifact/cli.rb +6 -6
- data/lib/bundles/inspec-compliance/http.rb +11 -3
- data/lib/bundles/inspec-compliance/target.rb +2 -2
- data/lib/bundles/inspec-supermarket/cli.rb +1 -1
- data/lib/fetchers/git.rb +1 -1
- data/lib/inspec/backend.rb +2 -2
- data/lib/inspec/base_cli.rb +1 -1
- data/lib/inspec/cached_fetcher.rb +2 -2
- data/lib/inspec/cli.rb +1 -0
- data/lib/inspec/control_eval_context.rb +0 -2
- data/lib/inspec/dependencies/lockfile.rb +6 -4
- data/lib/inspec/dependencies/requirement.rb +1 -1
- data/lib/inspec/dependencies/resolver.rb +4 -4
- data/lib/inspec/dsl.rb +2 -2
- data/lib/inspec/fetcher.rb +1 -1
- data/lib/inspec/file_provider.rb +4 -4
- data/lib/inspec/library_eval_context.rb +1 -1
- data/lib/inspec/objects/list.rb +1 -1
- data/lib/inspec/plugins.rb +1 -1
- data/lib/inspec/plugins/fetcher.rb +4 -4
- data/lib/inspec/plugins/resource.rb +0 -1
- data/lib/inspec/plugins/source_reader.rb +3 -3
- data/lib/inspec/profile.rb +4 -4
- data/lib/inspec/profile_context.rb +1 -1
- data/lib/inspec/resource.rb +2 -2
- data/lib/inspec/runner.rb +4 -4
- data/lib/inspec/secrets.rb +1 -1
- data/lib/inspec/shell.rb +1 -1
- data/lib/inspec/source_reader.rb +1 -1
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +7 -7
- data/lib/resources/apache_conf.rb +1 -1
- data/lib/resources/auditd_conf.rb +1 -1
- data/lib/resources/auditd_rules.rb +1 -1
- data/lib/resources/bridge.rb +1 -1
- data/lib/resources/etc_group.rb +2 -2
- data/lib/resources/file.rb +6 -6
- data/lib/resources/groups.rb +4 -4
- data/lib/resources/grub_conf.rb +3 -3
- data/lib/resources/host.rb +1 -1
- data/lib/resources/inetd_conf.rb +1 -1
- data/lib/resources/interface.rb +1 -1
- data/lib/resources/json.rb +1 -1
- data/lib/resources/limits_conf.rb +1 -1
- data/lib/resources/login_def.rb +1 -1
- data/lib/resources/mysql_conf.rb +1 -1
- data/lib/resources/ntp_conf.rb +1 -1
- data/lib/resources/packages.rb +2 -2
- data/lib/resources/parse_config.rb +1 -1
- data/lib/resources/port.rb +2 -2
- data/lib/resources/postgres_conf.rb +1 -1
- data/lib/resources/security_policy.rb +1 -1
- data/lib/resources/ssh_conf.rb +1 -1
- data/lib/resources/ssl.rb +1 -1
- data/lib/resources/users.rb +5 -5
- data/lib/resources/xinetd.rb +1 -1
- data/lib/utils/command_wrapper.rb +3 -3
- data/lib/utils/filter.rb +1 -1
- data/lib/utils/plugin_registry.rb +3 -3
- data/lib/utils/simpleconfig.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb888c0cb724974ef7d939097b4f2b035653d6d7
|
4
|
+
data.tar.gz: 9e7eb4ff65ea768f8fe1782ce998a1c0ed825896
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd794bbc14da92dc306a9c7b8faa71a2d4fecd272c95918de9de5e59ac5669e8d93b880684f2263b751a10ff20bc9eb843b06692c00673abba3af22707dba85e
|
7
|
+
data.tar.gz: a6e46c9012ad7d3a368d90f3f0912c02ab7ca62a90619af69d204f2864b641e22448f32b3679337f231665ca2fda820d490ddcfb7fea2bb71b93c82bd5a51192
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,25 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [1.
|
4
|
-
[Full Changelog](https://github.com/chef/inspec/compare/v1.
|
3
|
+
## [1.14.0](https://github.com/chef/inspec/tree/1.14.0) (2017-02-09)
|
4
|
+
[Full Changelog](https://github.com/chef/inspec/compare/v1.13.0...1.14.0)
|
5
|
+
|
6
|
+
**Fixed bugs:**
|
7
|
+
|
8
|
+
- map url to https for compliance plugin [\#1471](https://github.com/chef/inspec/pull/1471) ([arlimus](https://github.com/arlimus))
|
9
|
+
|
10
|
+
**Closed issues:**
|
11
|
+
|
12
|
+
- Display meaningful error message when uploading profiles to a server with self-signed certs [\#1469](https://github.com/chef/inspec/issues/1469)
|
13
|
+
|
14
|
+
**Merged pull requests:**
|
15
|
+
|
16
|
+
- Use RuboCop 0.39.0 \(same as chefstyle\) [\#1478](https://github.com/chef/inspec/pull/1478) ([tduffield](https://github.com/tduffield))
|
17
|
+
- bugfix: warn users about insecure login requirements [\#1472](https://github.com/chef/inspec/pull/1472) ([arlimus](https://github.com/arlimus))
|
18
|
+
- Add support for "inspec -v" showing the version [\#1470](https://github.com/chef/inspec/pull/1470) ([adamleff](https://github.com/adamleff))
|
19
|
+
- Replace slack invite form on Community, fix surprise code example [\#1468](https://github.com/chef/inspec/pull/1468) ([adamleff](https://github.com/adamleff))
|
20
|
+
|
21
|
+
## [v1.13.0](https://github.com/chef/inspec/tree/v1.13.0) (2017-02-07)
|
22
|
+
[Full Changelog](https://github.com/chef/inspec/compare/v1.12.0...v1.13.0)
|
5
23
|
|
6
24
|
**Implemented enhancements:**
|
7
25
|
|
data/Gemfile
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
---
|
2
|
+
lockfile_version: 1
|
3
|
+
depends:
|
4
|
+
- name: dev-sec/ssh-baseline
|
5
|
+
resolved_source:
|
6
|
+
url: https://github.com/dev-sec/ssh-baseline/archive/master.tar.gz
|
7
|
+
sha256: e25d521fb1093b4c23b31a7dc8f41b5540236f4a433960b151bc427523662ab6
|
8
|
+
version_constraints: ">= 0"
|
9
|
+
- name: ssl-benchmark
|
10
|
+
resolved_source:
|
11
|
+
url: https://github.com/dev-sec/ssl-benchmark/archive/master.tar.gz
|
12
|
+
sha256: 793adcbb91cfc2da0044bb9cbf0863773ae2cf89ce9b8343b4295b137f70897b
|
13
|
+
version_constraints: ">= 0"
|
14
|
+
- name: windows-patch-benchmark
|
15
|
+
resolved_source:
|
16
|
+
url: https://github.com/chris-rock/windows-patch-benchmark/archive/master.tar.gz
|
17
|
+
sha256: 3d473e72d8b70018386a53e0a105e92ccbb4115dc268cadc16ff53d550d2898e
|
18
|
+
version_constraints: ">= 0"
|
Binary file
|
Binary file
|
Binary file
|
@@ -154,17 +154,17 @@ module Artifact
|
|
154
154
|
p = Pathname.new(path_to_profile)
|
155
155
|
p = p.join('inspec.yml')
|
156
156
|
if not p.exist?
|
157
|
-
|
157
|
+
raise "#{path_to_profile} doesn't appear to be a valid Inspec profile"
|
158
158
|
end
|
159
159
|
yaml = YAML.load_file(p.to_s)
|
160
160
|
yaml = yaml.to_hash
|
161
161
|
|
162
162
|
if not yaml.key? 'name'
|
163
|
-
|
163
|
+
raise 'Profile is invalid, name is not defined'
|
164
164
|
end
|
165
165
|
|
166
166
|
if not yaml.key? 'version'
|
167
|
-
|
167
|
+
raise 'Profile is invalid, version is not defined'
|
168
168
|
end
|
169
169
|
rescue => e
|
170
170
|
# rewrap it and pass it up to the CLI
|
@@ -212,15 +212,15 @@ module Artifact
|
|
212
212
|
public_keyfile = "#{file_keyname}.pem.pub"
|
213
213
|
puts "Looking for #{public_keyfile} to verify artifact"
|
214
214
|
if not File.exist? public_keyfile
|
215
|
-
|
215
|
+
raise "Can't find #{public_keyfile}"
|
216
216
|
end
|
217
217
|
|
218
218
|
if not VALID_PROFILE_DIGESTS.member? file_alg
|
219
|
-
|
219
|
+
raise 'Invalid artifact digest algorithm detected'
|
220
220
|
end
|
221
221
|
|
222
222
|
if not VALID_PROFILE_VERSIONS.member? file_version
|
223
|
-
|
223
|
+
raise 'Invalid artifact version detected'
|
224
224
|
end
|
225
225
|
end
|
226
226
|
|
@@ -10,6 +10,7 @@ module Compliance
|
|
10
10
|
class HTTP
|
11
11
|
# generic get requires
|
12
12
|
def self.get(url, headers = nil, insecure)
|
13
|
+
url = "https://#{url}" if URI.parse(url).scheme.nil?
|
13
14
|
uri = URI.parse(url)
|
14
15
|
req = Net::HTTP::Get.new(uri.path)
|
15
16
|
if !headers.nil?
|
@@ -38,7 +39,7 @@ module Compliance
|
|
38
39
|
# post a file
|
39
40
|
def self.post_file(url, headers, file_path, insecure)
|
40
41
|
uri = URI.parse(url)
|
41
|
-
|
42
|
+
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
|
42
43
|
http = Net::HTTP.new(uri.host, uri.port)
|
43
44
|
|
44
45
|
# set connection flags
|
@@ -67,11 +68,18 @@ module Compliance
|
|
67
68
|
}
|
68
69
|
opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if insecure
|
69
70
|
|
70
|
-
|
71
|
-
res = Net::HTTP.start(uri.host, uri.port, opts) {|http|
|
71
|
+
raise "Unable to parse URI: #{uri}" if uri.nil? || uri.host.nil?
|
72
|
+
res = Net::HTTP.start(uri.host, uri.port, opts) { |http|
|
72
73
|
http.request(req)
|
73
74
|
}
|
74
75
|
res
|
76
|
+
|
77
|
+
rescue OpenSSL::SSL::SSLError => e
|
78
|
+
raise e unless e.message.include? 'certificate verify failed'
|
79
|
+
|
80
|
+
puts "Error: Failed to connect to #{uri}."
|
81
|
+
puts 'If the server uses a self-signed certificate, please re-run the login command with the --insecure option.'
|
82
|
+
exit 1
|
75
83
|
end
|
76
84
|
end
|
77
85
|
end
|
@@ -37,7 +37,7 @@ module Compliance
|
|
37
37
|
server = 'compliance'
|
38
38
|
msg = "inspec compliance login https://your_compliance_server --user admin --insecure --token 'PASTE TOKEN HERE' "
|
39
39
|
end
|
40
|
-
|
40
|
+
raise Inspec::FetcherFailure, <<EOF
|
41
41
|
|
42
42
|
Cannot fetch #{uri} because your #{server} token has not been
|
43
43
|
configured.
|
@@ -51,7 +51,7 @@ EOF
|
|
51
51
|
# verifies that the target e.g base/ssh exists
|
52
52
|
profile = uri.host + uri.path
|
53
53
|
if !Compliance::API.exist?(config, profile)
|
54
|
-
|
54
|
+
raise Inspec::FetcherFailure, "The compliance profile #{profile} was not found on the configured compliance server"
|
55
55
|
end
|
56
56
|
profile_fetch_url = Compliance::API.target_url(config, profile)
|
57
57
|
end
|
data/lib/fetchers/git.rb
CHANGED
@@ -86,7 +86,7 @@ module Fetchers
|
|
86
86
|
cmd = shellout("git ls-remote \"#{@remote_url}\" \"#{ref_name}*\"")
|
87
87
|
ref = parse_ls_remote(cmd.stdout, ref_name)
|
88
88
|
if !ref
|
89
|
-
|
89
|
+
raise "Unable to resolve #{ref_name} to a specific git commit for #{@remote_url}"
|
90
90
|
end
|
91
91
|
ref
|
92
92
|
end
|
data/lib/inspec/backend.rb
CHANGED
@@ -17,12 +17,12 @@ module Inspec
|
|
17
17
|
name = Train.validate_backend(conf)
|
18
18
|
transport = Train.create(name, conf)
|
19
19
|
if transport.nil?
|
20
|
-
|
20
|
+
raise "Can't find transport backend '#{name}'."
|
21
21
|
end
|
22
22
|
|
23
23
|
connection = transport.connection
|
24
24
|
if connection.nil?
|
25
|
-
|
25
|
+
raise "Can't connect to transport backend '#{name}'."
|
26
26
|
end
|
27
27
|
|
28
28
|
cls = Class.new do
|
data/lib/inspec/base_cli.rb
CHANGED
@@ -12,7 +12,7 @@ module Inspec
|
|
12
12
|
@fetcher = Inspec::Fetcher.resolve(target)
|
13
13
|
|
14
14
|
if @fetcher.nil?
|
15
|
-
|
15
|
+
raise("Could not fetch inspec profile in #{target.inspect}.")
|
16
16
|
end
|
17
17
|
|
18
18
|
@cache = cache
|
@@ -50,7 +50,7 @@ module Inspec
|
|
50
50
|
def assert_cache_sanity!
|
51
51
|
if target.respond_to?(:key?) && target.key?(:sha256)
|
52
52
|
if fetcher.resolved_source[:sha256] != target[:sha256]
|
53
|
-
|
53
|
+
raise <<EOF
|
54
54
|
The remote source #{fetcher} no longer has the requested content:
|
55
55
|
|
56
56
|
Request Content Hash: #{target[:sha256]}
|
data/lib/inspec/cli.rb
CHANGED
@@ -35,8 +35,6 @@ module Inspec
|
|
35
35
|
# @param profile_context [Inspec::ProfileContext]
|
36
36
|
# @param outer_dsl [OuterDSLClass]
|
37
37
|
# @return [ProfileContextClass]
|
38
|
-
#
|
39
|
-
# rubocop:disable Lint/NestedMethodDefinition
|
40
38
|
def self.create(profile_context, resources_dsl) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
41
39
|
rule_class = rule_context(resources_dsl)
|
42
40
|
profile_context_owner = profile_context
|
@@ -18,7 +18,7 @@ module Inspec
|
|
18
18
|
def self.from_content(content)
|
19
19
|
parsed_content = YAML.load(content)
|
20
20
|
version = parsed_content['lockfile_version']
|
21
|
-
|
21
|
+
raise "No lockfile_version set in #{path}!" if version.nil?
|
22
22
|
validate_lockfile_version!(version.to_i)
|
23
23
|
new(parsed_content)
|
24
24
|
end
|
@@ -28,9 +28,10 @@ module Inspec
|
|
28
28
|
from_content(content)
|
29
29
|
end
|
30
30
|
|
31
|
+
# rubocop:disable Style/GuardClause
|
31
32
|
def self.validate_lockfile_version!(version)
|
32
33
|
if version < MINIMUM_SUPPORTED_VERSION
|
33
|
-
|
34
|
+
raise <<EOF
|
34
35
|
This lockfile specifies a lockfile_version of #{version} which is
|
35
36
|
lower than the minimum supported version #{MINIMUM_SUPPORTED_VERSION}.
|
36
37
|
|
@@ -39,7 +40,7 @@ Please create a new lockfile for this project by running:
|
|
39
40
|
inspec vendor
|
40
41
|
EOF
|
41
42
|
elsif version > CURRENT_LOCKFILE_VERSION
|
42
|
-
|
43
|
+
raise <<EOF
|
43
44
|
This lockfile claims to be version #{version} which is greater than
|
44
45
|
the most recent lockfile version(#{CURRENT_LOCKFILE_VERSION}).
|
45
46
|
|
@@ -48,6 +49,7 @@ used to create the lockfile.
|
|
48
49
|
EOF
|
49
50
|
end
|
50
51
|
end
|
52
|
+
# rubocop:enable Style/GuardClause
|
51
53
|
|
52
54
|
attr_reader :version, :deps
|
53
55
|
def initialize(lockfile_content_hash)
|
@@ -80,7 +82,7 @@ EOF
|
|
80
82
|
else
|
81
83
|
# If we've gotten here, there is likely a mistake in the
|
82
84
|
# lockfile version validation in the constructor.
|
83
|
-
|
85
|
+
raise "No lockfile parser for version #{version}"
|
84
86
|
end
|
85
87
|
end
|
86
88
|
|
@@ -10,7 +10,7 @@ module Inspec
|
|
10
10
|
#
|
11
11
|
class Requirement
|
12
12
|
def self.from_metadata(dep, cache, opts)
|
13
|
-
|
13
|
+
raise 'Cannot load empty dependency.' if dep.nil? || dep.empty?
|
14
14
|
new(dep[:name], dep[:version], cache, opts[:cwd], opts.merge(dep))
|
15
15
|
end
|
16
16
|
|
@@ -26,7 +26,7 @@ module Inspec
|
|
26
26
|
def self.resolve(dependencies, cache, working_dir, backend)
|
27
27
|
reqs = dependencies.map do |dep|
|
28
28
|
req = Inspec::Requirement.from_metadata(dep, cache, cwd: working_dir, backend: backend)
|
29
|
-
req ||
|
29
|
+
req || raise("Cannot initialize dependency: #{req}")
|
30
30
|
end
|
31
31
|
new.resolve(reqs)
|
32
32
|
end
|
@@ -40,7 +40,7 @@ module Inspec
|
|
40
40
|
else
|
41
41
|
"the dependency information for #{path_string.split(' ').last}"
|
42
42
|
end
|
43
|
-
|
43
|
+
raise Inspec::DuplicateDep, "The dependency #{dep.name} is listed twice in #{problem_cookbook}"
|
44
44
|
else
|
45
45
|
seen_items_local << dep.name
|
46
46
|
end
|
@@ -65,13 +65,13 @@ module Inspec
|
|
65
65
|
end
|
66
66
|
|
67
67
|
if new_seen_items.key?(dep.resolved_source)
|
68
|
-
|
68
|
+
raise Inspec::CyclicDependencyError, "Dependency #{dep} would cause a dependency cycle (#{new_path_string})"
|
69
69
|
else
|
70
70
|
new_seen_items[dep.resolved_source] = true
|
71
71
|
end
|
72
72
|
|
73
73
|
if !dep.source_satisfies_spec?
|
74
|
-
|
74
|
+
raise Inspec::UnsatisfiedVersionSpecification, "The profile #{dep.name} from #{dep.resolved_source} has a version #{dep.source_version} which doesn't match #{dep.required_version}"
|
75
75
|
end
|
76
76
|
|
77
77
|
Inspec::Log.debug("Adding dependency #{dep.name} (#{dep.resolved_source})")
|
data/lib/inspec/dsl.rb
CHANGED
@@ -19,7 +19,7 @@ module Inspec::DSL
|
|
19
19
|
alias include_rules include_controls
|
20
20
|
|
21
21
|
def require_resource(options = {})
|
22
|
-
|
22
|
+
raise 'You must specify a specific resource name when calling require_resource()' if options[:resource].nil?
|
23
23
|
|
24
24
|
from_profile = options[:profile] || profile_name
|
25
25
|
target_name = options[:as] || options[:resource]
|
@@ -33,7 +33,7 @@ module Inspec::DSL
|
|
33
33
|
|
34
34
|
dep_entry = dependencies.list[profile_id]
|
35
35
|
if dep_entry.nil?
|
36
|
-
|
36
|
+
raise <<EOF
|
37
37
|
Cannot load #{profile_id} since it is not listed as a dependency
|
38
38
|
of #{bind_context.profile_name}.
|
39
39
|
|
data/lib/inspec/fetcher.rb
CHANGED
data/lib/inspec/file_provider.rb
CHANGED
@@ -17,7 +17,7 @@ module Inspec
|
|
17
17
|
elsif File.exist?(path)
|
18
18
|
DirProvider.new(path)
|
19
19
|
else
|
20
|
-
|
20
|
+
raise "No file provider for the provided path: #{path}"
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -25,11 +25,11 @@ module Inspec
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def read(_file)
|
28
|
-
|
28
|
+
raise "#{self} does not implement `read(...)`. This is required."
|
29
29
|
end
|
30
30
|
|
31
31
|
def files
|
32
|
-
|
32
|
+
raise "Fetcher #{self} does not implement `files()`. This is required."
|
33
33
|
end
|
34
34
|
|
35
35
|
def relative_provider
|
@@ -148,7 +148,7 @@ module Inspec
|
|
148
148
|
@parent = parent_provider
|
149
149
|
@prefix = get_prefix(parent.files)
|
150
150
|
if @prefix.nil?
|
151
|
-
|
151
|
+
raise "Could not determine path prefix for #{parent}"
|
152
152
|
end
|
153
153
|
@files = parent.files.find_all { |x| x.start_with?(prefix) && x != prefix }
|
154
154
|
.map { |x| x[prefix.length..-1] }
|
data/lib/inspec/objects/list.rb
CHANGED
data/lib/inspec/plugins.rb
CHANGED
@@ -51,7 +51,7 @@ module Inspec
|
|
51
51
|
def load(name)
|
52
52
|
path = @registry[name]
|
53
53
|
if path.nil?
|
54
|
-
|
54
|
+
raise "Couldn't find plugin #{name}. Searching in #{@home}"
|
55
55
|
end
|
56
56
|
# puts "Loading plugin #{name} from #{path}"
|
57
57
|
require path
|
@@ -36,7 +36,7 @@ module Inspec
|
|
36
36
|
# profile.
|
37
37
|
#
|
38
38
|
def archive_path
|
39
|
-
|
39
|
+
raise "Fetcher #{self} does not implement `archive_path()`. This is required."
|
40
40
|
end
|
41
41
|
|
42
42
|
#
|
@@ -49,7 +49,7 @@ module Inspec
|
|
49
49
|
# /foo/bar/baz.zip
|
50
50
|
#
|
51
51
|
def fetch(_path)
|
52
|
-
|
52
|
+
raise "Fetcher #{self} does not implement `fetch()`. This is required."
|
53
53
|
end
|
54
54
|
|
55
55
|
#
|
@@ -59,14 +59,14 @@ module Inspec
|
|
59
59
|
# tag will be resolved to an exact revision.
|
60
60
|
#
|
61
61
|
def resolved_source
|
62
|
-
|
62
|
+
raise "Fetcher #{self} does not implement `resolved_source()`. This is required for terminal fetchers."
|
63
63
|
end
|
64
64
|
|
65
65
|
#
|
66
66
|
# The unique key based on the content of the remote archive.
|
67
67
|
#
|
68
68
|
def cache_key
|
69
|
-
|
69
|
+
raise "Fetcher #{self} does not implement `cache_key()`. This is required for terminal fetchers."
|
70
70
|
end
|
71
71
|
|
72
72
|
#
|