Skip to content
Merged

1.5.1 #134

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e871a35
feat: align param names and make rule messages reflect configured par…
mels-h Jul 27, 2026
10f2429
fix: oar044 media type validation
mels-h Jul 27, 2026
b573366
fix: oar037 false positive on string enum without format
mels-h Jul 30, 2026
3fe91d9
fix: OAR085 message shows the OpenAPI version
mels-h Aug 3, 2026
a623bd3
Merge branch 'develop' into feature/2418/rule-param-alignment-and-dyn…
mels-h Aug 4, 2026
4f0c9b2
Merge pull request #122 from apiaddicts/feature/2418/rule-param-align…
rafael-goterris Aug 4, 2026
6674464
Merge branch 'develop' into fix/2322/oar044-parameters
mels-h Aug 4, 2026
558e427
Merge pull request #123 from apiaddicts/fix/2322/oar044-parameters
rafael-goterris Aug 4, 2026
590c4d1
Merge branch 'develop' into fix/2535/oar037-enums
mels-h Aug 4, 2026
21de9af
Merge pull request #125 from apiaddicts/fix/2535/oar037-enums
rafael-goterris Aug 4, 2026
4df23b4
Merge branch 'develop' into feature/2564/sonar-spectral
mels-h Aug 4, 2026
3b99a3d
update changelog
mels-h Aug 4, 2026
4f60b8d
Merge pull request #126 from apiaddicts/feature/2564/sonar-spectral
rafael-goterris Aug 4, 2026
717a748
fix: correct security opt-out handling and document rule behaviors
mels-h Aug 10, 2026
f5e62cc
Merge pull request #127 from apiaddicts/fix/2564/sonar-spectral-rules…
SebastianDT1 Aug 12, 2026
e63d129
feat: gate OAR022/OAR025 to paginated
mels-h Aug 15, 2026
fed7cf8
Merge pull request #128 from apiaddicts/feature/2322/align-collection…
SebastianDT1 Aug 21, 2026
4a9cebb
fix: oar025 integer type validation
mels-h Aug 24, 2026
d90cc87
fix: resolve schema-level $ref in OAR025 $limit type check
mels-h Aug 24, 2026
2f3f6fc
Merge pull request #133 from apiaddicts/fix/2322/oar025-type-integer
SebastianDT1 Aug 25, 2026
e1c127a
Release 1.5.1
SebastianDT1 Aug 25, 2026
e44876f
fix: Update changelog version
SebastianDT1 Aug 25, 2026
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
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file.
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] - 2026-08-25

### Changed

- OAR025 - The shared `apq-collection-query-param-required` function now also validates the parameter type for OAR025, keyed by rule code; when `$limit` is present but its type is not `integer`, a distinct type message is emitted.
- OAR022 - OrderbyParameterCheck - Now only applies to paginated collections (operations declaring a 206 response); added `no-pagination` fixtures/tests and corrected the HTML docs (real defaults + 206 condition).
- OAR025 - LimitParameterCheck - Same 206 gating for `$limit`; added `no-pagination` fixtures/tests and corrected the HTML docs.
- OAR020 - ExpandParameterCheck - Corrected the HTML docs to the real defaults (`/me,/health,/ping,/status`, `/exclude`).
- OAR021 - ExcludeParameterCheck - Corrected the HTML docs to the real defaults (`/me,/health,/ping,/status`, `/exclude`).

### Fixed

- OAR035 - AuthorizationResponses - Honor an operation-level `security: []` explicit opt-out: the operation is unsecured, so no 401 is required even under global security. Added `security-opt-out` (v3) fixtures and test.
- OAR096 - ForbiddenResponses - Same `security: []` opt-out fix in the shared `AbstractSecurityResponseCheck`: no 403 required for opted-out operations. Added `security-opt-out` (v3) fixtures and test.
- OAR014 / OAR015 - ResourceLevel - Issue message now interpolates the configured level values (min-level/max-level for OAR014, max-level-allowed for OAR015).
- OAR004 / OAR040 - Wso2Scopes - Issue message now includes the configured `pattern` (passed through `AbstractPatternWso2ScopesCheck`).
- OAR038 - StandardCreateResponse - Issue message now interpolates the configured `data-property` instead of the hardcoded `data`.
- OAR082 - BinaryOrByteFormat - Issue message now shows the configured `fields-to-apply`.
- OAR085 - OpenAPIVersion - Issue message now shows the configured `valid-versions`.
- OAR037 - StringFormat - Fixed false positive on string schemas constrained by `enum`. The check only inspected `format`/`pattern`, so a string with a non-empty `enum` and no `format` was wrongly reported even though the `enum` already constrains the allowed values. When no `format` is declared, a non-empty `enum` now satisfies the rule (like a valid `pattern`); a present-but-invalid `format` still fires even when an `enum` is declared.
- OAR044 - MediaTypeCheck - Media type parameters now follow RFC 9110 (charset without space, other parameter names, multiple parameters); type/subtype can no longer start with `.`.


## [1.5.1-beta-4] - 2026-08-24

### Changed

- OAR025 - The shared `apq-collection-query-param-required` function now also validates the parameter type for OAR025, keyed by rule code; when `$limit` is present but its type is not `integer`, a distinct type message is emitted.


## [1.5.1-beta-3] - 2026-08-14

### Changed

- OAR022 - OrderbyParameterCheck - Now only applies to paginated collections (operations declaring a 206 response); added `no-pagination` fixtures/tests and corrected the HTML docs (real defaults + 206 condition).
- OAR025 - LimitParameterCheck - Same 206 gating for `$limit`; added `no-pagination` fixtures/tests and corrected the HTML docs.
- OAR020 - ExpandParameterCheck - Corrected the HTML docs to the real defaults (`/me,/health,/ping,/status`, `/exclude`).
- OAR021 - ExcludeParameterCheck - Corrected the HTML docs to the real defaults (`/me,/health,/ping,/status`, `/exclude`).


## [1.5.1-beta-2] - 2026-08-12

### Fixed

- OAR035 - AuthorizationResponses - Honor an operation-level `security: []` explicit opt-out: the operation is unsecured, so no 401 is required even under global security. Added `security-opt-out` (v3) fixtures and test.
- OAR096 - ForbiddenResponses - Same `security: []` opt-out fix in the shared `AbstractSecurityResponseCheck`: no 403 required for opted-out operations. Added `security-opt-out` (v3) fixtures and test.


## [1.5.1-beta-1] - 2026-08-04

### Fixed

- OAR014 / OAR015 - ResourceLevel - Issue message now interpolates the configured level values (min-level/max-level for OAR014, max-level-allowed for OAR015).
- OAR004 / OAR040 - Wso2Scopes - Issue message now includes the configured `pattern` (passed through `AbstractPatternWso2ScopesCheck`).
- OAR038 - StandardCreateResponse - Issue message now interpolates the configured `data-property` instead of the hardcoded `data`.
- OAR082 - BinaryOrByteFormat - Issue message now shows the configured `fields-to-apply`.
- OAR085 - OpenAPIVersion - Issue message now shows the configured `valid-versions`.
- OAR037 - StringFormat - Fixed false positive on string schemas constrained by `enum`. The check only inspected `format`/`pattern`, so a string with a non-empty `enum` and no `format` was wrongly reported even though the `enum` already constrains the allowed values. When no `format` is declared, a non-empty `enum` now satisfies the rule (like a valid `pattern`); a present-but-invalid `format` still fires even when an `enum` is declared.
- OAR044 - MediaTypeCheck - Media type parameters now follow RFC 9110 (charset without space, other parameter names, multiple parameters); type/subtype can no longer start with `.`.


## [1.5.0] - 2026-07-28

### Added
Expand Down
2 changes: 1 addition & 1 deletion 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.0</version>
<version>1.5.1</version>
<packaging>sonar-plugin</packaging>

<name>SonarQube OpenAPI Community Rules</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,19 @@ protected void visitScope(JsonNode scope) {
if (fieldNode == null || fieldNode.isNull() || fieldNode.isMissing())
return;

String patternStr = getPatternStr() != null ? getPatternStr() : defaultPatternValue;
List<JsonNode> elements = fieldNode.elements();
if (!elements.isEmpty()) {
for (JsonNode element : elements) {
String roleText = element.getTokenValue();
if (roleText != null && !pattern.matcher(roleText).matches()) {
addIssue(ruleKey, translate(messageKey), element);
addIssue(ruleKey, translate(messageKey, patternStr), element);
}
}
} else {
String fieldText = fieldNode.getTokenValue();
if (fieldText != null && !pattern.matcher(fieldText).matches()) {
addIssue(ruleKey, translate(messageKey), fieldNode);
addIssue(ruleKey, translate(messageKey, patternStr), fieldNode);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,25 @@ public void validate(String type, String format, JsonNode typeNode, JsonNode nod
.map(f -> f.trim().toLowerCase())
.collect(Collectors.toSet());
if (!validFormats.contains(format.toLowerCase())) {
addIssue(KEY, translate(MESSAGE), typeNode.key());
addIssue(KEY, translate(MESSAGE, formatsAllowed), typeNode.key());
}
return;
}

if (hasEnum(node)) {
return;
}

if (!hasValidPattern(node)) {
addIssue(KEY, translate(MESSAGE), typeNode.key());
addIssue(KEY, translate(MESSAGE, formatsAllowed), typeNode.key());
}
}

private boolean hasEnum(JsonNode node) {
JsonNode enumNode = node.get("enum");
return !enumNode.isMissing() && !enumNode.elements().isEmpty();
}

private boolean hasValidPattern(JsonNode node) {
JsonNode patternNode = node.get("pattern");
if (patternNode.isMissing()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,20 @@
protected static final String MESSAGE_V3 = "OAR044.error.v3";
private final ExternalRefHandler handleExternalRef = new ExternalRefHandler();

private static final String RESTRICTED_NAME = "[a-zA-Z0-9][a-zA-Z0-9.!#$&^_+\\-]*";
private static final String OWS = "[ \\t]*";
private static final String TOKEN = "[a-zA-Z0-9!#$%&'*+\\-.^_`|~]+";

Check failure on line 49 in src/main/java/apiaddicts/sonar/openapi/checks/format/OAR044MediaTypeCheck.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TOKEN' detected in this expression, review this potentially hard-coded secret.

See more on https://sonarcloud.io/project/issues?id=apiaddicts_sonaropenapi-rules&issues=AaA52flbaEygsU7z1SnC&open=AaA52flbaEygsU7z1SnC&pullRequest=134
private static final String QUOTED_STRING = "\"(?:[^\"\\\\]|\\\\.)*\"";
private static final String PARAMETERS =
"(?:" + OWS + ";" + OWS + TOKEN + "=(?:" + TOKEN + "|" + QUOTED_STRING + "))*";

Check warning on line 52 in src/main/java/apiaddicts/sonar/openapi/checks/format/OAR044MediaTypeCheck.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this repetition that can lead to a stack overflow for large inputs.

See more on https://sonarcloud.io/project/issues?id=apiaddicts_sonaropenapi-rules&issues=AaA52flbaEygsU7z1SnD&open=AaA52flbaEygsU7z1SnD&pullRequest=134

Check warning on line 52 in src/main/java/apiaddicts/sonar/openapi/checks/format/OAR044MediaTypeCheck.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this repetition that can lead to a stack overflow for large inputs.

See more on https://sonarcloud.io/project/issues?id=apiaddicts_sonaropenapi-rules&issues=AaA52flbaEygsU7z1SnE&open=AaA52flbaEygsU7z1SnE&pullRequest=134

@VisibleForTesting
static final Pattern MIME_TYPE_PATTERN = Pattern.compile(
"[a-zA-Z0-9.][a-zA-Z0-9.!#$&_^+\\-]+/" +
"[a-zA-Z0-9.][a-zA-Z0-9.!#$&_^+\\-]+" +
"(; charset=[a-zA-Z0-9_\\-]+)?"
RESTRICTED_NAME + "/" + RESTRICTED_NAME + PARAMETERS
);
@VisibleForTesting
static final Pattern MEDIA_RANGE_PATTERN = Pattern.compile(
"(\\*|[a-zA-Z0-9.][a-zA-Z0-9.!#$&_^+\\-]+)/" +
"(\\*|" +
"[a-zA-Z0-9.][a-zA-Z0-9.!#$&_^+\\-]+" +
"(; charset=[a-zA-Z0-9_\\-]+)?" +
")"
"(\\*|" + RESTRICTED_NAME + ")/(\\*|" + RESTRICTED_NAME + ")" + PARAMETERS
);

@Override
Expand Down Expand Up @@ -114,8 +115,8 @@
for (JsonNode property : properties.values()) {
JsonNode keyNode = property.key();
String key = keyNode.getTokenValue();
if (!MIME_TYPE_PATTERN.matcher(key).matches()) {
addIssue(CHECK_KEY, translate(MESSAGE_V2), keyNode);
if (!MEDIA_RANGE_PATTERN.matcher(key).matches()) {
addIssue(CHECK_KEY, translate(MESSAGE_V3), keyNode);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@

public abstract class AbstractResourceLevelCheck extends BaseCheck {

private static final String MESSAGE = "generic.path-level";

private String key;

protected AbstractResourceLevelCheck(String key) {
Expand All @@ -31,9 +29,13 @@ public Set<AstNodeType> subscribedKinds() {
@Override
public void visitNode(JsonNode node) {
String path = node.key().getTokenValue();
if (matchLevel(path)) addIssue(key, translate(MESSAGE), node.key());
if (matchLevel(path)) addIssue(key, translate(messageKey(), messageArgs()), node.key());
}

protected abstract String messageKey();

protected abstract Object[] messageArgs();

private boolean matchLevel(String path) {
long literalCount = Stream.of(path.split("/"))
.filter(s -> !s.trim().isEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ public OAR014ResourceLevelWithinNonSuggestedRangeCheck() {
boolean matchLevel(long level) {
return minLevel <= level && level <= maxLevel;
}

@Override
protected String messageKey() {
return "OAR014.error";
}

@Override
protected Object[] messageArgs() {
return new Object[] { minLevel, maxLevel };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,14 @@ public OAR015ResourceLevelMaxAllowedCheck() {
boolean matchLevel(long level) {
return maxLevelAllowed < level;
}

@Override
protected String messageKey() {
return "OAR015.error";
}

@Override
protected Object[] messageArgs() {
return new Object[] { maxLevelAllowed };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ protected void visitV2ExplicitNode(JsonNode node) {
addIssue(KEY, translate("OAR038.error-required-one-property"), entry.getValue().key());
}
} else {
addIssue(KEY, translate("OAR038.error"), entry.getValue().key());
addIssue(KEY, translate("OAR038.error", dataNode), entry.getValue().key());
}
}

if (properties.isEmpty()) {
addIssue(KEY, translate("OAR038.error"), schemaNode.key());
addIssue(KEY, translate("OAR038.error", dataNode), schemaNode.key());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public abstract class AbstractQueryParameterCheck extends BaseCheck {
protected static final String PATH_STRATEGY_EXCLUDE = "/exclude";
protected static final String PATH_STRATEGY_INCLUDE = "/include";

protected static final String PAGINATED_RESPONSE_CODE = "206";

protected final String ruleKey;
protected final String messageKey;
protected final String defaultParameterName;
Expand Down Expand Up @@ -76,52 +78,98 @@ public void visitNode(JsonNode node) {
return;
}

boolean hasParameter = hasParameterInNode(node);
if (requiresPaginatedResponse() && !hasPaginatedResponse(node)) {
return;
}

if (!shouldIncludePath(path)) {
return;
}

JsonNode parameterNode = findMatchingParameter(node);

if (shouldIncludePath(path) && !hasParameter) {
if (parameterNode == null) {
addIssue(
ruleKey,
translate(messageKey, getParameterName()),
node.key()
);
} else if (getExpectedType() != null && !hasExpectedType(parameterNode)) {
addIssue(
ruleKey,
translate(getTypeMessageKey(), getParameterName()),
node.key()
);
}
}

protected boolean hasParameterInNode(JsonNode node) {
return findMatchingParameter(node) != null;
}

protected JsonNode findMatchingParameter(JsonNode node) {
JsonNode parametersNode = node.get("parameters");
if (parametersNode != null) {

for (JsonNode parameterNode : parametersNode.elements()) {
if (isRefParameter(parameterNode) && hasNamedRefParameter(parameterNode)) {
return true;
}
if (hasDirectParameter(parameterNode)) {
return true;
}
if (isRefParameter(parameterNode)) {
JsonNode refParameterNode = resolveReference(parameterNode.get("$ref").getTokenValue(), rootNode);
if (refParameterNode != null && matchesNameAndIn(refParameterNode)) {
return refParameterNode;
}
} else if (matchesNameAndIn(parameterNode)) {
return parameterNode;
}
}
}
return false;
return null;
}

protected boolean isRefParameter(JsonNode parameterNode) {
return parameterNode.get("$ref") != null;
JsonNode refNode = parameterNode.get("$ref");
return refNode != null && !refNode.isMissing();
}

protected boolean hasNamedRefParameter(JsonNode parameterNode) {
String refValue = parameterNode.get("$ref").getTokenValue();
JsonNode refParameterNode = resolveReference(refValue, rootNode);
if (refParameterNode != null) {
JsonNode nameNode = refParameterNode.get("name");
JsonNode inNode = refParameterNode.get("in");
return inNode != null && "query".equals(inNode.getTokenValue()) && nameNode != null && getParameterName().equals(nameNode.getTokenValue());
}
return false;
JsonNode refParameterNode = resolveReference(parameterNode.get("$ref").getTokenValue(), rootNode);
return refParameterNode != null && matchesNameAndIn(refParameterNode);
}

protected boolean hasDirectParameter(JsonNode parameterNode) {
return matchesNameAndIn(parameterNode);
}

protected boolean matchesNameAndIn(JsonNode parameterNode) {
JsonNode nameNode = parameterNode.get("name");
JsonNode inNode = parameterNode.get("in");
return inNode != null && "query".equals(inNode.getTokenValue()) && nameNode != null && getParameterName().equals(nameNode.getTokenValue());
return inNode != null && "query".equals(inNode.getTokenValue())
&& nameNode != null && getParameterName().equals(nameNode.getTokenValue());
}

protected String getExpectedType() {
return null;
}

protected String getTypeMessageKey() {
return null;
}

protected boolean hasExpectedType(JsonNode parameterNode) {
String expected = getExpectedType();
if (expected == null) {
return true;
}
JsonNode schemaNode = parameterNode.get("schema");
JsonNode typeNode;
if (schemaNode != null && !schemaNode.isMissing()) {
JsonNode schemaRef = schemaNode.get("$ref");
if (schemaRef != null && !schemaRef.isMissing()) {
schemaNode = resolveReference(schemaRef.getTokenValue(), rootNode);
}
typeNode = schemaNode != null ? schemaNode.get("type") : null;
} else {
typeNode = parameterNode.get("type"); // OpenAPI 2.0
}
return typeNode != null && !typeNode.isMissing() && expected.equals(typeNode.getTokenValue());
}

protected String getPath(JsonNode node) {
Expand All @@ -136,6 +184,23 @@ protected String getPath(JsonNode node) {
return pathBuilder.toString();
}

protected boolean requiresPaginatedResponse() {
return false;
}

protected boolean hasPaginatedResponse(JsonNode node) {
JsonNode responses = node.get("responses");
if (responses == null || responses.isMissing()) {
return false;
}
for (JsonNode responseNode : responses.propertyMap().values()) {
if (PAGINATED_RESPONSE_CODE.equals(responseNode.key().getTokenValue())) {
return true;
}
}
return false;
}

protected boolean shouldIncludePath(String path) {
if (paths.isEmpty()) {
return getPathCheckStrategy().equals(PATH_STRATEGY_EXCLUDE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ protected String getPathsStr() {
protected String getPathCheckStrategy() {
return pathCheckStrategy;
}

@Override
protected boolean requiresPaginatedResponse() {
return true;
}
}
Loading
Loading