Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.6.0-beta-1] - 2026-08-25

### Added

- OAR085 - Accept `3.0.4`, `3.1.1`, `3.1.2` in the default valid-versions.

### Changed

- 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`.
- 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] - 2026-08-25

### Changed
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.apiaddicts.apitools.dosonarapi</groupId>
<artifactId>sonaropenapi-rules-community</artifactId>
<version>1.5.1</version>
<version>1.6.0-beta-1</version>
<packaging>sonar-plugin</packaging>

<name>SonarQube OpenAPI Community Rules</name>
Expand Down Expand Up @@ -64,7 +64,7 @@

<sonar.version>8.7.0.41497</sonar.version>
<sonarQubeMinVersion>6.7</sonarQubeMinVersion>
<sonaropenapi.version>1.2.1</sonaropenapi.version>
<sonaropenapi.version>1.3.0-beta-1</sonaropenapi.version>
<sonaranalyzer.version>1.22.0.848</sonaranalyzer.version>
<orgjson.version>20231013</orgjson.version>
<junit.version>4.13.2</junit.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ private void validateDataProperty(String name, JSONObject schema, Map<String, Js
if ("any".equals(type)) type = TYPE_ANY;

validateProperty(properties, name, type, parent.key()).ifPresent(node -> {
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()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,7 +76,9 @@ private Map<String, String> extractSchemaTypes(JsonNode schemaNode) {
for (Map.Entry<String, JsonNode> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/apiaddicts/sonar/openapi/utils/JsonNodeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
openapi: 3.0.4
info:
title: Sample API
description: This is a sample API.
version: 1.0.0
paths: {}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
openapi: "3.1.1"
info:
title: Sample API
description: This is a sample API.
version: 1.0.0
paths: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
openapi: "3.1.2"
info:
title: Sample API
description: This is a sample API.
version: 1.0.0
paths: {}