From 887e09d5a5d1bcfdbb9315cd40c4af415d5774f0 Mon Sep 17 00:00:00 2001 From: Aldo Torres Date: Thu, 26 Jun 2025 15:57:37 -0500 Subject: [PATCH] fix: new rule oar115 analize value in the required fields on schemas --- .../sonar/openapi/checks/RulesLists.java | 3 +- .../format/OAR115VerifyRequiredFields.java | 91 +++++++++++++++++++ src/main/resources/messages/errors.properties | 1 + .../resources/messages/errors_es.properties | 1 + .../openapi/rules/openapi/format/OAR115.html | 83 +++++++++++++++++ .../openapi/rules/openapi/format/OAR115.json | 13 +++ .../OAR115VerifyRequiredFieldsTest.java | 42 +++++++++ .../checks/v2/format/OAR115/invalid.json | 40 ++++++++ .../checks/v2/format/OAR115/invalid.yaml | 27 ++++++ .../checks/v2/format/OAR115/valid.json | 35 +++++++ .../checks/v2/format/OAR115/valid.yaml | 26 ++++++ .../checks/v3/format/OAR115/invalid.json | 45 +++++++++ .../checks/v3/format/OAR115/invalid.yaml | 30 ++++++ .../checks/v3/format/OAR115/valid.json | 44 +++++++++ .../checks/v3/format/OAR115/valid.yaml | 29 ++++++ 15 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java create mode 100644 src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.html create mode 100644 src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.json create mode 100644 src/test/java/org/sonar/samples/openapi/checks/format/OAR115VerifyRequiredFieldsTest.java create mode 100644 src/test/resources/checks/v2/format/OAR115/invalid.json create mode 100644 src/test/resources/checks/v2/format/OAR115/invalid.yaml create mode 100644 src/test/resources/checks/v2/format/OAR115/valid.json create mode 100644 src/test/resources/checks/v2/format/OAR115/valid.yaml create mode 100644 src/test/resources/checks/v3/format/OAR115/invalid.json create mode 100644 src/test/resources/checks/v3/format/OAR115/invalid.yaml create mode 100644 src/test/resources/checks/v3/format/OAR115/valid.json create mode 100644 src/test/resources/checks/v3/format/OAR115/valid.yaml diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java b/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java index b61e5be8..8f855576 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java @@ -51,7 +51,8 @@ public static List> getFormatChecks() { OAR051DescriptionDiffersSummaryCheck.class, OAR110LicenseInformationCheck.class, OAR111ContactInformationCheck.class, - OAR113CustomFieldCheck.class + OAR113CustomFieldCheck.class, + OAR115VerifyRequiredFields.class ); } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java b/src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java new file mode 100644 index 00000000..fd39de30 --- /dev/null +++ b/src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java @@ -0,0 +1,91 @@ +package apiaddicts.sonar.openapi.checks.format; + +import apiaddicts.sonar.openapi.checks.BaseCheck; +import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.isExternalRef; +import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.resolve; +import com.google.common.collect.ImmutableSet; +import com.sonar.sslr.api.AstNodeType; +import java.util.HashSet; +import java.util.Set; +import org.apiaddicts.apitools.dosonarapi.api.v2.OpenApi2Grammar; +import org.apiaddicts.apitools.dosonarapi.api.v3.OpenApi3Grammar; +import org.apiaddicts.apitools.dosonarapi.api.v31.OpenApi31Grammar; +import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; +import org.sonar.check.Rule; + +@Rule(key = OAR115VerifyRequiredFields.KEY) +public class OAR115VerifyRequiredFields extends BaseCheck { + public static final String KEY = "OAR115"; + + protected JsonNode externalRefNode = null; + + + @Override + public Set subscribedKinds() { + return ImmutableSet.of(OpenApi2Grammar.SCHEMA,OpenApi2Grammar.RESPONSE, OpenApi3Grammar.SCHEMA,OpenApi31Grammar.SCHEMA,OpenApi3Grammar.RESPONSE, OpenApi31Grammar.RESPONSE); + } + + @Override + public void visitNode(JsonNode node) { + if(node.getType() == OpenApi3Grammar.RESPONSE || node.getType() == OpenApi31Grammar.RESPONSE ){ + JsonNode content = node.get("content"); + JsonNode json = content.get("application/json"); + JsonNode schema = json.get("schema"); + resolveExteralRef(schema); + }else if(node.getType() == OpenApi2Grammar.RESPONSE){ + JsonNode schema = node.get("schema"); + resolveExteralRef(schema); + } else { + verifyTypeObject(node); + } + } + + + public void resolveExteralRef(JsonNode node) { + if(!"null".equals(node.getTokenValue())){ + boolean externalRefManagement = false; + if (isExternalRef(node) && externalRefNode == null) { + externalRefNode = node; + externalRefManagement = true; + } + + node = resolve(node); + validateRequiredFields(node); + if (externalRefManagement) externalRefNode = null; + } + } + + public void verifyTypeObject(JsonNode node){ + JsonNode typeNode = node.get("type"); + if (typeNode != null && "object".equals(typeNode.getTokenValue())) { + validateRequiredFields(node); + } + } + + + private void validateRequiredFields(JsonNode objectNode) { + JsonNode requiredNode = objectNode.get("required"); + JsonNode propertiesNode = objectNode.get("properties"); + + if (requiredNode == null || !requiredNode.isArray()) return; + + Set properties = new HashSet<>(); + if (propertiesNode != null) { + for (JsonNode property : propertiesNode.getJsonChildren()) { + String propertyName = property.key().getTokenValue(); + if(!"null".equals(propertyName)){ + properties.add(propertyName); + } + } + } + + for (JsonNode requiredField : requiredNode.elements()) { + String requiredName = requiredField.getTokenValue(); + if (!properties.contains(requiredName)) { + addIssue(KEY, + translate("OAR115.error"), + requiredField); + } + } + } +} diff --git a/src/main/resources/messages/errors.properties b/src/main/resources/messages/errors.properties index 6ee6d4fd..88e94070 100644 --- a/src/main/resources/messages/errors.properties +++ b/src/main/resources/messages/errors.properties @@ -115,6 +115,7 @@ OAR109.error=Use default instead of directly specifying 5XX codes OAR110.error=License information cannot be empty OAR111.error=Contact information cannot be empty OAR113.error=Field or extension {0} must be at the assigned location +OAR115.error=This value does not exist, it must be defined in the schema properties generic.section=Section {0} is mandatory generic.consume=Should indicate the default request media type generic.produce=Should indicate the default response media type diff --git a/src/main/resources/messages/errors_es.properties b/src/main/resources/messages/errors_es.properties index dba9af5c..f79e9056 100644 --- a/src/main/resources/messages/errors_es.properties +++ b/src/main/resources/messages/errors_es.properties @@ -115,6 +115,7 @@ OAR109.error=Utilice default en lugar de especificar códigos 5XX directamente OAR110.error=La información de licencia no puede estar vacía OAR111.error=La información de contacto no puede estar vacía OAR113.error=El campo o extensión {0} debe estar en la ubicación asignada. +OAR115.error=Este valor no existe, debe estár definido en las propiedades del esquema generic.section=La sección {0} es obligatoria generic.consume=Debe indicar el tipo de medio de solicitud predeterminado generic.produce=Debe indicar el tipo de medio de respuesta predeterminado diff --git a/src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.html b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.html new file mode 100644 index 00000000..b0492b85 --- /dev/null +++ b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.html @@ -0,0 +1,83 @@ +

The data in the required field must exist in schema parameters

+

Noncompliant Solution (OpenAPI 2)

+
+    swagger: "2.0"
+info:
+  title: API de ejemplo
+  version: "1.0.0"
+paths: {}
+definitions:
+  ErrorResponse:
+    type: object
+    properties:
+      code:
+        type: integer
+      message:
+        type: string
+    required:
+      - code
+      - message
+      - otherfield # Noncompliant {{OAR115: This value does not exist, it must be defined in the schema properties}}
+
+
+

Compliant Solution (OpenAPI 2)

+
+    swagger: "2.0"
+info:
+  title: API de ejemplo
+  version: "1.0.0"
+paths: {}
+definitions:
+  ErrorResponse:
+    type: object
+    properties:
+      code:
+        type: integer
+      message:
+        type: string
+    required:
+      - code
+      - message
+
+

Noncompliant Solution (OpenAPI 3)

+
+   openapi: 3.0.0
+info:
+  title: API de ejemplo
+  version: "1.0.0"
+paths: {}
+components:
+  schemas:
+    ErrorResponse:
+      type: object
+      properties:
+        code:
+          type: integer
+        message:
+          type: string
+      required:
+        - code
+        - message
+        - otherfield # Noncompliant {{OAR115: This value does not exist, it must be defined in the schema properties}}
+
+
+

Compliant Solution (OpenAPI 3)

+
+    openapi: 3.0.3
+info:
+  title: API de ejemplo
+  version: "1.0.0"
+paths: {}
+components:
+  schemas:
+    ErrorResponse:
+      type: object
+      properties:
+        code:
+          type: integer
+        message:
+          type: string
+      required:
+        - code
+        - message
+
\ No newline at end of file diff --git a/src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.json b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.json new file mode 100644 index 00000000..db9e1614 --- /dev/null +++ b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/format/OAR115.json @@ -0,0 +1,13 @@ +{ + "title": "OAR115 - VerifyRequiredFields - the data in the required field must exist in schema parameters", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "30min" + }, + "tags": [ + "format" + ], + "defaultSeverity": "MINOR" +} \ No newline at end of file diff --git a/src/test/java/org/sonar/samples/openapi/checks/format/OAR115VerifyRequiredFieldsTest.java b/src/test/java/org/sonar/samples/openapi/checks/format/OAR115VerifyRequiredFieldsTest.java new file mode 100644 index 00000000..2cccaa95 --- /dev/null +++ b/src/test/java/org/sonar/samples/openapi/checks/format/OAR115VerifyRequiredFieldsTest.java @@ -0,0 +1,42 @@ +package org.sonar.samples.openapi.checks.format; + +import apiaddicts.sonar.openapi.checks.format.OAR115VerifyRequiredFields; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.samples.openapi.BaseCheckTest; + +public class OAR115VerifyRequiredFieldsTest extends BaseCheckTest { + @Before + public void init() { + ruleName = "OAR115"; + check = new OAR115VerifyRequiredFields(); + v2Path = getV2Path("format"); + v3Path = getV3Path("format"); + } + + @Test + public void verifyValidRequiredFieldV2() { + verifyV2("valid"); + } + @Test + public void verifyInvalidRequiredFieldV2() { + verifyV2("invalid"); + } + + @Test + public void verifyValidRequiredFieldV3() { + verifyV3("valid"); + } + @Test + public void verifyInvalidRequiredFieldV3() { + verifyV3("invalid"); + } + + + @Override + public void verifyRule() { + assertRuleProperties("OAR115 - VerifyRequiredFields - the data in the required field must exist in schema parameters", RuleType.BUG, Severity.MINOR, tags("format")); + } +} diff --git a/src/test/resources/checks/v2/format/OAR115/invalid.json b/src/test/resources/checks/v2/format/OAR115/invalid.json new file mode 100644 index 00000000..548a33a0 --- /dev/null +++ b/src/test/resources/checks/v2/format/OAR115/invalid.json @@ -0,0 +1,40 @@ +{ + "swagger": "2.0", + "info": { + "title": "API de ejemplo", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "Obtener lista de usuarios", + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Error de validación", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "ErrorResponse": { + "type": "object", + "properties": { + "code": { "type": "integer" }, + "message": { "type": "string" } + }, + "required": + [ + "code", + "message", + "otherfield" # Noncompliant {{OAR115: This value does not exist, it must be defined in the schema properties}} + ] + } + } +} diff --git a/src/test/resources/checks/v2/format/OAR115/invalid.yaml b/src/test/resources/checks/v2/format/OAR115/invalid.yaml new file mode 100644 index 00000000..e4b20802 --- /dev/null +++ b/src/test/resources/checks/v2/format/OAR115/invalid.yaml @@ -0,0 +1,27 @@ +swagger: "2.0" +info: + title: API de ejemplo + version: "1.0.0" +paths: + /users: + get: + summary: Obtener lista de usuarios + responses: + 200: + description: OK + 400: + description: Error de validación + schema: + $ref: "#/definitions/ErrorResponse" +definitions: + ErrorResponse: + type: object + properties: + code: + type: integer + message: + type: string + required: + - code + - message + - otherfield # Noncompliant {{OAR115: This value does not exist, it must be defined in the schema properties}} diff --git a/src/test/resources/checks/v2/format/OAR115/valid.json b/src/test/resources/checks/v2/format/OAR115/valid.json new file mode 100644 index 00000000..5e96f6d6 --- /dev/null +++ b/src/test/resources/checks/v2/format/OAR115/valid.json @@ -0,0 +1,35 @@ +{ + "swagger": "2.0", + "info": { + "title": "API de ejemplo", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "Obtener lista de usuarios", + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Error de validación", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "ErrorResponse": { + "type": "object", + "properties": { + "code": { "type": "integer" }, + "message": { "type": "string" } + }, + "required": ["code", "message"] + } + } +} diff --git a/src/test/resources/checks/v2/format/OAR115/valid.yaml b/src/test/resources/checks/v2/format/OAR115/valid.yaml new file mode 100644 index 00000000..3657106f --- /dev/null +++ b/src/test/resources/checks/v2/format/OAR115/valid.yaml @@ -0,0 +1,26 @@ +swagger: "2.0" +info: + title: API de ejemplo + version: "1.0.0" +paths: + /users: + get: + summary: Obtener lista de usuarios + responses: + 200: + description: OK + 400: + description: Error de validación + schema: + $ref: "#/definitions/ErrorResponse" +definitions: + ErrorResponse: + type: object + properties: + code: + type: integer + message: + type: string + required: + - code + - message diff --git a/src/test/resources/checks/v3/format/OAR115/invalid.json b/src/test/resources/checks/v3/format/OAR115/invalid.json new file mode 100644 index 00000000..5b686976 --- /dev/null +++ b/src/test/resources/checks/v3/format/OAR115/invalid.json @@ -0,0 +1,45 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "API de ejemplo", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "Obtener lista de usuarios", + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Error de validación", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ErrorResponse": { + "type": "object", + "properties": { + "code": { "type": "integer" }, + "message": { "type": "string" } + }, + "required":[ + "code", + "message", + "otherfield" # Noncompliant {{OAR115: This value does not exist, it must be defined in the schema properties}} + ] + } + } + } +} diff --git a/src/test/resources/checks/v3/format/OAR115/invalid.yaml b/src/test/resources/checks/v3/format/OAR115/invalid.yaml new file mode 100644 index 00000000..5f3876b2 --- /dev/null +++ b/src/test/resources/checks/v3/format/OAR115/invalid.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.0 +info: + title: API de ejemplo + version: "1.0.0" +paths: + /users: + get: + summary: Obtener lista de usuarios + responses: + '200': + description: OK + '400': + description: Error de validación + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" +components: + schemas: + ErrorResponse: + type: object + properties: + code: + type: integer + message: + type: string + required: + - code + - message + - otherfield # Noncompliant {{OAR115: This value does not exist, it must be defined in the schema properties}} diff --git a/src/test/resources/checks/v3/format/OAR115/valid.json b/src/test/resources/checks/v3/format/OAR115/valid.json new file mode 100644 index 00000000..1574947e --- /dev/null +++ b/src/test/resources/checks/v3/format/OAR115/valid.json @@ -0,0 +1,44 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "API de ejemplo", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "summary": "Obtener lista de usuarios", + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Error de validación", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ErrorResponse": { + "type": "object", + "properties": { + "code": { "type": "integer" }, + "message": { "type": "string" } + }, + "required":[ + "code", + "message" + ] + } + } + } +} diff --git a/src/test/resources/checks/v3/format/OAR115/valid.yaml b/src/test/resources/checks/v3/format/OAR115/valid.yaml new file mode 100644 index 00000000..c97cfb50 --- /dev/null +++ b/src/test/resources/checks/v3/format/OAR115/valid.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.0 +info: + title: API de ejemplo + version: "1.0.0" +paths: + /users: + get: + summary: Obtener lista de usuarios + responses: + '200': + description: OK + '400': + description: Error de validación + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" +components: + schemas: + ErrorResponse: + type: object + properties: + code: + type: integer + message: + type: string + required: + - code + - message