inspec 0.33.2 → 0.34.0

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
  SHA1:
3
- metadata.gz: 2f4f2b2442e3a19f101d6d04c0a13815dc086b66
4
- data.tar.gz: 1b02e5addd71a28377e73c7080ab7b88f2af28d4
3
+ metadata.gz: d0a1f7cf81639eade2bf2ac0203d87420d7b8a31
4
+ data.tar.gz: a9f5f10e11a0b00cbe9f8ae60434e0e19337f5d3
5
5
  SHA512:
6
- metadata.gz: e6317eb3c488e0590a5ffa5bae91fca369d3662555423271f5bdbf348ae93b2947a2a1b613403699a5ecf46926f4e1fe873db827b23bebe3f7b9e561d1e4472a
7
- data.tar.gz: 9b18e3a35eda7e93cdfcfb0f1566090d7eaaae81e6ceeae02b2af205edc281f9a74cfd8b2b637f3a99a43dda66c3857099e8015745039e91fa17d91e0525bd86
6
+ metadata.gz: 2a53d8b5b6e0d51912192f08d7b42a5691142976c507373996ac8c05f76dea3ccd96c77476dce6c3f4bfb82e8c0aeb47818dd618385f4a8e88081a42957deb91
7
+ data.tar.gz: cc3dc4d250b1d2eb10253b69c49f0f0d270bade569ebad3474ebd359fb1e5948b6e7b74ff0a60c4bd3b3ad0594a32839619b9bed120a3336cc496e7343619c9a
data/CHANGELOG.md CHANGED
@@ -1,7 +1,41 @@
1
1
  # Change Log
2
2
 
3
- ## [0.33.2](https://github.com/chef/inspec/tree/0.33.2) (2016-09-07)
4
- [Full Changelog](https://github.com/chef/inspec/compare/v0.33.1...0.33.2)
3
+ ## [0.34.0](https://github.com/chef/inspec/tree/0.34.0) (2016-09-12)
4
+ [Full Changelog](https://github.com/chef/inspec/compare/v0.33.2...0.34.0)
5
+
6
+ **Implemented enhancements:**
7
+
8
+ - Vendor Github and Supermarket dependencies [\#959](https://github.com/chef/inspec/issues/959)
9
+ - use simple config for security policy resource [\#1044](https://github.com/chef/inspec/pull/1044) ([chris-rock](https://github.com/chris-rock))
10
+ - identify enabled/disabled accounts for windows [\#1039](https://github.com/chef/inspec/pull/1039) ([chris-rock](https://github.com/chris-rock))
11
+
12
+ **Closed issues:**
13
+
14
+ - Compliance should allow the ability to upload the unconverted SCAP profiles from the agencies. [\#1055](https://github.com/chef/inspec/issues/1055)
15
+ - Multiple matchers in a describe block display only a single line [\#1025](https://github.com/chef/inspec/issues/1025)
16
+ - Create all content for inspec homepage demo [\#1021](https://github.com/chef/inspec/issues/1021)
17
+ - User resource should use Filtertable [\#948](https://github.com/chef/inspec/issues/948)
18
+
19
+ **Merged pull requests:**
20
+
21
+ - rename example to meta-profile [\#1051](https://github.com/chef/inspec/pull/1051) ([chris-rock](https://github.com/chris-rock))
22
+ - fix webpack start script for tutorial [\#1050](https://github.com/chef/inspec/pull/1050) ([vjeffrey](https://github.com/vjeffrey))
23
+ - Add Inspec::Fetcher\#relative\_target for compatibility [\#1046](https://github.com/chef/inspec/pull/1046) ([stevendanna](https://github.com/stevendanna))
24
+ - Typo supermarket -\> compliance [\#1041](https://github.com/chef/inspec/pull/1041) ([stevendanna](https://github.com/stevendanna))
25
+ - Improve duplicate and cycle detection in resolver [\#1038](https://github.com/chef/inspec/pull/1038) ([stevendanna](https://github.com/stevendanna))
26
+ - Add example of corporate profile [\#1037](https://github.com/chef/inspec/pull/1037) ([stevendanna](https://github.com/stevendanna))
27
+ - Ensure simplecov starts before everything else [\#1036](https://github.com/chef/inspec/pull/1036) ([stevendanna](https://github.com/stevendanna))
28
+ - add sys\_info resource to get information about the hostname [\#1035](https://github.com/chef/inspec/pull/1035) ([chris-rock](https://github.com/chris-rock))
29
+ - Add GitFetcher and rework Fetchers+SourceReaders [\#1034](https://github.com/chef/inspec/pull/1034) ([stevendanna](https://github.com/stevendanna))
30
+ - add demo content [\#1033](https://github.com/chef/inspec/pull/1033) ([vjeffrey](https://github.com/vjeffrey))
31
+ - add health graphs [\#1032](https://github.com/chef/inspec/pull/1032) ([arlimus](https://github.com/arlimus))
32
+ - fix table formatting in readme [\#1031](https://github.com/chef/inspec/pull/1031) ([arlimus](https://github.com/arlimus))
33
+ - remove old delivery tests [\#1029](https://github.com/chef/inspec/pull/1029) ([arlimus](https://github.com/arlimus))
34
+ - make demo better [\#1015](https://github.com/chef/inspec/pull/1015) ([vjeffrey](https://github.com/vjeffrey))
35
+ - user resource should support filtertable [\#990](https://github.com/chef/inspec/pull/990) ([ksubrama](https://github.com/ksubrama))
36
+
37
+ ## [v0.33.2](https://github.com/chef/inspec/tree/v0.33.2) (2016-09-07)
38
+ [Full Changelog](https://github.com/chef/inspec/compare/v0.33.1...v0.33.2)
5
39
 
6
40
  **Implemented enhancements:**
7
41
 
data/README.md CHANGED
@@ -311,6 +311,11 @@ InSpec is inspired by the wonderful [Serverspec](http://serverspec.org) project.
311
311
  1. Create new Pull Request
312
312
 
313
313
 
314
+ The InSpec community and maintainers are very active and helpful. This project benefits greatly from this activity.
315
+
316
+ [![InSpec health](https://graphs.waffle.io/chef/inspec/throughput.svg)](https://waffle.io/chef/inspec/metrics/throughput)
317
+
318
+
314
319
  ## Testing InSpec
315
320
 
316
321
  We perform `unit`, `resource` and `integration` tests.
@@ -380,21 +385,15 @@ transport:
380
385
  ```
381
386
 
382
387
 
383
- ### Chef Delivery Tests
384
-
385
- It may be informative to look at what [tests Chef Delivery](https://github.com/chef/inspec/blob/master/.delivery/build-cookbook/recipes/unit.rb) is running for CI.
386
-
387
388
  ## License
388
389
 
389
- | **Author:** | Dominik Richter (<drichter@chef.io>)
390
-
391
- | **Author:** | Christoph Hartmann (<chartmann@chef.io>)
392
-
393
- | **Copyright:** | Copyright (c) 2015 Chef Software Inc.
394
-
395
- | **Copyright:** | Copyright (c) 2015 Vulcano Security GmbH.
396
-
397
- | **License:** | Apache License, Version 2.0
390
+ | | |
391
+ | ------ | --- |
392
+ | **Author:** | Dominik Richter (<drichter@chef.io>) |
393
+ | **Author:** | Christoph Hartmann (<chartmann@chef.io>) |
394
+ | **Copyright:** | Copyright (c) 2015 Chef Software Inc. |
395
+ | **Copyright:** | Copyright (c) 2015 Vulcano Security GmbH. |
396
+ | **License:** | Apache License, Version 2.0 |
398
397
 
399
398
  Licensed under the Apache License, Version 2.0 (the "License");
400
399
  you may not use this file except in compliance with the License.
@@ -0,0 +1,11 @@
1
+ # meta-profile
2
+
3
+ The inspec.yml file in this profile shows how one can use dependencies
4
+ from non-local sources such as Git or an HTTP url. This feature can
5
+ be used to build up a environment-wide profile that is based on more
6
+ specific profiles managed by others.
7
+
8
+ # WARNING
9
+
10
+ This profile likely does not work yet. It exists as a target for
11
+ ongoing development work.
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+ # copyright: 2015, The Authors
3
+ # license: All rights reserved
4
+ include_controls 'ssh-hardening'
5
+ include_controls 'os-hardening'
6
+ include_controls 'ssl-benchmark'
7
+ include_controls 'linux'
8
+ include_controls 'windows-patch-benchmark'
@@ -0,0 +1,19 @@
1
+ name: meta-profile
2
+ title: Meta Compliance Profile
3
+ maintainer: InSpec Authors
4
+ copyright: InSpec Authors
5
+ copyright_email: support@chef.io
6
+ license: Apache 2
7
+ summary: InSpec Profile that is only consuming dependencies
8
+ version: 0.2.0
9
+ depends:
10
+ - name: ssh-hardening
11
+ supermarket: hardening/ssh-hardening
12
+ - name: os-hardening
13
+ url: https://github.com/dev-sec/tests-os-hardening/archive/master.zip
14
+ - name: ssl-benchmark
15
+ git: https://github.com/dev-sec/ssl-benchmark.git
16
+ - name: windows-patch-benchmark
17
+ git: https://github.com/chris-rock/windows-patch-benchmark.git
18
+ - name: linux
19
+ compliance: base/linux
@@ -4,7 +4,6 @@
4
4
 
5
5
  require 'uri'
6
6
  require 'inspec/fetcher'
7
- require 'fetchers/url'
8
7
 
9
8
  # InSpec Target Helper for Chef Compliance
10
9
  # reuses UrlHelper, but it knows the target server and the access token already
@@ -14,11 +13,14 @@ module Compliance
14
13
  name 'compliance'
15
14
  priority 500
16
15
 
17
- def self.resolve(target, _opts = {})
18
- return nil unless target.is_a?(String)
19
- # check for local scheme compliance://
20
- uri = URI(target)
21
- return nil unless URI(uri).scheme == 'compliance'
16
+ def self.resolve(target)
17
+ uri = if target.is_a?(String) && URI(target).scheme == 'compliance'
18
+ URI(target)
19
+ elsif target.respond_to?(:key?) && target.key?(:compliance)
20
+ URI("compliance://#{target[:compliance]}")
21
+ end
22
+
23
+ return nil if uri.nil?
22
24
 
23
25
  # check if we have a compliance token
24
26
  config = Compliance::Configuration.new
@@ -27,18 +29,33 @@ module Compliance
27
29
  # verifies that the target e.g base/ssh exists
28
30
  profile = uri.host + uri.path
29
31
  Compliance::API.exist?(config, profile)
30
- super(target_url(config, profile), config)
32
+ new(target_url(profile, config), config)
31
33
  rescue URI::Error => _e
32
34
  nil
33
35
  end
34
36
 
35
- def self.target_url(config, profile)
37
+ def self.target_url(profile, config)
36
38
  owner, id = profile.split('/')
37
39
  "#{config['server']}/owners/#{owner}/compliance/#{id}/tar"
38
40
  end
39
41
 
42
+ #
43
+ # We want to save compliance: in the lockfile rather than url: to
44
+ # make sure we go back through the ComplianceAPI handling.
45
+ #
46
+ def resolved_source
47
+ { compliance: supermarket_profile_name }
48
+ end
49
+
40
50
  def to_s
41
51
  'Chef Compliance Profile Loader'
42
52
  end
53
+
54
+ private
55
+
56
+ def supermarket_profile_name
57
+ m = %r{^#{@config['server']}/owners/(?<owner>[^/]+)/compliance/(?<id>[^/]+)/tar$}.match(@target)
58
+ "#{m[:owner]}/#{m[:id]}"
59
+ end
43
60
  end
44
61
  end
@@ -9,18 +9,14 @@ module Supermarket
9
9
  class API
10
10
  SUPERMARKET_URL = 'https://supermarket.chef.io'.freeze
11
11
 
12
- def self.supermarket_url
13
- SUPERMARKET_URL
14
- end
15
-
16
12
  # displays a list of profiles
17
- def self.profiles
18
- url = "#{SUPERMARKET_URL}/api/v1/tools-search"
13
+ def self.profiles(supermarket_url = SUPERMARKET_URL)
14
+ url = "#{supermarket_url}/api/v1/tools-search"
19
15
  _success, data = get(url, { q: 'compliance_profile' })
20
16
  if !data.nil?
21
17
  profiles = JSON.parse(data)
22
18
  profiles['items'].map { |x|
23
- m = %r{^#{Supermarket::API.supermarket_url}/api/v1/tools/(?<slug>[\w-]+)(/)?$}.match(x['tool'])
19
+ m = %r{^#{supermarket_url}/api/v1/tools/(?<slug>[\w-]+)(/)?$}.match(x['tool'])
24
20
  x['slug'] = m[:slug]
25
21
  x
26
22
  }
@@ -37,10 +33,10 @@ module Supermarket
37
33
  end
38
34
 
39
35
  # displays profile infos
40
- def self.info(profile)
36
+ def self.info(profile, supermarket_url = SUPERMARKET_URL)
41
37
  _tool_owner, tool_name = profile_name("supermarket://#{profile}")
42
38
  return if tool_name.nil? || tool_name.empty?
43
- url = "#{SUPERMARKET_URL}/api/v1/tools/#{tool_name}"
39
+ url = "#{supermarket_url}/api/v1/tools/#{tool_name}"
44
40
  _success, data = get(url, {})
45
41
  JSON.parse(data) if !data.nil?
46
42
  rescue JSON::ParserError
@@ -48,24 +44,24 @@ module Supermarket
48
44
  end
49
45
 
50
46
  # compares a profile with the supermarket tool info
51
- def self.same?(profile, supermarket_tool)
47
+ def self.same?(profile, supermarket_tool, supermarket_url = SUPERMARKET_URL)
52
48
  tool_owner, tool_name = profile_name(profile)
53
- tool = "#{SUPERMARKET_URL}/api/v1/tools/#{tool_name}"
49
+ tool = "#{supermarket_url}/api/v1/tools/#{tool_name}"
54
50
  supermarket_tool['tool_owner'] == tool_owner && supermarket_tool['tool'] == tool
55
51
  end
56
52
 
57
- def self.find(profile)
58
- profiles = Supermarket::API.profiles
53
+ def self.find(profile, supermarket_url)
54
+ profiles = Supermarket::API.profiles(supermarket_url=SUPERMARKET_URL)
59
55
  if !profiles.empty?
60
- index = profiles.index { |t| same?(profile, t) }
56
+ index = profiles.index { |t| same?(profile, t, supermarket_url) }
61
57
  # return profile or nil
62
58
  profiles[index] if !index.nil? && index >= 0
63
59
  end
64
60
  end
65
61
 
66
62
  # verifies that a profile exists
67
- def self.exist?(profile)
68
- !find(profile).nil?
63
+ def self.exist?(profile, supermarket_url = SUPERMARKET_URL)
64
+ !find(profile, supermarket_url).nil?
69
65
  end
70
66
 
71
67
  def self.get(url, params)
@@ -8,16 +8,21 @@ require 'fetchers/url'
8
8
 
9
9
  # InSpec Target Helper for Supermarket
10
10
  module Supermarket
11
- class Fetcher < Fetchers::Url
11
+ class Fetcher < Inspec.fetcher(1)
12
12
  name 'supermarket'
13
13
  priority 500
14
14
 
15
15
  def self.resolve(target, opts = {})
16
- return nil unless target.is_a?(String)
17
- return nil unless URI(target).scheme == 'supermarket'
18
- return nil unless Supermarket::API.exist?(target)
19
- tool_info = Supermarket::API.find(target)
20
- super(tool_info['tool_source_url'], opts)
16
+ supermarket_uri, supermarket_server = if target.is_a?(String) && URI(target).scheme == 'supermarket'
17
+ [target, Supermarket::API::SUPERMARKET_URL]
18
+ elsif target.respond_to?(:key?) && target.key?(:supermarket)
19
+ supermarket_server = target[:supermarket_url] || Supermarket::API::SUPERMARKET_URL
20
+ ["supermarket://#{target[:supermarket]}", supermarket_server]
21
+ end
22
+ return nil unless supermarket_uri
23
+ return nil unless Supermarket::API.exist?(supermarket_uri, supermarket_server)
24
+ tool_info = Supermarket::API.find(supermarket_uri, supermarket_server)
25
+ resolve_next(tool_info['tool_source_url'], opts)
21
26
  rescue URI::Error
22
27
  nil
23
28
  end
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ require 'mixlib/shellout'
5
+ require 'inspec/log'
6
+
7
+ module Fetchers
8
+ #
9
+ # The git fetcher uses the git binary to fetch remote git sources.
10
+ # Git-based sources should be specified with the `git:` key in the
11
+ # source hash. Additionally, we accept `:branch`, `:ref`, and `:tag`
12
+ # keys to allow users to pin to a particular revision.
13
+ #
14
+ # Parts of this class are derived from:
15
+ #
16
+ # https://github.com/chef/omnibus/blob/master/lib/omnibus/fetchers/git_fetcher.rb
17
+ #
18
+ # which is Copyright 2012-2014 Chef Software, Inc. and offered under
19
+ # the same Apache 2 software license as inspec.
20
+ #
21
+ # Many thanks to the omnibus authors!
22
+ #
23
+ # Note that we haven't replicated all of omnibus' features here. If
24
+ # you got to this file during debugging, you may want to look at the
25
+ # omnibus source for hints.
26
+ #
27
+ class Git < Inspec.fetcher(1)
28
+ name 'git'
29
+ priority 200
30
+
31
+ def self.resolve(target, opts = {})
32
+ if target.respond_to?(:has_key?) &&target.key?(:git)
33
+ new(target[:git], opts.merge(target))
34
+ end
35
+ end
36
+
37
+ def initialize(remote_url, opts = {})
38
+ @branch = opts[:branch]
39
+ @tag = opts[:tag]
40
+ @ref = opts[:ref]
41
+ @remote_url = remote_url
42
+ @repo_directory = nil
43
+ end
44
+
45
+ def fetch(dir)
46
+ @repo_directory = dir
47
+ if cloned?
48
+ checkout
49
+ else
50
+ Dir.mktmpdir do |tmpdir|
51
+ checkout(tmpdir)
52
+ Inspec::Log.debug("Checkout of #{resolved_ref} successful. Moving checkout to #{dir}")
53
+ FileUtils.cp_r(tmpdir, @repo_directory)
54
+ end
55
+ end
56
+ @repo_directory
57
+ end
58
+
59
+ def archive_path
60
+ @repo_directory
61
+ end
62
+
63
+ def resolved_source
64
+ { git: @remote_url, ref: resolved_ref }
65
+ end
66
+
67
+ private
68
+
69
+ def resolved_ref
70
+ @resolved_ref ||= if @ref
71
+ @ref
72
+ elsif @branch
73
+ resolve_ref(@branch)
74
+ elsif @tag
75
+ resolve_ref(@tag)
76
+ else
77
+ resolve_ref('master')
78
+ end
79
+ end
80
+
81
+ def resolve_ref(ref_name)
82
+ cmd = shellout("git ls-remote \"#{@remote_url}\" \"#{ref_name}*\"")
83
+ ref = parse_ls_remote(cmd.stdout, ref_name)
84
+ if !ref
85
+ fail "Unable to resolve #{ref_name} to a specific git commit for #{@remote_url}"
86
+ end
87
+ ref
88
+ end
89
+
90
+ #
91
+ # The following comment is a minor modification of the comment in
92
+ # the omnibus source for a similar function:
93
+ #
94
+ # Dereference annotated tags.
95
+ #
96
+ # The +remote_list+ parameter is assumed to look like this:
97
+ #
98
+ # a2ed66c01f42514bcab77fd628149eccb4ecee28 refs/tags/rel-0.11.0
99
+ # f915286abdbc1907878376cce9222ac0b08b12b8 refs/tags/rel-0.11.0^{}
100
+ #
101
+ # The SHA with ^{} is the commit pointed to by an annotated
102
+ # tag. If ref isn't an annotated tag, there will not be a line
103
+ # with trailing ^{}.
104
+ #
105
+ # @param [String] output
106
+ # output from `git ls-remote origin` command
107
+ # @param [String] ref_name
108
+ # the target git ref_name
109
+ #
110
+ # @return [String]
111
+ #
112
+ def parse_ls_remote(output, ref_name)
113
+ pairs = output.lines.map { |l| l.chomp.split("\t") }
114
+ tagged_commit = pairs.find { |m| m[1].end_with?("#{ref_name}^{}") }
115
+ if tagged_commit
116
+ tagged_commit.first
117
+ else
118
+ found = pairs.find { |m| m[1].end_with?(ref_name.to_s) }
119
+ if found
120
+ found.first
121
+ end
122
+ end
123
+ end
124
+
125
+ def cloned?
126
+ File.directory?(File.join(@repo_directory, '.git'))
127
+ end
128
+
129
+ def clone(dir = @repo_directory)
130
+ git_cmd("clone #{@remote_url} ./", dir) unless cloned?
131
+ @repo_directory
132
+ end
133
+
134
+ def checkout(dir = @repo_directory)
135
+ clone(dir)
136
+ git_cmd("checkout #{resolved_ref}", dir)
137
+ @repo_directory
138
+ end
139
+
140
+ def git_cmd(cmd, dir = @repo_directory)
141
+ cmd = shellout("git #{cmd}", cwd: dir)
142
+ cmd.error!
143
+ cmd.status
144
+ rescue Errno::ENOENT
145
+ raise 'To use git sources, you must have git installed.'
146
+ end
147
+
148
+ def shellout(cmd, opts = {})
149
+ Inspec::Log.debug("Running external command: #{cmd} (#{opts})")
150
+ cmd = Mixlib::ShellOut.new(cmd, opts)
151
+ cmd.run_command
152
+ Inspec::Log.debug("External command: completed with exit status: #{cmd.exitstatus}")
153
+ Inspec::Log.debug('External command: STDOUT BEGIN')
154
+ Inspec::Log.debug(cmd.stdout)
155
+ Inspec::Log.debug('External command: STDOUT END')
156
+ Inspec::Log.debug('External command: STDERR BEGIN')
157
+ Inspec::Log.debug(cmd.stderr)
158
+ Inspec::Log.debug('External command: STDERR END')
159
+ cmd
160
+ end
161
+ end
162
+ end
@@ -7,11 +7,29 @@ module Fetchers
7
7
  name 'local'
8
8
  priority 0
9
9
 
10
- attr_reader :files
11
-
12
10
  def self.resolve(target)
13
- return nil unless target.is_a?(String)
11
+ local_path = if target.is_a?(String)
12
+ resolve_from_string(target)
13
+ elsif target.is_a?(Hash)
14
+ resolve_from_hash(target)
15
+ end
16
+
17
+ if local_path
18
+ new(local_path)
19
+ end
20
+ end
21
+
22
+ def self.resolve_from_hash(target)
23
+ if target.key?(:path)
24
+ local_path = target[:path]
25
+ if target.key?(:cwd)
26
+ local_path = File.expand_path(local_path, target[:cwd])
27
+ end
28
+ local_path
29
+ end
30
+ end
14
31
 
32
+ def self.resolve_from_string(target)
15
33
  # Support "urls" in the form of file://
16
34
  if target.start_with?('file://')
17
35
  target = target.gsub(%r{^file://}, '')
@@ -20,26 +38,25 @@ module Fetchers
20
38
  target = target.tr('\\', '/')
21
39
  end
22
40
 
23
- if !File.exist?(target)
24
- nil
25
- else
26
- new(target)
41
+ if File.exist?(target)
42
+ target
27
43
  end
28
44
  end
29
45
 
30
46
  def initialize(target)
31
47
  @target = target
32
- if File.file?(target)
33
- @files = [target]
34
- else
35
- @files = Dir[File.join(target, '**', '*')]
36
- end
37
48
  end
38
49
 
39
- def read(file)
40
- return nil unless files.include?(file)
41
- return nil unless File.file?(file)
42
- File.read(file)
50
+ def fetch(_path)
51
+ archive_path
52
+ end
53
+
54
+ def archive_path
55
+ @target
56
+ end
57
+
58
+ def resolved_source
59
+ { path: @target }
43
60
  end
44
61
  end
45
62
  end
data/lib/fetchers/mock.rb CHANGED
@@ -16,12 +16,16 @@ module Fetchers
16
16
  @data = data
17
17
  end
18
18
 
19
- def files
20
- @data.keys
19
+ def fetch(_path)
20
+ archive_path
21
21
  end
22
22
 
23
- def read(file)
24
- @data[file]
23
+ def archive_path
24
+ { mock: @data }
25
+ end
26
+
27
+ def resolved_source
28
+ { mock_fetcher: true }
25
29
  end
26
30
  end
27
31
  end