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.
- checksums.yaml +4 -4
- data/lib/nex_client.rb +6 -0
- data/lib/nex_client/addon.rb +15 -0
- data/lib/nex_client/app.rb +4 -0
- data/lib/nex_client/base_resource.rb +15 -0
- data/lib/nex_client/cli.rb +35 -124
- data/lib/nex_client/commands.rb +5 -0
- data/lib/nex_client/commands/addons.rb +11 -31
- data/lib/nex_client/commands/apps.rb +8 -44
- data/lib/nex_client/commands/cube_instances.rb +0 -21
- data/lib/nex_client/commands/events.rb +60 -0
- data/lib/nex_client/commands/helpers.rb +0 -50
- data/lib/nex_client/commands/ip_whitelisting.rb +124 -0
- data/lib/nex_client/commands/logs.rb +58 -0
- data/lib/nex_client/commands/policies.rb +184 -0
- data/lib/nex_client/commands/waf.rb +134 -0
- data/lib/nex_client/cube_instance.rb +9 -0
- data/lib/nex_client/exec_cmd.rb +17 -0
- data/lib/nex_client/faraday_middleware.rb +8 -0
- data/lib/nex_client/faraday_middleware/handle_nex_api_errors.rb +39 -0
- data/lib/nex_client/version.rb +1 -1
- data/spec/nex_client/app_spec.rb +2 -0
- data/spec/nex_client/cube_instance_spec.rb +2 -0
- data/spec/nex_client/exec_cmd_spec.rb +14 -0
- data/spec/shared/base_resource.rb +38 -0
- data/spec/spec_helper.rb +8 -53
- data/spec/support/is_expected_block.rb +5 -0
- metadata +19 -5
@@ -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
|
@@ -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,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
|
data/lib/nex_client/version.rb
CHANGED
data/spec/nex_client/app_spec.rb
CHANGED
@@ -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
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
-
#
|
41
|
-
|
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
|
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.
|
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:
|
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:
|
199
|
+
version: 1.3.1
|
189
200
|
requirements: []
|
190
201
|
rubyforge_project:
|
191
|
-
rubygems_version: 2.6.
|
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
|