Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Magic Modules generated resource for SSL Policy #90

Merged
merged 2 commits into from
Jan 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ local
*.idea*
*.env
*inspec-gcp.tfvars
*gcp-inspec-attributes.yaml
*gcp-inspec-attributes.yaml
inspec-cassettes
*terraform.tfvars
*inspec.json
18 changes: 8 additions & 10 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
source 'https://rubygems.org'

gem 'inifile'
gem 'bundle'
gem 'google-api-client'
gem 'googleauth'
gem 'google-cloud'
gem 'bundle'
gem 'googleauth'
gem 'inifile'
gem 'inspec', '~> 3.0', '>= 3.0.25'
gem 'rubocop'

group :development do
gem 'rake'
gem 'rubocop'
gem 'github_changelog_generator'
gem 'pry-coolline'
gem 'passgen'
end

group :inspec do
gem 'inspec', '~> 3.0', '>= 3.0.25'
gem 'pry-coolline'
gem 'rake'
gem 'webmock'
end
12 changes: 8 additions & 4 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ namespace :test do
sh(cmd)
end

task :generate_integration_test_variables do
puts "----> Generating terraform and inspec variable files"
#p GCPInspecConfig.config[:gcp_project_id]
GCPInspecConfig.store_json(variable_file_name)
GCPInspecConfig.store_yaml(profile_attributes)
end

task :plan_integration_tests do

# Determine the storage account name and the admin password
Expand All @@ -56,10 +63,7 @@ namespace :test do
# Use the first 4 characters of the storage account to create a suffix
# suffix = sa_name[0..3]

puts "----> Generating terraform and inspec variable files"
#p GCPInspecConfig.config[:gcp_project_id]
GCPInspecConfig.store_json(variable_file_name)
GCPInspecConfig.store_yaml(profile_attributes)
Rake::Task["test:generate_integration_test_variables"].execute

puts "----> Setup"
# Create the plan that can be applied to GCP
Expand Down
43 changes: 43 additions & 0 deletions docs/resources/google_compute_ssl_policies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: About the SslPolicy resource
platform: gcp
---


## Syntax
A `google_compute_ssl_policies` is used to test a Google SslPolicy resource

## Examples
```
describe google_compute_ssl_policies({project: ''}) do
it { should exist }
its('names') { should include 'inspec-gcp-ssl-policy' }
its('profiles') { should include 'CUSTOM' }
its('count') { should eq 1 }
end

google_compute_ssl_policies({project: ''}).names.each do |policy_name|
describe google_compute_ssl_policy({project: '', name: policy_name}) do
its('min_tls_version') { should cmp 'TLS_1_2' }
end
end
```

## Properties
Properties that can be accessed from the `google_compute_ssl_policies` resource:

See [google_compute_ssl_policy.md](google_compute_ssl_policy.md) for more detailed information
* `creation_timestamps`: an array of `google_compute_ssl_policy` creation_timestamp
* `descriptions`: an array of `google_compute_ssl_policy` description
* `ids`: an array of `google_compute_ssl_policy` id
* `names`: an array of `google_compute_ssl_policy` name
* `profiles`: an array of `google_compute_ssl_policy` profile
* `min_tls_versions`: an array of `google_compute_ssl_policy` min_tls_version
* `enabled_features`: an array of `google_compute_ssl_policy` enabled_features
* `custom_features`: an array of `google_compute_ssl_policy` custom_features
* `fingerprints`: an array of `google_compute_ssl_policy` fingerprint
* `warnings`: an array of `google_compute_ssl_policy` warnings

## Filter Criteria
This resource supports all of the above properties as filter criteria, which can be used
with `where` as a block or a method.
46 changes: 46 additions & 0 deletions docs/resources/google_compute_ssl_policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: About the SslPolicy resource
platform: gcp
---


## Syntax
A `google_compute_ssl_policy` is used to test a Google SslPolicy resource

## Examples
```
describe google_compute_ssl_policy({project: '', name: 'inspec-gcp-ssl-policy'}) do
it { should exist }
its('min_tls_version') { should cmp 'TLS_1_2' }
its('profile') { should cmp 'CUSTOM' }
its('custom_features') { should include 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384' }
its('custom_features') { should include 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384' }
end
```

## Properties
Properties that can be accessed from the `google_compute_ssl_policy` resource:

* `creation_timestamp`: Creation timestamp in RFC3339 text format.

* `description`: An optional description of this resource.

* `id`: The unique identifier for the resource.

* `name`: Name of the resource. Provided by the client when the resource is created. The name must be 1-63 characters long, and comply with RFC1035. Specifically, the name must be 1-63 characters long and match the regular expression `[a-z]([-a-z0-9]*[a-z0-9])?` which means the first character must be a lowercase letter, and all following characters must be a dash, lowercase letter, or digit, except the last character, which cannot be a dash.

* `profile`: Profile specifies the set of SSL features that can be used by the load balancer when negotiating SSL with clients. This can be one of `COMPATIBLE`, `MODERN`, `RESTRICTED`, or `CUSTOM`. If using `CUSTOM`, the set of SSL features to enable must be specified in the `customFeatures` field.

* `min_tls_version`: The minimum version of SSL protocol that can be used by the clients to establish a connection with the load balancer. This can be one of `TLS_1_0`, `TLS_1_1`, `TLS_1_2`.

* `enabled_features`: The list of features enabled in the SSL policy.

* `custom_features`: A list of features enabled when the selected profile is CUSTOM. The method returns the set of features that can be specified in this list. This field must be empty if the profile is not CUSTOM.

* `fingerprint`: Fingerprint of this resource. A hash of the contents stored in this object. This field is used in optimistic locking.

* `warnings`: If potential misconfigurations are detected for this SSL policy, this field will be populated with warning messages.

* `code`: A warning code, if applicable.

* `message`: A human-readable description of the warning code.
213 changes: 213 additions & 0 deletions libraries/gcp_backend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#

require 'json'
require 'net/http'
require 'googleauth'

# Base class for GCP resources - depends on train GCP transport for connection
#
Expand All @@ -17,6 +19,10 @@ def initialize(opts)
@opts = opts
# ensure we have a GCP connection, resources can choose which of the clients to instantiate
@gcp = inspec.backend

# Magic Modules generated resources use an alternate transport method
# In the future this will be moved into the train-gcp plugin itself
@connection = GcpApiConnection.new if opts[:use_http_transport]
end

def failed_resource?
Expand Down Expand Up @@ -178,3 +184,210 @@ def camel_case(data)
camel_case_data.gsub(/[gb]/, &:upcase)
end
end

class GcpApiConnection
def initialize
@service_account_file = ENV['GOOGLE_APPLICATION_CREDENTIALS']
end

def fetch_auth
unless @service_account_file.nil?
return Network::Authorization.new.for!(
['https://www.googleapis.com/auth/compute.readonly'],
).from_service_account_json!(
@service_account_file,
)
end
Network::Authorization.new.from_application_default!
end

def fetch(base_url, template, var_data)
get_request = Network::Base.new(
build_uri(base_url, template, var_data),
fetch_auth,
)
return_if_object get_request.send
end

def fetch_all(base_url, template, var_data)
next_page(build_uri(base_url, template, var_data))
end

def next_page(uri, token = nil)
next_hash = {}
next_hash['pageToken'] = token unless token.nil?
current_params = Hash[URI.decode_www_form(uri.query || '')].merge(next_hash)
uri.query = URI.encode_www_form(current_params)
get_request = Network::Base.new(
uri,
fetch_auth,
)
result = JSON.parse(get_request.send.body)
next_page_token = result['nextPageToken']
return [result] if next_page_token.nil?

[result] + next_page(uri, next_page_token)
end

def return_if_object(response)
raise "Bad response: #{response.body}" \
if response.is_a?(Net::HTTPBadRequest)
raise "Bad response: #{response}" \
unless response.is_a?(Net::HTTPResponse)
return if response.is_a?(Net::HTTPNotFound)
return if response.is_a?(Net::HTTPNoContent)
result = JSON.parse(response.body)
raise_if_errors result, %w{error errors}, 'message'
raise "Bad response: #{response}" unless response.is_a?(Net::HTTPOK)
result
end

def raise_if_errors(response, err_path, msg_field)
errors = self.class.navigate(response, err_path)
raise_error(errors, msg_field) unless errors.nil?
end

def raise_error(errors, msg_field)
raise IOError, ['Operation failed:',
errors.map { |e| e[msg_field] }.join(', ')].join(' ')
end

def build_uri(base_url, template, var_data)
URI.join(
base_url,
expand_variables(template, var_data),
)
end

# Allows fetching objects within a tree path.
def self.navigate(source, path, default = nil)
key = path.take(1)[0]
path = path.drop(1)
return default unless source.key?(key)
result = source.fetch(key)
return navigate(result, path, default) unless path.empty?
return result if path.empty?
end

def extract_variables(template)
template.scan(/{{[^}]*}}/).map { |v| v.gsub(/{{([^}]*)}}/, '\1') }
.map(&:to_sym)
end

def expand_variables(template, var_data)
extract_variables(template).each do |v|
unless var_data.key?(v)
raise "Missing variable :#{v} in #{var_data} on #{caller.join("\n")}}"
end
template.gsub!(/{{#{v}}}/, CGI.escape(var_data[v].to_s))
end
template
end
end

# A handler for authenticated network request
module Network
class Base
def initialize(link, cred)
@link = link
@cred = cred
end

def builder
Net::HTTP.const_get('Get')
end

def send
request = @cred.authorize(builder.new(@link))
request['User-Agent'] = generate_user_agent
response = transport(request).request(request)
unless ENV['GOOGLE_HTTP_VERBOSE'].nil?
puts ["network(#{request}: [#{response.code}]",
response.body.split("\n").map(&:strip).join(' ')].join(' ')
end
response
end

def transport(request)
uri = request.uri
puts "network(#{request}: #{uri})" \
unless ENV['GOOGLE_HTTP_VERBOSE'].nil?
transport = Net::HTTP.new(uri.host, uri.port)
transport.use_ssl = uri.is_a?(URI::HTTPS)
transport.verify_mode = OpenSSL::SSL::VERIFY_PEER
transport.set_debug_output $stderr \
unless ENV['GOOGLE_HTTP_DEBUG'].nil?
transport
end

private

def generate_user_agent
'inspec-google/1.0.0'
end
end

# A class to aquire credentials and authorize Google API calls.
class Authorization
def initialize
@authorization = nil
@scopes = []
end

def authorize(obj)
raise ArgumentError, 'A from_* method needs to be called before' \
unless @authorization

if obj.class <= URI::HTTPS || obj.class <= URI::HTTP
authorize_uri obj
elsif obj.class < Net::HTTPRequest
authorize_http obj
else
obj.authorization = @authorization
obj
end
end

def for!(*scopes)
@scopes = scopes
self
end

def from_service_account_json!(service_account_file)
raise 'Missing argument for scopes' if @scopes.empty?
@authorization = ::Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: File.open(service_account_file),
scope: @scopes,
)
self
end

def from_application_default!
@authorization = ::Google::Auth.get_application_default
self
end

private

def authorize_uri(obj)
http = Net::HTTP.new(obj.host, obj.port)
http.use_ssl = obj.instance_of?(URI::HTTPS)
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
[http, authorize_http(Net::HTTP::Get.new(obj.request_uri))]
end

def authorize_http(req)
req.extend TokenProperty
auth = {}
@authorization.apply!(auth)
req['Authorization'] = auth[:authorization]
req.token = auth[:authorization].split(' ')[1]
req
end
end
# Extension methods to enable retrieving the authentication token.
module TokenProperty
attr_reader :token
attr_writer :token
end
end
Loading