From 4f49a4dc49e48692925be97ac71664ac910631f2 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 29 Jun 2026 10:48:12 +0200 Subject: [PATCH 1/4] Add S8914 metadata and SonarWay profile entry --- .../org/sonar/l10n/java/rules/java/S8914.html | 72 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S8914.json | 34 +++++++++ .../rules/java/Sonar_agentic_AI_profile.json | 25 +++---- .../java/rules/java/Sonar_way_profile.json | 35 ++++----- 4 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.json diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.html new file mode 100644 index 00000000000..25292036c58 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.html @@ -0,0 +1,72 @@ +

=== Documentation + +* Quarkus WebSockets Next - Security - https://quarkus.io/guides/websockets-next-reference#websocket-next-security[Official Quarkus documentation on securing WebSocket endpoints, explaining HTTP upgrade security] + +* Quarkus WebSockets Next - Secure HTTP Upgrade - https://quarkus.io/guides/websockets-next-reference#secure-http-upgrade[Detailed guidance on securing the HTTP upgrade handshake with security annotations] + + +=== What is the potential impact? + +==== Unauthorized Access + +Unauthorized clients can establish WebSocket connections to endpoints that lack proper authentication during the HTTP upgrade handshake. While individual message-handling callback methods may implement their own authorization checks, the connection remains open, potentially allowing: + +* Access to sensitive real-time data streams +* Information disclosure through connection metadata +* Sending messages to the endpoint +* Exploitation of any unsecured callback methods +* Bypass of intended access controls + +This is particularly severe in applications handling sensitive data or operations through WebSocket communication. + +==== Resource Exhaustion + +Without proper security controls during the HTTP upgrade process: + +* Attackers can open multiple unauthorized connections +* Server resources (memory, CPU, network bandwidth) are consumed maintaining these connections +* Legitimate users may experience degraded service +* Connection pools may be exhausted + +==== False Sense of Security + +Developers may incorrectly believe their WebSocket endpoints are secured when they are not: + +* Security reviews may miss the misconfiguration at the connection level +* Penetration testing might not catch connection-level vulnerabilities if it only tests message-level authorization +* Compliance requirements may not be met despite perceived security measures + +=== Standards + +* OWASP Top 10 2021 - https://owasp.org/Top10/A01_2021-Broken_Access_Control/[OWASP Top 10 2021-A1: Broken Access Control - Improperly secured endpoints allow unauthorized access] + +* OWASP Top 10 2021 - https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/[OWASP Top 10 2021-A7: Identification and Authentication Failures - Authentication not enforced at connection establishment] + + +This is an issue when security annotations that control access based on authentication status or user roles are placed on individual lifecycle or message handling callback methods within a WebSocket endpoint class instead of on the endpoint class itself. + +== Why is this an issue? + +WebSocket connections begin with an HTTP upgrade handshake. When security annotations are placed only on callback methods, they secure those specific methods but *not* the initial HTTP upgrade itself. + +This creates a critical security gap: + +* *Authorization happens too late*: The HTTP upgrade authorization is performed early and only once. Method-level annotations only check authorization when those specific callbacks are invoked, after the connection is already established. + +* *Unauthorized connections succeed*: An attacker can establish a WebSocket connection without proper credentials because the HTTP upgrade is not protected. Even though individual methods might reject unauthorized actions, the connection itself remains open. + +* *False sense of security*: Developers may believe their WebSocket is secured when only methods are annotated, not realizing the handshake is vulnerable. + +When securing WebSocket endpoints, the placement of security annotations determines what gets protected: + +* **Annotations on callback methods**: These only secure the specific callback method execution. The HTTP upgrade handshake remains unsecured, allowing unauthorized clients to establish connections. +* **Annotations on the endpoint class**: These secure the HTTP upgrade handshake itself, preventing unauthorized connections from being established in the first place. + +If not properly secured at the upgrade stage, unauthorized clients can: + +* Establish WebSocket connections +* Consume server resources +* Potentially access data through unsecured callbacks +* Create a false sense of security where developers believe the endpoint is protected + +To properly secure a WebSocket endpoint, security annotations must be placed on the class level, adjacent to the WebSocket endpoint annotation. This ensures authorization is enforced during the HTTP upgrade, preventing unauthorized connections from being established in the first place. This approach is more efficient than checking authorization on every message and prevents bypasses through unsecured callback methods.

\ No newline at end of file diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.json new file mode 100644 index 00000000000..7e6b995dc97 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8914.json @@ -0,0 +1,34 @@ +{ + "title": "Security annotations should be placed on WebSocket endpoint classes, not methods", + "type": "VULNERABILITY", + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "5min" + }, + "tags": [ + "owasp", + "quarkus", + "authorization" + ], + "defaultSeverity": "Critical", + "ruleSpecification": "RSPEC-8914", + "sqKey": "S8914", + "scope": "All", + "defaultQualityProfiles": [ + "Sonar way" + ], + "quickfix": "unknown", + "code": { + "impacts": { + "SECURITY": "HIGH" + }, + "attribute": "COMPLETE" + }, + "securityStandards": { + "OWASP Top 10 2021": [ + "A1", + "A7" + ] + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json index a669258ac60..2ff68894bac 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json @@ -2,25 +2,17 @@ "name": "Sonar agentic AI", "ruleKeys": [ "S106", - "S107", - "S108", - "S112", - "S119", - "S120", - "S125", - "S127", - "S128", - "S131", - "S135", - "S899", "S1065", "S1068", + "S107", + "S108", "S1111", "S1113", "S1116", "S1117", "S1118", "S1119", + "S112", "S1121", "S1123", "S1125", @@ -51,11 +43,13 @@ "S1182", "S1185", "S1186", + "S119", "S1190", "S1191", "S1192", "S1193", "S1199", + "S120", "S1201", "S1206", "S1210", @@ -67,11 +61,16 @@ "S1221", "S1223", "S1226", + "S125", "S1264", + "S127", + "S128", "S1301", + "S131", "S1313", "S1317", "S1319", + "S135", "S1450", "S1452", "S1479", @@ -465,6 +464,8 @@ "S8696", "S8715", "S8745", - "S8786" + "S8786", + "S8914", + "S899" ] } diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 27529004769..0ca346d5e65 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -4,26 +4,13 @@ "S100", "S101", "S106", - "S107", - "S108", - "S110", - "S112", - "S114", - "S115", - "S116", - "S117", - "S119", - "S120", - "S125", - "S127", - "S128", - "S131", - "S135", - "S899", "S1065", "S1066", "S1068", + "S107", "S1075", + "S108", + "S110", "S1110", "S1111", "S1113", @@ -31,6 +18,7 @@ "S1117", "S1118", "S1119", + "S112", "S1121", "S1123", "S1124", @@ -41,18 +29,22 @@ "S1133", "S1134", "S1135", + "S114", "S1141", "S1143", "S1144", + "S115", "S1150", "S1153", "S1155", "S1157", "S1158", + "S116", "S1161", "S1163", "S1165", "S1168", + "S117", "S1170", "S1171", "S1172", @@ -62,6 +54,7 @@ "S1182", "S1185", "S1186", + "S119", "S1190", "S1191", "S1192", @@ -69,6 +62,7 @@ "S1195", "S1197", "S1199", + "S120", "S1201", "S1206", "S1210", @@ -80,11 +74,16 @@ "S1221", "S1223", "S1226", + "S125", "S1264", + "S127", + "S128", "S1301", + "S131", "S1313", "S1317", "S1319", + "S135", "S1450", "S1452", "S1479", @@ -534,6 +533,8 @@ "S8745", "S8786", "S8911", - "S8924" + "S8914", + "S8924", + "S899" ] } From d35ef1e5987974673a6b7176cb04c0be148f94f1 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 29 Jun 2026 11:26:07 +0200 Subject: [PATCH 2/4] Implement new rule S8914 Detects when security annotations (@Authenticated, @RolesAllowed) are placed on individual WebSocket callback methods instead of on the WebSocket endpoint class itself. According to Quarkus documentation, only class-level security annotations secure the HTTP upgrade handshake. --- java-checks-test-sources/default/pom.xml | 18 +++ ...ebSocketSecurityAnnotationCheckSample.java | 146 ++++++++++++++++++ ...arkusWebSocketSecurityAnnotationCheck.java | 108 +++++++++++++ ...sWebSocketSecurityAnnotationCheckTest.java | 44 ++++++ .../rules/java/Sonar_agentic_AI_profile.json | 24 +-- .../java/rules/java/Sonar_way_profile.json | 34 ++-- .../java/JavaAgenticWayProfileTest.java | 2 +- 7 files changed, 347 insertions(+), 29 deletions(-) create mode 100644 java-checks-test-sources/default/src/main/java/checks/QuarkusWebSocketSecurityAnnotationCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheckTest.java diff --git a/java-checks-test-sources/default/pom.xml b/java-checks-test-sources/default/pom.xml index 9bf55e511d8..bac8ea81c29 100644 --- a/java-checks-test-sources/default/pom.xml +++ b/java-checks-test-sources/default/pom.xml @@ -1022,6 +1022,24 @@ 1.6.0 test + + io.quarkus + quarkus-websockets-next + 3.15.1 + provided + + + io.quarkus + quarkus-security + 3.15.1 + provided + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + provided + diff --git a/java-checks-test-sources/default/src/main/java/checks/QuarkusWebSocketSecurityAnnotationCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/QuarkusWebSocketSecurityAnnotationCheckSample.java new file mode 100644 index 00000000000..862d9a567ad --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/QuarkusWebSocketSecurityAnnotationCheckSample.java @@ -0,0 +1,146 @@ +package checks; + +import io.quarkus.security.Authenticated; +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnPongMessage; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import jakarta.annotation.security.RolesAllowed; + +class QuarkusWebSocketSecurityAnnotationCheckSample { + + @WebSocket(path = "/secure") + class AuthenticatedOnMethod { + @Authenticated // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnTextMessage + String echo(String msg) { + return msg; + } + } + + @WebSocket(path = "/admin") + class RolesAllowedOnMethod { + @RolesAllowed("admin") // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnTextMessage + String handleMessage(String msg) { + return msg; + } + } + + @WebSocket(path = "/multi") + class MultipleCallbacksWithSecurity { + @Authenticated // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnOpen + void onOpen() { + } + + @RolesAllowed("admin") // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnTextMessage + String onMessage(String msg) { + return msg; + } + + @Authenticated // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnClose + void onClose() { + } + } + + @WebSocket(path = "/callbacks") + class DifferentCallbackTypes { + @Authenticated // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnBinaryMessage + void onBinary(byte[] data) { + } + + @RolesAllowed("user") // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnError + void onError(Throwable t) { + } + + @Authenticated // Noncompliant {{Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.}} + @OnPongMessage + void onPong() { + } + } + + @WebSocket(path = "/secure-class") + @Authenticated + class ClassLevelAuthenticated { + @OnTextMessage + String echo(String msg) { + return msg; + } + } + + @WebSocket(path = "/admin-class") + @RolesAllowed("admin") + class ClassLevelRolesAllowed { + @OnTextMessage + String handleMessage(String msg) { + return msg; + } + } + + @WebSocket(path = "/hybrid") + @Authenticated + class HybridSecurity { + @OnTextMessage + String normalMessage(String msg) { + return msg; + } + + @RolesAllowed("admin") + @OnTextMessage + String adminMessage(String msg) { + return msg; + } + } + + @WebSocket(path = "/multi-secure") + @RolesAllowed("user") + class SecureClassMultipleMethods { + @OnOpen + void onOpen() { + } + + @OnTextMessage + String onMessage(String msg) { + return msg; + } + + @OnClose + void onClose() { + } + } + + @WebSocket(path = "/public") + class PublicEndpoint { + @OnTextMessage + String echo(String msg) { + return msg; + } + } + + class NotAWebSocket { + @Authenticated + void someMethod() { + } + } + + @WebSocket(path = "/helper") + class SecurityOnNonCallback { + @OnTextMessage + String echo(String msg) { + return process(msg); + } + + @Authenticated + String process(String msg) { + return msg.toUpperCase(); + } + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java b/java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java new file mode 100644 index 00000000000..b7d729c2285 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java @@ -0,0 +1,108 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S8914") +public class QuarkusWebSocketSecurityAnnotationCheck extends IssuableSubscriptionVisitor { + + private static final String WEBSOCKET_ANNOTATION = "io.quarkus.websockets.next.WebSocket"; + private static final String AUTHENTICATED_ANNOTATION = "io.quarkus.security.Authenticated"; + private static final String ROLES_ALLOWED_ANNOTATION = "jakarta.annotation.security.RolesAllowed"; + + private static final List WEBSOCKET_CALLBACK_ANNOTATIONS = List.of( + "io.quarkus.websockets.next.OnOpen", + "io.quarkus.websockets.next.OnTextMessage", + "io.quarkus.websockets.next.OnBinaryMessage", + "io.quarkus.websockets.next.OnClose", + "io.quarkus.websockets.next.OnError", + "io.quarkus.websockets.next.OnPongMessage" + ); + + private static final List SECURITY_ANNOTATIONS = List.of( + AUTHENTICATED_ANNOTATION, + ROLES_ALLOWED_ANNOTATION + ); + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.CLASS); + } + + @Override + public void visitNode(Tree tree) { + ClassTree classTree = (ClassTree) tree; + + // Check if the class is annotated with @WebSocket + boolean isWebSocketEndpoint = classTree.modifiers().annotations().stream() + .anyMatch(annotation -> annotation.annotationType().symbolType().is(WEBSOCKET_ANNOTATION)); + + if (!isWebSocketEndpoint) { + return; + } + + // Check if the class has class-level security annotations + boolean hasClassLevelSecurity = classTree.modifiers().annotations().stream() + .anyMatch(annotation -> SECURITY_ANNOTATIONS.stream() + .anyMatch(securityAnnotation -> annotation.annotationType().symbolType().is(securityAnnotation))); + + // If class already has security at class level, no issues to report + if (hasClassLevelSecurity) { + return; + } + + // Check methods for security annotations on WebSocket callbacks + classTree.members().stream() + .filter(member -> member.is(Tree.Kind.METHOD)) + .map(MethodTree.class::cast) + .forEach(method -> checkMethod(method)); + } + + private void checkMethod(MethodTree methodTree) { + // Check if the method is a WebSocket callback + boolean isWebSocketCallback = methodTree.modifiers().annotations().stream() + .anyMatch(annotation -> WEBSOCKET_CALLBACK_ANNOTATIONS.stream() + .anyMatch(callbackAnnotation -> annotation.annotationType().symbolType().is(callbackAnnotation))); + + if (!isWebSocketCallback) { + return; + } + + // Find security annotations on the method + List securityAnnotations = methodTree.modifiers().annotations().stream() + .filter(annotation -> SECURITY_ANNOTATIONS.stream() + .anyMatch(securityAnnotation -> annotation.annotationType().symbolType().is(securityAnnotation))) + .toList(); + + // Report issue for each security annotation on the method + for (AnnotationTree securityAnnotation : securityAnnotations) { + reportIssue( + securityAnnotation, + "Move this security annotation to the WebSocket endpoint class to secure the HTTP upgrade handshake.", + List.of(), + null + ); + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheckTest.java new file mode 100644 index 00000000000..5c6626b512e --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheckTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class QuarkusWebSocketSecurityAnnotationCheckTest { + private static final String SAMPLE_FILE = mainCodeSourcesPath("checks/QuarkusWebSocketSecurityAnnotationCheckSample.java"); + private static final QuarkusWebSocketSecurityAnnotationCheck CHECK = new QuarkusWebSocketSecurityAnnotationCheck(); + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(SAMPLE_FILE) + .withCheck(CHECK) + .verifyIssues(); + } + + @Test + void testWithoutSemantic() { + CheckVerifier.newVerifier() + .onFile(SAMPLE_FILE) + .withCheck(CHECK) + .withoutSemantic() + .verifyNoIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json index 2ff68894bac..6f2e5a40f82 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json @@ -2,17 +2,25 @@ "name": "Sonar agentic AI", "ruleKeys": [ "S106", - "S1065", - "S1068", "S107", "S108", + "S112", + "S119", + "S120", + "S125", + "S127", + "S128", + "S131", + "S135", + "S899", + "S1065", + "S1068", "S1111", "S1113", "S1116", "S1117", "S1118", "S1119", - "S112", "S1121", "S1123", "S1125", @@ -43,13 +51,11 @@ "S1182", "S1185", "S1186", - "S119", "S1190", "S1191", "S1192", "S1193", "S1199", - "S120", "S1201", "S1206", "S1210", @@ -61,16 +67,11 @@ "S1221", "S1223", "S1226", - "S125", "S1264", - "S127", - "S128", "S1301", - "S131", "S1313", "S1317", "S1319", - "S135", "S1450", "S1452", "S1479", @@ -465,7 +466,8 @@ "S8715", "S8745", "S8786", + "S8911", "S8914", - "S899" + "S8924" ] } diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 0ca346d5e65..adae2df61f8 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -4,13 +4,26 @@ "S100", "S101", "S106", + "S107", + "S108", + "S110", + "S112", + "S114", + "S115", + "S116", + "S117", + "S119", + "S120", + "S125", + "S127", + "S128", + "S131", + "S135", + "S899", "S1065", "S1066", "S1068", - "S107", "S1075", - "S108", - "S110", "S1110", "S1111", "S1113", @@ -18,7 +31,6 @@ "S1117", "S1118", "S1119", - "S112", "S1121", "S1123", "S1124", @@ -29,22 +41,18 @@ "S1133", "S1134", "S1135", - "S114", "S1141", "S1143", "S1144", - "S115", "S1150", "S1153", "S1155", "S1157", "S1158", - "S116", "S1161", "S1163", "S1165", "S1168", - "S117", "S1170", "S1171", "S1172", @@ -54,7 +62,6 @@ "S1182", "S1185", "S1186", - "S119", "S1190", "S1191", "S1192", @@ -62,7 +69,6 @@ "S1195", "S1197", "S1199", - "S120", "S1201", "S1206", "S1210", @@ -74,16 +80,11 @@ "S1221", "S1223", "S1226", - "S125", "S1264", - "S127", - "S128", "S1301", - "S131", "S1313", "S1317", "S1319", - "S135", "S1450", "S1452", "S1479", @@ -534,7 +535,6 @@ "S8786", "S8911", "S8914", - "S8924", - "S899" + "S8924" ] } diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java index 5d255874ee4..5f3f73b65ab 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java @@ -53,7 +53,7 @@ void profile_is_registered_as_expected() { BuiltInQualityProfilesDefinition.BuiltInQualityProfile actualProfile = profilesPerLanguages.get("java").get("Sonar agentic AI"); assertThat(actualProfile.isDefault()).isFalse(); assertThat(actualProfile.rules()) - .hasSize(465) + .hasSize(470) .extracting(BuiltInQualityProfilesDefinition.BuiltInActiveRule::ruleKey) .doesNotContainAnyElementsOf(List.of( "S101", From 2283ea54228dfb96761e122151f4ffe06824a018 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 30 Jun 2026 08:19:57 +0200 Subject: [PATCH 3/4] Fix CI failures - Replaced three external Quarkus/Jakarta Maven dependencies (quarkus-websockets-next, quarkus-security, jakarta.annotation-api) with local stub annotation files in java-checks-test-sources/default/src/main/java/ - Added S8914 entry to autoscan-diff-by-rules.json with hasTruePositives=false, falseNegatives=8, falsePositives=0 - Fixed 1 code smell: replaced lambda with method reference `this::checkMethod` in QuarkusWebSocketSecurityAnnotationCheck.java (S1612) --- .../autoscan/autoscan-diff-by-rules.json | 6 ++++++ java-checks-test-sources/default/pom.xml | 18 ------------------ .../io/quarkus/security/Authenticated.java | 11 +++++++++++ .../websockets/next/OnBinaryMessage.java | 11 +++++++++++ .../io/quarkus/websockets/next/OnClose.java | 11 +++++++++++ .../io/quarkus/websockets/next/OnError.java | 11 +++++++++++ .../io/quarkus/websockets/next/OnOpen.java | 11 +++++++++++ .../quarkus/websockets/next/OnPongMessage.java | 11 +++++++++++ .../quarkus/websockets/next/OnTextMessage.java | 11 +++++++++++ .../io/quarkus/websockets/next/WebSocket.java | 12 ++++++++++++ .../annotation/security/RolesAllowed.java | 12 ++++++++++++ ...uarkusWebSocketSecurityAnnotationCheck.java | 2 +- 12 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/security/Authenticated.java create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnBinaryMessage.java create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnClose.java create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnError.java create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnOpen.java create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnPongMessage.java create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnTextMessage.java create mode 100644 java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/WebSocket.java create mode 100644 java-checks-test-sources/default/src/main/java/jakarta/annotation/security/RolesAllowed.java diff --git a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json index f6b92127a6e..843d8371ee3 100644 --- a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json +++ b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json @@ -3190,5 +3190,11 @@ "hasTruePositives": true, "falseNegatives": 1, "falsePositives": 0 + }, + { + "ruleKey": "S8914", + "hasTruePositives": false, + "falseNegatives": 8, + "falsePositives": 0 } ] diff --git a/java-checks-test-sources/default/pom.xml b/java-checks-test-sources/default/pom.xml index bac8ea81c29..9bf55e511d8 100644 --- a/java-checks-test-sources/default/pom.xml +++ b/java-checks-test-sources/default/pom.xml @@ -1022,24 +1022,6 @@ 1.6.0 test - - io.quarkus - quarkus-websockets-next - 3.15.1 - provided - - - io.quarkus - quarkus-security - 3.15.1 - provided - - - jakarta.annotation - jakarta.annotation-api - 2.1.1 - provided - diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/security/Authenticated.java b/java-checks-test-sources/default/src/main/java/io/quarkus/security/Authenticated.java new file mode 100644 index 00000000000..f4050abef1f --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/security/Authenticated.java @@ -0,0 +1,11 @@ +package io.quarkus.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Authenticated { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnBinaryMessage.java b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnBinaryMessage.java new file mode 100644 index 00000000000..c68c21edc57 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnBinaryMessage.java @@ -0,0 +1,11 @@ +package io.quarkus.websockets.next; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface OnBinaryMessage { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnClose.java b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnClose.java new file mode 100644 index 00000000000..4773f32a727 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnClose.java @@ -0,0 +1,11 @@ +package io.quarkus.websockets.next; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface OnClose { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnError.java b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnError.java new file mode 100644 index 00000000000..1c9c558ed4f --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnError.java @@ -0,0 +1,11 @@ +package io.quarkus.websockets.next; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface OnError { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnOpen.java b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnOpen.java new file mode 100644 index 00000000000..bbb62cd725e --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnOpen.java @@ -0,0 +1,11 @@ +package io.quarkus.websockets.next; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface OnOpen { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnPongMessage.java b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnPongMessage.java new file mode 100644 index 00000000000..30b6d304486 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnPongMessage.java @@ -0,0 +1,11 @@ +package io.quarkus.websockets.next; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface OnPongMessage { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnTextMessage.java b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnTextMessage.java new file mode 100644 index 00000000000..7e7d4112ef4 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/OnTextMessage.java @@ -0,0 +1,11 @@ +package io.quarkus.websockets.next; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface OnTextMessage { +} diff --git a/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/WebSocket.java b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/WebSocket.java new file mode 100644 index 00000000000..421789c1bec --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/io/quarkus/websockets/next/WebSocket.java @@ -0,0 +1,12 @@ +package io.quarkus.websockets.next; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface WebSocket { + String path(); +} diff --git a/java-checks-test-sources/default/src/main/java/jakarta/annotation/security/RolesAllowed.java b/java-checks-test-sources/default/src/main/java/jakarta/annotation/security/RolesAllowed.java new file mode 100644 index 00000000000..5ab411623b1 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/jakarta/annotation/security/RolesAllowed.java @@ -0,0 +1,12 @@ +package jakarta.annotation.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RolesAllowed { + String[] value(); +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java b/java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java index b7d729c2285..50eeb4759e4 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/QuarkusWebSocketSecurityAnnotationCheck.java @@ -76,7 +76,7 @@ public void visitNode(Tree tree) { classTree.members().stream() .filter(member -> member.is(Tree.Kind.METHOD)) .map(MethodTree.class::cast) - .forEach(method -> checkMethod(method)); + .forEach(this::checkMethod); } private void checkMethod(MethodTree methodTree) { From 2579c838184a2c9dfb1b0229989e838b27ee899e Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 30 Jun 2026 08:53:06 +0200 Subject: [PATCH 4/4] Fix CI failures - Updated `hasSize(470)` to `hasSize(469)` in JavaAgenticWayProfileTest after merge from master removed S6548 from the Agentic AI profile (-1 rule net change) - Added diff_S8914.json with hasTruePositives=true, FN=0, FP=0 and updated autoscan-diff-by-rules.json for S8914 - No changes needed: the S1612 issue (line 79 in QuarkusWebSocketSecurityAnnotationCheck.java) is already fixed - the code already uses the method reference 'this::checkMethod' in the forEach call. --- .../src/test/resources/autoscan/autoscan-diff-by-rules.json | 4 ++-- .../src/test/resources/autoscan/diffs/diff_S8914.json | 6 ++++++ .../org/sonar/plugins/java/JavaAgenticWayProfileTest.java | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S8914.json diff --git a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json index 843d8371ee3..c305bea2dd1 100644 --- a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json +++ b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json @@ -3193,8 +3193,8 @@ }, { "ruleKey": "S8914", - "hasTruePositives": false, - "falseNegatives": 8, + "hasTruePositives": true, + "falseNegatives": 0, "falsePositives": 0 } ] diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8914.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8914.json new file mode 100644 index 00000000000..484d003996c --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8914.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8914", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} \ No newline at end of file diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java index 5f3f73b65ab..555e40aaddb 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java @@ -53,7 +53,7 @@ void profile_is_registered_as_expected() { BuiltInQualityProfilesDefinition.BuiltInQualityProfile actualProfile = profilesPerLanguages.get("java").get("Sonar agentic AI"); assertThat(actualProfile.isDefault()).isFalse(); assertThat(actualProfile.rules()) - .hasSize(470) + .hasSize(469) .extracting(BuiltInQualityProfilesDefinition.BuiltInActiveRule::ruleKey) .doesNotContainAnyElementsOf(List.of( "S101",