Skip to content

Commit

Permalink
Add a new fuzzer to check for security schemes definition
Browse files Browse the repository at this point in the history
  • Loading branch information
en-milie committed Mar 1, 2021
1 parent 8f87745 commit 0c27df7
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.endava.cats.fuzzer.contract;

import com.endava.cats.fuzzer.ContractInfoFuzzer;
import com.endava.cats.http.HttpMethod;
import com.endava.cats.model.FuzzingData;
import com.endava.cats.report.TestCaseListener;
import io.github.ludovicianul.prettylogger.PrettyLogger;
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@ContractInfoFuzzer
@Component
public class SecuritySchemesContractInfoFuzzer extends BaseContractInfoFuzzer {
private final PrettyLogger log = PrettyLoggerFactory.getLogger(this.getClass());

protected SecuritySchemesContractInfoFuzzer(TestCaseListener tcl) {
super(tcl);
}

@Override
public void process(FuzzingData data) {
testCaseListener.addScenario(log, "Scenario: Check if the current path has security schemes defined either globally or at path level");
testCaseListener.addExpectedResult(log, "[at least a security schemes] must be present either globally or at path level");

Map<String, SecurityScheme> securitySchemeMap = Optional.ofNullable(data.getOpenApi().getComponents()).orElse(new Components()).getSecuritySchemes();
List<SecurityRequirement> securityRequirementList = Optional.ofNullable(data.getOpenApi().getSecurity()).orElse(Collections.emptyList());

boolean hasTopLevelSecuritySchemes = !securityRequirementList.isEmpty();
boolean areGlobalSecuritySchemesDefined = securityRequirementList.stream().allMatch(securityRequirement -> securitySchemeMap.keySet().containsAll(securityRequirement.keySet()));

Operation operation = HttpMethod.getOperation(data.getMethod(), data.getPathItem());

boolean hasSecuritySchemesAtTagLevel = !Optional.ofNullable(operation.getSecurity()).orElse(Collections.emptyList()).isEmpty();

if (hasTopLevelSecuritySchemes || hasSecuritySchemesAtTagLevel) {
if (areGlobalSecuritySchemesDefined) {
testCaseListener.reportInfo(log, "The current path has security scheme(s) properly defined");
} else {
testCaseListener.reportWarn(log, "The current path has security scheme(s) defined, but they are not present in the [components->securitySchemes] contract element");
}
} else {
testCaseListener.reportError(log, "The current path does not have security scheme(s) defined and there are none defined globally");
}
}

@Override
protected String runKey(FuzzingData data) {
return data.getPath() + data.getMethod();
}

@Override
public String description() {
return "verifies if the OpenApi contract contains valid security schemas for all paths, either globally configured or per path";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.endava.cats.fuzzer.contract;

import com.endava.cats.http.HttpMethod;
import com.endava.cats.io.TestCaseExporter;
import com.endava.cats.model.FuzzingData;
import com.endava.cats.report.ExecutionStatisticsListener;
import com.endava.cats.report.TestCaseListener;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;

@ExtendWith(SpringExtension.class)
class SecuritySchemesContractInfoFuzzerTest {
@SpyBean
private TestCaseListener testCaseListener;

@MockBean
private ExecutionStatisticsListener executionStatisticsListener;

@MockBean
private TestCaseExporter testCaseExporter;

@SpyBean
private BuildProperties buildProperties;

private SecuritySchemesContractInfoFuzzer securitySchemesContractInfoFuzzer;

@BeforeAll
static void init() {
System.setProperty("name", "cats");
System.setProperty("version", "4.3.2");
System.setProperty("time", "100011111");
}

@BeforeEach
void setup() {
securitySchemesContractInfoFuzzer = new SecuritySchemesContractInfoFuzzer(testCaseListener);
}

@Test
void shouldNotReportAnyError() throws Exception {
OpenAPI openAPI = new OpenAPIParser().readContents(new String(Files.readAllBytes(Paths.get("src/test/resources/openapi.yml"))), null, null).getOpenAPI();
FuzzingData data = FuzzingData.builder().openApi(openAPI).path("/pet").tags(Collections.singletonList("pet")).method(HttpMethod.POST).pathItem(openAPI.getPaths().get("/pet")).build();
securitySchemesContractInfoFuzzer.fuzz(data);

Mockito.verify(testCaseListener, Mockito.times(1)).reportInfo(Mockito.any(), Mockito.eq("The current path has security scheme(s) properly defined"));
}

@Test
void shouldReportError() throws Exception {
OpenAPI openAPI = new OpenAPIParser().readContents(new String(Files.readAllBytes(Paths.get("src/test/resources/contract-no-security.yml"))), null, null).getOpenAPI();
FuzzingData data = FuzzingData.builder().openApi(openAPI).path("/pet").tags(Collections.singletonList("pet")).method(HttpMethod.PUT).pathItem(openAPI.getPaths().get("/pet")).build();
securitySchemesContractInfoFuzzer.fuzz(data);

Mockito.verify(testCaseListener, Mockito.times(1)).reportError(Mockito.any(), Mockito.eq("The current path does not have security scheme(s) defined and there are none defined globally"));
}

@Test
void shouldNotReportErrorWithSecurityAtPathLevel() throws Exception {
OpenAPI openAPI = new OpenAPIParser().readContents(new String(Files.readAllBytes(Paths.get("src/test/resources/contract-no-path-tags.yml"))), null, null).getOpenAPI();
FuzzingData data = FuzzingData.builder().openApi(openAPI).path("/pet").method(HttpMethod.PUT).pathItem(openAPI.getPaths().get("/pet")).build();
securitySchemesContractInfoFuzzer.fuzz(data);

Mockito.verify(testCaseListener, Mockito.times(1)).reportInfo(Mockito.any(), Mockito.eq("The current path has security scheme(s) properly defined"));
}

@Test
void shouldNotReportErrorWithSecurityGlobal() throws Exception {
OpenAPI openAPI = new OpenAPIParser().readContents(new String(Files.readAllBytes(Paths.get("src/test/resources/contract-path-tags-mismatch.yml"))), null, null).getOpenAPI();
FuzzingData data = FuzzingData.builder().openApi(openAPI).path("/pet").method(HttpMethod.PUT).tags(Collections.singletonList("petsCats")).pathItem(openAPI.getPaths().get("/pet")).build();
securitySchemesContractInfoFuzzer.fuzz(data);

Mockito.verify(testCaseListener, Mockito.times(1)).reportInfo(Mockito.any(), Mockito.eq("The current path has security scheme(s) properly defined"));
}

@Test
void shouldReportWarningWithSecurityGlobalAndSchemeNotDefined() throws Exception {
OpenAPI openAPI = new OpenAPIParser().readContents(new String(Files.readAllBytes(Paths.get("src/test/resources/contract-security-mismatch-schemes.yml"))), null, null).getOpenAPI();
FuzzingData data = FuzzingData.builder().openApi(openAPI).path("/pet").method(HttpMethod.PUT).tags(Collections.singletonList("petsCats")).pathItem(openAPI.getPaths().get("/pet")).build();
securitySchemesContractInfoFuzzer.fuzz(data);

Mockito.verify(testCaseListener, Mockito.times(1)).reportWarn(Mockito.any(), Mockito.eq("The current path has security scheme(s) defined, but they are not present in the [components->securitySchemes] contract element"));
}

@Test
void shouldNotRunOnSecondAttempt() throws Exception {
OpenAPI openAPI = new OpenAPIParser().readContents(new String(Files.readAllBytes(Paths.get("src/test/resources/openapi.yml"))), null, null).getOpenAPI();
FuzzingData data = FuzzingData.builder().openApi(openAPI).path("/pet").method(HttpMethod.POST).tags(Collections.singletonList("pet")).pathItem(openAPI.getPaths().get("/pet")).build();
securitySchemesContractInfoFuzzer.fuzz(data);

Mockito.verify(testCaseListener, Mockito.times(1)).reportInfo(Mockito.any(), Mockito.eq("The current path has security scheme(s) properly defined"));

Mockito.reset(testCaseListener);
securitySchemesContractInfoFuzzer.fuzz(data);
Mockito.verify(testCaseListener, Mockito.times(0)).reportInfo(Mockito.any(), Mockito.eq("The current path has security scheme(s) properly defined"));
}

@Test
void shouldReturnSimpleClassNameForToString() {
Assertions.assertThat(securitySchemesContractInfoFuzzer).hasToString(securitySchemesContractInfoFuzzer.getClass().getSimpleName());
}

@Test
void shouldReturnMeaningfulDescription() {
Assertions.assertThat(securitySchemesContractInfoFuzzer.description()).isEqualTo("verifies if the OpenApi contract contains valid security schemas for all paths, either globally configured or per path");
}
}
43 changes: 43 additions & 0 deletions src/test/resources/contract-no-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
openapi: 3.0.0
info:
title: OpenAPI Petstore
description: 'This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. For OAuth2 flow, you may use `user` as both username and password when asked to login.'
license:
name: Apache-2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
version: 1.0.0
contact:
name: CATS team
url: https://github.com/Endava/cats
email: [email protected]
externalDocs:
description: Find out more about OpenAPI generator
url: 'https://openapi-generator.tech'
servers:
- url: /v3
description: This is the production server
tags:
- name: pet
description: Everything about your Pets
- name: store
description: Access to Petstore orders
- name: user
description: Operations about the user
paths:
/pet:
put:
summary: Update an existing pet
operationId: updatePet
requestBody:
$ref: '#/components/requestBodies/Pet'
responses:
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
x-accepts: application/json
x-tags:
- tag: pet
x-contentType: application/json
12 changes: 8 additions & 4 deletions src/test/resources/contract-path-tags-mismatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ paths:
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
x-accepts: application/json
x-tags:
- tag: pet
x-contentType: application/json
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: [ ]
53 changes: 53 additions & 0 deletions src/test/resources/contract-security-mismatch-schemes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
openapi: 3.0.0
info:
title: OpenAPI Petstore
description: 'This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. For OAuth2 flow, you may use `user` as both username and password when asked to login.'
license:
name: Apache-2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
version: 1.0.0
contact:
name: CATS team
url: https://github.com/Endava/cats
email: [email protected]
externalDocs:
description: Find out more about OpenAPI generator
url: 'https://openapi-generator.tech'
servers:
- url: /v3
description: This is the production server
tags:
- name: pet
description: Everything about your Pets
- name: store
description: Access to Petstore orders
- name: user
description: Operations about the user
paths:
/pet:
put:
tags:
- petCATS
summary: Update an existing pet
operationId: updatePet
requestBody:
$ref: '#/components/requestBodies/Pet'
responses:
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
x-accepts: application/json
x-tags:
- tag: pet
x-contentType: application/json
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- mismatch: [ ]

0 comments on commit 0c27df7

Please sign in to comment.