Overhaul shadow docs (#3156)

* Overhaul shadow docs
* Feedback from stocksy
* Edits to shadow resource
* PR feedback correction
* Attempt to clarify grammar for expiry_date criterion

Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Clinton Wolfe 2018-07-05 16:15:03 -04:00 committed by Jared Quick
parent eb40d23fb0
commit d0e2608ec3

View file

@ -5,148 +5,253 @@ platform: linux
# shadow
Use the `shadow` InSpec audit resource to test the contents of `/etc/shadow`, which contains password details that are only readable by the `root` user. The format for `/etc/shadow` includes:
Use the `shadow` InSpec audit resource to test the contents of `/etc/shadow`, which contains password details that are readable only by the `root` user. `shadow` is a [plural resource](https://www.inspec.io/docs/reference/glossary/#plural_resource). Like all plural resources, it functions by performing searches across multiple entries in the shadow file.
The format for `/etc/shadow` includes:
* A username
* The hashed password for that user
* The last time a password was changed
* The last date a password was changed, as the number of days since Jan 1 1970
* The minimum number of days a password must exist, before it may be changed
* The maximum number of days after which a password must be changed
* The number of days a user is warned about an expiring password
* The number of days a user must be inactive before the user account is disabled
* The number of days a user account has been disabled
* The date on which a user account was disabled, as the number of days since Jan 1 1970
These entries are defined as a colon-delimited row in the file, one row per user:
dannos:Gb7crrO5CDF.:10063:0:99999:7:::
The `shadow` resource understands this format, allows you to search on the fields, and exposes the selected users' properties.
<br>
## Syntax
## Resource Parameters
A `shadow` resource block declares user properties to be tested:
The `shadow` resource takes one optional parameter: the path to the shadow file. If omitted, `/etc/shadow` is assumed.
# Expect a file to exist at the default location and have 32 users
describe shadow do
its('users') { should_not include 'forbidden_user' }
its('count') { should eq 32 }
end
Properties can be used as a single query:
describe shadow.user('root') do
its('count') { should eq 1 }
# Use a custom location
describe shadow('/etc/my-custom-place/shadow') do
its('count') { should eq 32 }
end
Use the `.where` method to find properties that match a value:
describe shadow.where { min_days == '0' } do
its ('users') { should include 'nfs' }
end
describe shadow.where { password =~ /[x|!|*]/ } do
its('count') { should eq 0 }
end
The following properties are available:
* `users`
* `passwords`
* `last_changes`
* `min_days`
* `max_days`
* `warn_days`
* `inactive_days`
* `expiry_dates`
* `reserved`
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test for a forbidden user
A `shadow` resource block uses `where` to filter entries from the shadow file. If `where` is omitted, all entries are selected.
# Select all users. Among them, there should not be a user with the name 'forbidden_user'.
describe shadow do
its('users') { should_not include 'forbidden_user' }
end
### Test that a user appears one time
describe shadow.users('bin') do
its('passwords') { should cmp 'x' }
# Ensure there is only one user named 'root' (Select all with name 'root', then count them).
describe shadow.where(user: 'root') do
its('count') { should eq 1 }
end
Use `where` to match any of the supported [filter criteria](#filter_criteria). `where` has a method form for simple equality and a block form for more complex queries.
# Method form, simple
# Select just the root user (direct equality)
describe shadow.where(user: 'root') do
its ('count') { should eq 1 }
end
# Method form, with a regex
# Select all users whose names begin with smb
describe shadow.where(user: /^smb/) do
its ('count') { should eq 2 }
end
# Block form
# Select users whose passwords have expired
describe shadow.where { expiry_date > 0 } do
# This test directly asserts that there should be 0 such users
its('count') { should eq 0 }
# But if the count test fails, this test outputs the users that are causing the failure.
its('users') { should be_empty }
end
<br>
## Properties
As a [plural resource](https://www.inspec.io/docs/reference/glossary/#plural_resource), all of `shadow`'s properties return lists (that is, Ruby Arrays). `include` and `be_empty` are two useful matchers when working with lists. You can also perform manipulation of the lists, such as calling `uniq`, `sort`, `count`, `first`, `last`, `min`, and `max`.
### users
The `users` property tests if the username exists `/etc/shadow`:
A list of strings, representing the usernames matched by the filter.
its('users') { should include 'root' }
describe shadow
its('users') { should include 'root' }
end
### passwords
The `passwords` property returns the encrypted password string from the shadow file. The returned string may not be an encrypted password, but rather a `*` or similar which indicates that direct logins are not allowed.
A list of strings, representing the encrypted password strings for entries matched by the `where` filter. Each string may not be an encrypted password, but rather a `*` or similar which indicates that direct logins are not allowed. Different operating systems use different flags here (such as `*LK*` to indicate the account is locked).
For example:
its('passwords') { should cmp '*' }
# Use uniq to remove duplicates, then determine
# if the only password left on the list is '*'
describe shadow.where(user: /adm$/) do
its('passwords.uniq.first') { should cmp '*' }
its('passwords.uniq.count') { should eq 1 }
end
### last\_changes
The `last_changes` property tests the last time a password was changed:
A list of integers, indicating the number of days since Jan 1 1970 since the password for each matching entry was changed.
its('last_changes') { should be_empty }
# Ensure all entries have changed their password in the last 90 days. (Probably want a filter on that)
describe shadow do
its('last_changes.min') { should be < Date.today - 90 - Date.new(1970,1,1) }
end
### min\_days
The `min_days` property tests the minimum number of days a password must exist, before it may be changed:
A list of integers reflecting the minimum number of days a password must exist, before it may be changed, for the users that matched the filter.
its('min_days') { should eq 0 }
# min_days seems crazy today; make sure it is zero for everyone
describe shadow do
its('min_days.uniq') { should eq [0] }
end
### max\_days
The `max_days` property tests the maximum number of days after which a password must be changed:
A list of integers reflecting the maximum number of days after which the password must be changed for each user matching the filter.
its('max_days') { should eq 90 }
# Make sure there is no policy allowing longer than 90 days
describe shadow do
its('max_days.max') { should be < 90 }
end
### warn\_days
The `warn_days` property tests the number of days a user is warned about an expiring password:
A list of integers reflecting the number of days a user is warned about an expiring password for each user matching the filter.
its('warn_days') { should eq 7 }
# Ensure everyone gets the same 7-day policy
describe shadow do
its('warn_days.uniq.count') { should eq 1 }
its('warn_days.uniq.first') { should eq 7 }
end
### inactive\_days
The `inactive_days` property tests the number of days a user must be inactive before the user account is disabled:
A list of integers reflecting the number of days a user must be inactive before the user account is disabled for each user matching the filter.
its('inactive_days') { should be_empty }
# Ensure everyone except admins has an stale policy of no more than 14 days
describe shadow.where { user !~ /adm$/ } do
its('inactive_days.max') { should be <= 14 }
end
### expiry\_dates
The `expiry_dates` property tests the number of days a user account has been disabled:
A list of integers reflecting the number of days since Jan 1 1970 that a user account has been disabled, for each user matching the filter. Value is `nil` if the account has not expired.
its('expiry_dates') { should be_empty }
# No one should have an expired account.
describe shadow do
its('expiry_dates.compact') { should be_empty }
end
### count
The `count` property tests the number of times the named property appears:
The `count` property tests the number of records that the filter matched.
# Should probably only have one root user
describe shadow.user('root') do
its('count') { should eq 1 }
end
This property is best used in conjunction with filters. For example:
<br>
describe shadow.where { password =~ /[x|!|*]/ } do
its('count') { should eq 0 }
## Filter Criteria
You may use any of these filter criteria with the `where` function. They are named after the columns in the shadow file. Each has a related list [property](#properties).
### user
The string username of a user. Always present. Not required to be unique.
# Expect all users whose name ends in adm to have a disabled password via the '*' flag
describe shadow.where(user: /adm$/) do
its('password.uniq') { should eq ['*'] }
end
<br>
### password
The encrypted password strings, or an account status string. Each string may not be an encrypted password, but rather a `*` or similar which indicates that direct logins are not allowed. Different operating systems use other flags here (such as `*LK*` to indicate the account is locked).
# Find 'locked' accounts and ensure 'nobody' is on the list
describe shadow.where(password: '*LK*') do
its('users') { should include 'nobody' }
end
### last_change
An integer reflecting the number of days since Jan 1 1970 since the user's password was changed.
# Find users who have not changed their password within 90 days
describe shadow.where { last_change > Date.today - 90 - Date.new(1970,1,1) } do
its('users') { should be_empty }
end
### min_days
An integer reflecting the minimum number of days a user is required to wait before
changing their password again.
# Find users who have a nonzero wait time
describe shadow.where { min_days > 0 } do
its('users') { should be_empty }
end
### max_days
An integer reflecting the maximum number of days a user may go without changing their password.
# All users should have a 30-day policy
describe shadow.where { max_days != 30 } do
its('users') { should be_empty }
end
### warn_days
An integer reflecting the number of days before a password expiration that a user recieves an alert.
# All users should have a 7-day warning policy
describe shadow.where { warn_days != 7 } do
its('users') { should be_empty }
end
### inactive_days
An integer reflecting the number of days that must pass before a user who has not logged in will be disabled.
# Ensure everyone has a stale policy of no more than 14 days.
describe shadow.where { inactive_days.nil? || inactive_days > 14 } do
its('users') { should be_empty }
end
### expiry_date
An integer reflecting the number of days since Jan 1, 1970 on which the user was disabled. The `expiry_date` criterion is `nil` for enabled users.
# Ensure no one is disabled due to a old password
describe shadow.where { !expiry_date.nil? } do
its('users') { should be_empty }
end
# Ensure no one is disabled for more than 14 days
describe shadow.where { !expiry_date.nil? && expiry_date - Date.new(1970,1,1) > 14} do
its('users') { should be_empty }
end
## Matchers
For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
This resource has no resource-specific matchers.
For a full list of available matchers, please visit our [Universal Matchers page](https://www.inspec.io/docs/reference/matchers/).