api-tester 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/push.yml +39 -0
  4. data/.github/workflows/test.yml +31 -0
  5. data/.rubocop.yml +61 -0
  6. data/Gemfile +2 -0
  7. data/Guardfile +70 -0
  8. data/README.md +65 -61
  9. data/Rakefile +8 -3
  10. data/api-tester.gemspec +29 -24
  11. data/changelog.txt +10 -0
  12. data/lib/api-tester.rb +6 -3
  13. data/lib/api-tester/config.rb +16 -13
  14. data/lib/api-tester/definition/boundary_case.rb +4 -1
  15. data/lib/api-tester/definition/contract.rb +8 -3
  16. data/lib/api-tester/definition/endpoint.rb +32 -23
  17. data/lib/api-tester/definition/fields/array_field.rb +20 -19
  18. data/lib/api-tester/definition/fields/boolean_field.rb +12 -9
  19. data/lib/api-tester/definition/fields/email_field.rb +14 -11
  20. data/lib/api-tester/definition/fields/enum_field.rb +14 -11
  21. data/lib/api-tester/definition/fields/field.rb +46 -45
  22. data/lib/api-tester/definition/fields/number_field.rb +11 -8
  23. data/lib/api-tester/definition/fields/object_field.rb +34 -31
  24. data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
  25. data/lib/api-tester/definition/method.rb +7 -2
  26. data/lib/api-tester/definition/request.rb +43 -16
  27. data/lib/api-tester/definition/response.rb +29 -26
  28. data/lib/api-tester/method_case_test.rb +67 -53
  29. data/lib/api-tester/modules/extra_verbs.rb +29 -9
  30. data/lib/api-tester/modules/format.rb +23 -7
  31. data/lib/api-tester/modules/good_case.rb +25 -10
  32. data/lib/api-tester/modules/injection_module.rb +32 -17
  33. data/lib/api-tester/modules/required_fields.rb +51 -0
  34. data/lib/api-tester/modules/server_information.rb +13 -10
  35. data/lib/api-tester/modules/typo.rb +36 -13
  36. data/lib/api-tester/modules/unexpected_fields.rb +61 -0
  37. data/lib/api-tester/modules/unused_fields.rb +12 -6
  38. data/lib/api-tester/reporter/api_report.rb +24 -16
  39. data/lib/api-tester/reporter/missing_field_report.rb +12 -13
  40. data/lib/api-tester/reporter/report.rb +11 -8
  41. data/lib/api-tester/reporter/status_code_report.rb +9 -2
  42. data/lib/api-tester/test_helper.rb +6 -6
  43. data/lib/api-tester/util/response_evaluator.rb +70 -57
  44. data/lib/api-tester/util/supported_verbs.rb +8 -5
  45. data/lib/api-tester/version.rb +3 -1
  46. metadata +99 -25
  47. data/.travis.yml +0 -6
  48. data/lib/api-tester/reporter/missing_response_field_report.rb +0 -21
@@ -1,69 +1,83 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/util/response_evaluator.rb'
2
4
 
3
5
  module ApiTester
6
+ # Class for testing methods
4
7
  class MethodCaseTest
5
- attr_accessor :expected_response
6
- attr_accessor :payload
7
- attr_accessor :response
8
- attr_accessor :reports
9
- attr_accessor :url
10
- attr_accessor :module_name
8
+ attr_accessor :expected_response
9
+ attr_accessor :payload
10
+ attr_accessor :response
11
+ attr_accessor :reports
12
+ attr_accessor :url
13
+ attr_accessor :module_name
11
14
 
12
- def initialize response, payload, expected_response, url, verb, module_name
13
- self.payload = payload
14
- self.response = response
15
- self.expected_response = expected_response
16
- self.reports = []
17
- self.url = "#{verb} #{url}"
18
- self.module_name = module_name
19
- end
15
+ def initialize(response:, payload:, expected_response:, url:, verb:, module_name:)
16
+ self.payload = payload
17
+ self.response = response
18
+ self.expected_response = expected_response
19
+ self.reports = []
20
+ self.url = "#{verb} #{url}"
21
+ self.module_name = module_name
22
+ end
20
23
 
21
- def response_code_report
22
- report = StatusCodeReport.new "#{module_name} - Incorrect response code", self.url, self.payload, self.expected_response.code, self.response.code
23
- self.reports << report
24
- nil
25
- end
24
+ def response_code_report
25
+ report = StatusCodeReport.new description: "#{module_name} - Incorrect response code",
26
+ url: url,
27
+ request: payload,
28
+ expected_status_code: expected_response.code,
29
+ actual_status_code: "#{response.code} : #{response.body}"
30
+ reports << report
31
+ nil
32
+ end
26
33
 
27
- def missing_field_report field
28
- report = Report.new "#{module_name} - Missing field #{field}", self.url, self.payload, self.expected_response, self.response
29
- self.reports << report
30
- nil
31
- end
34
+ def missing_field_report(field)
35
+ report = Report.new description: "#{module_name} - Missing field #{field}",
36
+ url: url,
37
+ request: payload,
38
+ expected_response: expected_response,
39
+ actual_response: response
40
+ reports << report
41
+ nil
42
+ end
32
43
 
33
- def extra_field_report field
34
- report = Report.new "#{module_name} - Found extra field #{field}", self.url, self.payload, self.expected_response, self.response
35
- self.reports << report
36
- nil
37
- end
44
+ def extra_field_report(field)
45
+ report = Report.new description: "#{module_name} - Found extra field #{field}",
46
+ url: url,
47
+ request: payload,
48
+ expected_response: expected_response,
49
+ actual_response: response
50
+ reports << report
51
+ nil
52
+ end
38
53
 
39
- def check
40
- if check_response_code
41
- evaluator = ApiTester::ResponseEvaluator.new json_parse(self.response.body), self.expected_response
42
- evaluator.missing_fields.map{|field| missing_field_report(field)}
43
- evaluator.extra_fields.map{|field| extra_field_report(field)}
44
- increment_fields evaluator.seen_fields
45
- end
46
- return self.reports
54
+ def check
55
+ if check_response_code
56
+ evaluator = ApiTester::ResponseEvaluator.new actual_body: json_parse(response.body),
57
+ expected_fields: expected_response
58
+ evaluator.missing_fields.map { |field| missing_field_report(field) }
59
+ evaluator.extra_fields.map { |field| extra_field_report(field) }
60
+ increment_fields evaluator.seen_fields
47
61
  end
62
+ reports
63
+ end
48
64
 
49
- def check_response_code
50
- if response.code != expected_response.code
51
- response_code_report
52
- return false
53
- end
54
- return true
65
+ def check_response_code
66
+ if response.code != expected_response.code
67
+ response_code_report
68
+ return false
55
69
  end
70
+ true
71
+ end
56
72
 
57
- def increment_fields seen_fields
58
- seen_fields.each do |field|
59
- field.seen
60
- end
61
- end
73
+ def increment_fields(seen_fields)
74
+ seen_fields.each(&:seen)
75
+ end
62
76
 
63
- def json_parse body
64
- JSON.parse!(body)
65
- rescue JSON::ParserError
66
- body
67
- end
77
+ def json_parse(body)
78
+ JSON.parse!(body)
79
+ rescue JSON::ParserError
80
+ body
81
+ end
68
82
  end
69
83
  end
@@ -1,18 +1,32 @@
1
- require 'api-tester/util/supported_verbs'
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/util/supported_verbs'
2
4
 
3
5
  module ApiTester
6
+ # Check verbs not explicitly defined in contract
4
7
  module ExtraVerbs
5
- def self.go contract
8
+ def self.go(contract)
6
9
  reports = []
7
10
 
8
11
  contract.endpoints.each do |endpoint|
9
12
  extras = ApiTester::SupportedVerbs.all - endpoint.verbs
10
13
  headers = endpoint.methods[0].request.default_headers
11
14
  extras.each do |verb|
12
- verb_case = BoundaryCase.new("Verb check with #{verb} for #{endpoint.name}", {}, headers)
13
- method = ApiTester::Method.new verb, ApiTester::Response.new, ApiTester::Request.new
14
- response = endpoint.call method, verb_case.payload, verb_case.headers
15
- test = VerbClass.new response, verb_case.payload, endpoint.not_allowed_response, endpoint.url, verb
15
+ verb_case = BoundaryCase.new description: "Verb check with #{verb} for #{endpoint.name}",
16
+ payload: {},
17
+ headers: headers
18
+ method = ApiTester::Method.new verb: verb,
19
+ response: ApiTester::Response.new,
20
+ request: ApiTester::Request.new
21
+ response = endpoint.call base_url: contract.base_url,
22
+ method: method,
23
+ payload: verb_case.payload,
24
+ headers: verb_case.headers
25
+ test = VerbClass.new response: response,
26
+ payload: verb_case.payload,
27
+ expected_response: endpoint.not_allowed_response,
28
+ url: endpoint.url,
29
+ verb: verb
16
30
  reports.concat test.check
17
31
  end
18
32
  end
@@ -25,9 +39,15 @@ module ApiTester
25
39
  end
26
40
  end
27
41
 
42
+ # Test template used for module
28
43
  class VerbClass < MethodCaseTest
29
- def initialize response, payload, expected_response, url, verb
30
- super response, payload, expected_response, url, verb, "VerbModule"
31
- end
44
+ def initialize(response:, payload:, expected_response:, url:, verb:)
45
+ super response: response,
46
+ payload: payload,
47
+ expected_response: expected_response,
48
+ url: url,
49
+ verb: verb,
50
+ module_name: 'VerbModule'
51
+ end
32
52
  end
33
53
  end
@@ -1,16 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/status_code_report'
2
4
  require 'api-tester/method_case_test'
3
5
 
4
6
  module ApiTester
5
- class Format
6
- def self.go contract
7
+ # Checks the format constraints defined in contract
8
+ module Format
9
+ def self.go(contract)
7
10
  reports = []
8
11
  contract.endpoints.each do |endpoint|
9
12
  endpoint.methods.each do |method|
10
13
  cases = method.request.cases
11
14
  cases.each do |format_case|
12
- response = endpoint.call method, format_case.payload, format_case.headers
13
- test = FormatTest.new response, format_case.payload, endpoint.bad_request_response, endpoint.url, method.verb
15
+ response = endpoint.call base_url: contract.base_url,
16
+ method: method,
17
+ payload: format_case.payload,
18
+ headers: format_case.headers
19
+ test = FormatTest.new response: response,
20
+ payload: format_case.payload,
21
+ expected_response: endpoint.bad_request_response,
22
+ url: endpoint.url,
23
+ verb: method.verb
14
24
  reports.concat test.check
15
25
  end
16
26
  end
@@ -23,9 +33,15 @@ module ApiTester
23
33
  end
24
34
  end
25
35
 
36
+ # Test layout used by Format module
26
37
  class FormatTest < MethodCaseTest
27
- def initialize response, payload, expected_response, url, verb
28
- super response, payload, expected_response, url, verb, "FormatModule"
29
- end
38
+ def initialize(response:, payload:, expected_response:, url:, verb:)
39
+ super response: response,
40
+ payload: payload,
41
+ expected_response: expected_response,
42
+ url: url,
43
+ verb: verb,
44
+ module_name: 'FormatModule'
45
+ end
30
46
  end
31
47
  end
@@ -1,16 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/status_code_report'
2
4
  require 'api-tester/method_case_test'
3
5
 
4
6
  module ApiTester
5
- class GoodCase
6
- def self.go contract
7
+ # Checks the good case as defined in contract
8
+ module GoodCase
9
+ def self.go(contract)
7
10
  reports = []
8
11
 
9
12
  contract.endpoints.each do |endpoint|
10
13
  endpoint.methods.each do |method|
11
- default_case = BoundaryCase.new endpoint.url, method.request.default_payload, method.request.default_headers
12
- response = endpoint.call method, default_case.payload, default_case.headers
13
- test = GoodCaseTest.new response, endpoint.url, method
14
+ default_case = BoundaryCase.new description: contract.base_url + endpoint.url,
15
+ payload: method.request.default_payload,
16
+ headers: method.request.default_headers
17
+ response = endpoint.call base_url: contract.base_url,
18
+ method: method,
19
+ payload: default_case.payload,
20
+ headers: default_case.headers
21
+ test = GoodCaseTest.new response: response,
22
+ url: contract.base_url + endpoint.url,
23
+ method: method
14
24
  reports.concat test.check
15
25
  end
16
26
  end
@@ -18,14 +28,19 @@ module ApiTester
18
28
  end
19
29
 
20
30
  def self.order
21
- 1
31
+ 1
22
32
  end
23
33
  end
24
34
 
25
-
35
+ # Test layout used by module
26
36
  class GoodCaseTest < MethodCaseTest
27
- def initialize response, url, method
28
- super response, method.request.default_payload, method.expected_response, url, method.verb, "GoodCaseModule"
29
- end
37
+ def initialize(response:, url:, method:)
38
+ super response: response,
39
+ payload: method.request.default_payload,
40
+ expected_response: method.expected_response,
41
+ url: url,
42
+ verb: method.verb,
43
+ module_name: 'GoodCaseModule'
44
+ end
30
45
  end
31
46
  end
@@ -1,29 +1,39 @@
1
- require "injection_vulnerability_library"
1
+ # frozen_string_literal: true
2
+
3
+ require 'injection_vulnerability_library'
2
4
 
3
5
  module ApiTester
6
+ # Tests injection cases
4
7
  module InjectionModule
5
- def self.go contract
8
+ def self.go(contract)
6
9
  reports = []
7
10
  contract.endpoints.each do |endpoint|
8
11
  endpoint.methods.each do |method|
9
- reports.concat inject_payload endpoint, method
12
+ reports.concat inject_payload contract.base_url, endpoint, method
10
13
  end
11
14
  end
12
15
  reports
13
16
  end
14
17
 
15
- def self.inject_payload endpoint, method
18
+ def self.inject_payload(base_url, endpoint, method)
16
19
  reports = []
17
20
  sql_injections = InjectionVulnerabilityLibrary.sql_vulnerabilities
18
21
 
19
22
  method.request.fields.each do |field|
20
23
  sql_injections.each do |injection|
21
24
  injection_value = "#{field.default_value}#{injection}"
22
- payload = method.request.altered_payload(field.name, injection_value)
23
- response = endpoint.call method, payload, method.request.default_headers
24
- if(!check_response(response, endpoint)) then
25
- reports << InjectionReport.new("sql", endpoint.url, payload, response)
26
- end
25
+ payload = method.request.altered_payload field_name: field.name,
26
+ value: injection_value
27
+ response = endpoint.call base_url: base_url,
28
+ method: method,
29
+ payload: payload,
30
+ headers: method.request.default_headers
31
+ next if check_response(response, endpoint)
32
+
33
+ reports << InjectionReport.new('sql',
34
+ endpoint.url,
35
+ payload,
36
+ response)
27
37
  end
28
38
  end
29
39
 
@@ -34,22 +44,27 @@ module ApiTester
34
44
  response.code == 200 || check_error(response, endpoint)
35
45
  end
36
46
 
37
- def self.check_error response, endpoint
38
- evaluator = ApiTester::ResponseEvaluator.new response.body, endpoint.bad_request_response
47
+ def self.check_error(response, endpoint)
48
+ evaluator = ApiTester::ResponseEvaluator.new(
49
+ actual_body: response.body,
50
+ expected_fields: endpoint.bad_request_response
51
+ )
39
52
  missing_fields = evaluator.missing_fields
40
53
  extra_fields = evaluator.extra_fields
41
- response.code == endpoint.bad_request_response.code && missing_fields.size == 0 && extra_fields.size == 0
54
+ response.code == endpoint.bad_request_response.code &&
55
+ missing_fields.size.zero? && extra_fields.size.zero?
42
56
  end
43
57
  end
44
58
  end
45
59
 
60
+ # Report for InjectionModule
46
61
  class InjectionReport
47
62
  attr_accessor :injection_type
48
63
  attr_accessor :url
49
64
  attr_accessor :payload
50
65
  attr_accessor :response
51
66
 
52
- def initialize injection_type, url, payload, response
67
+ def initialize(injection_type, url, payload, response)
53
68
  self.injection_type = injection_type
54
69
  self.url = url
55
70
  self.payload = payload
@@ -57,10 +72,10 @@ class InjectionReport
57
72
  end
58
73
 
59
74
  def print
60
- puts "Found potential #{self.injection_type}: "
61
- puts " Requested #{self.url} with payload:"
62
- puts " #{self.payload}"
75
+ puts "Found potential #{injection_type}: "
76
+ puts " Requested #{url} with payload:"
77
+ puts " #{payload}"
63
78
  puts ' Received: '
64
- puts " #{self.response}"
79
+ puts " #{response}"
65
80
  end
66
81
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiTester
4
+ # Ensures the fields marked as required in contract are guarded
5
+ module RequiredFields
6
+ def self.go(contract)
7
+ reports = []
8
+ contract.endpoints.each do |endpoint|
9
+ endpoint.methods.each do |method|
10
+ request_def = method.request
11
+ required_fields = request_def.fields.keep_if(&:required)
12
+ combinations = (1..required_fields.size).flat_map { |size| required_fields.combination(size).to_a }
13
+ combinations.each do |remove_fields|
14
+ fields = remove_fields.map do |field|
15
+ { name: field.name, value: nil }
16
+ end
17
+ payload = request_def.altered_payload_with fields
18
+ response = endpoint.call base_url: contract.base_url,
19
+ method: method,
20
+ payload: payload,
21
+ headers: request_def.default_headers
22
+ test = RequiredFieldsTest.new response: response,
23
+ payload: payload,
24
+ expected_response: endpoint.bad_request_response,
25
+ url: endpoint.url,
26
+ verb: method.verb
27
+ reports.concat test.check
28
+ end
29
+ end
30
+ end
31
+
32
+ reports
33
+ end
34
+
35
+ def self.order
36
+ 5
37
+ end
38
+ end
39
+
40
+ # Test layout used for RequiredFieldsModule
41
+ class RequiredFieldsTest < MethodCaseTest
42
+ def initialize(response:, payload:, expected_response:, url:, verb:)
43
+ super response: response,
44
+ payload: payload,
45
+ expected_response: expected_response,
46
+ url: url,
47
+ verb: verb,
48
+ module_name: 'RequiredFieldsModule'
49
+ end
50
+ end
51
+ end
@@ -1,15 +1,17 @@
1
- require 'pry'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module ApiTester
4
+ # Module for ensuring the server isn't broadcasting information about itself
4
5
  module ServerInformation
5
- def self.go contract
6
+ def self.go(contract)
6
7
  reports = []
7
8
  endpoint = contract.endpoints[0]
8
- response = endpoint.default_call
9
+ response = endpoint.default_call contract.base_url
9
10
 
10
- [:server, :x_powered_by, :x_aspnetmvc_version, :x_aspnet_version].each do |server_key|
11
- if response.headers[server_key] then
12
- reports << ServerBroadcastReport.new(response.headers[server_key], server_key)
11
+ %i[server x_powered_by x_aspnetmvc_version x_aspnet_version].each do |key|
12
+ if response.headers[key]
13
+ reports << ServerBroadcastReport.new(response.headers[key],
14
+ key)
13
15
  end
14
16
  end
15
17
 
@@ -22,18 +24,19 @@ module ApiTester
22
24
  end
23
25
  end
24
26
 
27
+ # Report used by module
25
28
  class ServerBroadcastReport
26
29
  attr_accessor :server_info
27
30
  attr_accessor :server_key
28
31
 
29
- def initialize server_info, server_key
32
+ def initialize(server_info, server_key)
30
33
  self.server_info = server_info
31
34
  self.server_key = server_key
32
35
  end
33
36
 
34
37
  def print
35
- puts "Found server information being broadcast in headers:"
36
- puts " #{self.server_info}"
37
- puts " as #{self.server_key}"
38
+ puts 'Found server information being broadcast in headers:'
39
+ puts " #{server_info}"
40
+ puts " as #{server_key}"
38
41
  end
39
42
  end