controls 1.0.0.beta.2

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.
@@ -0,0 +1,31 @@
1
+ module Controls
2
+ class Client
3
+ # A module to encapsulate API methods related to security controls and
4
+ # configurations
5
+ # @since API v1.0
6
+ # @version v1.0.0
7
+ module Configurations
8
+ # @!group Configuration Methods
9
+
10
+ # @param [String] configuration the name of the configuration to search
11
+ # for
12
+ # @return [Array<Hash>] a list of hashes representing configurations
13
+ def configurations(configuration = nil)
14
+ if configuration
15
+ get "/configurations/#{configuration}"
16
+ else
17
+ get "/configurations"
18
+ end
19
+ end
20
+
21
+ # @param [String] control the security control look up configurations for
22
+ # @return [Array<Hash>] a list of hashes representing configurations
23
+ def security_control_configurations(control)
24
+ get "/security_controls/#{control}/configurations"
25
+ end
26
+ alias_method :configurations_by_security_control, :security_control_configurations
27
+
28
+ # @!endgroup
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ require 'controls/client/prioritized_guidance'
2
+
3
+ module Controls
4
+ class Client
5
+ # A module to encapsulate API methods related to guidance
6
+ # @since API v1.0
7
+ # @version v1.0.0
8
+ module Guidance
9
+ include PrioritizedGuidance
10
+
11
+ # @!group Guidance Methods
12
+
13
+ # @param [String] name the name of the guidance to search for
14
+ # @return [Hash] a hash representing the matching guidance
15
+ def guidance(name)
16
+ get "/guidance/#{name}"
17
+ end
18
+
19
+ # @param [String] configuration the configuration name to search by
20
+ # @return [Array<Hash>] an array of "guidance hashes"
21
+ def guidance_by_configuration(security_control, configuration)
22
+ get "/configurations/#{configuration}/guidance"
23
+ end
24
+
25
+ # @param [String] security_control the security control name to search by
26
+ # @return [Array<Hash>] an array of "guidance hashes"
27
+ def guidance_by_security_control(security_control)
28
+ get "/security_controls/#{security_control}/guidance"
29
+ end
30
+
31
+ # @param [String] threat the threat name to search by
32
+ # @return [Array<Hash>] an array of "guidance hashes"
33
+ def guidance_by_threat(threat)
34
+ get "/threats/#{threat}/guidance"
35
+ end
36
+
37
+ # @param [String] threat_vector the threat name to search by
38
+ # @return [Array<Hash>] an array of "guidance hashes"
39
+ def guidance_by_threat_vector(threat_vector)
40
+ get "/threat_vectors/#{threat_vector}/guidance"
41
+ end
42
+
43
+ # @!endgroup
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ module Controls
2
+ class Client
3
+ # A module to encapsulate API methods related to guidance
4
+ # @since API v1.0
5
+ # @version v1.0.0
6
+ module PrioritizedGuidance
7
+ # @!group Prioritized Guidance Methods
8
+
9
+ # @param [String] security_control the security control name to search by
10
+ # @return [Array<Hash>] an array of "guidance hashes"
11
+ def prioritized_guidance_by_security_control(security_control)
12
+ get "/security_controls/#{security_control}/prioritized_guidance"
13
+ end
14
+
15
+ # @param [String] configuration the configuration name to search by
16
+ # @return [Array<Hash>] an array of "guidance hashes"
17
+ def prioritized_guidance_by_configuration(configuration)
18
+ get "/configurations/#{configuration}/prioritized_guidance"
19
+ end
20
+
21
+ # @param [String] threat the threat name to search by
22
+ # @return [Array<Hash>] an array of "guidance hashes"
23
+ def prioritized_guidance_by_threat(threat)
24
+ get "/threats/#{threat}/prioritized_guidance"
25
+ end
26
+
27
+ # @param [String] threat_vector the threat name to search by
28
+ # @return [Array<Hash>] an array of "guidance hashes"
29
+ def prioritized_guidance_by_threat_vector(threat_vector)
30
+ get "/threat_vectors/#{threat_vector}/prioritized_guidance"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ require 'controls/client/configurations'
2
+
3
+ module Controls
4
+ class Client
5
+ # A module to encapsulate API methods related to security controls and
6
+ # configurations
7
+ # @since API v1.0
8
+ # @version v1.0.0
9
+ module SecurityControls
10
+ include Configurations
11
+
12
+ #!@group Security Control Methods
13
+
14
+ # @param [String] control the name of the security control name to
15
+ # retrieve
16
+ # @return [Hash] a hash representing a security control
17
+ def security_controls(control = nil)
18
+ if control
19
+ get "/security_controls/#{control}"
20
+ else
21
+ get '/security_controls'
22
+ end
23
+ end
24
+
25
+ # @param [String] vector the threat vector to search for securuty controls
26
+ # by
27
+ # @return [Array<Hash>] a list of hashes representing threats
28
+ def threat_vector_security_controls(vector)
29
+ get "/threat_vectors/#{vector}/security_controls"
30
+ end
31
+ alias_method :security_controls_by_threat_vector, :threat_vector_security_controls
32
+
33
+ # @!endgroup
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ module Controls
2
+ class Client
3
+ # A module to encapsulate API methods related to threats and threat vectors
4
+ # @since API v1.0
5
+ # @version v1.0.0
6
+ module ThreatVectors
7
+ # @!group Threat Vector Methods
8
+
9
+ # @param [String] vector the threat vector to search for
10
+ # @return [String] a hash representing the specified threat vector
11
+ def threat_vectors(vector = nil)
12
+ if vector
13
+ get "/threat_vectors/#{vector}"
14
+ else
15
+ get '/threat_vectors'
16
+ end
17
+ end
18
+
19
+ # @param [String] threat the threat to search for threat vectors by
20
+ # @return [Array<Hash>] a list of hashes representing threats
21
+ def threat_threat_vectors(threat)
22
+ get "/threats/#{threat}/threat_vectors"
23
+ end
24
+ alias_method :threat_vectors_by_threat, :threat_threat_vectors
25
+
26
+ # @!endgroup
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'controls/client/threat_vectors'
2
+
3
+ module Controls
4
+ class Client
5
+ # A module to encapsulate API methods related to threats and threat vectors
6
+ # @since API v1.0
7
+ # @version v1.0.0
8
+ module Threats
9
+ include ThreatVectors
10
+
11
+ # @!group Threat Methods
12
+
13
+ # @param [String] threat the threat name to search for
14
+ # @return [String] a hash representing the specified threat
15
+ def threats(threat = nil)
16
+ if threat
17
+ get "/threats/#{threat}"
18
+ else
19
+ get '/threats'
20
+ end
21
+ end
22
+
23
+ # @!endgroup
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,83 @@
1
+ module Controls
2
+ # A module to hold configurable options to be used by other classes such as
3
+ # the {Controls} eigenclass or the {Controls::Client} class
4
+ module Configurable
5
+ # @!attribute api_endpoint
6
+ # @return [String] the endpoint to connect to. default: https://nexpose.local:3780/insight/controls/api/1.0
7
+ # @!attribute api_endpoint
8
+ # @return [String] the endpoint to connect to. default: https://nexpose.local:3780/insight/controls/api/1.0
9
+ # @!attribute connection_options
10
+ # @return [Hash] the current connection options (headers, etc.)
11
+ # @!attribute default_media_type
12
+ # @return [String] the default media type to send with requests. default: application/json
13
+ # @!attribute middleware
14
+ # @return [Faraday::Connection] the middleware used to send requests
15
+ # @!attribute netrc
16
+ # @return [Boolean] whether to use the netrc credentials to authentcicate with the **controls**insight API
17
+ # @!attribute netrc_file
18
+ # @return [String] the path of the .netrc file to look for credentials in. default: ~/.netrc
19
+ # @!attribute user_agent
20
+ # @return [String] the user agent to send with API requests. example: "controls/v1.0.0.beta (ruby; 2.0.0p247; [x86_64-darwin12.4.0]; Faraday v0.8.8)"
21
+ # @!attribute username
22
+ # @return [String] the username to use for authentication
23
+ # @!attribute web_endpoint
24
+ # @return [String] the endpoint to connect to. default: https://nexpose.local:3780/insight/controls
25
+ attr_accessor :api_endpoint, :api_version, :connection_options,
26
+ :default_media_type, :middleware, :netrc, :netrc_file,
27
+ :user_agent, :username, :web_endpoint
28
+
29
+ # @!attribute [w] password
30
+ # @return [String] the password specified on login
31
+ attr_writer :password
32
+
33
+ class << self
34
+ # @return [Array<Symbol>] a list of configurable keys
35
+ def keys
36
+ @keys ||= [
37
+ :api_endpoint,
38
+ :api_version,
39
+ :connection_options,
40
+ :default_media_type,
41
+ :middleware,
42
+ :netrc,
43
+ :netrc_file,
44
+ :password,
45
+ :user_agent,
46
+ :username,
47
+ :web_endpoint
48
+ ]
49
+ end
50
+ end
51
+
52
+ # @yield [self]
53
+ def configure
54
+ yield self
55
+ end
56
+
57
+ def netrc?
58
+ !!@netrc
59
+ end
60
+
61
+ # Configures {Controls::Configurable} to use options found in
62
+ # {Controls::Default}
63
+ #
64
+ # @return [self]
65
+ def setup
66
+ Controls::Configurable.keys.each do |key|
67
+ instance_variable_set(:"@#{key}", Controls::Default.options[key])
68
+ end
69
+
70
+ self
71
+ end
72
+ # NOTE: This method currently leaves some "updated defaults" intact
73
+ # alias_method :reset!, :setup
74
+
75
+ private
76
+
77
+ # @return [Hash] a hash representation of the options mapping key names to
78
+ # their instance variable counterpart's value
79
+ def options
80
+ Hash[Controls::Configurable.keys.map { |key| [key, instance_variable_get(:"@#{key}")] }]
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,108 @@
1
+ require 'controls/version'
2
+
3
+ module Controls
4
+ # Default options merged with environment specific overrides to satisfy the
5
+ # options specified in the {Controls::Configurable} module
6
+ module Default
7
+ # @return [String] the API version to connect to. default: 1.0
8
+ # @since API v1.0.0
9
+ API_VERSION = '1.0'.freeze
10
+
11
+ # @return [String] the API endpoint to connect to. default: https://nexpose.local:3780/insight/controls/api/1.0
12
+ # @since API v1.0.0
13
+ API_ENDPOINT = "https://localhost:3780/insight/controls/api/#{API_VERSION}".freeze
14
+
15
+ # @return [String] the default media type to send with requests. default: application/json
16
+ # @since API v1.0.0
17
+ MEDIA_TYPE = 'application/json'
18
+
19
+ # @return [String] the user agent to send with API requests. example: "controls/v1.0.0.beta (ruby; 2.0.0p247; [x86_64-darwin12.4.0]; Faraday v0.8.8)"
20
+ USER_AGENT = "controls/v#{Controls::VERSION} (#{(RUBY_DESCRIPTION.split[0..1] + [RUBY_DESCRIPTION.split.last]).join('; ')}; Faraday v#{Faraday::VERSION})".freeze
21
+
22
+ # @return [String] the web endpoint to connect to. default: https://nexpose.local:3780/insight/controls
23
+ WEB_ENDPOINT = 'https://localhost:3780/insight/controls'.freeze
24
+
25
+ class << self
26
+ # @return [Hash] options as a Hash, mapped by keys from {Controls::Configurable}
27
+ def options
28
+ Hash[Controls::Configurable.keys.map { |key| [key, send(key)] }]
29
+ end
30
+
31
+ # @return [String] the API endpoint's URI as a URL
32
+ def api_endpoint
33
+ endpoint = ENV['CONTROLS_API_ENDPOINT'] || API_ENDPOINT
34
+ URI.parse(endpoint).to_s
35
+ end
36
+
37
+ # @return [String] the API version to connect to
38
+ def api_version
39
+ if ENV['CONTROLS_API_VERSION'].to_s =~ /\d+.\d+/
40
+ ENV['CONTROLS_API_VERSION']
41
+ else
42
+ API_VERSION
43
+ end
44
+ end
45
+
46
+ # @return [Hash] the current connection options (headers, etc.)
47
+ def connection_options
48
+ {
49
+ headers: {
50
+ accept: default_media_type,
51
+ user_agent: user_agent
52
+ }
53
+ }
54
+ end
55
+
56
+ # @return [String] the environment specific default media type.
57
+ # default: {MEDIA_TYPE}
58
+ def default_media_type
59
+ ENV['CONTROLS_MEDIA_TYPE'] || MEDIA_TYPE
60
+ end
61
+
62
+ # REVIEW: Ensure that middleware is unique to the client instance
63
+ # @return [Faraday::Connection] the middleware used to send requests
64
+ def middleware
65
+ @middleware ||= Faraday.new(api_endpoint, connection_options) do |conn|
66
+ conn.adapter Faraday.default_adapter
67
+ conn.response :logger if ENV['CONTROLS_DEBUG']
68
+ conn.use Controls::Response::RaiseError
69
+ end
70
+ end
71
+
72
+ # @return [Boolean] whether to fallback on authentication using the
73
+ # specified netrc file
74
+ def netrc
75
+ ENV['CONTROLS_NETRC'] || false
76
+ end
77
+
78
+ # @return [String] the netrc file to use for authentication.
79
+ # default: ~/.netrc
80
+ def netrc_file
81
+ ENV['CONTROLS_NETRC_FILE'] || File.join(Dir.home, '.netrc')
82
+ end
83
+
84
+
85
+ # @return [String] the password to use for authentication
86
+ def password
87
+ ENV['CONTROLS_PASSWORD']
88
+ end
89
+
90
+ # @return [String] the user agent that will be sent along any requests
91
+ # sent using {#connection_options}
92
+ def user_agent
93
+ ENV['CONTROLS_USER_AGENT'] || USER_AGENT
94
+ end
95
+
96
+ # @return [String] the username to use for authentication
97
+ def username
98
+ ENV['CONTROLS_USERNAME']
99
+ end
100
+
101
+ # @return [String] the web endpoint's URI as a URL
102
+ def web_endpoint
103
+ endpoint = ENV['CONTROLS_WEB_ENDPOINT'] || WEB_ENDPOINT
104
+ URI.parse(endpoint).to_s
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,132 @@
1
+ module Controls
2
+ # A namespace for errors in the Controls module
3
+ class Error < StandardError
4
+ # @!attribute [r] error_message
5
+ # @return [String] the error message
6
+ # @!attribute [r] error_status
7
+ # @return [Fixnum] the error message
8
+ # @!attribute [r] error_json
9
+ # @return [Hash] the JSON body from the error message
10
+ attr_reader :error_json
11
+ attr_reader :error_message
12
+ attr_reader :error_status
13
+
14
+ # @raise [BadRequest,Unauthorized,NotFound,InternalServerError] a subclass
15
+ # of {Controls::Error}
16
+ # @return [nil] if no error was raised
17
+ def self.from_response(response)
18
+ error = case response[:status].to_i
19
+ when 302
20
+ # TODO: Nexpose/ControlsInsight returns 302 when you either visit a
21
+ # none existant path (Nexpose) or unauthenticated
22
+ # (ControlsInsight).
23
+ Found # if response[:body].empty?
24
+ when 400 then BadRequest
25
+ when 401 then Unauthorized
26
+ when 404 then NotFound
27
+ when 500 then InternalServerError
28
+ end
29
+
30
+ error.new(response) if error
31
+ end
32
+
33
+ # @return [self] generates an error and passes it to super
34
+ def initialize(response = nil)
35
+ @response = response
36
+ super(generate_error)
37
+ end
38
+
39
+ private
40
+
41
+ # @return [String] an error message to be used by {#generate_error}
42
+ def response_message
43
+ return @response_message if @response_message
44
+
45
+ resp = @response[:response]
46
+
47
+ if resp.headers['content-type']
48
+ resp.headers['content-type'][/(\S+);charset=utf-8/i]
49
+ html = Regexp.last_match && Regexp.last_match[1].eql?('text/html')
50
+ end
51
+
52
+ @response_message = if html
53
+ doc = Nokogiri::XML.parse(resp.body)
54
+
55
+ if doc.css('title').text.eql? 'ControlsInsight'
56
+ message = ['message'].zip(doc.css('h1').map(&:text))
57
+ reason = ['reason'].zip([doc.css('p').map(&:text)])
58
+ Hash[message + reason]
59
+ else
60
+ Hash[doc.css('HR').children.map { |elem| elem.text.split(' ', 2) }]
61
+ end
62
+ else
63
+ if resp.body.empty?
64
+ @error_json = {}
65
+ else
66
+ @error_json = JSON.parse(resp.body)
67
+ @error_message = @error_json['message']
68
+ @error_status = @error_json['status'].to_i
69
+
70
+ @error_json
71
+ end
72
+ end
73
+ end
74
+
75
+ # @return [String] the error message passed to super on {#initialize}
76
+ def generate_error
77
+ return unless @response
78
+
79
+ message = "#{@response[:method]} ".upcase
80
+ message << "#{@response[:url].path}"
81
+
82
+ if response_message.is_a? Hash
83
+ message << ": #{response_message['message']}\n" if response_message['message']
84
+
85
+ if response_message['reason'].respond_to?(:join)
86
+ message << response_message['reason'].join("\n")
87
+ elsif response_message['reason']
88
+ message << response_message['reason'].to_s
89
+ end
90
+ elsif response_message.is_a? String
91
+ message << response_message
92
+ else
93
+ message << "#{@response[:status]} -"
94
+ message << self.class.to_s.split('::', 2).last
95
+ end
96
+
97
+ message.strip
98
+ end
99
+ end
100
+
101
+ # @!group Generic errors
102
+
103
+ # TODO REVIEW: To be raised when a user hasn't explicitly supplied credentials or set up
104
+ # any environment defaults.
105
+ Unauthenticated = Class.new(StandardError)
106
+
107
+ # @!endgroup
108
+
109
+ # @!group HTTP errors
110
+
111
+ # @return [Found] an error to be raised when a status code of 401 is
112
+ # returned by the API
113
+ Found = Class.new(Error)
114
+
115
+ # @return [Unauthorized] an error to be raised when a status code of 401 is
116
+ # returned by the API
117
+ Unauthorized = Class.new(Error)
118
+
119
+ # @return [BadRequest] an error to be raised when a status code of 400 is
120
+ # returned by the API
121
+ BadRequest = Class.new(Error)
122
+
123
+ # @return [NotFound] an error to be raised when a status code of 404 is
124
+ # returned by the API
125
+ NotFound = Class.new(Error)
126
+
127
+ # @return [InternalServerError] an error to be raised when a status code of
128
+ # 500 is returned by the API
129
+ InternalServerError = Class.new(Error)
130
+
131
+ # @!endgroup
132
+ end