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,8 +9,9 @@ This extensions offers the following features:
|
|
|
9
9
|
To use the CLI, this InSpec add-on adds the following commands:
|
|
10
10
|
|
|
11
11
|
* `$ inspec compliance login` - authentication of the API token against Chef Compliance
|
|
12
|
+
* `$ inspec compliance login_automate` - authentication of the API token against Chef Automate
|
|
12
13
|
* `$ inspec compliance profiles` - list all available Chef Compliance profiles
|
|
13
|
-
* `$ inspec
|
|
14
|
+
* `$ inspec exec compliance://profile` - runs a Chef Compliance profile
|
|
14
15
|
* `$ inspec compliance upload path/to/local/profile` - uploads a local profile to Chef Compliance
|
|
15
16
|
* `$ inspec compliance logout` - logout of Chef Compliance
|
|
16
17
|
|
|
@@ -19,8 +20,37 @@ Compliance profiles can be executed in two mays:
|
|
|
19
20
|
- via compliance exec: `inspec compliance exec profile`
|
|
20
21
|
- via compliance scheme: `inspec exec compliance://profile`
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
## Usage
|
|
23
25
|
|
|
26
|
+
### Command options
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
$ inspec compliance
|
|
30
|
+
Commands:
|
|
31
|
+
inspec compliance download PROFILE # downloads a profile from Chef Compliance
|
|
32
|
+
inspec compliance exec PROFILE # executes a Chef Compliance profile
|
|
33
|
+
inspec compliance help [COMMAND] # Describe subcommands or one specific subcommand
|
|
34
|
+
inspec compliance login SERVER # Log in to a Chef Compliance SERVER
|
|
35
|
+
inspec compliance login_automate SERVER # Log in to an Automate SERVER
|
|
36
|
+
inspec compliance logout # user logout from Chef Compliance
|
|
37
|
+
inspec compliance profiles # list all available profiles in Chef Compliance
|
|
38
|
+
inspec compliance upload PATH # uploads a local profile to Chef Compliance
|
|
39
|
+
inspec compliance version # displays the version of the Chef Compliance server
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Login with Chef Automate
|
|
43
|
+
|
|
44
|
+
You need a Chef Automate server up and running. Compliance features need to [be activated](https://docs.chef.io/install_chef_automate.html#compliance), too.
|
|
45
|
+
|
|
46
|
+
Now, you need a user token. You can retrieve that via [UI](https://docs.chef.io/api_delivery.html) or [CLI](https://docs.chef.io/ctl_delivery.html#delivery-token).
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
inspec compliance login_automate https://automate.compliance.test --insecure --user 'admin' --ent 'brewinc' --usertoken 'zuop..._KzE'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Login with Chef Compliance
|
|
53
|
+
|
|
24
54
|
Before you start using the compliance plugin, you need a running [Chef Compliance](https://www.chef.io/compliance/) server. Please login and gather the access token:
|
|
25
55
|
|
|
26
56
|

|
|
@@ -28,24 +58,17 @@ Before you start using the compliance plugin, you need a running [Chef Complianc
|
|
|
28
58
|
You can choose the access token (`--token`) or the refresh token (`--refresh_token`)
|
|
29
59
|
|
|
30
60
|
```
|
|
31
|
-
$ inspec compliance
|
|
32
|
-
Commands:
|
|
33
|
-
inspec compliance exec PROFILE # executes a Chef Compliance profile
|
|
34
|
-
inspec compliance help [COMMAND] # Describe subcommands or one specific subcommand
|
|
35
|
-
inspec compliance login SERVER # Log in to a Chef Compliance SERVER
|
|
36
|
-
inspec compliance logout # user logout from Chef Compliance
|
|
37
|
-
inspec compliance profiles # list all available profiles in Chef Compliance
|
|
38
|
-
inspec compliance upload PATH # uploads a local profile to Chef Compliance
|
|
39
|
-
inspec compliance version # displays the version of the Chef Compliance server
|
|
40
|
-
|
|
41
61
|
# login to chef compliance server
|
|
42
62
|
$ inspec compliance login https://compliance.test --user admin --insecure --token '...'
|
|
43
63
|
|
|
44
64
|
# display the chef compliance server version
|
|
45
65
|
$ inspec compliance version
|
|
46
66
|
Chef Compliance version: 1.0.11
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### List available profiles via Chef Compliance / Automate
|
|
47
70
|
|
|
48
|
-
|
|
71
|
+
```
|
|
49
72
|
$ inspec compliance profiles
|
|
50
73
|
Available profiles:
|
|
51
74
|
-------------------
|
|
@@ -65,8 +88,11 @@ Available profiles:
|
|
|
65
88
|
* cis/cis-ubuntu12.04lts-level2
|
|
66
89
|
* cis/cis-ubuntu14.04lts-level1
|
|
67
90
|
* cis/cis-ubuntu14.04lts-level2
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Upload a profile to Chef Compliance / Automate
|
|
68
94
|
|
|
69
|
-
|
|
95
|
+
```
|
|
70
96
|
$ inspec compliance version
|
|
71
97
|
Chef Compliance version: 1.0.11
|
|
72
98
|
➜ inspec git:(chris-rock/cc-error-not-loggedin) ✗ b inspec compliance upload examples/profile
|
|
@@ -103,8 +129,11 @@ Available profiles:
|
|
|
103
129
|
* cis/cis-ubuntu12.04lts-level2
|
|
104
130
|
* cis/cis-ubuntu14.04lts-level1
|
|
105
131
|
* cis/cis-ubuntu14.04lts-level2
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Run a profile from Chef Compliance / Automate on Workstation
|
|
106
135
|
|
|
107
|
-
|
|
136
|
+
```
|
|
108
137
|
$ inspec exec compliance://admin/profile
|
|
109
138
|
.*...
|
|
110
139
|
|
|
@@ -119,7 +148,7 @@ Finished in 0.02862 seconds (files took 0.62628 seconds to load)
|
|
|
119
148
|
5 examples, 0 failures, 1 pending
|
|
120
149
|
```
|
|
121
150
|
|
|
122
|
-
|
|
151
|
+
### To Logout from Chef Compliance
|
|
123
152
|
|
|
124
153
|
```
|
|
125
154
|
$ inspec compliance logout
|
|
@@ -6,14 +6,26 @@ require 'net/http'
|
|
|
6
6
|
require 'uri'
|
|
7
7
|
|
|
8
8
|
module Compliance
|
|
9
|
+
class ServerConfigurationMissing < StandardError
|
|
10
|
+
end
|
|
11
|
+
|
|
9
12
|
# API Implementation does not hold any state by itself,
|
|
10
13
|
# everything will be stored in local Configuration store
|
|
11
|
-
class API
|
|
14
|
+
class API # rubocop:disable Metrics/ClassLength
|
|
12
15
|
# return all compliance profiles available for the user
|
|
13
16
|
def self.profiles(config)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
# Chef Compliance
|
|
18
|
+
if is_compliance_server?(config)
|
|
19
|
+
url = "#{config['server']}/user/compliance"
|
|
20
|
+
# Chef Automate
|
|
21
|
+
elsif is_automate_server?(config)
|
|
22
|
+
url = "#{config['server']}/profiles/#{config['user']}"
|
|
23
|
+
else
|
|
24
|
+
raise ServerConfigurationMissing
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
headers = get_headers(config)
|
|
28
|
+
response = Compliance::HTTP.get(url, headers, config['insecure'])
|
|
17
29
|
data = response.body
|
|
18
30
|
response_code = response.code
|
|
19
31
|
case response_code
|
|
@@ -21,11 +33,21 @@ module Compliance
|
|
|
21
33
|
msg = 'success'
|
|
22
34
|
profiles = JSON.parse(data)
|
|
23
35
|
# iterate over profiles
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
if is_compliance_server?(config)
|
|
37
|
+
mapped_profiles = []
|
|
38
|
+
profiles.values.each { |org|
|
|
39
|
+
mapped_profiles += org.values
|
|
40
|
+
}
|
|
41
|
+
# Chef Automate pre 0.8.0
|
|
42
|
+
elsif is_automate_server_pre_080?(config)
|
|
43
|
+
mapped_profiles = profiles.values.flatten
|
|
44
|
+
else
|
|
45
|
+
owner_id = config['user']
|
|
46
|
+
mapped_profiles = profiles.map { |e|
|
|
47
|
+
e['owner_id'] = owner_id
|
|
48
|
+
e
|
|
49
|
+
}
|
|
50
|
+
end
|
|
29
51
|
return msg, mapped_profiles
|
|
30
52
|
when '401'
|
|
31
53
|
msg = '401 Unauthorized. Please check your token.'
|
|
@@ -39,28 +61,30 @@ module Compliance
|
|
|
39
61
|
# return the server api version
|
|
40
62
|
# NB this method does not use Compliance::Configuration to allow for using
|
|
41
63
|
# it before we know the version (e.g. oidc or not)
|
|
42
|
-
def self.version(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
def self.version(config)
|
|
65
|
+
url = config['server']
|
|
66
|
+
insecure = config['insecure']
|
|
67
|
+
|
|
68
|
+
raise ServerConfigurationMissing if url.nil?
|
|
69
|
+
|
|
70
|
+
headers = get_headers(config)
|
|
71
|
+
response = Compliance::HTTP.get(url+'/version', headers, insecure)
|
|
72
|
+
return {} if response.code == '404'
|
|
73
|
+
|
|
74
|
+
data = response.body
|
|
75
|
+
return {} if data.nil? || data.empty?
|
|
76
|
+
|
|
77
|
+
parsed = JSON.parse(data)
|
|
78
|
+
return {} unless parsed.key?('version') && !parsed['version'].empty?
|
|
79
|
+
|
|
80
|
+
parsed
|
|
57
81
|
end
|
|
58
82
|
|
|
59
83
|
# verifies that a profile
|
|
60
84
|
def self.exist?(config, profile)
|
|
61
85
|
_msg, profiles = Compliance::API.profiles(config)
|
|
62
86
|
if !profiles.empty?
|
|
63
|
-
index = profiles.index { |p| "#{p[
|
|
87
|
+
index = profiles.index { |p| "#{p['owner_id']}/#{p['name']}" == profile }
|
|
64
88
|
!index.nil? && index >= 0
|
|
65
89
|
else
|
|
66
90
|
false
|
|
@@ -68,13 +92,23 @@ Please login using `inspec compliance login https://compliance.test --user admin
|
|
|
68
92
|
end
|
|
69
93
|
|
|
70
94
|
def self.upload(config, owner, profile_name, archive_path)
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
# Chef Compliance
|
|
96
|
+
if is_compliance_server?(config)
|
|
97
|
+
url = "#{config['server']}/owners/#{owner}/compliance/#{profile_name}/tar"
|
|
98
|
+
# Chef Automate pre 0.8.0
|
|
99
|
+
elsif is_automate_server_pre_080?(config)
|
|
100
|
+
url = "#{config['server']}/#{config['user']}"
|
|
101
|
+
# Chef Automate
|
|
102
|
+
else
|
|
103
|
+
url = "#{config['server']}/profiles/#{config['user']}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
headers = get_headers(config)
|
|
107
|
+
res = Compliance::HTTP.post_file(url, headers, archive_path, config['insecure'])
|
|
74
108
|
[res.is_a?(Net::HTTPSuccess), res.body]
|
|
75
109
|
end
|
|
76
110
|
|
|
77
|
-
# Use username and
|
|
111
|
+
# Use username and refresh_token to get an API access token
|
|
78
112
|
def self.get_token_via_refresh_token(url, refresh_token, insecure)
|
|
79
113
|
uri = URI.parse("#{url}/login")
|
|
80
114
|
req = Net::HTTP::Post.new(uri.path)
|
|
@@ -121,5 +155,77 @@ Please login using `inspec compliance login https://compliance.test --user admin
|
|
|
121
155
|
|
|
122
156
|
[success, msg, access_token]
|
|
123
157
|
end
|
|
158
|
+
|
|
159
|
+
def self.get_headers(config)
|
|
160
|
+
token = get_token(config)
|
|
161
|
+
if is_automate_server?(config)
|
|
162
|
+
headers = { 'chef-delivery-enterprise' => config['automate']['ent'] }
|
|
163
|
+
if config['automate']['token_type'] == 'dctoken'
|
|
164
|
+
headers['x-data-collector-token'] = token
|
|
165
|
+
else
|
|
166
|
+
headers['chef-delivery-user'] = config['user']
|
|
167
|
+
headers['chef-delivery-token'] = token
|
|
168
|
+
end
|
|
169
|
+
else
|
|
170
|
+
headers = { 'Authorization' => "Bearer #{token}" }
|
|
171
|
+
end
|
|
172
|
+
headers
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.get_token(config)
|
|
176
|
+
return config['token'] unless config['refresh_token']
|
|
177
|
+
_success, _msg, token = get_token_via_refresh_token(config['server'], config['refresh_token'], config['insecure'])
|
|
178
|
+
token
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.target_url(config, profile)
|
|
182
|
+
if is_automate_server?(config)
|
|
183
|
+
owner, id = profile.split('/')
|
|
184
|
+
target = "#{config['server']}/profiles/#{owner}/#{id}/tar"
|
|
185
|
+
else
|
|
186
|
+
owner, id = profile.split('/')
|
|
187
|
+
target = "#{config['server']}/owners/#{owner}/compliance/#{id}/tar"
|
|
188
|
+
end
|
|
189
|
+
target
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# returns a parsed url for `admin/profile` or `compliance://admin/profile`
|
|
193
|
+
def self.sanitize_profile_name(profile)
|
|
194
|
+
if URI(profile).scheme == 'compliance'
|
|
195
|
+
uri = URI(profile)
|
|
196
|
+
else
|
|
197
|
+
uri = URI("compliance://#{profile}")
|
|
198
|
+
end
|
|
199
|
+
uri.to_s.sub(%r{^compliance:\/\/}, '')
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.is_compliance_server?(config)
|
|
203
|
+
config['server_type'] == 'compliance'
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.is_automate_server_pre_080?(config)
|
|
207
|
+
# Automate versions before 0.8.x do not have a valid version in the config
|
|
208
|
+
return false unless config['server_type'] == 'automate'
|
|
209
|
+
server_version_from_config(config).nil?
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.is_automate_server_080_and_later?(config)
|
|
213
|
+
# Automate versions 0.8.x and later will have a "version" key in the config
|
|
214
|
+
# that is properly parsed out via server_version_from_config below
|
|
215
|
+
return false unless config['server_type'] == 'automate'
|
|
216
|
+
!server_version_from_config(config).nil?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.is_automate_server?(config)
|
|
220
|
+
config['server_type'] == 'automate'
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def self.server_version_from_config(config)
|
|
224
|
+
# Automate versions 0.8.x and later will have a "version" key in the config
|
|
225
|
+
# that looks like: "version":{"api":"compliance","version":"0.8.24"}
|
|
226
|
+
return nil unless config.key?('version')
|
|
227
|
+
return nil unless config['version'].is_a?(Hash)
|
|
228
|
+
config['version']['version']
|
|
229
|
+
end
|
|
124
230
|
end
|
|
125
231
|
end
|
|
@@ -36,6 +36,7 @@ module Compliance
|
|
|
36
36
|
|
|
37
37
|
options['server'] = server
|
|
38
38
|
url = options['server'] + options['apipath']
|
|
39
|
+
|
|
39
40
|
if !options['user'].nil? && !options['password'].nil?
|
|
40
41
|
# username / password
|
|
41
42
|
_success, msg = login_username_password(url, options['user'], options['password'], options['insecure'])
|
|
@@ -56,22 +57,62 @@ module Compliance
|
|
|
56
57
|
puts '', msg
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
desc "login_automate SERVER --insecure --user='USER' --ent='ENT' --usertoken='TOKEN'", 'Log in to an Automate SERVER'
|
|
61
|
+
long_desc <<-LONGDESC
|
|
62
|
+
`login_automate` allows you to use InSpec with Chef Automate
|
|
63
|
+
|
|
64
|
+
You need to a user-token for communication with Chef Automate. More
|
|
65
|
+
information about token retrieval for Automate is available at:
|
|
66
|
+
https://docs.chef.io/api_delivery.html
|
|
67
|
+
LONGDESC
|
|
68
|
+
option :dctoken, type: :string,
|
|
69
|
+
desc: 'Data Collector token'
|
|
70
|
+
option :usertoken, type: :string,
|
|
71
|
+
desc: 'Automate user token'
|
|
72
|
+
option :user, type: :string,
|
|
73
|
+
desc: 'Automate username'
|
|
74
|
+
option :ent, type: :string,
|
|
75
|
+
desc: 'Enterprise for Chef Automate reporting'
|
|
76
|
+
option :insecure, aliases: :k, type: :boolean,
|
|
77
|
+
desc: 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
|
|
78
|
+
def login_automate(server) # rubocop:disable Metrics/AbcSize
|
|
79
|
+
options['server'] = server
|
|
80
|
+
url = options['server'] + '/compliance'
|
|
81
|
+
|
|
82
|
+
if url && !options['user'].nil? && !options['ent'].nil? && (!options['dctoken'].nil? || !options['usertoken'].nil?)
|
|
83
|
+
msg = login_automate_config(url, options['user'], options['dctoken'], options['usertoken'], options['ent'], options['insecure'])
|
|
84
|
+
puts '', msg
|
|
85
|
+
exit 0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# parameters are missing if we reach here
|
|
89
|
+
puts 'Please specify an user using `--user \'USER\'`' if options['user'].nil?
|
|
90
|
+
puts 'Please specify an enterprise using `--ent \'cd\'`' if options['ent'].nil?
|
|
91
|
+
puts 'Please specify a token using `--usertoken=\'AUTOMATE_TOKEN\'`' if options['usertoken'].nil? && options['dctoken'].nil?
|
|
92
|
+
exit 1
|
|
93
|
+
end
|
|
94
|
+
|
|
59
95
|
desc 'profiles', 'list all available profiles in Chef Compliance'
|
|
96
|
+
|
|
60
97
|
def profiles
|
|
61
98
|
config = Compliance::Configuration.new
|
|
62
99
|
return if !loggedin(config)
|
|
63
100
|
|
|
64
101
|
msg, profiles = Compliance::API.profiles(config)
|
|
102
|
+
profiles.sort_by! { |hsh| hsh['title'] }
|
|
65
103
|
if !profiles.empty?
|
|
66
104
|
# iterate over profiles
|
|
67
105
|
headline('Available profiles:')
|
|
68
106
|
profiles.each { |profile|
|
|
69
|
-
li("#{profile[
|
|
107
|
+
li("#{profile['title']} #{mark_text(profile['owner_id'] + '/' + profile['name'])}")
|
|
70
108
|
}
|
|
71
109
|
else
|
|
72
110
|
puts msg, 'Could not find any profiles'
|
|
73
111
|
exit 1
|
|
74
112
|
end
|
|
113
|
+
rescue Compliance::ServerConfigurationMissing
|
|
114
|
+
puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
|
|
115
|
+
exit 1
|
|
75
116
|
end
|
|
76
117
|
|
|
77
118
|
desc 'exec PROFILE', 'executes a Chef Compliance profile'
|
|
@@ -79,15 +120,43 @@ module Compliance
|
|
|
79
120
|
def exec(*tests)
|
|
80
121
|
config = Compliance::Configuration.new
|
|
81
122
|
return if !loggedin(config)
|
|
82
|
-
|
|
83
123
|
# iterate over tests and add compliance scheme
|
|
84
|
-
tests = tests.map { |t| 'compliance://' + t }
|
|
85
|
-
|
|
124
|
+
tests = tests.map { |t| 'compliance://' + Compliance::API.sanitize_profile_name(t) }
|
|
86
125
|
# execute profile from inspec exec implementation
|
|
87
126
|
diagnose
|
|
88
127
|
run_tests(tests, opts)
|
|
89
128
|
end
|
|
90
129
|
|
|
130
|
+
desc 'download PROFILE', 'downloads a profile from Chef Compliance'
|
|
131
|
+
option :name, type: :string,
|
|
132
|
+
desc: 'Name of the archive filename (file type will be added)'
|
|
133
|
+
def download(profile_name)
|
|
134
|
+
o = options.dup
|
|
135
|
+
configure_logger(o)
|
|
136
|
+
|
|
137
|
+
config = Compliance::Configuration.new
|
|
138
|
+
return if !loggedin(config)
|
|
139
|
+
|
|
140
|
+
profile_name = Compliance::API.sanitize_profile_name(profile_name)
|
|
141
|
+
if Compliance::API.exist?(config, profile_name)
|
|
142
|
+
puts "Downloading `#{profile_name}`"
|
|
143
|
+
|
|
144
|
+
fetcher = Compliance::Fetcher.resolve(
|
|
145
|
+
{
|
|
146
|
+
compliance: profile_name,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# we provide a name, the fetcher adds the extension
|
|
151
|
+
_owner, id = profile_name.split('/')
|
|
152
|
+
file_name = fetcher.fetch(o.name || id)
|
|
153
|
+
puts "Profile stored to #{file_name}"
|
|
154
|
+
else
|
|
155
|
+
puts "Profile #{profile_name} is not available in Chef Compliance."
|
|
156
|
+
exit 1
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
91
160
|
desc 'upload PATH', 'uploads a local profile to Chef Compliance'
|
|
92
161
|
option :overwrite, type: :boolean, default: false,
|
|
93
162
|
desc: 'Overwrite existing profile on Chef Compliance.'
|
|
@@ -100,6 +169,8 @@ module Compliance
|
|
|
100
169
|
exit 1
|
|
101
170
|
end
|
|
102
171
|
|
|
172
|
+
vendor_deps(path, options) if File.directory?(path)
|
|
173
|
+
|
|
103
174
|
o = options.dup
|
|
104
175
|
configure_logger(o)
|
|
105
176
|
# check the profile, we only allow to upload valid profiles
|
|
@@ -120,7 +191,7 @@ module Compliance
|
|
|
120
191
|
end
|
|
121
192
|
|
|
122
193
|
# determine user information
|
|
123
|
-
if config['token'].nil? || config['user'].nil?
|
|
194
|
+
if (config['token'].nil? && config['refresh_token'].nil?) || config['user'].nil?
|
|
124
195
|
error.call('Please login via `inspec compliance login`')
|
|
125
196
|
end
|
|
126
197
|
|
|
@@ -144,7 +215,6 @@ module Compliance
|
|
|
144
215
|
# if it is a directory, tar it to tmp directory
|
|
145
216
|
if File.directory?(path)
|
|
146
217
|
archive_path = Dir::Tmpname.create([profile_name, '.tar.gz']) {}
|
|
147
|
-
# archive_path = file.path
|
|
148
218
|
puts "Generate temporary profile archive at #{archive_path}"
|
|
149
219
|
profile.archive({ output: archive_path, ignore_errors: false, overwrite: true })
|
|
150
220
|
else
|
|
@@ -154,7 +224,8 @@ module Compliance
|
|
|
154
224
|
puts "Start upload to #{owner}/#{profile_name}"
|
|
155
225
|
pname = ERB::Util.url_encode(profile_name)
|
|
156
226
|
|
|
157
|
-
|
|
227
|
+
Compliance::API.is_automate_server?(config) ? upload_msg = 'Uploading to Chef Automate' : upload_msg = 'Uploading to Chef Compliance'
|
|
228
|
+
puts upload_msg
|
|
158
229
|
success, msg = Compliance::API.upload(config, owner, pname, archive_path)
|
|
159
230
|
|
|
160
231
|
if success
|
|
@@ -169,24 +240,27 @@ module Compliance
|
|
|
169
240
|
desc 'version', 'displays the version of the Chef Compliance server'
|
|
170
241
|
def version
|
|
171
242
|
config = Compliance::Configuration.new
|
|
172
|
-
info = Compliance::API.version(config
|
|
243
|
+
info = Compliance::API.version(config)
|
|
173
244
|
if !info.nil? && info['version']
|
|
174
|
-
puts "
|
|
245
|
+
puts "Name: #{info['api']}"
|
|
246
|
+
puts "Version: #{info['version']}"
|
|
175
247
|
else
|
|
176
248
|
puts 'Could not determine server version.'
|
|
177
249
|
exit 1
|
|
178
250
|
end
|
|
251
|
+
rescue Compliance::ServerConfigurationMissing
|
|
252
|
+
puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
|
|
253
|
+
exit 1
|
|
179
254
|
end
|
|
180
255
|
|
|
181
256
|
desc 'logout', 'user logout from Chef Compliance'
|
|
182
257
|
def logout
|
|
183
258
|
config = Compliance::Configuration.new
|
|
184
|
-
unless config.supported?(:oidc) || config['token'].nil?
|
|
259
|
+
unless config.supported?(:oidc) || config['token'].nil? || config['server_type'] == 'automate'
|
|
185
260
|
config = Compliance::Configuration.new
|
|
186
261
|
url = "#{config['server']}/logout"
|
|
187
262
|
Compliance::API.post(url, config['token'], config['insecure'], !config.supported?(:oidc))
|
|
188
263
|
end
|
|
189
|
-
|
|
190
264
|
success = config.destroy
|
|
191
265
|
|
|
192
266
|
if success
|
|
@@ -198,14 +272,42 @@ module Compliance
|
|
|
198
272
|
|
|
199
273
|
private
|
|
200
274
|
|
|
275
|
+
def login_automate_config(url, user, dctoken, usertoken, ent, insecure) # rubocop:disable Metrics/ParameterLists
|
|
276
|
+
config = Compliance::Configuration.new
|
|
277
|
+
config.clean
|
|
278
|
+
config['user'] = user
|
|
279
|
+
config['server'] = url
|
|
280
|
+
config['automate'] = {}
|
|
281
|
+
config['automate']['ent'] = ent
|
|
282
|
+
config['server_type'] = 'automate'
|
|
283
|
+
config['insecure'] = insecure
|
|
284
|
+
|
|
285
|
+
# determine token method being used
|
|
286
|
+
if !dctoken.nil?
|
|
287
|
+
config['token'] = dctoken
|
|
288
|
+
token_type = 'dctoken'
|
|
289
|
+
token_msg = 'data collector token'
|
|
290
|
+
else
|
|
291
|
+
config['token'] = usertoken
|
|
292
|
+
token_type = 'usertoken'
|
|
293
|
+
token_msg = 'automate user token'
|
|
294
|
+
end
|
|
295
|
+
config['automate']['token_type'] = token_type
|
|
296
|
+
config['version'] = Compliance::API.version(config)
|
|
297
|
+
config.store
|
|
298
|
+
msg = "Stored configuration for Chef Automate: '#{url}' with user: '#{user}', ent: '#{ent}' and your #{token_msg}"
|
|
299
|
+
msg
|
|
300
|
+
end
|
|
301
|
+
|
|
201
302
|
def login_refreshtoken(url, options)
|
|
202
|
-
success, msg,
|
|
303
|
+
success, msg, _access_token = Compliance::API.get_token_via_refresh_token(url, options['refresh_token'], options['insecure'])
|
|
203
304
|
if success
|
|
204
305
|
config = Compliance::Configuration.new
|
|
306
|
+
config.clean
|
|
205
307
|
config['server'] = url
|
|
206
|
-
config['token'] = access_token
|
|
207
308
|
config['insecure'] = options['insecure']
|
|
208
|
-
config['
|
|
309
|
+
config['server_type'] = 'compliance'
|
|
310
|
+
config['version'] = Compliance::API.version(config)
|
|
209
311
|
config.store
|
|
210
312
|
end
|
|
211
313
|
|
|
@@ -214,13 +316,15 @@ module Compliance
|
|
|
214
316
|
|
|
215
317
|
def login_username_password(url, username, password, insecure)
|
|
216
318
|
config = Compliance::Configuration.new
|
|
319
|
+
config.clean
|
|
217
320
|
success, msg, api_token = Compliance::API.get_token_via_password(url, username, password, insecure)
|
|
218
321
|
if success
|
|
219
322
|
config['server'] = url
|
|
220
323
|
config['user'] = username
|
|
221
324
|
config['token'] = api_token
|
|
222
325
|
config['insecure'] = insecure
|
|
223
|
-
config['
|
|
326
|
+
config['server_type'] = 'compliance'
|
|
327
|
+
config['version'] = Compliance::API.version(config)
|
|
224
328
|
config.store
|
|
225
329
|
success = true
|
|
226
330
|
end
|
|
@@ -230,11 +334,13 @@ module Compliance
|
|
|
230
334
|
# saves a user access token (limited time)
|
|
231
335
|
def store_access_token(url, user, token, insecure)
|
|
232
336
|
config = Compliance::Configuration.new
|
|
337
|
+
config.clean
|
|
233
338
|
config['server'] = url
|
|
234
339
|
config['insecure'] = insecure
|
|
235
340
|
config['user'] = user
|
|
236
341
|
config['token'] = token
|
|
237
|
-
config['
|
|
342
|
+
config['server_type'] = 'compliance'
|
|
343
|
+
config['version'] = Compliance::API.version(config)
|
|
238
344
|
config.store
|
|
239
345
|
|
|
240
346
|
[true, 'API access token stored']
|
|
@@ -243,22 +349,23 @@ module Compliance
|
|
|
243
349
|
# saves a refresh token supplied by the user
|
|
244
350
|
def store_refresh_token(url, refresh_token, verify, user, insecure)
|
|
245
351
|
config = Compliance::Configuration.new
|
|
352
|
+
config.clean
|
|
246
353
|
config['server'] = url
|
|
247
354
|
config['refresh_token'] = refresh_token
|
|
248
355
|
config['user'] = user
|
|
249
356
|
config['insecure'] = insecure
|
|
250
|
-
config['
|
|
357
|
+
config['server_type'] = 'compliance'
|
|
358
|
+
config['version'] = Compliance::API.version(config)
|
|
251
359
|
|
|
252
360
|
if !verify
|
|
253
361
|
config.store
|
|
254
362
|
success = true
|
|
255
363
|
msg = 'API refresh token stored'
|
|
256
364
|
else
|
|
257
|
-
success, msg,
|
|
365
|
+
success, msg, _access_token= Compliance::API.get_token_via_refresh_token(url, refresh_token, insecure)
|
|
258
366
|
if success
|
|
259
|
-
config['token'] = access_token
|
|
260
367
|
config.store
|
|
261
|
-
msg = 'API access token verified
|
|
368
|
+
msg = 'API access token verified'
|
|
262
369
|
end
|
|
263
370
|
end
|
|
264
371
|
|
|
@@ -267,7 +374,7 @@ module Compliance
|
|
|
267
374
|
|
|
268
375
|
def loggedin(config)
|
|
269
376
|
serverknown = !config['server'].nil?
|
|
270
|
-
puts 'You need to login first with `inspec compliance login`' if !serverknown
|
|
377
|
+
puts 'You need to login first with `inspec compliance login` or `inspec compliance login_automate`' if !serverknown
|
|
271
378
|
serverknown
|
|
272
379
|
end
|
|
273
380
|
end
|