diff --git a/Gemfile b/Gemfile index 60fbde1d8..c26b56b9f 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ group :test do gem 'mocha', '~> 1.1' gem 'ruby-progressbar', '~> 1.8' gem 'nokogiri', '~> 1.6' + gem 'webmock', '~> 2.3.2' end group :integration do diff --git a/docs/resources/http.md.erb b/docs/resources/http.md.erb new file mode 100644 index 000000000..eac13e3a0 --- /dev/null +++ b/docs/resources/http.md.erb @@ -0,0 +1,94 @@ +--- +title: About the http Resource +--- + +# http + +Use the `http` InSpec audit resource to test an http endpoint. + +## Syntax + +An `http` resource block declares the configuration settings to be tested: + + describe http('url', auth: {user: 'user', pass: 'test'}, params: {params}, method: 'method', headers: {headers}, body: body) do + its('status') { should eq number } + its('body') { should eq 'body' } + its('header') { should eq 'header' } + end + +where + +* `('url')` is the url to test +* `{user: 'user', pass: 'test'}` may be specified for basic auth request +* `{params}` may be specified for http request parameters +* `'method'` may be specified for http request method (default to 'GET') +* `{headers}` may be specified for http request headers +* `body` may be specified for http request body + +## Matchers + +This InSpec audit resource has the following matchers: + +### be + +<%= partial "/shared/matcher_be" %> + +### body + +The `body` matcher tests body content of http response: + + its('body') { should eq 'hello\n' } + +### cmp + +<%= partial "/shared/matcher_cmp" %> + +### eq + +<%= partial "/shared/matcher_eq" %> + +### header + +The `header` matcher tests arbitrary header for the http response: + + its('header') { should eq 'value' } + +### include + +<%= partial "/shared/matcher_include" %> + +### match + +<%= partial "/shared/matcher_match" %> + +### status + +The `status` matcher tests status of the http response: + + its('status') { should eq 200 } + +## Examples + +The following examples show how to use this InSpec audit resource. + +### Simple http test + +For example, a service is listening on default http port can be tested like this: + + describe http('http://localhost') do + its('status') { should cmp 200 } + end + +### Complex http test + + describe http('http://localhost:8080/ping', + auth: {user: 'user', pass: 'test'}, + params: {format: 'html'}, + method: 'POST', + headers: {'Content-Type' => 'application/json'}, + data: '{"data":{"a":"1","b":"five"}}') do + its('status') { should cmp 200 } + its('body') { should cmp 'pong' } + its('Content-Type') { should cmp 'text/html' } + end + diff --git a/inspec.gemspec b/inspec.gemspec index ef9891770..dacd26091 100644 --- a/inspec.gemspec +++ b/inspec.gemspec @@ -38,4 +38,5 @@ Gem::Specification.new do |spec| spec.add_dependency 'sslshake', '~> 1' spec.add_dependency 'parallel', '~> 1.9' spec.add_dependency 'rspec_junit_formatter', '~> 0.2.3' + spec.add_dependency 'http', '~> 2.1.0' end diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index c06962020..14e90ea2b 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -86,6 +86,7 @@ require 'resources/gem' require 'resources/groups' require 'resources/grub_conf' require 'resources/host' +require 'resources/http' require 'resources/iis_site' require 'resources/inetd_conf' require 'resources/interface' diff --git a/lib/resources/http.rb b/lib/resources/http.rb new file mode 100644 index 000000000..e14f82b94 --- /dev/null +++ b/lib/resources/http.rb @@ -0,0 +1,54 @@ +# encoding: utf-8 +# copyright: 2017, Criteo +# author: Guilhem Lettron +# license: Apache v2 + +require 'http' + +module Inspec::Resources + class Http < Inspec.resource(1) + name 'http' + desc 'Use the http InSpec audit resource to test http call.' + example " + describe http('http://localhost:8080/ping', auth: {user: 'user', pass: 'test'}, params: {format: 'html'}) do + its('status') { should cmp 200 } + its('body') { should cmp 'pong' } + its('Content-Type') { should cmp 'text/html' } + end + " + + # rubocop:disable ParameterLists + def initialize(url, method: 'GET', params: nil, auth: {}, headers: {}, data: nil) + @url = url + @method = method + @params = params + @auth = auth + @headers = headers + @data = data + end + + def status + response.status + end + + def body + response.to_s + end + + def method_missing(name) + response.headers[name.to_s] + end + + def to_s + "http #{@method} on #{@url}: #{response}" + end + + private + + def response + http = HTTP.headers(@headers) + http = http.basic_auth(@auth) unless @auth.empty? + @response ||= http.request(@method, @url, { body: @data, params: @params }) + end + end +end diff --git a/test/helper.rb b/test/helper.rb index e96a1cac3..183d4d5f1 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -11,6 +11,7 @@ end require 'minitest/autorun' require 'minitest/spec' +require 'webmock/minitest' require 'mocha/setup' require 'fileutils' require 'pathname' diff --git a/test/unit/resources/http_test.rb b/test/unit/resources/http_test.rb new file mode 100644 index 000000000..d850a60dc --- /dev/null +++ b/test/unit/resources/http_test.rb @@ -0,0 +1,54 @@ +# encoding: utf-8 +# author: Guilhem Lettron + +require 'helper' +require 'inspec/resource' + +describe 'Inspec::Resources::Http' do + it 'verify simple http' do + stub_request(:get, "www.example.com").to_return(status: 200, body: 'pong') + + resource = load_resource('http', 'http://www.example.com') + _(resource.status).must_equal 200 + _(resource.body).must_equal 'pong' + end + + it 'verify http with basic auth' do + stub_request(:get, "www.example.com").with(basic_auth: ['user', 'pass']).to_return(status: 200, body: 'auth ok') + + resource = load_resource('http', 'http://www.example.com', auth: { user: 'user',pass: 'pass'}) + _(resource.status).must_equal 200 + _(resource.body).must_equal 'auth ok' + end + + it 'verify http post with data' do + stub_request(:post, "www.example.com").with(body: {data: {a: '1', b: 'five'}}).to_return(status: 200, body: 'post ok') + + resource = load_resource('http', 'http://www.example.com', + method: 'POST', + data: '{"data":{"a":"1","b":"five"}}', + headers: {'content-type' => 'application/json'}) + _(resource.status).must_equal 200 + _(resource.body).must_equal 'post ok' + end + + it 'verify http headers' do + stub_request(:post, "www.example.com").with(headers: {'content-type' => 'application/json'}).to_return(status: 200, body: 'headers ok', headers: {'mock' => 'ok'}) + + resource = load_resource('http', 'http://www.example.com', + method: 'POST', + data: '{"data":{"a":"1","b":"five"}}', + headers: {'content-type' => 'application/json'}) + _(resource.status).must_equal 200 + _(resource.body).must_equal 'headers ok' + _(resource.mock).must_equal 'ok' + end + + it 'verify http with params' do + stub_request(:get, "www.example.com").with(query: {a: 'b'}).to_return(status: 200, body: 'params ok') + + resource = load_resource('http', 'http://www.example.com', params: {a: 'b'}) + _(resource.status).must_equal 200 + _(resource.body).must_equal 'params ok' + end +end