From e5d2d218cb2d40454589bc68e5301ace16f13269 Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Tue, 18 Aug 2026 13:54:18 -0500 Subject: [PATCH 1/2] feat: support OpenAPI 3.0.4/3.1.1/3.1.2 and handle 3.1 array-form type --- CHANGELOG.md | 20 ++++++++++++++++ pom.xml | 4 ++-- .../checks/format/AbstractFormatCheck.java | 3 ++- .../format/OAR115VerifyRequiredFields.java | 3 ++- .../owasp/OAR070BrokenAccessControlCheck.java | 16 ++++++------- .../OAR029StandardResponseSchemaCheck.java | 2 +- .../schemas/OAR108SchemaValidatorCheck.java | 5 +++- .../OAR074NumericParameterIntegrityCheck.java | 6 ++--- .../OAR075StringParameterIntegrityCheck.java | 4 +++- .../OAR082BinaryOrByteFormatCheck.java | 10 +++++--- .../security/OAR085OpenAPIVersionCheck.java | 2 +- .../sonar/openapi/utils/JsonNodeUtils.java | 20 +++++++++++++++- .../OAR082BinaryOrByteFormatCheckTest.java | 5 ++++ .../OAR085OpenAPIVersionCheckTest.java | 17 +++++++++++++- .../OAR085/valid-openapi-version-304.yaml | 6 +++++ .../security/OAR082/array-type-format.yaml | 23 +++++++++++++++++++ .../OAR085/valid-openapi-version-311.yaml | 6 +++++ .../OAR085/valid-openapi-version-312.yaml | 6 +++++ 18 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 src/test/resources/checks/v3/security/OAR085/valid-openapi-version-304.yaml create mode 100644 src/test/resources/checks/v31/security/OAR082/array-type-format.yaml create mode 100644 src/test/resources/checks/v31/security/OAR085/valid-openapi-version-311.yaml create mode 100644 src/test/resources/checks/v31/security/OAR085/valid-openapi-version-312.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ef5475..608bb082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.1-beta-3] + +### Added + +- OAR085 - Accept `3.0.4`, `3.1.1`, `3.1.2` in the default valid-versions. + +### Changed + +- Bump `sonar-openapi` core to `1.2.2-beta-1` (parses the new versions). +- `JsonNodeUtils` - `isType`/`getPrimaryType` accept array-form `type` (OpenAPI 3.1). +- OAR082 - Accept array-form `type`; accept `contentEncoding`/`contentMediaType` as byte/binary. +- OAR029 - Accept array-form `type`. +- OAR070 - Accept array-form `type`. +- OAR074 - Accept array-form `type`. +- OAR075 - Accept array-form `type`. +- OAR108 - Accept array-form `type`. +- OAR115 - Accept array-form `type`. +- OAR016 / OAR037 / OAR052 / OAR076 - Accept array-form `type` via `AbstractFormatCheck`. + + ## [1.5.1-beta-2] ### Fixed diff --git a/pom.xml b/pom.xml index 41d2b466..ab58a265 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.apiaddicts.apitools.dosonarapi sonaropenapi-rules-community - 1.5.1-beta-2 + 1.5.1-beta-3 sonar-plugin SonarQube OpenAPI Community Rules @@ -64,7 +64,7 @@ 8.7.0.41497 6.7 - 1.2.1 + 1.2.2-beta-1 1.22.0.848 20231013 4.13.2 diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/format/AbstractFormatCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/format/AbstractFormatCheck.java index adb15cde..5f54f027 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/format/AbstractFormatCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/format/AbstractFormatCheck.java @@ -7,6 +7,7 @@ import org.apiaddicts.apitools.dosonarapi.api.v31.OpenApi31Grammar; import org.apiaddicts.apitools.dosonarapi.api.v32.OpenApi32Grammar; import apiaddicts.sonar.openapi.checks.BaseCheck; +import apiaddicts.sonar.openapi.utils.JsonNodeUtils; import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; import java.util.Set; @@ -25,7 +26,7 @@ public void visitNode(JsonNode node) { private void visitV2Node(JsonNode node) { JsonNode typeNode = node.get("type"); - String type = typeNode.getTokenValue(); + String type = JsonNodeUtils.getPrimaryType(typeNode); JsonNode formatNode = node.get("format"); if (formatNode.isMissing()) { validate(type, null, typeNode, node); diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java b/src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java index 235065ec..01ac76e2 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/format/OAR115VerifyRequiredFields.java @@ -2,6 +2,7 @@ import apiaddicts.sonar.openapi.checks.BaseCheck; import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.isExternalRef; +import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.isObjectType; import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.resolve; import com.google.common.collect.ImmutableSet; import com.sonar.sslr.api.AstNodeType; @@ -58,7 +59,7 @@ public void resolveExteralRef(JsonNode node) { public void verifyTypeObject(JsonNode node){ JsonNode typeNode = node.get("type"); - if (typeNode != null && "object".equals(typeNode.getTokenValue())) { + if (typeNode != null && isObjectType(typeNode)) { validateRequiredFields(node); } } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/owasp/OAR070BrokenAccessControlCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/owasp/OAR070BrokenAccessControlCheck.java index 3ed3cd59..3caf3280 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/owasp/OAR070BrokenAccessControlCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/owasp/OAR070BrokenAccessControlCheck.java @@ -4,6 +4,8 @@ import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; import org.sonar.check.Rule; +import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.isType; + @Rule(key = OAR070BrokenAccessControlCheck.KEY) public class OAR070BrokenAccessControlCheck extends AbstractParameterCheck { @@ -24,20 +26,18 @@ protected void visitParameterNode(JsonNode node) { JsonNode schemaNode = node.get("schema"); boolean isNumericType = - typeNode != null && - ("integer".equals(typeNode.getTokenValue()) || - "number".equals(typeNode.getTokenValue()) || - "float".equals(typeNode.getTokenValue())); + isType(typeNode, "integer") || + isType(typeNode, "number") || + isType(typeNode, "float"); if (!isNumericType && schemaNode != null) { JsonNode schemaTypeNode = schemaNode.get("type"); isNumericType = - schemaTypeNode != null && - ("integer".equals(schemaTypeNode.getTokenValue()) || - "number".equals(schemaTypeNode.getTokenValue()) || - "float".equals(schemaTypeNode.getTokenValue())); + isType(schemaTypeNode, "integer") || + isType(schemaTypeNode, "number") || + isType(schemaTypeNode, "float"); typeNode = schemaTypeNode; } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR029StandardResponseSchemaCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR029StandardResponseSchemaCheck.java index e61aee17..93f7e452 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR029StandardResponseSchemaCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR029StandardResponseSchemaCheck.java @@ -173,7 +173,7 @@ private void validateDataProperty(String name, JSONObject schema, Map { - boolean isArray = "array".equals(parent.get("type").getTokenValue()); + boolean isArray = isArrayType(parent.get("type")); if (getAllProperties(node).isEmpty() && !isArray) { addIssue(KEY, translate("OAR029.error-required-one-property", name), handleExternalRef.getTrueNode(node.key())); } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR108SchemaValidatorCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR108SchemaValidatorCheck.java index 8153a58c..51871ce2 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR108SchemaValidatorCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/schemas/OAR108SchemaValidatorCheck.java @@ -4,6 +4,7 @@ import com.sonar.sslr.api.AstNodeType; import org.sonar.check.Rule; import apiaddicts.sonar.openapi.checks.BaseCheck; +import apiaddicts.sonar.openapi.utils.JsonNodeUtils; import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; import org.apiaddicts.apitools.dosonarapi.api.v2.OpenApi2Grammar; import org.apiaddicts.apitools.dosonarapi.api.v3.OpenApi3Grammar; @@ -75,7 +76,9 @@ private Map extractSchemaTypes(JsonNode schemaNode) { for (Map.Entry entry : propertiesNode.propertyMap().entrySet()) { String propertyName = entry.getKey(); JsonNode propertyTypeNode = entry.getValue().get("type"); - String propertyType = propertyTypeNode != null ? propertyTypeNode.stringValue() : null; + String propertyType = (propertyTypeNode != null && propertyTypeNode.isArray()) + ? JsonNodeUtils.getPrimaryType(propertyTypeNode) + : (propertyTypeNode != null ? propertyTypeNode.stringValue() : null); schemaTypes.put(propertyName, propertyType); } } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR074NumericParameterIntegrityCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR074NumericParameterIntegrityCheck.java index 0eb84e65..ca1d63b3 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR074NumericParameterIntegrityCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR074NumericParameterIntegrityCheck.java @@ -3,6 +3,8 @@ import org.sonar.check.Rule; import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; +import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.isType; + @Rule(key = OAR074NumericParameterIntegrityCheck.KEY) public class OAR074NumericParameterIntegrityCheck extends AbstractTypedParameterIntegrityCheck { @@ -15,9 +17,7 @@ public OAR074NumericParameterIntegrityCheck() { @Override protected boolean isTargetType(JsonNode typeNode) { - if(typeNode == null || typeNode.isMissing()) return false; - String t = typeNode.getTokenValue(); - return "integer".equals(t) || "number".equals(t) || "float".equals(t); + return isType(typeNode, "integer") || isType(typeNode, "number") || isType(typeNode, "float"); } @Override diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR075StringParameterIntegrityCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR075StringParameterIntegrityCheck.java index 5e2a3b8c..4bcd52a7 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR075StringParameterIntegrityCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR075StringParameterIntegrityCheck.java @@ -7,6 +7,8 @@ import java.util.Set; import java.util.stream.Collectors; +import static apiaddicts.sonar.openapi.utils.JsonNodeUtils.isStringType; + @Rule(key = OAR075StringParameterIntegrityCheck.KEY) public class OAR075StringParameterIntegrityCheck extends AbstractTypedParameterIntegrityCheck { @@ -27,7 +29,7 @@ public OAR075StringParameterIntegrityCheck() { @Override protected boolean isTargetType(JsonNode typeNode) { - return typeNode != null && !typeNode.isMissing() && "string".equals(typeNode.getTokenValue()); + return isStringType(typeNode); } @Override diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheck.java index 2f619e19..4411d026 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheck.java @@ -13,6 +13,8 @@ import org.apiaddicts.apitools.dosonarapi.api.v32.OpenApi32Grammar; import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; +import apiaddicts.sonar.openapi.utils.JsonNodeUtils; + import java.util.Arrays; import java.util.List; import java.util.Set; @@ -57,11 +59,13 @@ private void visitV2Node(JsonNode node) { if (fieldNode == null || fieldNode.isMissing()) continue; JsonNode typeNode = fieldNode.get("type"); - String type = typeNode.isMissing() ? null : typeNode.getTokenValue(); - if ("string".equals(type)) { + if (JsonNodeUtils.isStringType(typeNode)) { String format = fieldNode.get("format").getTokenValue(); - if (!"binary".equals(format) && !"byte".equals(format)) { + boolean hasBinaryFormat = "binary".equals(format) || "byte".equals(format); + boolean hasContentEncoding = !fieldNode.get("contentEncoding").isMissing() + || !fieldNode.get("contentMediaType").isMissing(); + if (!hasBinaryFormat && !hasContentEncoding) { addIssue(KEY, translate(MESSAGE, fieldsApply), typeNode.key()); } } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheck.java b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheck.java index 4ba0c578..bd0614f7 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheck.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheck.java @@ -20,7 +20,7 @@ public class OAR085OpenAPIVersionCheck extends BaseCheck { public static final String KEY = "OAR085"; private static final String MESSAGE = "OAR085.error"; - private static final String DEFAULT_VALID_VERSIONS = "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.2.0"; + private static final String DEFAULT_VALID_VERSIONS = "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.0.4,3.1.0,3.1.1,3.1.2,3.2.0"; @RuleProperty( key = "valid-versions", diff --git a/src/main/java/apiaddicts/sonar/openapi/utils/JsonNodeUtils.java b/src/main/java/apiaddicts/sonar/openapi/utils/JsonNodeUtils.java index 1763698f..cd070798 100644 --- a/src/main/java/apiaddicts/sonar/openapi/utils/JsonNodeUtils.java +++ b/src/main/java/apiaddicts/sonar/openapi/utils/JsonNodeUtils.java @@ -184,8 +184,26 @@ public static boolean isBooleanType(JsonNode schemaNode) { return isType(schemaNode, TYPE_BOOLEAN); } + public static String getPrimaryType(JsonNode typeNode) { + if (typeNode == null || typeNode.isMissing()) return null; + if (typeNode.isArray()) { + for (JsonNode element : typeNode.elements()) { + String value = element.getTokenValue(); + if (value != null && !"null".equals(value)) return value; + } + return null; + } + return typeNode.getTokenValue(); + } + public static boolean isType(JsonNode type, String name) { - return TYPE_ANY.equals(name) || name.equals(type.getTokenValue()); + if (TYPE_ANY.equals(name)) return true; + if (type == null || type.isMissing()) return false; + if (name.equals(type.getTokenValue())) return true; + for (JsonNode element : type.elements()) { + if (name.equals(element.getTokenValue())) return true; + } + return false; } public static boolean isOperation(JsonNode node) { diff --git a/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheckTest.java b/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheckTest.java index 8efe8794..72a09a2e 100644 --- a/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheckTest.java +++ b/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR082BinaryOrByteFormatCheckTest.java @@ -35,6 +35,11 @@ public void verifyvalidV32() { verifyV32("valid-format"); } + @Test + public void verifyArrayFormTypeV31() { + verifyV31("array-type-format.yaml"); + } + @Override public void verifyRule() { assertRuleProperties("OAR082 - BinaryOrByte - The string properties of the specified parameters must define a byte or binary format.", RuleType.VULNERABILITY, Severity.MAJOR, tags("safety")); diff --git a/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheckTest.java b/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheckTest.java index 3d09c539..1968df46 100644 --- a/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheckTest.java +++ b/src/test/java/apiaddicts/sonar/openapi/checks/security/OAR085OpenAPIVersionCheckTest.java @@ -50,6 +50,21 @@ public void verifyInV32() { verifyV32("valid-openapi-version"); } + @Test + public void verifyValidOpenApiVersion304InV3() { + verifyV3("valid-openapi-version-304.yaml"); + } + + @Test + public void verifyValidOpenApiVersion311InV31() { + verifyV31("valid-openapi-version-311.yaml"); + } + + @Test + public void verifyValidOpenApiVersion312InV31() { + verifyV31("valid-openapi-version-312.yaml"); + } + @Test public void verifyInvalidOpenApiVersionInV3() { verifyV3("invalid-openapi-version"); @@ -137,6 +152,6 @@ public void verifyRule() { @Override public void verifyParameters() { assertNumberOfParameters(1); - assertParameterProperties("valid-versions", "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.2.0", RuleParamType.STRING); + assertParameterProperties("valid-versions", "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.0.4,3.1.0,3.1.1,3.1.2,3.2.0", RuleParamType.STRING); } } \ No newline at end of file diff --git a/src/test/resources/checks/v3/security/OAR085/valid-openapi-version-304.yaml b/src/test/resources/checks/v3/security/OAR085/valid-openapi-version-304.yaml new file mode 100644 index 00000000..4fec745f --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR085/valid-openapi-version-304.yaml @@ -0,0 +1,6 @@ +openapi: 3.0.4 +info: + title: Sample API + description: This is a sample API. + version: 1.0.0 +paths: {} diff --git a/src/test/resources/checks/v31/security/OAR082/array-type-format.yaml b/src/test/resources/checks/v31/security/OAR082/array-type-format.yaml new file mode 100644 index 00000000..10f83e4a --- /dev/null +++ b/src/test/resources/checks/v31/security/OAR082/array-type-format.yaml @@ -0,0 +1,23 @@ +openapi: "3.1.1" +info: + version: "1.0.0" + title: "Swagger Petstore" +paths: + /invoices: + get: + responses: + '200': + description: A invoice. + content: + application/json: + schema: + type: object + properties: + product: + type: ["string", "null"] # Noncompliant {{OAR082: The string properties among product,line,price must define a byte or binary format}} + line: + type: ["string", "null"] + contentEncoding: base64 + price: + type: string + format: binary diff --git a/src/test/resources/checks/v31/security/OAR085/valid-openapi-version-311.yaml b/src/test/resources/checks/v31/security/OAR085/valid-openapi-version-311.yaml new file mode 100644 index 00000000..99a531e9 --- /dev/null +++ b/src/test/resources/checks/v31/security/OAR085/valid-openapi-version-311.yaml @@ -0,0 +1,6 @@ +openapi: "3.1.1" +info: + title: Sample API + description: This is a sample API. + version: 1.0.0 +paths: {} diff --git a/src/test/resources/checks/v31/security/OAR085/valid-openapi-version-312.yaml b/src/test/resources/checks/v31/security/OAR085/valid-openapi-version-312.yaml new file mode 100644 index 00000000..038eebbf --- /dev/null +++ b/src/test/resources/checks/v31/security/OAR085/valid-openapi-version-312.yaml @@ -0,0 +1,6 @@ +openapi: "3.1.2" +info: + title: Sample API + description: This is a sample API. + version: 1.0.0 +paths: {} From 27655738d27afe08efa7a19e9118651019afc87a Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Tue, 25 Aug 2026 13:20:16 -0500 Subject: [PATCH 2/2] update version and changelog --- CHANGELOG.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36ef6797..779db725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `sonar-openapi` core to `1.2.2-beta-1` (parses the new versions). +- Bump `sonar-openapi` core to `1.3.0-beta-1` (parses the new versions). - `JsonNodeUtils` - `isType`/`getPrimaryType` accept array-form `type` (OpenAPI 3.1). - OAR082 - Accept array-form `type`; accept `contentEncoding`/`contentMediaType` as byte/binary. - OAR029 - Accept array-form `type`. diff --git a/pom.xml b/pom.xml index d2929d5f..22b75648 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ 8.7.0.41497 6.7 - 1.2.2-beta-1 + 1.3.0-beta-1 1.22.0.848 20231013 4.13.2