api-tester 1.0.0 → 1.1.1
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.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/push.yml +39 -0
- data/.github/workflows/test.yml +31 -0
- data/.rubocop.yml +61 -0
- data/Gemfile +2 -0
- data/Guardfile +70 -0
- data/README.md +65 -61
- data/Rakefile +8 -3
- data/api-tester.gemspec +29 -24
- data/changelog.txt +10 -0
- data/lib/api-tester.rb +6 -3
- data/lib/api-tester/config.rb +16 -13
- data/lib/api-tester/definition/boundary_case.rb +4 -1
- data/lib/api-tester/definition/contract.rb +8 -3
- data/lib/api-tester/definition/endpoint.rb +32 -23
- data/lib/api-tester/definition/fields/array_field.rb +20 -19
- data/lib/api-tester/definition/fields/boolean_field.rb +12 -9
- data/lib/api-tester/definition/fields/email_field.rb +14 -11
- data/lib/api-tester/definition/fields/enum_field.rb +14 -11
- data/lib/api-tester/definition/fields/field.rb +46 -45
- data/lib/api-tester/definition/fields/number_field.rb +11 -8
- data/lib/api-tester/definition/fields/object_field.rb +34 -31
- data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
- data/lib/api-tester/definition/method.rb +7 -2
- data/lib/api-tester/definition/request.rb +43 -16
- data/lib/api-tester/definition/response.rb +29 -26
- data/lib/api-tester/method_case_test.rb +67 -53
- data/lib/api-tester/modules/extra_verbs.rb +29 -9
- data/lib/api-tester/modules/format.rb +23 -7
- data/lib/api-tester/modules/good_case.rb +25 -10
- data/lib/api-tester/modules/injection_module.rb +32 -17
- data/lib/api-tester/modules/required_fields.rb +51 -0
- data/lib/api-tester/modules/server_information.rb +13 -10
- data/lib/api-tester/modules/typo.rb +36 -13
- data/lib/api-tester/modules/unexpected_fields.rb +61 -0
- data/lib/api-tester/modules/unused_fields.rb +12 -6
- data/lib/api-tester/reporter/api_report.rb +24 -16
- data/lib/api-tester/reporter/missing_field_report.rb +12 -13
- data/lib/api-tester/reporter/report.rb +11 -8
- data/lib/api-tester/reporter/status_code_report.rb +9 -2
- data/lib/api-tester/test_helper.rb +6 -6
- data/lib/api-tester/util/response_evaluator.rb +70 -57
- data/lib/api-tester/util/supported_verbs.rb +8 -5
- data/lib/api-tester/version.rb +3 -1
- metadata +99 -25
- data/.travis.yml +0 -6
- 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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
end
|
73
|
+
def increment_fields(seen_fields)
|
74
|
+
seen_fields.each(&:seen)
|
75
|
+
end
|
62
76
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
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
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
6
|
-
|
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
|
13
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
6
|
-
|
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,
|
12
|
-
|
13
|
-
|
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
|
-
|
31
|
+
1
|
22
32
|
end
|
23
33
|
end
|
24
34
|
|
25
|
-
|
35
|
+
# Test layout used by module
|
26
36
|
class GoodCaseTest < MethodCaseTest
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
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
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
38
|
-
evaluator = ApiTester::ResponseEvaluator.new
|
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 &&
|
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
|
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 #{
|
61
|
-
puts " Requested #{
|
62
|
-
puts " #{
|
75
|
+
puts "Found potential #{injection_type}: "
|
76
|
+
puts " Requested #{url} with payload:"
|
77
|
+
puts " #{payload}"
|
63
78
|
puts ' Received: '
|
64
|
-
puts " #{
|
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
|
-
|
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
|
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
|
-
[
|
11
|
-
if response.headers[
|
12
|
-
reports << ServerBroadcastReport.new(response.headers[
|
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
|
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
|
36
|
-
puts " #{
|
37
|
-
puts " as #{
|
38
|
+
puts 'Found server information being broadcast in headers:'
|
39
|
+
puts " #{server_info}"
|
40
|
+
puts " as #{server_key}"
|
38
41
|
end
|
39
42
|
end
|