api-tester 0.1.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/.rspec +1 -0
- data/.rubocop.yml +61 -0
- data/Gemfile +2 -0
- data/Guardfile +70 -0
- data/README.md +106 -74
- data/Rakefile +8 -3
- data/api-tester.gemspec +31 -23
- data/changelog.txt +35 -0
- data/lib/api-tester.rb +15 -0
- data/lib/api-tester/config.rb +43 -0
- data/lib/api-tester/definition/boundary_case.rb +16 -0
- data/lib/api-tester/definition/contract.rb +20 -0
- data/lib/api-tester/definition/endpoint.rb +84 -0
- data/lib/api-tester/definition/fields/array_field.rb +47 -0
- data/lib/api-tester/definition/fields/boolean_field.rb +23 -0
- data/lib/api-tester/definition/fields/email_field.rb +25 -0
- data/lib/api-tester/definition/fields/enum_field.rb +32 -0
- data/lib/api-tester/definition/fields/field.rb +50 -0
- data/lib/api-tester/definition/fields/number_field.rb +22 -0
- data/lib/api-tester/definition/fields/object_field.rb +47 -0
- data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
- data/lib/api-tester/definition/method.rb +22 -0
- data/lib/api-tester/definition/request.rb +96 -0
- data/lib/api-tester/definition/response.rb +39 -0
- data/lib/api-tester/method_case_test.rb +83 -0
- data/lib/api-tester/modules/extra_verbs.rb +53 -0
- data/lib/api-tester/modules/format.rb +47 -0
- data/lib/api-tester/modules/good_case.rb +46 -0
- data/lib/api-tester/modules/injection_module.rb +81 -0
- data/lib/api-tester/modules/required_fields.rb +51 -0
- data/lib/api-tester/modules/server_information.rb +42 -0
- data/lib/api-tester/modules/typo.rb +70 -0
- data/lib/api-tester/modules/unexpected_fields.rb +61 -0
- data/lib/api-tester/modules/unused_fields.rb +31 -0
- data/lib/api-tester/reporter/api_report.rb +47 -0
- data/lib/api-tester/reporter/missing_field_report.rb +24 -0
- data/lib/api-tester/reporter/report.rb +30 -0
- data/lib/api-tester/reporter/status_code_report.rb +21 -0
- data/lib/api-tester/test_helper.rb +12 -0
- data/lib/api-tester/util/response_evaluator.rb +88 -0
- data/lib/api-tester/util/supported_verbs.rb +39 -0
- data/lib/api-tester/version.rb +5 -0
- metadata +159 -42
- data/.travis.yml +0 -6
- data/lib/tester.rb +0 -7
- data/lib/tester/api_tester.rb +0 -50
- data/lib/tester/definition/api_contract.rb +0 -13
- data/lib/tester/definition/api_method.rb +0 -11
- data/lib/tester/definition/boundary_case.rb +0 -11
- data/lib/tester/definition/endpoint.rb +0 -57
- data/lib/tester/definition/fields/array_field.rb +0 -44
- data/lib/tester/definition/fields/boolean_field.rb +0 -18
- data/lib/tester/definition/fields/email_field.rb +0 -20
- data/lib/tester/definition/fields/enum_field.rb +0 -27
- data/lib/tester/definition/fields/field.rb +0 -47
- data/lib/tester/definition/fields/number_field.rb +0 -17
- data/lib/tester/definition/fields/object_field.rb +0 -42
- data/lib/tester/definition/request.rb +0 -49
- data/lib/tester/definition/response.rb +0 -34
- data/lib/tester/method_case_test.rb +0 -67
- data/lib/tester/modules/extra_verbs.rb +0 -25
- data/lib/tester/modules/format.rb +0 -26
- data/lib/tester/modules/good_case.rb +0 -29
- data/lib/tester/modules/module.rb +0 -18
- data/lib/tester/modules/typo.rb +0 -41
- data/lib/tester/modules/unused_fields.rb +0 -22
- data/lib/tester/reporter/api_report.rb +0 -33
- data/lib/tester/reporter/missing_field_report.rb +0 -23
- data/lib/tester/reporter/missing_response_field_report.rb +0 -19
- data/lib/tester/reporter/report.rb +0 -25
- data/lib/tester/reporter/status_code_report.rb +0 -12
- data/lib/tester/test_helper.rb +0 -10
- data/lib/tester/util/response_evaluator.rb +0 -73
- data/lib/tester/util/supported_verbs.rb +0 -34
- data/lib/tester/version.rb +0 -3
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'injection_vulnerability_library'
|
4
|
+
|
5
|
+
module ApiTester
|
6
|
+
# Tests injection cases
|
7
|
+
module InjectionModule
|
8
|
+
def self.go(contract)
|
9
|
+
reports = []
|
10
|
+
contract.endpoints.each do |endpoint|
|
11
|
+
endpoint.methods.each do |method|
|
12
|
+
reports.concat inject_payload contract.base_url, endpoint, method
|
13
|
+
end
|
14
|
+
end
|
15
|
+
reports
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.inject_payload(base_url, endpoint, method)
|
19
|
+
reports = []
|
20
|
+
sql_injections = InjectionVulnerabilityLibrary.sql_vulnerabilities
|
21
|
+
|
22
|
+
method.request.fields.each do |field|
|
23
|
+
sql_injections.each do |injection|
|
24
|
+
injection_value = "#{field.default_value}#{injection}"
|
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)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
reports
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.check_response(response, endpoint)
|
44
|
+
response.code == 200 || check_error(response, endpoint)
|
45
|
+
end
|
46
|
+
|
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
|
+
)
|
52
|
+
missing_fields = evaluator.missing_fields
|
53
|
+
extra_fields = evaluator.extra_fields
|
54
|
+
response.code == endpoint.bad_request_response.code &&
|
55
|
+
missing_fields.size.zero? && extra_fields.size.zero?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Report for InjectionModule
|
61
|
+
class InjectionReport
|
62
|
+
attr_accessor :injection_type
|
63
|
+
attr_accessor :url
|
64
|
+
attr_accessor :payload
|
65
|
+
attr_accessor :response
|
66
|
+
|
67
|
+
def initialize(injection_type, url, payload, response)
|
68
|
+
self.injection_type = injection_type
|
69
|
+
self.url = url
|
70
|
+
self.payload = payload
|
71
|
+
self.response = response
|
72
|
+
end
|
73
|
+
|
74
|
+
def print
|
75
|
+
puts "Found potential #{injection_type}: "
|
76
|
+
puts " Requested #{url} with payload:"
|
77
|
+
puts " #{payload}"
|
78
|
+
puts ' Received: '
|
79
|
+
puts " #{response}"
|
80
|
+
end
|
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
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiTester
|
4
|
+
# Module for ensuring the server isn't broadcasting information about itself
|
5
|
+
module ServerInformation
|
6
|
+
def self.go(contract)
|
7
|
+
reports = []
|
8
|
+
endpoint = contract.endpoints[0]
|
9
|
+
response = endpoint.default_call contract.base_url
|
10
|
+
|
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)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
reports
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.order
|
22
|
+
10
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Report used by module
|
28
|
+
class ServerBroadcastReport
|
29
|
+
attr_accessor :server_info
|
30
|
+
attr_accessor :server_key
|
31
|
+
|
32
|
+
def initialize(server_info, server_key)
|
33
|
+
self.server_info = server_info
|
34
|
+
self.server_key = server_key
|
35
|
+
end
|
36
|
+
|
37
|
+
def print
|
38
|
+
puts 'Found server information being broadcast in headers:'
|
39
|
+
puts " #{server_info}"
|
40
|
+
puts " as #{server_key}"
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'api-tester/reporter/status_code_report'
|
4
|
+
require 'api-tester/util/supported_verbs'
|
5
|
+
|
6
|
+
module ApiTester
|
7
|
+
# Module checking various not found scenarios
|
8
|
+
module Typo
|
9
|
+
def self.go(contract)
|
10
|
+
reports = []
|
11
|
+
|
12
|
+
contract.endpoints.each do |endpoint|
|
13
|
+
allowances(endpoint).each do
|
14
|
+
reports.concat check_typo_url(contract.base_url, endpoint)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
reports
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.check_typo_url(base_url, endpoint)
|
22
|
+
bad_url = "#{endpoint.url}gibberishadsfasdf"
|
23
|
+
bad_endpoint = ApiTester::Endpoint.new name: 'Bad URL',
|
24
|
+
relative_url: bad_url
|
25
|
+
typo_case = BoundaryCase.new description: 'Typo URL check',
|
26
|
+
payload: {},
|
27
|
+
headers: {}
|
28
|
+
method = ApiTester::Method.new verb: ApiTester::SupportedVerbs::GET,
|
29
|
+
response: ApiTester::Response.new(
|
30
|
+
status_code: 200
|
31
|
+
),
|
32
|
+
request: ApiTester::Request.new
|
33
|
+
response = bad_endpoint.call base_url: base_url,
|
34
|
+
method: method,
|
35
|
+
payload: typo_case.payload,
|
36
|
+
headers: typo_case.headers
|
37
|
+
|
38
|
+
test = TypoClass.new response,
|
39
|
+
typo_case.payload,
|
40
|
+
endpoint.not_found_response,
|
41
|
+
bad_url,
|
42
|
+
ApiTester::SupportedVerbs::GET
|
43
|
+
test.check
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.allowances(endpoint)
|
47
|
+
allowances = []
|
48
|
+
endpoint.methods.each do |method|
|
49
|
+
allowances << method.verb
|
50
|
+
end
|
51
|
+
allowances.uniq
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.order
|
55
|
+
4
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Test layout for TypoModule
|
60
|
+
class TypoClass < MethodCaseTest
|
61
|
+
def initialize(response, payload, expected_response, url, verb)
|
62
|
+
super response: response,
|
63
|
+
payload: payload,
|
64
|
+
expected_response: expected_response,
|
65
|
+
url: url,
|
66
|
+
verb: verb,
|
67
|
+
module_name: 'TypoModule'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
module ApiTester
|
6
|
+
# Module checking nothing shows up in response which is not defined in contract
|
7
|
+
module UnexpectedFields
|
8
|
+
def self.go(contract)
|
9
|
+
reports = []
|
10
|
+
|
11
|
+
contract.endpoints.each do |endpoint|
|
12
|
+
endpoint.methods.each do |method|
|
13
|
+
default_case = BoundaryCase.new description: endpoint.url,
|
14
|
+
payload: method.request.default_payload,
|
15
|
+
headers: method.request.default_headers
|
16
|
+
response = endpoint.call base_url: contract.base_url,
|
17
|
+
method: method,
|
18
|
+
payload: default_case.payload,
|
19
|
+
headers: default_case.headers
|
20
|
+
test = UnexpectedFieldsTest.new response, endpoint.url, method
|
21
|
+
reports.concat test.check
|
22
|
+
end
|
23
|
+
end
|
24
|
+
reports
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.order
|
28
|
+
90
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Test layout for UnexpectedFields module
|
33
|
+
class UnexpectedFieldsTest < MethodCaseTest
|
34
|
+
def initialize(response, url, method)
|
35
|
+
super response: response,
|
36
|
+
payload: method.request.default_payload,
|
37
|
+
expected_response: method.expected_response,
|
38
|
+
url: url,
|
39
|
+
verb: method.verb,
|
40
|
+
module_name: 'UnexpectedFieldsModule'
|
41
|
+
end
|
42
|
+
|
43
|
+
def check
|
44
|
+
evaluator = ApiTester::ResponseEvaluator.new actual_body: json_parse(response.body),
|
45
|
+
expected_fields: expected_response
|
46
|
+
response_fields = evaluator.response_field_array
|
47
|
+
expected_fields = evaluator.expected_fields
|
48
|
+
increment_fields evaluator.seen_fields
|
49
|
+
extra = response_fields - expected_fields
|
50
|
+
extra.each do |extra_field|
|
51
|
+
report = Report.new description: "UnexpectedFieldsModule - Found unexpected field #{extra_field}",
|
52
|
+
url: url,
|
53
|
+
request: payload,
|
54
|
+
expected_response: expected_response,
|
55
|
+
actual_response: response
|
56
|
+
reports << report
|
57
|
+
end
|
58
|
+
reports
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'api-tester/reporter/missing_field_report'
|
4
|
+
|
5
|
+
module ApiTester
|
6
|
+
# Ensures all fields defined in contract are returned during test suite
|
7
|
+
module UnusedFields
|
8
|
+
def self.go(contract)
|
9
|
+
reports = []
|
10
|
+
|
11
|
+
contract.endpoints.each do |endpoint|
|
12
|
+
endpoint.methods.each do |method|
|
13
|
+
method.expected_response.body.each do |field|
|
14
|
+
next unless field.is_seen.zero?
|
15
|
+
|
16
|
+
reports << MissingFieldReport.new(url: endpoint.url,
|
17
|
+
verb: method.verb,
|
18
|
+
expected_field: field.name,
|
19
|
+
description: 'UnusedFieldsModule')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
reports
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.order
|
28
|
+
99
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'api-tester/reporter/report'
|
4
|
+
|
5
|
+
module ApiTester
|
6
|
+
# class for dealing with reports generated by the modules during test suite
|
7
|
+
class ApiReport
|
8
|
+
attr_accessor :reports
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
self.reports = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_new(url:, request:, expected_response:, actual_response:, description: 'case')
|
15
|
+
report = Report.new description,
|
16
|
+
url,
|
17
|
+
request,
|
18
|
+
expected_response,
|
19
|
+
actual_response
|
20
|
+
reports << report
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_new_report(report)
|
24
|
+
reports << report
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_reports(reports)
|
28
|
+
reports.each do |report|
|
29
|
+
add_new_report(report)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def print
|
34
|
+
if reports.size.zero?
|
35
|
+
puts 'No issues found'
|
36
|
+
else
|
37
|
+
puts "Issues discovered: #{reports.size}"
|
38
|
+
reports.each do |report|
|
39
|
+
report.print
|
40
|
+
puts '\n'
|
41
|
+
puts '\n'
|
42
|
+
end
|
43
|
+
puts "Total issues: #{reports.size}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiTester
|
4
|
+
# Report used for when response is missing a field
|
5
|
+
class MissingFieldReport
|
6
|
+
attr_accessor :url
|
7
|
+
attr_accessor :verb
|
8
|
+
attr_accessor :expected_field
|
9
|
+
attr_accessor :description
|
10
|
+
|
11
|
+
def initialize(url:, verb:, expected_field:, description:)
|
12
|
+
self.url = url
|
13
|
+
self.verb = verb
|
14
|
+
self.expected_field = expected_field
|
15
|
+
self.description = description
|
16
|
+
end
|
17
|
+
|
18
|
+
def print
|
19
|
+
puts "#{description}:"
|
20
|
+
puts " #{verb} #{url} is missing response field:"
|
21
|
+
puts " #{expected_field}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiTester
|
4
|
+
# Standard report format for differing responses
|
5
|
+
class Report
|
6
|
+
attr_accessor :description
|
7
|
+
attr_accessor :url
|
8
|
+
attr_accessor :request
|
9
|
+
attr_accessor :expected_response
|
10
|
+
attr_accessor :actual_response
|
11
|
+
|
12
|
+
def initialize(description:, url:, request:, expected_response:, actual_response:)
|
13
|
+
self.description = description
|
14
|
+
self.url = url
|
15
|
+
self.request = request
|
16
|
+
self.expected_response = expected_response
|
17
|
+
self.actual_response = actual_response
|
18
|
+
end
|
19
|
+
|
20
|
+
def print
|
21
|
+
puts "#{description}: "
|
22
|
+
puts " Requested #{url} with payload:"
|
23
|
+
puts " #{request.to_json}"
|
24
|
+
puts ' Expecting: '
|
25
|
+
puts ' ' + expected_response.to_s
|
26
|
+
puts ' Receiving: '
|
27
|
+
puts " #{actual_response}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|