nex_client 0.17.0 → 0.18.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NexClient
4
+ module Commands
5
+ module Waf
6
+ extend Helpers
7
+
8
+ WAF_TITLE = "WAF Rules".colorize(:cyan)
9
+ WAF_HEADERS = ['Rule Group','Value'].map(&:upcase)
10
+
11
+ DEFAULT_WAF_RULES = {
12
+ mode: "SIMULATE",
13
+ ignore_rule: [],
14
+ ignore_ruleset: [],
15
+ add_ruleset_string: [],
16
+ sieve_rule: []
17
+ }.stringify_keys.freeze
18
+
19
+ def self.configure(c)
20
+ c.syntax = 'nex-cli apps:waf APP_NAME [options]'
21
+ c.summary = 'Manage Web Application Firewall rules'
22
+ c.description = <<~HEREDOC
23
+ Manage Web Application Firewall Rules
24
+
25
+ Specify a JSON (.json) or YAML (.yml) file describing the security rules to apply/ignore. If no files are specified you will be prompted to
26
+ paste rules in JSON format in the terminal.
27
+ The WAF always runs in SIMULATE mode by default - this means security events will be logged but no requests will be blocked.
28
+
29
+ The list of base rules applied by default is available here: https://github.com/p0pr0ck5/lua-resty-waf/tree/master/rules
30
+
31
+ The base WAF behaviour can be extended by passing a JSON (or YAML) manifest to '--ruleset' with the following format:
32
+ {
33
+ // Whether the WAF should be enabled, disabled or do log-only
34
+ // 'ACTIVE', 'INACTIVE' or 'SIMULATE'
35
+ "mode": "SIMULATE",
36
+
37
+ // Array of rule IDs to ignore
38
+ "ignore_rule": [],
39
+
40
+ // Array of rulesets to ignore
41
+ "ignore_ruleset": [],
42
+
43
+ // Array of ['rulename','rule'] objects
44
+ "add_ruleset_string": [],
45
+
46
+ // Rules sieves allow you to apply/ignore rules based on a
47
+ // specific context such as request parameters
48
+ // Array of ['rule_id', [{ sieve_cond }, { sieve_cond }] ] objects
49
+ "sieve_rule": []
50
+ }
51
+ See this for more info: https://github.com/p0pr0ck5/lua-resty-waf
52
+ See this for rule sieves: https://github.com/p0pr0ck5/lua-resty-waf/wiki/Rule-Sieves
53
+
54
+ HEREDOC
55
+ c.example 'update WAF rules via command line prompt', 'nex-cli apps:waf myapp --ruleset'
56
+ c.example 'update WAF rules via file input', 'nex-cli apps:waf myapp --ruleset /tmp/myruleset.json'
57
+ c.option '--ruleset [PATH]', String, 'specify web application firewall rules using JSON or YAML file (prompt will appear otherwise). [restart required]'
58
+ c.option '--clear-ruleset', String, 'remove all rules and revert back to SIMULATE mode'
59
+
60
+ c.action do |args, options|
61
+ manage(args, options)
62
+ end
63
+ end
64
+
65
+ def self.manage(args, opts)
66
+ name = args.first
67
+ app = NexClient::App.find(name: name).first
68
+
69
+ # Display error
70
+ unless app
71
+ error("Error! Could not find app: #{name}")
72
+ return false
73
+ end
74
+
75
+ a = update(app, opts)
76
+ show(a)
77
+ end
78
+
79
+ def self.update(app, opts)
80
+ # Deep duplicate
81
+ app_opts = Marshal.load(Marshal.dump((app.opts || {})))
82
+
83
+ # Clear all constraints
84
+ if opts.clear_ruleset
85
+ app_opts.delete('waf_rules')
86
+ end
87
+
88
+ # Add WAF rules
89
+ if opts.ruleset.present?
90
+ waf_rules = begin
91
+ if opts.ruleset.is_a?(String)
92
+ hash_from_file(opts.ruleset)
93
+ else
94
+ val = ask("Copy/paste your WAF configuration below in JSON format:") { |q| q.gather = "" }
95
+ JSON.parse(val.join(""))
96
+ end
97
+ end
98
+
99
+ app_opts['waf_rules'] = waf_rules
100
+ end
101
+
102
+ # Update policies
103
+ if app.opts != app_opts
104
+ app.update_attributes({ opts: app_opts })
105
+ success("Successfully updated WAF rules. Please restart your app to apply these changes...")
106
+ end
107
+
108
+ # Return updated app
109
+ app
110
+ end
111
+
112
+ def self.show(app)
113
+ ruleset = DEFAULT_WAF_RULES.merge(app.opts['waf_rules'] || {})
114
+
115
+ table = Terminal::Table.new title: WAF_TITLE, headings: WAF_HEADERS do |t|
116
+ ruleset.each do |group,val|
117
+ formatted_val = case
118
+ when val.blank?
119
+ 'None'
120
+ when val.is_a?(Hash) || val.is_a?(Array)
121
+ JSON.pretty_generate(val)
122
+ else
123
+ val
124
+ end
125
+
126
+ t.add_row([group, formatted_val])
127
+ end
128
+ end
129
+ puts table
130
+ puts "\n"
131
+ end
132
+ end
133
+ end
134
+ end
@@ -23,5 +23,14 @@ module NexClient
23
23
 
24
24
  # PATCH <api_root>/cube_instances/:id/stop
25
25
  custom_endpoint :stop, on: :member, request_method: :patch
26
+
27
+ # Common name
28
+ def self.entity_name
29
+ 'cube'
30
+ end
31
+
32
+ def self.main_key
33
+ :uuid
34
+ end
26
35
  end
27
36
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module NexClient
3
+ class ExecCmd < BaseResource
4
+ property :name, type: :string
5
+ property :status, type: :string
6
+ property :script, type: :text
7
+ property :opts, type: :text
8
+ property :output, type: :string
9
+ property :local, type: :boolean
10
+ property :parallel, type: :boolean
11
+
12
+ has_one :executor, polymorphic: true
13
+
14
+ # PATCH <api_root>/exec_cmds/:id/execute
15
+ custom_endpoint :execute, on: :member, request_method: :patch
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require 'colorize'
3
+
4
+ module NexClient
5
+ module FaradayMiddleware
6
+ autoload :HandleNexApiErrors, 'nex_client/faraday_middleware/handle_nex_api_errors'
7
+ end
8
+ end
@@ -0,0 +1,39 @@
1
+ module NexClient
2
+ module FaradayMiddleware
3
+ class MfaTokenInvalidError < Faraday::Error::ClientError
4
+ end
5
+
6
+ class HandleNexApiErrors < Faraday::Response::Middleware
7
+ ClientErrorStatuses = 400...600
8
+
9
+ def call(env)
10
+ env[:last_request_body] = env[:body]
11
+ super
12
+ end
13
+
14
+ def on_complete(env)
15
+ case env[:status]
16
+ when 404
17
+ raise Faraday::Error::ResourceNotFound, response_values(env)
18
+ when 407
19
+ # mimic the behavior that we get with proxy requests with HTTPS
20
+ raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
21
+ when 499
22
+ if !env.request_headers['X-MFA-Token'] && NexClient.interactive?
23
+ env.request_headers['X-MFA-Token'] = ask("Enter your MFA token: ")
24
+ env[:body] = env[:last_request_body] # after failure env[:body] is set to the response body
25
+ call(env)
26
+ else
27
+ raise MfaTokenInvalidError, response_values(env)
28
+ end
29
+ when ClientErrorStatuses
30
+ raise Faraday::Error::ClientError, response_values(env)
31
+ end
32
+ end
33
+
34
+ def response_values(env)
35
+ {:status => env.status, :headers => env.response_headers, :body => env.body}
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,3 @@
1
1
  module NexClient
2
- VERSION ||= '0.17.0'
2
+ VERSION ||= '0.18.0.pre1'
3
3
  end
@@ -4,6 +4,8 @@ describe NexClient::App do
4
4
  subject { described_class.new(id: 1) }
5
5
  let(:api_key) { ENV['NEX_API_KEY'] }
6
6
 
7
+ it_behaves_like NexClient::BaseResource
8
+
7
9
  describe 'restart' do
8
10
  let!(:stub) { stub_request(:patch, "#{api_key}:@nex-test.com/api/v1/apps/#{subject.id}/restart") }
9
11
  before { subject.restart }
@@ -4,6 +4,8 @@ describe NexClient::CubeInstance do
4
4
  subject { described_class.new(id: 1) }
5
5
  let(:api_key) { ENV['NEX_API_KEY'] }
6
6
 
7
+ it_behaves_like NexClient::BaseResource
8
+
7
9
  describe 'restart' do
8
10
  let!(:stub) { stub_request(:patch, "#{api_key}:@nex-test.com/api/v1/cube_instances/#{subject.id}/restart") }
9
11
  before { subject.restart }
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe NexClient::ExecCmd do
4
+ subject { described_class.new(id: 1) }
5
+ let(:api_key) { ENV['NEX_API_KEY'] }
6
+
7
+ it_behaves_like NexClient::BaseResource
8
+
9
+ describe 'execute' do
10
+ let!(:stub) { stub_request(:patch, "#{api_key}:@nex-test.com/api/v1/exec_cmds/#{subject.id}/execute") }
11
+ before { subject.execute }
12
+ it { expect(stub).to have_been_requested }
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples NexClient::BaseResource do
4
+ let(:resource_api_name) { described_class.to_s.split('::').last.underscore.pluralize }
5
+ let(:api_key) { ENV['NEX_API_KEY'] }
6
+ let(:api_host) { 'nex-test.com' }
7
+
8
+ describe 'basic authentication' do
9
+ let!(:stub) { stub_request(:get, "#{api_key}:@#{api_host}/api/v1/#{resource_api_name}") }
10
+ before { described_class.all }
11
+ it { expect(stub).to have_been_requested }
12
+ end
13
+
14
+ describe 'non-interactive MFA authentication' do
15
+ subject { described_class.all }
16
+
17
+ before { stub_request(:get, "#{api_key}:@#{api_host}/api/v1/#{resource_api_name}").to_return(status: 499) }
18
+ it { is_expected_block.to raise_error(NexClient::FaradayMiddleware::MfaTokenInvalidError) }
19
+ end
20
+
21
+ describe 'successful interactive MFA authentication' do
22
+ subject { described_class.all }
23
+
24
+ before { allow(NexClient).to receive(:interactive?).and_return(true) }
25
+ before { stub_request(:get, "#{api_key}:@#{api_host}/api/v1/#{resource_api_name}").to_return(status: 499).then.to_return(status: 200) }
26
+ before { expect_any_instance_of(Object).to receive(:ask).with("Enter your MFA token: ").and_return('12345') }
27
+ it { is_expected.to eq([]) }
28
+ end
29
+
30
+ describe 'failed interactive MFA authentication' do
31
+ subject { described_class.all }
32
+
33
+ before { allow(NexClient).to receive(:interactive?).and_return(true) }
34
+ before { stub_request(:get, "#{api_key}:@#{api_host}/api/v1/#{resource_api_name}").to_return(status: 499).then.to_return(status: 499) }
35
+ before { expect_any_instance_of(Object).to receive(:ask).with("Enter your MFA token: ").and_return('12345') }
36
+ it { is_expected_block.to raise_error(NexClient::FaradayMiddleware::MfaTokenInvalidError) }
37
+ end
38
+ end
@@ -4,8 +4,14 @@ ENV['NEX_ENV'] = 'test'
4
4
  $:.unshift File.expand_path("../../lib", __FILE__)
5
5
 
6
6
  require 'nex_client'
7
+ require 'nex_client/cli'
7
8
  require 'webmock/rspec'
8
9
 
10
+ # Add helper and shared specs folders
11
+ spec_dir = File.dirname(File.absolute_path(__FILE__))
12
+ Dir[File.join(spec_dir,"support/**/*.rb")].each { |f| require f }
13
+ Dir[File.join(spec_dir,"shared/**/*.rb")].each { |f| require f }
14
+
9
15
  RSpec.configure do |config|
10
16
  # rspec-expectations config goes here. You can use an alternate
11
17
  # assertion/expectation library such as wrong or the stdlib/minitest
@@ -37,57 +43,6 @@ RSpec.configure do |config|
37
43
  # triggering implicit auto-inclusion in groups with matching metadata.
38
44
  config.shared_context_metadata_behavior = :apply_to_host_groups
39
45
 
40
- # The settings below are suggested to provide a good initial experience
41
- # with RSpec, but feel free to customize to your heart's content.
42
- =begin
43
- # This allows you to limit a spec run to individual examples or groups
44
- # you care about by tagging them with `:focus` metadata. When nothing
45
- # is tagged with `:focus`, all examples get run. RSpec also provides
46
- # aliases for `it`, `describe`, and `context` that include `:focus`
47
- # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
48
- config.filter_run_when_matching :focus
49
-
50
- # Allows RSpec to persist some state between runs in order to support
51
- # the `--only-failures` and `--next-failure` CLI options. We recommend
52
- # you configure your source control system to ignore this file.
53
- config.example_status_persistence_file_path = "spec/examples.txt"
54
-
55
- # Limits the available syntax to the non-monkey patched syntax that is
56
- # recommended. For more details, see:
57
- # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
58
- # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
59
- # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
60
- config.disable_monkey_patching!
61
-
62
- # This setting enables warnings. It's recommended, but in some cases may
63
- # be too noisy due to issues in dependencies.
64
- config.warnings = true
65
-
66
- # Many RSpec users commonly either run the entire suite or an individual
67
- # file, and it's useful to allow more verbose output when running an
68
- # individual spec file.
69
- if config.files_to_run.one?
70
- # Use the documentation formatter for detailed output,
71
- # unless a formatter has already been configured
72
- # (e.g. via a command-line flag).
73
- config.default_formatter = 'doc'
74
- end
75
-
76
- # Print the 10 slowest examples and example groups at the
77
- # end of the spec run, to help surface which specs are running
78
- # particularly slow.
79
- config.profile_examples = 10
80
-
81
- # Run specs in random order to surface order dependencies. If you find an
82
- # order dependency and want to debug it, you can fix the order by providing
83
- # the seed, which is printed after each run.
84
- # --seed 1234
85
- config.order = :random
86
-
87
- # Seed global randomization in this process using the `--seed` CLI option.
88
- # Setting this allows you to use `--seed` to deterministically reproduce
89
- # test failures related to randomization by passing the same `--seed` value
90
- # as the one that triggered the failure.
91
- Kernel.srand config.seed
92
- =end
46
+ # Allow is_expected_block to be called instead of: expect { subject }
47
+ config.include IsExpectedBlock
93
48
  end
@@ -0,0 +1,5 @@
1
+ module IsExpectedBlock
2
+ def is_expected_block
3
+ expect { subject }
4
+ end
5
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nex_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arnaud Lachaume
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-12-10 00:00:00.000000000 Z
11
+ date: 2018-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_api_client
@@ -144,18 +144,26 @@ files:
144
144
  - lib/nex_client/commands/cube_instances.rb
145
145
  - lib/nex_client/commands/cube_templates.rb
146
146
  - lib/nex_client/commands/domains.rb
147
+ - lib/nex_client/commands/events.rb
147
148
  - lib/nex_client/commands/exec_tasks.rb
148
149
  - lib/nex_client/commands/helpers.rb
150
+ - lib/nex_client/commands/ip_whitelisting.rb
151
+ - lib/nex_client/commands/logs.rb
149
152
  - lib/nex_client/commands/organizations.rb
153
+ - lib/nex_client/commands/policies.rb
150
154
  - lib/nex_client/commands/racks.rb
151
155
  - lib/nex_client/commands/ssl_certificates.rb
152
156
  - lib/nex_client/commands/users.rb
157
+ - lib/nex_client/commands/waf.rb
153
158
  - lib/nex_client/compute_rack.rb
154
159
  - lib/nex_client/cube_instance.rb
155
160
  - lib/nex_client/cube_template.rb
156
161
  - lib/nex_client/domain.rb
157
162
  - lib/nex_client/event.rb
163
+ - lib/nex_client/exec_cmd.rb
158
164
  - lib/nex_client/exec_task.rb
165
+ - lib/nex_client/faraday_middleware.rb
166
+ - lib/nex_client/faraday_middleware/handle_nex_api_errors.rb
159
167
  - lib/nex_client/gateway_rack.rb
160
168
  - lib/nex_client/me.rb
161
169
  - lib/nex_client/organization.rb
@@ -167,7 +175,10 @@ files:
167
175
  - lib/nex_client/version.rb
168
176
  - spec/nex_client/app_spec.rb
169
177
  - spec/nex_client/cube_instance_spec.rb
178
+ - spec/nex_client/exec_cmd_spec.rb
179
+ - spec/shared/base_resource.rb
170
180
  - spec/spec_helper.rb
181
+ - spec/support/is_expected_block.rb
171
182
  homepage: https://maestrano.com
172
183
  licenses:
173
184
  - Apache-2.0
@@ -183,16 +194,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
183
194
  version: '0'
184
195
  required_rubygems_version: !ruby/object:Gem::Requirement
185
196
  requirements:
186
- - - ">="
197
+ - - ">"
187
198
  - !ruby/object:Gem::Version
188
- version: '0'
199
+ version: 1.3.1
189
200
  requirements: []
190
201
  rubyforge_project:
191
- rubygems_version: 2.6.8
202
+ rubygems_version: 2.6.14
192
203
  signing_key:
193
204
  specification_version: 4
194
205
  summary: Maestrano Nex!™ Client
195
206
  test_files:
196
207
  - spec/nex_client/app_spec.rb
197
208
  - spec/nex_client/cube_instance_spec.rb
209
+ - spec/nex_client/exec_cmd_spec.rb
210
+ - spec/shared/base_resource.rb
198
211
  - spec/spec_helper.rb
212
+ - spec/support/is_expected_block.rb