nex_client 0.17.0 → 0.18.0.pre1

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,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