InSpec使用

Chef InSpec介绍

任何业务都依赖于基础设施环境。近年来,基础设施领域已发生了巨大的变化。从最初的传统数据中心到数据中心托管服务,而如今,基础设施即服务和云平台在企业中更受欢迎。因此,大多数企业正在将其工作负载从本地数据中心迁移到云平台。

为了部署和管理这些复杂的基础设施配置,很多企业使用代码即基础设施(IaC)。当您使用IaC在AWS云平台上创建成百上千个资源时,如何知道代码按预期方式部署了这些资源?这些资源是否遵守合规性和安全性措施?我们并不确定,因此需要基础设施测试程序。

Chef InSpec是一个用于测试和审核应用程序和基础设施的开源代码框架。Chef InSpec的工作原理是将系统的实际状态与您编写的Chef InSpec代码表达的期望状态进行比较。Chef InSpec可以检测违规并以报告的形式显示。

Chef InSpec入门

Chef InSpec安装

Chef InSpec提供了适配多种操作系统环境的安装包,这里使用一键脚本

1
curl https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec

安装完成后可以看到inspec命令可以正常运行,并提示inspec命令可以使用的参数帮助信息

image-20220421171700452

Chef InSpec配置介绍

使用Chef InSpec时主要需要弄清楚下面的三部分:

  • Command-line interface
  • Profile
  • Resource

InSpec CLI使用local,SSH,WinRM,Docker或AWS等连接方式,可针对不同基础设施目标运行测试和审核。常用命令说明如下:

命令 说明
archive 将配置文件压缩成tar.gz(默认)或zip文件
check 验证指定路径上的所有测试
exec 针对指定目标运行所有测试文件。加载给定的配置文件,并在需要时获取其依赖项。然后连接到目标并执行配置文件中包含的所有控件。输出测试结果。
help 查看特定命令的帮助信息
shell 打开一个交互式的调试终端
version 打印此工具的版本

Chef InSpec支持创建复杂的测试和合规性配置文件,每个profile文件都是独立的结构,具有自己的执行流程。用户可以自己编写profile文件,还可以在社区支持的Chef Supermarket和GitHub上找到别人写的profile。Profile文件通常具有下面的目录结构:

1
2
3
4
5
6
7
examples/profile
├── README.md # md是用于解释profile的适用范围和用法
├── controls # controls是所有测试用例所在的目录
│ ├── example.rb
├── libraries # libraries是用于存放扩展Chef InSpec资源代码的目录
│ └── extension.rb
└── inspec.yml # yml包含配置文件的描述

Chef InSpec拥有80多种可供使用的资源。如果您需要的资源尚未提供,也可以编写自己的自定义资源。Chef InSpec支持的资源列表如下:https://docs.chef.io/inspec/resources/

Chef InSpec测试AWS资源

典型的使用环境如下图所示,在数据中心内使用Chef InSpec对AWS进行基础设施资源测试。

img

从本地数据中心连接AWS环境需要使用Access Key和Secret Access Key(AKSK)。在企业环境中,通常会将IAM User集中在一个Master账号中进行管理,用户使用IAM User登陆AWS环境,然后再用Assume Role的方式操作其它AWS账户。

使用Chef InSpec连接AWS前,首先需要创建IAM用户,并生成AKSK。另外在UseCase账号中,需要创建Assume Role,给予适当的权限并允许Master账号中的IAM User切换到Use Case账号。

在本地数据中心连接AWS环境的配置示例如下:

1
2
3
4
5
6
7
8
9
10
# cat .aws/credentials 
[default]
aws_access_key_id = <Access Key>
aws_secret_access_key = <Secret Access Key>
# cat ~/.aws/config
[default]
region = cn-north-1
output = json
[profile usecase]
role_arn = <UseCase Account IAM Role Arn>

使用如下命令创建示例profile,该命令会自动生成profile目录结构。

1
2
3
4
5
6
7
8
9
# inspec init profile --platform aws example
────────────── InSpec Code Generator ──────────────

Creating new profile at /root/example
• Creating file README.md
• Creating file attributes.yml
• Creating directory controls
• Creating file controls/example.rb
• Creating file inspec.yml

示例代码的测试内容在controls/example.rb文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# copyright: 2018, The Authors

title "Sample Section"

aws_vpc_id = attribute("aws_vpc_id", value: "", description: "Optional AWS VPC identifier.")

# You add controls here
control "aws-single-vpc-exists-check" do # A unique ID for this control.
only_if { aws_vpc_id != "" } # Only run this control if the `aws_vpc_id` attribute is provided.
impact 1.0 # The criticality, if this control fails.
title "Check to see if custom VPC exists." # A human-readable title.
describe aws_vpc(aws_vpc_id) do # The test itself.
it { should exist }
end
end

# Plural resources can be inspected to check for specific resource details.
control "aws-vpcs-check" do
impact 1.0
title "Check in all the VPCs for default sg not allowing 22 inwards"
aws_vpcs.vpc_ids.each do |vpc_id|
describe aws_security_group(vpc_id: vpc_id, group_name: "default") do
it { should allow_in(port: 22) }
end
end
end

control "aws-vpcs-multi-region-status-check" do # A unique ID for this control.
impact 1.0 # The criticality, if this control fails.
title 'Check AWS VPCs in all regions have status "available"' # A human-readable title.
aws_regions.region_names.each do |region| # Loop over all available AWS regions
aws_vpcs(aws_region: region).vpc_ids.each do |vpc| # Find all VPCs in a single AWS region
describe aws_vpc(aws_region: region, vpc_id: vpc) do # The test itself.
it { should exist } # Confirms AWS VPC exists
it { should be_available } # Confirms AWS VPC has status "available"
end
end
end
end

该测试的主要内容包含:

  • 如果传入指定的VPC ID,则检查指定的VPC是否存在
  • 检查所有VPC的安全组内是否包含允许22端口访问的策略
  • 检查所有区域内的状态为available的VPC

执行如下命令运行该profile对AWS环境进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# inspec exec example -t aws://cn-north-1/usecase

Profile: AWS InSpec Profile (example)
Version: 0.1.0
Target: aws://cn-north-1

↺ aws-single-vpc-exists-check: Check to see if custom VPC exists.
↺ Skipped control due to only_if condition.
✔ aws-vpcs-check: Check in all the VPCs for default sg not allowing 22 inwards
✔ EC2 Security Group ID: sg-01d67dds39bcffe17 Name: default VPC ID: vpc-032cdd155d561c7bf is expected to allow in {:port=>22}
✔ aws-vpcs-multi-region-status-check: Check AWS VPCs in all regions have status "available"
✔ VPC vpc-032cdd155d561c7bf in cn-north-1 is expected to exist
✔ VPC vpc-032cdd155d561c7bf in cn-north-1 is expected to be available

Profile: Amazon Web Services Resource Pack (inspec-aws)
Version: 1.31.0
Target: aws://cn-north-1

No tests executed.

Profile Summary: 2 successful controls, 0 control failures, 1 control skipped
Test Summary: 3 successful, 0 failures, 1 skipped

InSpec命令行

exec

在指定位置运行所有测试文件。

1
inspec exec profile [option]

该子命令加载给定的配置文件,如果需要,获取它们的依赖项,然后连接到目标并执行配置文件中包含的任何控件。一个或多个报告器用于生成输出。

exec允许使用不同位置的测试文件

使用Chef Supermarket:

1
inspec exec supermarket://username/linux-baseline

使用本地配置(执行controls中的所有test文件)

1
inspec exec /path/to/profile

使用本地配置(执行单个test文件)

1
inspec exec /path/to/a_test.rb

使用git仓库

通过SSH链接

1
inspec exec git@github.com:dev-sec/linux-baseline.git

使用HTTPS链接

1
inspec exec https://github.com/dev-sec/linux-baseline.git

私有仓库

1
inspec exec https://api_token@github.com/dev-sec/linux-baseline.git

exec 参数

  • -t,--target=TARGET 使用 URI 的简单定位选项;

InSpec项目结构

配置文件应具有以下结构:

1
2
3
4
5
6
7
8
9
10
examples/profile
├── README.md
├── controls
│ ├── example.rb
│ └── control_etc.rb
├── libraries
│ └── extension.rb
|── files
│ └── extras.conf
└── inspec.yml

说明:

  • inspec.yml包括配置文件描述(必需)
  • controls是所有测试所在的目录(必填)
  • libraries是所有 Chef InSpec 资源扩展所在的目录(可选)
  • files是包含配置文件可以访问的附加文件的目录(可选)
  • README.md应该用来解释概要、它的范围和用途

inspec.yml配置文件

配置文件如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#指定配置文件的唯一名称。必需的。
name: ssh
# title为配置文件指定一个人类可读的名称。
title: Basic SSH
# 指定配置文件维护者
maintainer: Chef Software, Inc.
# 指定版权所有者。
copyright: Chef Software, Inc.
# 指定配置文件的支持联系信息,通常是电子邮件地址。
copyright_email: support@chef.io
# 指定配置文件的许可证。
license: Proprietary, All rights reserved
# 为配置文件指定一行摘要
summary: Verify that SSH Server and SSH Client are configured securely
# 指定配置文件版本
version: 1.0.0
# 指定支持的平台目标列表
supports:
- platform-family: linux
# 定义此配置文件所依赖的配置文件列表
depends:
- name: profile
path: ../path/to/profile
gem_dependencies:
- name: "gem-name"
version: ">= 2.0.0"
# 在配置文件可以运行的 Chef InSpec 版本限制
inspec_version: "~> 2.1"

验证profile是否缺少文件

1
inspec check

如下所示

image-20220421191315673

supports,平台支持

使用文件中的supports设置inspec.yml来指定配置文件所针对的一个(或多个)平台。支持的平台列表可能包含以下内容:

  • platform-family限制到特定平台系列。
  • platform-name限制特定平台名称。platform-name支持星号 ( *) 通配符使用。
  • release限制特定平台版本,并与platform-name. release支持星号 ( *) 通配符使用。
  • platform限制平台名称或平台系列。

举例:

例如,要针对任何运行 Debian Linux 的设备

1
2
3
name: ssh
supports:
- platform-name: debian

要仅针对 Ubuntu 版本 20.04,请使用:

1
2
3
4
name: ssh
supports:
- platform-name: ubuntu
release: 20.04

要针对整个 Windows 2019 平台系列,包括数据中心和核心服务器,请使用:

1
2
3
name: ssh
supports:
- platform-name: windows_server_2019*

要针对在 Amazon AWS 上运行的任何内容,请使用:

1
2
3
name: ssh
supports:
- platform: aws

depends,配置文件依赖项

在配置文件可以使用来自另一个配置文件的控件之前,需要inspec.yml在该部分的包含配置文件的文件中指定要包含的配置文件depends 。对于要包含的每个配置文件,应包括从中获取配置文件的位置和配置文件的名称。例如:

1
2
3
4
5
depends:
- name: linux-baseline
url: https://github.com/dev-sec/linux-baseline/archive/master.tar.gz
- name: ssh-baseline
url: https://github.com/dev-sec/ssh-baseline/archive/master.tar.gz

path,该path设置定义了位于磁盘上的配置文件。此设置通常在开发配置文件和调试配置文件时使用。

1
2
3
4
5
depends:
- name: my-profile
path: /absolute/path
- name: another
path: ../relative/path

url设置指定位于基于 HTTP 或 HTTPS 的 URL 的配置文件。配置文件必须可通过 HTTP GET 操作访问,并且必须是有效的配置文件存档(zip、tar 或 tar.gz 格式)。

1
2
3
4
5
depends:
- name: my-profile
url: https://my.domain/path/to/profile.tgz
- name: profile-via-git
url: https://github.com/myusername/myprofile-repo/archive/master.tar.gz

git

1
2
3
4
5
6
7
8
depends:
- name: git-profile
git: http://url/to/repo
branch: desired_branch
tag: desired_version
commit: pinned_commit
version: semver_via_tags
relative_path: relative/optional/path/to/profile

surpermarkt

1
2
3
depends:
- name: supermarket-profile
supermarket: supermarket-username/supermarket-profile

controls 配置

包括配置文件中的所有控件

使用配置文件中的include_controls命令,每次执行包含配置文件时,都将执行命名配置文件中的所有控件。

包括控件

在上面的例子中,每次my-app-profile执行时,所有的控件my-baseline也都被执行。因此,将执行以下控制:

  • myapp-1
  • myapp-2
  • myapp-3
  • baseline-1
  • baseline-2

这是一个很好的提醒,当包含来自其他配置文件的控件时,为控件设置良好的命名约定有助于避免混淆!

从配置文件中跳过控件

如果包含的配置文件中的一项控制不适用于您的环境怎么办?幸运的是,不必为了删除控件而维护包含的配置文件的稍微修改的副本。该skip_control命令告诉 Chef InSpec 不要运行特定控件。

包括带有跳过的控件

在上面的示例中,除了来自配置文件的控制之外my-app-profilemy-baseline每次执行时都会my-app-profile执行来自配置文件的所有控件。baseline-2``my-baseline

修改控件

假设仍应运行包含配置文件中的特定控件,但影响不合适?也许测试应该仍然运行,但如果它失败了,它应该被视为低严重性而不是高严重性?当包含一个控件时,它也可以被修改!

包含带有修改的控件

使用包含的配置文件中的资源

默认情况下,列出的依赖项中的所有自定义资源都可用于您的配置文件。如果您的两个依赖项提供了同名的资源,则可以使用require_resourceDSL 函数来消除两者的歧义:

1
2
require_resource(profile: 'my_dep', resource: 'my_res',
as: 'my_res2')

InSpec语法

语句块

image-20220421170635101

配置文件输入

Chef InSpec 配置文件可能包含可在测试期间访问的其他文件。配置文件使您能够将测试的逻辑与您的测试检查的数据分开,例如,您需要打开的端口列表。

要访问这些文件,它们必须存储在files配置文件的根目录中。它们是通过相对于该文件夹的名称访问的,带有 inspec.profile.file(...).这是一个读取和测试端口列表的示例。文件夹结构为:

1
2
3
4
5
6
examples/profile
├── controls
│ ├── example.rb
│── files
│ └── services.yml
└── inspec.yml

services.yml内容

1
2
3
4
- service_name: httpd-alpha
port: 80
- service_name: httpd-beta
port: 8080

example.rb现在可以访问此文件

1
2
3
4
5
6
7
8
9
10
11
my_services = yaml(content: inspec.profile.file('services.yml')).params

my_services.each do |s|
describe service(s['service_name']) do
it { should be_running }
end

describe port(s['port']) do
it { should be_listening }
end
end

有关使用配置文件的更完整示例,请参阅 Learn Chef Rally 上的探索 Chef InSpec 资源。

“should” vs. “expect” syntax

Chef InSpec 将继续支持这两种编写测试的方法。考虑这个file测试:

1
2
3
describe file('/tmp/test.txt') do
it { should be_file }
end

expect这可以用语法重写

1
2
3
4
5
describe file('/tmp/test.txt') do
it 'should be a file' do
expect(subject).to(be_file)
end
end

上述两个示例的输出如下所示:

1
2
File /tmp/test.txt
✔ should be a file

Chef InSpec 建议使用该should 语法,因为它往往更容易为那些技术不高的用户阅读。

此外,您可以使用subject关键字来进一步控制您的输出,如果您选择:

1
2
3
4
5
6
describe 'test file' do
subject { file('/tmp/test.txt') }
it 'should be a file' do
expect(subject).to(be_file)
end
end

关键词

1
2
3
describe Entity(Property) do
Matcher
end

通用匹配器参考

be

这个匹配器后面可以跟许多不同的比较运算符。始终确保使用数字而不是字符串进行这些比较。

1
2
3
4
describe file('/proc/cpuinfo') do
its('size') { should be >= 10 }
its('size') { should be < 1000 }
end

CMP

eq,不同cmp的是,用于限制较少的比较的匹配器。它将尝试将实际值与您要与之进行比较的类型相匹配。这是为了让用户不必编写类型转换和解析。

1
2
3
4
5
6
7
describe sshd_config do
its('Protocol') { should cmp 2 }
end

describe passwd.uid(0) do
its('users') { should cmp 'root' }
end

cmp行为方式如下:

  • 比较字符串和数字
1
2
3
4
5
6
7
8
describe sshd_config do
# Only `'2'` works
its('Protocol') { should eq '2' }

# Both of these work
its('Protocol') { should cmp '2' }
its('Protocol') { should cmp 2 }
end
  • 字符串比较不区分大小写
1
2
3
4
describe auditd_conf do
its('log_format') { should cmp 'raw' }
its('log_format') { should cmp 'RAW' }
end
  • 识别嵌入在字符串中的版本
1
2
3
describe package('curl') do
its('version') { should cmp > '7.35.0-1ubuntu2.10' }
end
  • 将只有一个条目的数组与一个值进行比较
1
2
3
4
describe passwd.uids(0) do
its('users') { should cmp 'root' }
its('users') { should cmp ['root'] }
end
  • 字符串的单值数组也可以与正则表达式进行比较
1
2
3
describe auditd_conf do
its('log_format') { should cmp /raw/i }
end
  • 改进了八进制比较的打印
1
2
3
4
5
6
describe file('/proc/cpuinfo') do
its('mode') { should cmp '0345' }
end

expected: 0345
got: 0444

eq

测试两个值是否完全相等。

1
2
3
4
describe sshd_config do
its('RSAAuthentication') { should_not eq 'no' }
its('Protocol') { should eq '2' }
end

eq如果类型不匹配则失败。在比较数字的配置条目时,请记住这一点:

1
2
3
4
its('Port') { should eq '22' } # ok

its('Port') { should eq 22 }
# fails: '2' != 2 (string vs int)

对于限制较少的比较,请使用cmp.

include

验证值是否包含在列表中。

1
2
3
describe passwd do
its('users') { should include 'my_user' }
end

be_in

验证对象是否包含在列表中。

1
2
3
describe resource do
its('item') { should be_in LIST }
end

match

检查字符串是否匹配正则表达式。

1
2
3
describe sshd_config do
its('Ciphers') { should_not match /cbc/ }
end

语句

以下资源测试 |ssh| 服务器配置。例如,一个简单的控件可以描述为:

1
2
3
describe sshd_config do
its('Port') { should cmp 22 }
end

在各种用例中,例如在不同部门实施 IT 合规性,使用元数据扩展控制变得很方便。每个控件都可以定义一个附加impacttitledesc。一个示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
control 'sshd-8' do
impact 0.6
title 'Server: Configure the service port'
desc 'Always specify which port the SSH server should listen.'
desc 'rationale', 'This ensures that there are no unexpected settings' # Requires Chef InSpec >=2.3.4
tag 'ssh','sshd','openssh-server'
tag cce: 'CCE-27072-8'
ref 'NSA-RH6-STIG - Section 3.5.2.1', url: 'https://www.nsa.gov/ia/_files/os/redhat/rhel5-guide-i731.pdf'

describe sshd_config do
its('Port') { should cmp 22 }
end
end

元数据说明:

  • ‘sshd-8’是控件的名称
  • ref是对外部文档的引用
  • describe是一个包含至少一个测试的块。一个control块必须至少包含一个describe块,但可以包含任意多个
  • sshd_config是 Chef InSpec 资源。有关 Chef InSpec 资源的完整列表,请参阅 Chef InSpec 资源文档。
  • its(‘Port’)是Chef InSpecde1;{ should eq ‘22’ }是单元测试。一个describe块必须至少包含一个匹配器,但可以包含任意数量的匹配器。

检查至少一个条件是否通过describe.one

使用 Chef InSpec,您可以检查一组检查中的至少一个是否为真。例如,如果您在两个不同的位置配置一个设置,那么您可能想要测试是否设置了配置 A 或配置 B。用块做这个任务describe.onedescribe.one定义一组describe块,其中只有一个块需要通过。

1
2
3
4
5
6
7
8
9
describe.one do
describe ConfigurationA do
its('setting_1') { should eq true }
end

describe ConfigurationB do
its('setting_2') { should eq true }
end
end

describe.one使用说明

  • 如果其中一个describe.one嵌套describe块的所有断言都通过了,则该块通过。一个describe.one块需要整个describe块才能通过,而不仅仅是一个断言。
  • Chef InSpec 将始终评估describe.one. describe它不会在评估通过的块时短路。
  • 不支持将一个块嵌套describe.one在另一个块内。describe.one

demo

以下示例显示了使用单个control模块的简单合规性测试。

测试系统事件日志

以下测试显示了如何审核运行 Windows 2012 R2 且启用了密码复杂性的计算机:

1
2
3
4
5
6
7
8
control 'windows-account-102' do
impact 'critical'
title 'Windows Password Complexity is Enabled'
desc 'Password must meet complexity requirement'
describe security_policy do
its('PasswordComplexity') { should cmp 1 }
end
end

测试 PostgreSQL 密码是否为空

以下测试展示了如何审计运行 PostgreSQL 的机器以确保密码不为空。

复制

1
2
3
4
5
6
7
control 'postgres-7' do
impact 1.0
title "Don't allow empty passwords"
describe postgres_session('user', 'pass').query("SELECT * FROM pg_shadow WHERE passwd IS NULL;") do
its('output') { should cmp '' }
end
end

测试 MySQL 密码是否在 ENV 中

以下测试显示了如何审计运行 MySQL 的机器以确保密码不存储在ENV

复制

1
2
3
4
5
6
7
8
9
10
11
control 'mysql-3' do
impact 1.0
title 'Do not store your MySQL password in your ENV'
desc '
Storing credentials in your ENV may easily expose
them to an attacker. Prevent this at all costs.
'
describe command('env') do
its('stdout') { should_not match /^MYSQL_PWD=/ }
end
end

测试是否/etc/ssh是目录

以下测试显示了如何审计机器以确保它/etc/ssh是一个目录:

复制

1
2
3
4
5
6
7
8
9
10
11
control 'basic-1' do
impact 1.0
title '/etc/ssh should be a directory'
desc '
In order for OpenSSH to function correctly, its
configuration path must be a folder.
'
describe file('/etc/ssh') do
it { should be_directory }
end
end

测试 Apache 是否运行

以下测试显示了如何审计机器以确保 Apache 已启用并正在运行:

复制

1
2
3
4
5
6
7
8
control 'apache-1' do
impact 'medium'
title 'Apache2 should be configured and running'
describe service(apache.service) do
it { should be_enabled }
it { should be_running }
end
end

测试是否安装了不安全的软件包

以下测试显示了如何审计机器是否存在不安全的包:

复制

1
2
3
4
5
6
7
8
9
10
control 'cis-os-services-5.1.3' do
impact 0.7
title '5.1.3 Ensure rsh client is not installed'
describe package('rsh') do
it { should_not be_installed }
end
describe package('rsh-redone-client') do
it { should_not be_installed }
end
end

测试 Windows 注册表项

以下测试显示如何审核机器以确保启用安全 DLL 搜索模式:

复制

1
2
3
4
5
6
7
8
9
10
11
control 'windows-base-101' do
impact 1.0
title 'Safe DLL Search Mode is Enabled'
desc '
@link: https://msdn.microsoft.com/en-us/library/ms682586(v=vs.85).aspx
'
describe registry_key('HKLM\\System\\CurrentControlSet\\Control\\Session Manager') do
it { should exist }
it { should_not have_property_value('SafeDllSearchMode', :type_dword, '0') }
end
end

用于only_if排除特定控件

此示例说明如果使用 不满足条件,如何允许跳过某些控件only_if。在本例中,如果redis-cli命令不存在,则不会执行控制。可选消息可以说明它被跳过的原因。

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
control 'nutcracker-connect-redis-001' do
impact 'critical'
title 'Check if nutcracker can pass commands to redis'
desc 'execute redis-cli set key command, to check connectivity of the service'

only_if('redis is not installed.') do
command('redis-cli').exist?
end

describe command('redis-cli SET test_inspec "HELLO"') do
its('stdout') { should match /OK/ }
end
end

此示例检查是否安装了某些 pip 包,但前提是 ‘/root/.aws’ 存在:

复制

1
2
3
4
5
6
7
8
9
10
11
control 'pip-packages-installed' do
title 'Check if essential pips are installed'
only_if('aws-cli config not created.') do
directory('/root/.aws').exist?
end
%w(aws-mfa PyYAML awscli).each do |aws_pip_deps|
describe pip(aws_pip_deps) do
it { should be_installed }
end
end
end

将其与其他条件混合,例如检查文件是否存在,有助于使用 Chef InSpec 测试不同的测试路径。通过这种方式,您可以跳过某些控件,这些控件由于服务器的准备方式而 100% 失败,但是您知道相同的控件套件稍后会在不同的情况下被不同的团队重用。

关于only_if

  • only_if适用于整个control. 如果only_if 块的结果评估为 false,则作为块的一部分提及的任何 Chef InSpec 资源 describe都不会运行。此外,描述块的内容将不会运行。但是,only_if 语句之前的裸 Ruby 表达式和裸 Chef InSpec 资源(不与描述块关联)将运行

为了显示:

复制

1
2
3
4
5
6
7
8
control "whatruns" do
command("do_something") # This will ALWAYS run
describe command("do_another_thing") do # This will not run
command("do_yet_another_thing") # This will not run
end
only_if { false }
command("do_something_else") # This will not run
end
  • 每个块只only_if允许一个。control如果存在多个only_if块,则仅使用最后一个only_if
  • 如果在控制块之外使用,only_if则跳过当前文件中的所有控件
  • 要实现复杂的逻辑,请在块中使用 Ruby ‘or’ ( ||) 和 ‘and’ ( &&) only_if

复制

1
2
3
only_if('ready for launch') do
rocket_is_ready && weather_is_clear
end

控件的附加元数据

以下示例说明了添加标签和引用的各种方法control

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
control 'ssh-1' do
impact 1.0

title 'Allow only SSH Protocol 2'
desc '
Only SSH protocol version 2 connections should be permitted.
The default setting in /etc/ssh/sshd_config is correct, and can be
verified by ensuring that the following line appears: Protocol 2
'

tag 'production','development'
tag 'ssh','sshd','openssh-server'

tag cce: 'CCE-27072-8'
tag disa: 'RHEL-06-000227'

tag remediation: 'stig_rhel6/recipes/sshd-config.rb'
tag remediation: 'https://supermarket.chef.io/cookbooks/ssh-hardening'

ref 'NSA-RH6-STIG - Section 3.5.2.1', url: 'https://www.nsa.gov/ia/_files/os/redhat/rhel5-guide-i731.pdf'
ref 'http://people.redhat.com/swells/scap-security-guide/RHEL/6/output/ssg-centos6-guide-C2S.html'

describe ssh_config do
its('Protocol') { should cmp 2 }
end
end

在 InSpec 中使用 Ruby

Chef InSpec 语言是一种基于 Ruby 的语言。这使您可以灵活地使用控件中的 Ruby 代码:

复制

1
2
3
4
json_obj = json('/file.json')
json_obj['keys'].each do |value|
..
end

Ruby 允许很多自由,但应该限制控件,以便它们保持可移植性和易于理解。请参阅我们的个人资料风格指南

核心和自定义资源被编写为继承自 Inspec.resource.

Ruby 语法

Ruby数据类型

整数

1
2
3
123                  # Fixnum 十进制
1_234 # Fixnum 带有下划线的十进制
-500 # 负的 Fixnum

浮点型
Ruby 支持浮点数。它们是带有小数的数字。浮点数是类 Float 的对象,且可以是下列中任意一个。

1
2
3
123.4                # 浮点值
1.0e6 # 科学记数法
4E20 # 不是必需的

算术操作

加减乘除操作符:+-/;指数操作符为*

指数不必是整数,例如

1
2
3
4
5
6
7
8
#指数算术,结果 16
puts 2**(1/4)
# 加法运算,结果4
puts 2+2
# 除法,结果2
puts 2/1
# 混合运算
puts 16**(1/4.0)#1与4.0的商为0.25(四分之一),然后开四次方根

字符串类型

Ruby 字符串简单地说是一个 8 位字节序列,它们是类 String 的对象。

双引号标记的字符串允许替换和使用反斜线符号,单引号标记的字符串不允许替换,且只允许使用 \ 和 ' 两个反斜线符号。进行转义

1
2
3
4
#!/usr/bin/ruby -w

puts 'escape using "\\"';
puts 'That\'s right';

结果

1
2
escape using "\"
That's right

可以使用序列 #{ expr } 替换任意 Ruby 表达式的值为一个字符串。在这里,expr 可以是任意的 Ruby 表达式。

1
2
3
4
5
#!/usr/bin/ruby -w

name="Ruby"
puts name
puts "#{name+",ok"}"

输出结果为:

1
2
Ruby
Ruby,ok

数据结构

Ruby支持以下三种数据结构:

  • 数组
  • 哈希类型
  • 范围类型

数组

数组字面量通过[]中以逗号分隔定义,且支持range定义。

  • 数组通过[]索引访问
  • 通过赋值操作插入、删除、替换元素
  • 通过+,-号进行合并和删除元素,且集合做为新集合出现
  • 通过<<号向原数据追加元素
  • 通过*号重复数组元素
  • 通过|和&符号做并集和交集操作(注意顺序)
1
2
3
4
5
#!/usr/bin/ruby
ary = [ "fred", 10, 3.14, "This is a string", "last element", ]
ary.each do |i|
puts i
end

创建数组

有多种方式创建或初始化数组。一种方式是通过 new 类方法:

1
names = Array.new

您可以在创建数组的同时设置数组的大小:

1
names = Array.new(20)

数组 names 的大小或长度为 20 个元素。您可以使用 size 或 length 方法返回数组的大小:

1
2
3
4
5
#!/usr/bin/ruby

names = Array.new(20)
puts names.size # 返回 20
puts names.length # 返回 20

赋值

1
2
3
names = Array.new(4, "mac")
names[1]='windows'
puts "#{names}" # 输出结果,["mac", "windows", "mac", "mac"]

哈希类型

Ruby 哈希是在大括号内放置一系列键/值对,键和值之间使用逗号和序列 => 分隔。尾部的逗号会被忽略。

1
2
3
4
5
6
#!/usr/bin/ruby

hsh = colors = { "red" => 0xf00, "green" => 0x0f0, "blue" => 0x00f }
hsh.each do |key, value|
print key, " is ", value, "\n"
end

这将产生以下结果:

1
2
3
red is 3840
green is 240
blue is 15

创建哈希
与数组一样,有各种不同的方式来创建哈希。您可以通过 new 类方法创建一个空的哈希:

1
months = Hash.new

赋值

1
2
3
mh={'a'=>'apple','b'=>'banan'}
mh['a']='thrid'
puts mh #结论 {"a"=>"thrid", "b"=>"banan"}

范围类型

一个范围表示一个区间。

范围是通过设置一个开始值和一个结束值来表示。范围可使用 s..e 和 s…e 来构造,或者通过 Range.new 来构造。使用 .. 构造的范围从开始值运行到结束值(包含结束值)。使用 … 构造的范围从开始值运行到结束值(不包含结束值)。当作为一个迭代器使用时,范围会返回序列中的每个值。范围 (1..5) 意味着它包含值 1, 2, 3, 4, 5,范围 (1…5) 意味着它包含值 1, 2, 3, 4 。

条件判断

1
2
3
4
5
6
7
if conditional [then]
code...
[elsif conditional [then]
code...]...
[else
code...]
end

通常我们省略保留字 then 。若想在一行内写出完整的 if 式,则必须以 then 隔开条件式和程式区块。如下所示:

1
if a == 4 then a = 7 end

实例代码

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/ruby
# -*- coding: UTF-8 -*-

x=1
if x > 2
puts "x 大于 2"
elsif x <= 2 and x!=0
puts "x 是 1"
else
puts "无法得知 x 的值"
end

循环

while语句

有两种模式的while语句

1
2
3
while conditional [do]
code
end

demo

1
2
3
while 1<2 :
puts "successd"
end

until语句

1
2
3
until conditional [do]
code
end

for循环

1
2
3
for variable [, variable ...] in expression [do]
code
end

先计算表达式得到一个对象,然后针对 expression 中的每个元素分别执行一次 code

1
2
3
4
5
6
#!/usr/bin/ruby
# -*- coding: UTF-8 -*-

for i in 0..5
puts "局部变量的值为 #{i}"
end

参考文档

ruby例子:https://github.com/inspec/inspec/tree/main/examples