inspec 1.0.0 → 1.35.1
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 +933 -101
- data/Gemfile +8 -17
- data/MAINTAINERS.md +2 -1
- data/MAINTAINERS.toml +9 -4
- data/README.md +49 -28
- data/Rakefile +54 -39
- data/bin/inspec +1 -1
- data/docs/dsl_inspec.md +17 -4
- data/docs/dsl_resource.md +1 -1
- data/docs/habitat.md +192 -0
- data/docs/inspec_and_friends.md +4 -2
- data/docs/matchers.md +11 -0
- data/docs/migration.md +293 -0
- data/docs/profiles.md +124 -76
- data/docs/resources/aide_conf.md.erb +81 -0
- data/docs/resources/apache_conf.md.erb +10 -10
- data/docs/resources/apt.md.erb +13 -13
- data/docs/resources/audit_policy.md.erb +10 -10
- data/docs/resources/auditd_conf.md.erb +10 -10
- data/docs/resources/auditd_rules.md.erb +11 -11
- data/docs/resources/bash.md.erb +12 -12
- data/docs/resources/bond.md.erb +15 -15
- data/docs/resources/bridge.md.erb +11 -11
- data/docs/resources/bsd_service.md.erb +11 -11
- data/docs/resources/command.md.erb +23 -23
- data/docs/resources/crontab.md.erb +84 -0
- data/docs/resources/csv.md.erb +10 -10
- data/docs/resources/dh_params.md +191 -0
- data/docs/resources/directory.md.erb +8 -8
- data/docs/resources/docker.md.erb +160 -0
- data/docs/resources/docker_container.md.erb +89 -0
- data/docs/resources/docker_image.md.erb +86 -0
- data/docs/resources/etc_group.md.erb +16 -16
- data/docs/resources/etc_hosts.md.erb +62 -0
- data/docs/resources/file.md.erb +80 -58
- data/docs/resources/gem.md.erb +32 -13
- data/docs/resources/group.md.erb +12 -12
- data/docs/resources/grub_conf.md.erb +11 -11
- data/docs/resources/host.md.erb +27 -18
- data/docs/resources/http.md.erb +104 -0
- data/docs/resources/iis_app.md.erb +126 -0
- data/docs/resources/iis_site.md.erb +17 -17
- data/docs/resources/inetd_conf.md.erb +10 -10
- data/docs/resources/ini.md.erb +9 -9
- data/docs/resources/interface.md.erb +13 -13
- data/docs/resources/iptables.md.erb +21 -14
- data/docs/resources/json.md.erb +11 -11
- data/docs/resources/kernel_module.md.erb +73 -12
- data/docs/resources/kernel_parameter.md.erb +12 -12
- data/docs/resources/key_rsa.md.erb +70 -0
- data/docs/resources/launchd_service.md.erb +11 -11
- data/docs/resources/limits_conf.md.erb +10 -10
- data/docs/resources/login_def.md.erb +12 -12
- data/docs/resources/mount.md.erb +14 -14
- data/docs/resources/mssql_session.md.erb +62 -0
- data/docs/resources/mysql_conf.md.erb +14 -14
- data/docs/resources/mysql_session.md.erb +22 -10
- data/docs/resources/npm.md.erb +13 -13
- data/docs/resources/ntp_conf.md.erb +9 -9
- data/docs/resources/oneget.md.erb +11 -11
- data/docs/resources/oracledb_session.md.erb +53 -0
- data/docs/resources/os.md.erb +17 -17
- data/docs/resources/os_env.md.erb +12 -12
- data/docs/resources/package.md.erb +29 -16
- data/docs/resources/parse_config.md.erb +18 -18
- data/docs/resources/parse_config_file.md.erb +29 -23
- data/docs/resources/{etc_passwd.md.erb → passwd.md.erb} +17 -17
- data/docs/resources/pip.md.erb +15 -15
- data/docs/resources/port.md.erb +23 -23
- data/docs/resources/postgres_conf.md.erb +15 -13
- data/docs/resources/postgres_hba_conf.md.erb +104 -0
- data/docs/resources/postgres_ident_conf.md.erb +87 -0
- data/docs/resources/postgres_session.md.erb +28 -21
- data/docs/resources/powershell.md.erb +14 -14
- data/docs/resources/processes.md.erb +123 -0
- data/docs/resources/rabbitmq_config.md.erb +55 -0
- data/docs/resources/registry_key.md.erb +44 -21
- data/docs/resources/runit_service.md.erb +11 -11
- data/docs/resources/security_policy.md.erb +10 -10
- data/docs/resources/service.md.erb +18 -18
- data/docs/resources/{etc_shadow.md.erb → shadow.md.erb} +19 -19
- data/docs/resources/ssh_config.md.erb +13 -13
- data/docs/resources/sshd_config.md.erb +15 -15
- data/docs/resources/ssl.md.erb +13 -13
- data/docs/resources/sys_info.md.erb +10 -10
- data/docs/resources/systemd_service.md.erb +11 -11
- data/docs/resources/sysv_service.md.erb +11 -11
- data/docs/resources/upstart_service.md.erb +11 -11
- data/docs/resources/user.md.erb +20 -20
- data/docs/resources/users.md.erb +19 -19
- data/docs/resources/vbscript.md.erb +13 -13
- data/docs/resources/virtualization.md.erb +71 -0
- data/docs/resources/windows_feature.md.erb +10 -10
- data/docs/resources/windows_task.md.erb +103 -0
- data/docs/resources/wmi.md.erb +10 -10
- data/docs/resources/x509_certificate.md.erb +146 -0
- data/docs/resources/xinetd_conf.md.erb +17 -17
- data/docs/resources/xml.md.erb +75 -0
- data/docs/resources/yaml.md.erb +25 -11
- data/docs/resources/yum.md.erb +24 -16
- data/docs/resources/zfs_dataset.md.erb +63 -0
- data/docs/resources/zfs_pool.md.erb +57 -0
- data/docs/ruby_usage.md +50 -0
- data/docs/shared/matcher_cmp.md.erb +4 -6
- data/examples/inheritance/README.md +49 -3
- data/examples/inheritance/controls/example.rb +4 -1
- data/examples/inheritance/inspec.yml +3 -1
- data/examples/meta-profile/README.md +29 -3
- data/examples/meta-profile/controls/example.rb +10 -5
- data/examples/meta-profile/inspec.yml +4 -8
- data/examples/profile/README.md +1 -1
- data/examples/profile/controls/example.rb +0 -1
- data/examples/profile/controls/gordon.rb +0 -1
- data/examples/profile/inspec.yml +1 -1
- data/examples/profile-attribute/inspec.yml +1 -1
- data/examples/profile-sensitive/README.md +29 -0
- data/examples/profile-sensitive/controls/sensitive-failures.rb +9 -0
- data/examples/profile-sensitive/controls/sensitive.rb +9 -0
- data/examples/profile-sensitive/inspec.yml +8 -0
- data/inspec.gemspec +10 -2
- data/lib/bundles/inspec-artifact/README.md +1 -0
- data/lib/bundles/inspec-artifact/cli.rb +284 -0
- data/lib/bundles/inspec-artifact.rb +7 -0
- data/lib/bundles/inspec-compliance/README.md +44 -15
- data/lib/bundles/inspec-compliance/api.rb +135 -29
- data/lib/bundles/inspec-compliance/cli.rb +128 -21
- data/lib/bundles/inspec-compliance/configuration.rb +8 -0
- data/lib/bundles/inspec-compliance/http.rb +26 -19
- data/lib/bundles/inspec-compliance/target.rb +50 -25
- data/lib/bundles/inspec-habitat/cli.rb +31 -0
- data/lib/bundles/inspec-habitat/log.rb +10 -0
- data/lib/bundles/inspec-habitat/profile.rb +372 -0
- data/lib/bundles/inspec-habitat.rb +12 -0
- data/lib/bundles/inspec-init/templates/profile/controls/example.rb +0 -1
- data/lib/bundles/inspec-init/templates/profile/inspec.yml +1 -1
- data/lib/bundles/inspec-supermarket/api.rb +7 -5
- data/lib/bundles/inspec-supermarket/cli.rb +2 -2
- data/lib/fetchers/git.rb +4 -2
- data/lib/fetchers/local.rb +4 -1
- data/lib/fetchers/url.rb +64 -10
- data/lib/inspec/archive/tar.rb +1 -1
- data/lib/inspec/backend.rb +26 -3
- data/lib/inspec/base_cli.rb +44 -7
- data/lib/inspec/cached_fetcher.rb +2 -2
- data/lib/inspec/cli.rb +34 -12
- data/lib/inspec/completions/fish.sh.erb +34 -0
- data/lib/inspec/control_eval_context.rb +0 -2
- data/lib/inspec/dependencies/cache.rb +0 -1
- data/lib/inspec/dependencies/dependency_set.rb +2 -2
- data/lib/inspec/dependencies/lockfile.rb +13 -6
- data/lib/inspec/dependencies/requirement.rb +6 -6
- data/lib/inspec/dependencies/resolver.rb +4 -4
- data/lib/inspec/dsl.rb +3 -5
- data/lib/inspec/env_printer.rb +9 -1
- data/lib/inspec/exceptions.rb +8 -0
- data/lib/inspec/fetcher.rb +1 -1
- data/lib/inspec/file_provider.rb +47 -11
- data/lib/inspec/library_eval_context.rb +1 -1
- data/lib/inspec/metadata.rb +62 -18
- data/lib/inspec/method_source.rb +24 -0
- data/lib/inspec/objects/attribute.rb +21 -0
- data/lib/inspec/objects/control.rb +21 -3
- data/lib/inspec/objects/each_loop.rb +2 -1
- data/lib/inspec/objects/list.rb +1 -1
- data/lib/inspec/objects/or_test.rb +17 -2
- data/lib/inspec/objects/tag.rb +27 -0
- data/lib/inspec/objects/test.rb +9 -8
- data/lib/inspec/objects.rb +1 -0
- data/lib/inspec/plugins/fetcher.rb +4 -5
- data/lib/inspec/plugins/resource.rb +1 -2
- data/lib/inspec/plugins/source_reader.rb +3 -3
- data/lib/inspec/plugins.rb +1 -1
- data/lib/inspec/polyfill.rb +0 -1
- data/lib/inspec/profile.rb +92 -23
- data/lib/inspec/profile_context.rb +1 -1
- data/lib/inspec/profile_vendor.rb +66 -0
- data/lib/inspec/resource.rb +29 -3
- data/lib/inspec/rspec_json_formatter.rb +672 -285
- data/lib/inspec/rule.rb +12 -16
- data/lib/inspec/runner.rb +26 -15
- data/lib/inspec/runner_rspec.rb +1 -0
- data/lib/inspec/runtime_profile.rb +26 -0
- data/lib/inspec/schema.rb +185 -0
- data/lib/inspec/secrets/yaml.rb +8 -1
- data/lib/inspec/secrets.rb +1 -1
- data/lib/inspec/shell.rb +92 -22
- data/lib/inspec/source_reader.rb +1 -1
- data/lib/inspec/version.rb +1 -1
- data/lib/inspec.rb +1 -1
- data/lib/matchers/matchers.rb +67 -17
- data/lib/resources/aide_conf.rb +162 -0
- data/lib/resources/apache.rb +0 -1
- data/lib/resources/apache_conf.rb +3 -3
- data/lib/resources/apt.rb +2 -1
- data/lib/resources/audit_policy.rb +0 -1
- data/lib/resources/auditd_conf.rb +1 -2
- data/lib/resources/auditd_rules.rb +5 -5
- data/lib/resources/bond.rb +1 -1
- data/lib/resources/bridge.rb +1 -1
- data/lib/resources/command.rb +5 -3
- data/lib/resources/crontab.rb +102 -0
- data/lib/resources/csv.rb +18 -1
- data/lib/resources/dh_params.rb +84 -0
- data/lib/resources/directory.rb +7 -3
- data/lib/resources/docker.rb +198 -0
- data/lib/resources/docker_container.rb +114 -0
- data/lib/resources/docker_image.rb +96 -0
- data/lib/resources/etc_group.rb +2 -3
- data/lib/resources/etc_hosts.rb +81 -0
- data/lib/resources/file.rb +88 -30
- data/lib/resources/gem.rb +26 -6
- data/lib/resources/groups.rb +4 -5
- data/lib/resources/grub_conf.rb +3 -4
- data/lib/resources/host.rb +175 -35
- data/lib/resources/http.rb +73 -0
- data/lib/resources/iis_app.rb +103 -0
- data/lib/resources/iis_site.rb +23 -0
- data/lib/resources/inetd_conf.rb +9 -4
- data/lib/resources/interface.rb +4 -3
- data/lib/resources/json.rb +37 -14
- data/lib/resources/kernel_module.rb +77 -10
- data/lib/resources/kernel_parameter.rb +0 -1
- data/lib/resources/key_rsa.rb +67 -0
- data/lib/resources/limits_conf.rb +3 -4
- data/lib/resources/login_def.rb +2 -3
- data/lib/resources/mount.rb +34 -4
- data/lib/resources/mssql_session.rb +81 -12
- data/lib/resources/mysql.rb +0 -1
- data/lib/resources/mysql_conf.rb +1 -2
- data/lib/resources/mysql_session.rb +18 -7
- data/lib/resources/nginx_conf.rb +95 -0
- data/lib/resources/ntp_conf.rb +2 -3
- data/lib/resources/oracledb_session.rb +138 -0
- data/lib/resources/os_env.rb +7 -5
- data/lib/resources/package.rb +70 -13
- data/lib/resources/packages.rb +111 -0
- data/lib/resources/parse_config.rb +3 -4
- data/lib/resources/passwd.rb +4 -5
- data/lib/resources/pip.rb +29 -21
- data/lib/resources/port.rb +133 -14
- data/lib/resources/postgres.rb +53 -43
- data/lib/resources/postgres_conf.rb +40 -13
- data/lib/resources/postgres_hba_conf.rb +101 -0
- data/lib/resources/postgres_ident_conf.rb +79 -0
- data/lib/resources/postgres_session.rb +28 -20
- data/lib/resources/powershell.rb +2 -2
- data/lib/resources/processes.rb +61 -26
- data/lib/resources/rabbitmq_conf.rb +53 -0
- data/lib/resources/registry_key.rb +34 -11
- data/lib/resources/security_policy.rb +2 -2
- data/lib/resources/service.rb +17 -7
- data/lib/resources/ssh_conf.rb +17 -6
- data/lib/resources/ssl.rb +3 -4
- data/lib/resources/sys_info.rb +1 -1
- data/lib/resources/toml.rb +26 -0
- data/lib/resources/users.rb +41 -41
- data/lib/resources/virtualization.rb +252 -0
- data/lib/resources/windows_task.rb +106 -0
- data/lib/resources/x509_certificate.rb +143 -0
- data/lib/resources/xinetd.rb +18 -2
- data/lib/resources/xml.rb +27 -0
- data/lib/resources/yaml.rb +10 -2
- data/lib/resources/yum.rb +25 -7
- data/lib/resources/zfs_dataset.rb +60 -0
- data/lib/resources/zfs_pool.rb +49 -0
- data/lib/source_readers/flat.rb +2 -1
- data/lib/source_readers/inspec.rb +30 -11
- data/lib/utils/command_wrapper.rb +3 -3
- data/lib/utils/database_helpers.rb +77 -0
- data/lib/utils/erlang_parser.rb +192 -0
- data/lib/utils/filter.rb +2 -1
- data/lib/utils/filter_array.rb +0 -1
- data/lib/utils/find_files.rb +1 -2
- data/lib/utils/latest_version.rb +21 -0
- data/lib/utils/nginx_parser.rb +76 -0
- data/lib/utils/object_traversal.rb +5 -4
- data/lib/utils/parser.rb +23 -4
- data/lib/utils/plugin_registry.rb +3 -3
- data/lib/utils/simpleconfig.rb +25 -8
- data/lib/utils/spdx.rb +13 -0
- data/lib/utils/spdx.txt +344 -0
- metadata +168 -22
- data/docs/cli.md +0 -342
- data/docs/resources/process.md.erb +0 -73
- /data/docs/{plugin_kitchen_inspec.html.md → plugin_kitchen_inspec.md} +0 -0
|
@@ -9,16 +9,13 @@ module Compliance
|
|
|
9
9
|
# implements a simple http abstraction on top of Net::HTTP
|
|
10
10
|
class HTTP
|
|
11
11
|
# generic get requires
|
|
12
|
-
def self.get(url,
|
|
13
|
-
uri =
|
|
12
|
+
def self.get(url, headers = nil, insecure)
|
|
13
|
+
uri = _parse_url(url)
|
|
14
14
|
req = Net::HTTP::Get.new(uri.path)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
req.basic_auth(token, '')
|
|
20
|
-
else
|
|
21
|
-
req['Authorization'] = "Bearer #{token}"
|
|
15
|
+
if !headers.nil?
|
|
16
|
+
headers.each do |key, value|
|
|
17
|
+
req.add_field(key, value)
|
|
18
|
+
end
|
|
22
19
|
end
|
|
23
20
|
send_request(uri, req, insecure)
|
|
24
21
|
end
|
|
@@ -26,7 +23,7 @@ module Compliance
|
|
|
26
23
|
# generic post request
|
|
27
24
|
def self.post(url, token, insecure, basic_auth = false)
|
|
28
25
|
# form request
|
|
29
|
-
uri =
|
|
26
|
+
uri = _parse_url(url)
|
|
30
27
|
req = Net::HTTP::Post.new(uri.path)
|
|
31
28
|
if basic_auth
|
|
32
29
|
req.basic_auth token, ''
|
|
@@ -39,9 +36,9 @@ module Compliance
|
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
# post a file
|
|
42
|
-
def self.post_file(url,
|
|
43
|
-
uri =
|
|
44
|
-
|
|
39
|
+
def self.post_file(url, headers, file_path, insecure)
|
|
40
|
+
uri = _parse_url(url)
|
|
41
|
+
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
|
|
45
42
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
46
43
|
|
|
47
44
|
# set connection flags
|
|
@@ -49,10 +46,8 @@ module Compliance
|
|
|
49
46
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
|
|
50
47
|
|
|
51
48
|
req = Net::HTTP::Post.new(uri.path)
|
|
52
|
-
|
|
53
|
-
req.
|
|
54
|
-
else
|
|
55
|
-
req['Authorization'] = "Bearer #{token}"
|
|
49
|
+
headers.each do |key, value|
|
|
50
|
+
req.add_field(key, value)
|
|
56
51
|
end
|
|
57
52
|
|
|
58
53
|
req.body_stream=File.open(file_path, 'rb')
|
|
@@ -72,11 +67,23 @@ module Compliance
|
|
|
72
67
|
}
|
|
73
68
|
opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if insecure
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
res = Net::HTTP.start(uri.host, uri.port, opts) {|http|
|
|
70
|
+
raise "Unable to parse URI: #{uri}" if uri.nil? || uri.host.nil?
|
|
71
|
+
res = Net::HTTP.start(uri.host, uri.port, opts) { |http|
|
|
77
72
|
http.request(req)
|
|
78
73
|
}
|
|
79
74
|
res
|
|
75
|
+
|
|
76
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
77
|
+
raise e unless e.message.include? 'certificate verify failed'
|
|
78
|
+
|
|
79
|
+
puts "Error: Failed to connect to #{uri}."
|
|
80
|
+
puts 'If the server uses a self-signed certificate, please re-run the login command with the --insecure option.'
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self._parse_url(url)
|
|
85
|
+
url = "https://#{url}" if URI.parse(url).scheme.nil?
|
|
86
|
+
URI.parse(url)
|
|
80
87
|
end
|
|
81
88
|
end
|
|
82
89
|
end
|
|
@@ -13,8 +13,7 @@ module Compliance
|
|
|
13
13
|
class Fetcher < Fetchers::Url
|
|
14
14
|
name 'compliance'
|
|
15
15
|
priority 500
|
|
16
|
-
|
|
17
|
-
def self.resolve(target)
|
|
16
|
+
def self.resolve(target) # rubocop:disable PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
18
17
|
uri = if target.is_a?(String) && URI(target).scheme == 'compliance'
|
|
19
18
|
URI(target)
|
|
20
19
|
elsif target.respond_to?(:key?) && target.key?(:compliance)
|
|
@@ -23,41 +22,54 @@ module Compliance
|
|
|
23
22
|
|
|
24
23
|
return nil if uri.nil?
|
|
25
24
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
# we have detailed information available in our lockfile, no need to ask the server
|
|
26
|
+
if target.respond_to?(:key?) && target.key?(:url)
|
|
27
|
+
profile_fetch_url = target[:url]
|
|
28
|
+
config = {}
|
|
29
|
+
else
|
|
30
|
+
# check if we have a compliance token
|
|
31
|
+
config = Compliance::Configuration.new
|
|
32
|
+
if config['token'].nil? && config['refresh_token'].nil?
|
|
33
|
+
if config['server_type'] == 'automate'
|
|
34
|
+
server = 'automate'
|
|
35
|
+
msg = 'inspec compliance login_automate https://your_automate_server --user USER --ent ENT --dctoken DCTOKEN or --usertoken USERTOKEN'
|
|
36
|
+
else
|
|
37
|
+
server = 'compliance'
|
|
38
|
+
msg = "inspec compliance login https://your_compliance_server --user admin --insecure --token 'PASTE TOKEN HERE' "
|
|
39
|
+
end
|
|
40
|
+
raise Inspec::FetcherFailure, <<EOF
|
|
30
41
|
|
|
31
|
-
Cannot fetch #{uri} because your
|
|
42
|
+
Cannot fetch #{uri} because your #{server} token has not been
|
|
32
43
|
configured.
|
|
33
44
|
|
|
34
45
|
Please login using
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
#{msg}
|
|
37
48
|
EOF
|
|
38
|
-
|
|
49
|
+
end
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
# verifies that the target e.g base/ssh exists
|
|
52
|
+
profile = Compliance::API.sanitize_profile_name(uri)
|
|
53
|
+
if !Compliance::API.exist?(config, profile)
|
|
54
|
+
raise Inspec::FetcherFailure, "The compliance profile #{profile} was not found on the configured compliance server"
|
|
55
|
+
end
|
|
56
|
+
profile_fetch_url = Compliance::API.target_url(config, profile)
|
|
44
57
|
end
|
|
45
|
-
|
|
58
|
+
# We need to pass the token to the fetcher
|
|
59
|
+
config['token'] = Compliance::API.get_token(config)
|
|
60
|
+
new(profile_fetch_url, config)
|
|
46
61
|
rescue URI::Error => _e
|
|
47
62
|
nil
|
|
48
63
|
end
|
|
49
64
|
|
|
50
|
-
def self.target_url(profile, config)
|
|
51
|
-
owner, id = profile.split('/')
|
|
52
|
-
"#{config['server']}/owners/#{owner}/compliance/#{id}/tar"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
#
|
|
56
65
|
# We want to save compliance: in the lockfile rather than url: to
|
|
57
|
-
# make sure we go back through the
|
|
58
|
-
#
|
|
66
|
+
# make sure we go back through the Compliance API handling.
|
|
59
67
|
def resolved_source
|
|
60
|
-
|
|
68
|
+
@resolved_source ||= {
|
|
69
|
+
compliance: compliance_profile_name,
|
|
70
|
+
url: @target,
|
|
71
|
+
sha256: sha256,
|
|
72
|
+
}
|
|
61
73
|
end
|
|
62
74
|
|
|
63
75
|
def to_s
|
|
@@ -66,8 +78,21 @@ EOF
|
|
|
66
78
|
|
|
67
79
|
private
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
# determine the owner_id and the profile name from the url
|
|
82
|
+
def compliance_profile_name
|
|
83
|
+
m = if Compliance::API.is_automate_server_pre_080?(@config)
|
|
84
|
+
%r{^#{@config['server']}/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
|
|
85
|
+
elsif Compliance::API.is_automate_server_080_and_later?(@config)
|
|
86
|
+
%r{^#{@config['server']}/profiles/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
|
|
87
|
+
else
|
|
88
|
+
%r{^#{@config['server']}/owners/(?<owner>[^/]+)/compliance/(?<id>[^/]+)/tar$}
|
|
89
|
+
end.match(@target)
|
|
90
|
+
|
|
91
|
+
raise 'Unable to determine compliance profile name. This can be caused by ' \
|
|
92
|
+
'an incorrect server in your configuration. Try to login to compliance ' \
|
|
93
|
+
'via the `inspec compliance login` or `inspec compliance login_automate` ' \
|
|
94
|
+
'commands.' if m.nil?
|
|
95
|
+
|
|
71
96
|
"#{m[:owner]}/#{m[:id]}"
|
|
72
97
|
end
|
|
73
98
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# author: Adam Leff
|
|
3
|
+
|
|
4
|
+
require 'thor'
|
|
5
|
+
|
|
6
|
+
module Habitat
|
|
7
|
+
class HabitatProfileCLI < Thor
|
|
8
|
+
namespace 'habitat profile'
|
|
9
|
+
|
|
10
|
+
desc 'create PATH', 'Create a Habitat artifact for the profile found at PATH'
|
|
11
|
+
option :output_dir, type: :string, required: false,
|
|
12
|
+
desc: 'Directory in which to save the generated Habitat artifact. Default: current directory'
|
|
13
|
+
def create(path)
|
|
14
|
+
Habitat::Profile.create(path, options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc 'upload PATH', 'Create a Habitat artifact for the profile found at PATH, and upload it to a Habitat Depot'
|
|
18
|
+
def upload(path)
|
|
19
|
+
Habitat::Profile.upload(path, options)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class HabitatCLI < Inspec::BaseCLI
|
|
24
|
+
namespace 'habitat'
|
|
25
|
+
|
|
26
|
+
desc 'profile', 'Manage InSpec profiles as Habitat artifacts'
|
|
27
|
+
subcommand 'profile', HabitatProfileCLI
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Inspec::Plugins::CLI.add_subcommand(HabitatCLI, 'habitat', 'habitat SUBCOMMAND ...', 'Commands for InSpec + Habitat Integration', {})
|
|
31
|
+
end
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# author: Adam Leff
|
|
3
|
+
|
|
4
|
+
require 'inspec/profile_vendor'
|
|
5
|
+
require 'mixlib/shellout'
|
|
6
|
+
require 'toml'
|
|
7
|
+
|
|
8
|
+
module Habitat
|
|
9
|
+
class Profile # rubocop:disable Metrics/ClassLength
|
|
10
|
+
attr_reader :options, :path, :profile
|
|
11
|
+
|
|
12
|
+
def self.create(path, options = {})
|
|
13
|
+
creator = new(path, options)
|
|
14
|
+
hart_file = creator.create
|
|
15
|
+
creator.copy(hart_file)
|
|
16
|
+
ensure
|
|
17
|
+
creator.delete_work_dir
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.upload(path, options = {})
|
|
21
|
+
uploader = new(path, options)
|
|
22
|
+
uploader.upload
|
|
23
|
+
ensure
|
|
24
|
+
uploader.delete_work_dir
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(path, options = {})
|
|
28
|
+
@path = path
|
|
29
|
+
@options = options
|
|
30
|
+
|
|
31
|
+
log_level = options.fetch('log_level', 'info')
|
|
32
|
+
Habitat::Log.level(log_level.to_sym)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create
|
|
36
|
+
Habitat::Log.info("Creating a Habitat artifact for profile: #{path}")
|
|
37
|
+
|
|
38
|
+
validate_habitat_installed
|
|
39
|
+
validate_habitat_origin
|
|
40
|
+
create_profile_object
|
|
41
|
+
verify_profile
|
|
42
|
+
vendor_profile_dependencies
|
|
43
|
+
copy_profile_to_work_dir
|
|
44
|
+
create_plan
|
|
45
|
+
create_run_hook
|
|
46
|
+
create_settings_file
|
|
47
|
+
create_default_config
|
|
48
|
+
|
|
49
|
+
# returns the path to the .hart file in the work directory
|
|
50
|
+
build_hart
|
|
51
|
+
rescue => e
|
|
52
|
+
Habitat::Log.debug(e.backtrace.join("\n"))
|
|
53
|
+
exit_with_error(
|
|
54
|
+
'Unable to generate Habitat artifact.',
|
|
55
|
+
"#{e.class} -- #{e.message}",
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def copy(hart_file)
|
|
60
|
+
validate_output_dir
|
|
61
|
+
|
|
62
|
+
Habitat::Log.info("Copying artifact to #{output_dir}...")
|
|
63
|
+
copy_hart(hart_file)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def upload
|
|
67
|
+
validate_habitat_auth_token
|
|
68
|
+
hart_file = create
|
|
69
|
+
upload_hart(hart_file)
|
|
70
|
+
rescue => e
|
|
71
|
+
Habitat::Log.debug(e.backtrace.join("\n"))
|
|
72
|
+
exit_with_error(
|
|
73
|
+
'Unable to upload Habitat artifact.',
|
|
74
|
+
"#{e.class} -- #{e.message}",
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def delete_work_dir
|
|
79
|
+
Habitat::Log.debug("Deleting work directory #{work_dir}")
|
|
80
|
+
FileUtils.rm_rf(work_dir) if Dir.exist?(work_dir)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def create_profile_object
|
|
86
|
+
@profile = Inspec::Profile.for_target(
|
|
87
|
+
path,
|
|
88
|
+
backend: Inspec::Backend.create(target: 'mock://'),
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def verify_profile
|
|
93
|
+
Habitat::Log.info('Checking to see if the profile is valid...')
|
|
94
|
+
|
|
95
|
+
unless profile.check[:summary][:valid]
|
|
96
|
+
exit_with_error('Profile check failed. Please fix the profile before creating a Habitat artifact.')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Habitat::Log.info('Profile is valid.')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def vendor_profile_dependencies
|
|
103
|
+
profile_vendor = Inspec::ProfileVendor.new(path)
|
|
104
|
+
if profile_vendor.lockfile.exist? && profile_vendor.cache_path.exist?
|
|
105
|
+
Habitat::Log.info("Profile's dependencies are already vendored, skipping vendor process.")
|
|
106
|
+
else
|
|
107
|
+
Habitat::Log.info("Vendoring the profile's dependencies...")
|
|
108
|
+
profile_vendor.vendor!
|
|
109
|
+
|
|
110
|
+
Habitat::Log.info('Ensuring all vendored content has read permissions...')
|
|
111
|
+
profile_vendor.make_readable
|
|
112
|
+
|
|
113
|
+
# refresh the profile object since the profile now has new files
|
|
114
|
+
create_profile_object
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_habitat_installed
|
|
119
|
+
Habitat::Log.info('Checking to see if Habitat is installed...')
|
|
120
|
+
cmd = Mixlib::ShellOut.new('hab --version')
|
|
121
|
+
cmd.run_command
|
|
122
|
+
if cmd.error?
|
|
123
|
+
exit_with_error('Unable to run Habitat commands.', cmd.stderr)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate_habitat_origin
|
|
128
|
+
if habitat_origin.nil?
|
|
129
|
+
exit_with_error(
|
|
130
|
+
'Unable to determine Habitat origin name.',
|
|
131
|
+
'Run `hab setup` or set the HAB_ORIGIN environment variable.',
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def validate_habitat_auth_token
|
|
137
|
+
if habitat_auth_token.nil?
|
|
138
|
+
exit_with_error(
|
|
139
|
+
'Unable to determine Habitat auth token for publishing.',
|
|
140
|
+
'Run `hab setup` or set the HAB_AUTH_TOKEN environment variable.',
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def validate_output_dir
|
|
146
|
+
exit_with_error("Output directory #{output_dir} is not a directory or does not exist.") unless
|
|
147
|
+
File.directory?(output_dir)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def work_dir
|
|
151
|
+
return @work_dir if @work_dir
|
|
152
|
+
|
|
153
|
+
@work_dir ||= Dir.mktmpdir('inspec-habitat-exporter')
|
|
154
|
+
Dir.mkdir(File.join(@work_dir, 'src'))
|
|
155
|
+
Dir.mkdir(File.join(@work_dir, 'habitat'))
|
|
156
|
+
Dir.mkdir(File.join(@work_dir, 'habitat', 'config'))
|
|
157
|
+
Dir.mkdir(File.join(@work_dir, 'habitat', 'hooks'))
|
|
158
|
+
Habitat::Log.debug("Generated work directory #{@work_dir}")
|
|
159
|
+
|
|
160
|
+
@work_dir
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def copy_profile_to_work_dir
|
|
164
|
+
Habitat::Log.info('Copying profile contents to the work directory...')
|
|
165
|
+
profile.files.each do |f|
|
|
166
|
+
src = File.join(profile.root_path, f)
|
|
167
|
+
dst = File.join(work_dir, 'src', f)
|
|
168
|
+
if File.directory?(f)
|
|
169
|
+
Habitat::Log.debug("Creating directory #{dst}")
|
|
170
|
+
FileUtils.mkdir_p(dst)
|
|
171
|
+
else
|
|
172
|
+
Habitat::Log.debug("Copying file #{src} to #{dst}")
|
|
173
|
+
FileUtils.cp_r(src, dst)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def create_plan
|
|
179
|
+
plan_file = File.join(work_dir, 'habitat', 'plan.sh')
|
|
180
|
+
Habitat::Log.info("Generating Habitat plan at #{plan_file}...")
|
|
181
|
+
File.write(plan_file, plan_contents)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def create_run_hook
|
|
185
|
+
run_hook_file = File.join(work_dir, 'habitat', 'hooks', 'run')
|
|
186
|
+
Habitat::Log.info("Generating a Habitat run hook at #{run_hook_file}...")
|
|
187
|
+
File.write(run_hook_file, run_hook_contents)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def create_settings_file
|
|
191
|
+
settings_file = File.join(work_dir, 'habitat', 'config', 'settings.sh')
|
|
192
|
+
Habitat::Log.info("Generating a settings file at #{settings_file}...")
|
|
193
|
+
File.write(settings_file, "SLEEP_TIME={{cfg.sleep_time}}\n")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def create_default_config
|
|
197
|
+
default_toml = File.join(work_dir, 'habitat', 'default.toml')
|
|
198
|
+
Habitat::Log.info("Generating Habitat's default.toml configuration...")
|
|
199
|
+
File.write(default_toml, 'sleep_time = 300')
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def build_hart
|
|
203
|
+
Habitat::Log.info('Building our Habitat artifact...')
|
|
204
|
+
|
|
205
|
+
env = {
|
|
206
|
+
'TERM' => 'vt100',
|
|
207
|
+
'HAB_ORIGIN' => habitat_origin,
|
|
208
|
+
'HAB_NONINTERACTIVE' => 'true',
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
env['RUST_LOG'] = 'debug' if Habitat::Log.level == :debug
|
|
212
|
+
|
|
213
|
+
# TODO: Would love to use Mixlib::ShellOut here, but it doesn't
|
|
214
|
+
# seem to preserve the STDIN tty, and docker gets angry.
|
|
215
|
+
Dir.chdir(work_dir) do
|
|
216
|
+
unless system(env, 'hab pkg build .')
|
|
217
|
+
exit_with_error('Unable to build the Habitat artifact.')
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
hart_files = Dir.glob(File.join(work_dir, 'results', '*.hart'))
|
|
222
|
+
|
|
223
|
+
if hart_files.length > 1
|
|
224
|
+
exit_with_error('More than one Habitat artifact was created which was not expected.')
|
|
225
|
+
elsif hart_files.empty?
|
|
226
|
+
exit_with_error('No Habitat artifact was created.')
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
hart_files.first
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def copy_hart(working_dir_hart)
|
|
233
|
+
hart_basename = File.basename(working_dir_hart)
|
|
234
|
+
dst = File.join(output_dir, hart_basename)
|
|
235
|
+
FileUtils.cp(working_dir_hart, dst)
|
|
236
|
+
|
|
237
|
+
dst
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def upload_hart(hart_file)
|
|
241
|
+
Habitat::Log.info('Uploading the Habitat artifact to our Depot...')
|
|
242
|
+
|
|
243
|
+
env = {
|
|
244
|
+
'TERM' => 'vt100',
|
|
245
|
+
'HAB_AUTH_TOKEN' => habitat_auth_token,
|
|
246
|
+
'HAB_NONINTERACTIVE' => 'true',
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
env['HAB_DEPOT_URL'] = ENV['HAB_DEPOT_URL'] if ENV['HAB_DEPOT_URL']
|
|
250
|
+
|
|
251
|
+
cmd = Mixlib::ShellOut.new("hab pkg upload #{hart_file}", env: env)
|
|
252
|
+
cmd.run_command
|
|
253
|
+
if cmd.error?
|
|
254
|
+
exit_with_error(
|
|
255
|
+
'Unable to upload Habitat artifact to the Depot.',
|
|
256
|
+
cmd.stdout,
|
|
257
|
+
cmd.stderr,
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
Habitat::Log.info('Upload complete!')
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def habitat_origin
|
|
265
|
+
ENV['HAB_ORIGIN'] || habitat_cli_config['origin']
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def habitat_auth_token
|
|
269
|
+
ENV['HAB_AUTH_TOKEN'] || habitat_cli_config['auth_token']
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def habitat_cli_config
|
|
273
|
+
return @cli_config if @cli_config
|
|
274
|
+
|
|
275
|
+
config_file = File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')
|
|
276
|
+
return {} unless File.exist?(config_file)
|
|
277
|
+
|
|
278
|
+
@cli_config = TOML.load_file(config_file)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def output_dir
|
|
282
|
+
options[:output_dir] || Dir.pwd
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def exit_with_error(*errors)
|
|
286
|
+
errors.each do |error_msg|
|
|
287
|
+
Habitat::Log.error(error_msg)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
exit 1
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def package_name
|
|
294
|
+
"inspec-profile-#{profile.name}"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def plan_contents
|
|
298
|
+
plan = <<-EOL
|
|
299
|
+
pkg_name=#{package_name}
|
|
300
|
+
pkg_version=#{profile.version}
|
|
301
|
+
pkg_origin=#{habitat_origin}
|
|
302
|
+
pkg_deps=(chef/inspec core/ruby core/hab)
|
|
303
|
+
pkg_svc_user=root
|
|
304
|
+
EOL
|
|
305
|
+
|
|
306
|
+
plan += "pkg_license='#{profile.metadata.params[:license]}'\n\n" if profile.metadata.params[:license]
|
|
307
|
+
|
|
308
|
+
plan += <<-EOL
|
|
309
|
+
|
|
310
|
+
do_build() {
|
|
311
|
+
cp -vr $PLAN_CONTEXT/../src/* $HAB_CACHE_SRC_PATH/$pkg_dirname
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
do_install() {
|
|
315
|
+
local profile_contents
|
|
316
|
+
local excludes
|
|
317
|
+
profile_contents=($(ls))
|
|
318
|
+
excludes=(habitat results *.hart)
|
|
319
|
+
|
|
320
|
+
for item in ${excludes[@]}; do
|
|
321
|
+
profile_contents=(${profile_contents[@]/$item/})
|
|
322
|
+
done
|
|
323
|
+
|
|
324
|
+
cp -r ${profile_contents[@]} ${pkg_prefix}/dist/
|
|
325
|
+
}
|
|
326
|
+
EOL
|
|
327
|
+
|
|
328
|
+
plan
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def run_hook_contents
|
|
332
|
+
<<-EOL
|
|
333
|
+
#!/bin/sh
|
|
334
|
+
|
|
335
|
+
# redirect stderr to stdout
|
|
336
|
+
# ultimately, we'd like to log this somewhere useful, but due to
|
|
337
|
+
# https://github.com/habitat-sh/habitat/issues/2395, we need to
|
|
338
|
+
# avoid doing that for now.
|
|
339
|
+
exec 2>&1
|
|
340
|
+
|
|
341
|
+
# InSpec will try to create a .cache directory in the user's home directory
|
|
342
|
+
# so this needs to be someplace writeable by the hab user
|
|
343
|
+
export HOME={{pkg.svc_var_path}}
|
|
344
|
+
|
|
345
|
+
PROFILE_IDENT="{{pkg.origin}}/{{pkg.name}}"
|
|
346
|
+
RESULTS_DIR="{{pkg.svc_var_path}}/inspec_results"
|
|
347
|
+
RESULTS_FILE="${RESULTS_DIR}/{{pkg.name}}.json"
|
|
348
|
+
|
|
349
|
+
# Create a directory for inspec formatter output
|
|
350
|
+
mkdir -p {{pkg.svc_var_path}}/inspec_results
|
|
351
|
+
|
|
352
|
+
while true; do
|
|
353
|
+
echo "Executing InSpec for ${PROFILE_IDENT}"
|
|
354
|
+
inspec exec {{pkg.path}}/dist --format=json > ${RESULTS_FILE}
|
|
355
|
+
|
|
356
|
+
if [ $? -eq 0 ]; then
|
|
357
|
+
echo "InSpec run completed successfully."
|
|
358
|
+
else
|
|
359
|
+
echo "InSpec run did not complete successfully. If you do not see any errors above,"
|
|
360
|
+
echo "control failures were detected. Check the InSpec results here for details:"
|
|
361
|
+
echo ${RESULTS_FILE}
|
|
362
|
+
echo "Otherwise, troubleshoot any errors shown above."
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
source {{pkg.svc_config_path}}/settings.sh
|
|
366
|
+
echo "sleeping for ${SLEEP_TIME} seconds"
|
|
367
|
+
sleep ${SLEEP_TIME}
|
|
368
|
+
done
|
|
369
|
+
EOL
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# author: Adam Leff
|
|
3
|
+
|
|
4
|
+
libdir = File.dirname(__FILE__)
|
|
5
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
|
6
|
+
|
|
7
|
+
module Habitat
|
|
8
|
+
autoload :Log, 'inspec-habitat/log'
|
|
9
|
+
autoload :Profile, 'inspec-habitat/profile'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require 'inspec-habitat/cli'
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# author: Dominik Richter
|
|
5
5
|
|
|
6
6
|
require 'net/http'
|
|
7
|
+
require 'addressable/uri'
|
|
7
8
|
|
|
8
9
|
module Supermarket
|
|
9
10
|
class API
|
|
@@ -12,7 +13,7 @@ module Supermarket
|
|
|
12
13
|
# displays a list of profiles
|
|
13
14
|
def self.profiles(supermarket_url = SUPERMARKET_URL)
|
|
14
15
|
url = "#{supermarket_url}/api/v1/tools-search"
|
|
15
|
-
_success, data = get(url, {
|
|
16
|
+
_success, data = get(url, { type: 'compliance_profile', items: 100 })
|
|
16
17
|
if !data.nil?
|
|
17
18
|
profiles = JSON.parse(data)
|
|
18
19
|
profiles['items'].map { |x|
|
|
@@ -26,9 +27,10 @@ module Supermarket
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def self.profile_name(profile)
|
|
29
|
-
|
|
30
|
+
# We use Addressable::URI here because URI has a bug in Ruby 2.1.x where it doesn't allow underscore in host
|
|
31
|
+
uri = Addressable::URI.parse profile
|
|
30
32
|
[uri.host, uri.path[1..-1]]
|
|
31
|
-
rescue
|
|
33
|
+
rescue
|
|
32
34
|
nil
|
|
33
35
|
end
|
|
34
36
|
|
|
@@ -50,8 +52,8 @@ module Supermarket
|
|
|
50
52
|
supermarket_tool['tool_owner'] == tool_owner && supermarket_tool['tool'] == tool
|
|
51
53
|
end
|
|
52
54
|
|
|
53
|
-
def self.find(profile, supermarket_url)
|
|
54
|
-
profiles = Supermarket::API.profiles(supermarket_url
|
|
55
|
+
def self.find(profile, supermarket_url = SUPERMARKET_URL)
|
|
56
|
+
profiles = Supermarket::API.profiles(supermarket_url)
|
|
55
57
|
if !profiles.empty?
|
|
56
58
|
index = profiles.index { |t| same?(profile, t, supermarket_url) }
|
|
57
59
|
# return profile or nil
|
|
@@ -22,7 +22,7 @@ module Supermarket
|
|
|
22
22
|
|
|
23
23
|
headline('Available profiles:')
|
|
24
24
|
supermarket_profiles.each { |p|
|
|
25
|
-
li("#{p['
|
|
25
|
+
li("#{p['tool_name']} #{mark_text(p['tool_owner'] + '/' + p['slug'])}")
|
|
26
26
|
}
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -45,7 +45,7 @@ module Supermarket
|
|
|
45
45
|
"#{p['tool_owner']}/#{p['slug']}" == profile
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
if found.
|
|
48
|
+
if found.empty?
|
|
49
49
|
puts "#{mark_text(profile)} is not available on Supermarket"
|
|
50
50
|
return
|
|
51
51
|
end
|