From 904938996c5277fab6e008f879b9ee79815d4249 Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Wed, 10 Jun 2026 12:22:01 +0200 Subject: [PATCH 1/9] SONARJAVA-6440 S2245: Add security-context heuristic to reduce false positives Port the DART-276 heuristic to Java. PRNG usages are now reported only when the surrounding code is plausibly security-relevant, determined by: - Part 1: file contains an import from a known crypto/auth package prefix (java.security., javax.crypto., org.springframework.security., org.bouncycastle., io.jsonwebtoken., com.auth0.jwt., at.favre.lib.crypto., de.mkammerer.argon2.) - Part 2: the enclosing method/constructor scope (or enclosing class for field/static initializers) contains an identifier whose camelCase / snake_case tokens intersect with a fixed set of 29 security keywords. The heuristic is applied to all six PRNG sources the rule currently flags (Random, JVMRandom, Math.random, ThreadLocalRandom, RandomUtils, RandomStringUtils). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PseudoRandomCheckCryptoImportSample.java | 22 +++ .../PseudoRandomCheckFieldScopeSample.java | 33 ++++ .../PseudoRandomCheckNoContextSample.java | 33 ++++ ...eudoRandomCheckSecurityKeywordsSample.java | 61 ++++++++ .../sonar/java/checks/PseudoRandomCheck.java | 148 +++++++++++++++++- .../java/checks/PseudoRandomCheckTest.java | 32 ++++ 6 files changed, 322 insertions(+), 7 deletions(-) create mode 100644 java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckCryptoImportSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckFieldScopeSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckCryptoImportSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckCryptoImportSample.java new file mode 100644 index 00000000000..61648f18961 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckCryptoImportSample.java @@ -0,0 +1,22 @@ +package checks; + +import java.util.Random; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +// SONARJAVA-6440 Part 1: file imports `org.bouncycastle.*` -> all PRNG calls flagged +// regardless of any per-scope keyword check. +class PseudoRandomCheckCryptoImportSample { + + // BouncyCastle reference so the import is used. + static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider(); + + void noKeywordsInScope() { + int counter = 0; + Random r = new Random(); // Noncompliant {{Make sure that using this pseudorandom number generator is safe here.}} + counter += r.nextInt(); + } + + double anotherNeutralMethod() { + return Math.random(); // Noncompliant + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckFieldScopeSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckFieldScopeSample.java new file mode 100644 index 00000000000..4039f2d05fc --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckFieldScopeSample.java @@ -0,0 +1,33 @@ +package checks; + +import java.util.Random; + +// SONARJAVA-6440: no crypto import. Random call appears outside any method (field initializer +// and static initializer). Scope falls back to the enclosing class; the class identifiers +// drive the keyword check. + +class PseudoRandomCheckFieldScopeSampleSecure { + // Class name contains keyword: `TokenGenerator` tokens -> [token, generator]. `token` matches. + static class TokenGenerator { + static final Random RNG = new Random(); // Noncompliant {{Make sure that using this pseudorandom number generator is safe here.}} + } + + // No method scope, but a sibling field name contains a security keyword. + static class Holder { + String password; + static final Random R = new Random(); // Noncompliant + } + + // Static initializer: no enclosing method; class identifiers carry the keyword. + static class CipherBundle { + static Random r; + static { + r = new Random(); // Noncompliant + } + } +} + +class PseudoRandomCheckFieldScopeSampleNeutral { + // No method, no security keyword anywhere in the enclosing class -> Compliant. + static final Random R = new Random(); // Compliant +} diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java new file mode 100644 index 00000000000..62598fa72d1 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java @@ -0,0 +1,33 @@ +package checks; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import org.apache.commons.lang.math.JVMRandom; +import org.apache.commons.lang.math.RandomUtils; +import org.apache.commons.lang.RandomStringUtils; + +// SONARJAVA-6440: no crypto import, no security-keyword identifiers in scope -> +// the security-context heuristic suppresses the issue. +class PseudoRandomCheckNoContextSample { + + void neutralMethod() { + Random r = new Random(); // Compliant + int v = r.nextInt(); + + JVMRandom j = new JVMRandom(); // Compliant + double d1 = j.nextDouble(); + + double d2 = Math.random(); // Compliant + int v2 = ThreadLocalRandom.current().nextInt(); // Compliant + + RandomUtils ru = new RandomUtils(); + float f1 = RandomUtils.nextFloat(); // Compliant + + RandomStringUtils rsu = new RandomStringUtils(); + String s1 = RandomStringUtils.random(1); // Compliant + } + + int doWork(int input) { + return input + 1; + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java new file mode 100644 index 00000000000..6d8b26b0284 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java @@ -0,0 +1,61 @@ +package checks; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.RandomStringUtils; + +// SONARJAVA-6440: no crypto import. Security keywords reached via per-scope identifier scan. +class PseudoRandomCheckSecurityKeywordsSample { + + // --- Method-scope keyword tokenized from camelCase (`userPassword` -> [user, password]). --- + void camelCaseLocal() { + String userPassword = "x"; + Random r = new Random(); // Noncompliant {{Make sure that using this pseudorandom number generator is safe here.}} + r.nextInt(); + } + + // --- Method-scope keyword tokenized from snake_case (`user_token` -> [user, token]). --- + void snakeCaseLocal() { + String user_token = "x"; + double d = Math.random(); // Noncompliant + } + + // --- Method-scope keyword from all-uppercase (`HMAC` -> [hmac]). --- + void upperCaseLocal() { + final int HMAC = 32; + int v = ThreadLocalRandom.current().nextInt(); // Noncompliant + } + + // --- Keyword in the method name itself. --- + void encryptPayload() { + float f = RandomUtils.nextFloat(); // Noncompliant + } + + // --- Keyword in a parameter name. --- + String randomFromToken(String token) { + return RandomStringUtils.random(1); // Noncompliant + } + + // --- Whole-identifier match (`password`). --- + void wholeIdentifierMatch() { + String password = "x"; + Random r = new Random(); // Noncompliant + } + + // --- No keyword in scope: must NOT be flagged (heuristic suppresses it). --- + void unrelatedScope() { + int counter = 0; + Random r = new Random(); // Compliant + counter += r.nextInt(); + } + + // --- camelCase that DOES NOT match keyword after tokenization. --- + // `randomBytes` tokenizes to [random, bytes]; neither is in the keyword set + // (the keyword `randombytes` only matches an all-lowercase or all-uppercase literal). + void splitRandomBytes() { + byte[] randomBytes = new byte[16]; + Random r = new Random(); // Compliant + r.nextBytes(randomBytes); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java index 6ae9b886afb..c92995308c5 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java @@ -17,17 +17,26 @@ package org.sonar.java.checks; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import org.sonar.check.Rule; -import org.sonarsource.analyzer.commons.collections.SetUtils; +import org.sonar.java.checks.helpers.ExpressionsHelper; import org.sonar.java.model.ExpressionUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.CompilationUnitTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.ImportTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.NewClassTree; import org.sonar.plugins.java.api.tree.Tree; +import org.sonarsource.analyzer.commons.collections.SetUtils; @Rule(key = "S2245") public class PseudoRandomCheck extends IssuableSubscriptionVisitor { @@ -66,22 +75,47 @@ public class PseudoRandomCheck extends IssuableSubscriptionVisitor { "org.apache.commons.lang.math.JVMRandom" ); + // SONARJAVA-6440: ported from DART-276 security-context heuristic. + // When any of these prefixes appear in the file's imports, all PRNG usages in the file are reported. + private static final List CRYPTO_IMPORT_PREFIXES = List.of( + "java.security.", + "javax.crypto.", + "org.springframework.security.", + "org.bouncycastle.", + "io.jsonwebtoken.", + "com.auth0.jwt.", + "at.favre.lib.crypto.", + "de.mkammerer.argon2." + ); + + private static final Set SECURITY_KEYWORDS = SetUtils.immutableSetOf( + "aes", "asymmetric", "auth", "certificate", "chacha20", "cipher", "crypto", "cryptography", + "decrypt", "ecc", "encrypt", "encryption", "hmac", "iv", "nonce", "password", "pbkdf2", + "poly1305", "randombytes", "rsa", "salt", "scrypt", "secret", "secure", "security", + "sessionid", "signature", "token", "verify" + ); + + private boolean cryptoImportPresent = false; + @Override public List nodesToVisit() { - return Arrays.asList(Tree.Kind.NEW_CLASS, Tree.Kind.METHOD_INVOCATION); + return Arrays.asList(Tree.Kind.COMPILATION_UNIT, Tree.Kind.NEW_CLASS, Tree.Kind.METHOD_INVOCATION); } @Override public void visitNode(Tree tree) { + if (tree.is(Tree.Kind.COMPILATION_UNIT)) { + cryptoImportPresent = hasCryptoImport((CompilationUnitTree) tree); + return; + } if (tree instanceof MethodInvocationTree mit) { - IdentifierTree reportLocation = ExpressionUtils.methodName(mit); - - if (isStaticCallToInsecureRandomMethod(mit)) { - reportIssue(reportLocation, MESSAGE); + if (isStaticCallToInsecureRandomMethod(mit) && isInSecurityContext(mit)) { + reportIssue(ExpressionUtils.methodName(mit), MESSAGE); } } else { NewClassTree newClass = (NewClassTree) tree; - if (RANDOM_CONSTRUCTOR_TYPES.contains(newClass.symbolType().fullyQualifiedName())) { + if (RANDOM_CONSTRUCTOR_TYPES.contains(newClass.symbolType().fullyQualifiedName()) + && isInSecurityContext(newClass)) { reportIssue(newClass.identifier(), MESSAGE); } } @@ -94,4 +128,104 @@ private static boolean isStaticCallToInsecureRandomMethod(MethodInvocationTree m && mit.methodSymbol().isStatic(); } + private static boolean hasCryptoImport(CompilationUnitTree cut) { + for (Tree importClause : cut.imports()) { + if (!importClause.is(Tree.Kind.IMPORT)) { + continue; + } + Tree qualifiedIdentifier = ((ImportTree) importClause).qualifiedIdentifier(); + if (!(qualifiedIdentifier instanceof ExpressionTree exprTree)) { + continue; + } + String importName = ExpressionsHelper.concatenate(exprTree); + for (String prefix : CRYPTO_IMPORT_PREFIXES) { + if (importName.startsWith(prefix)) { + return true; + } + } + } + return false; + } + + private boolean isInSecurityContext(Tree tree) { + if (cryptoImportPresent) { + return true; + } + Tree scope = findDeclarationScope(tree); + if (scope == null) { + return false; + } + IdentifierCollector collector = new IdentifierCollector(); + scope.accept(collector); + for (String identifier : collector.identifiers) { + for (String token : tokenizeIdentifier(identifier)) { + if (SECURITY_KEYWORDS.contains(token)) { + return true; + } + } + } + return false; + } + + // Mirrors Dart's `declarationScope`: the closest enclosing MethodTree (method/constructor) for + // local code; falls back to the enclosing ClassTree for field/initializer-block code. + private static Tree findDeclarationScope(Tree tree) { + Tree current = tree.parent(); + while (current != null) { + if (current instanceof MethodTree methodTree) { + return methodTree; + } + if (current instanceof ClassTree classTree) { + return classTree; + } + current = current.parent(); + } + return null; + } + + // Mirrors Dart's `_splitIntoWords`: split on underscores first; for each part either keep it as + // a single lowercase word when all-uppercase, or split further on capital-letter boundaries. + static List tokenizeIdentifier(String identifier) { + List words = new java.util.ArrayList<>(); + for (String part : identifier.split("_")) { + if (part.isEmpty()) { + continue; + } + if (isAllUppercaseWithLetter(part)) { + words.add(part.toLowerCase(Locale.ROOT)); + } else { + for (String sub : part.split("(?=[A-Z])")) { + if (!sub.isEmpty()) { + words.add(sub.toLowerCase(Locale.ROOT)); + } + } + } + } + return words; + } + + private static boolean isAllUppercaseWithLetter(String part) { + boolean hasLetter = false; + for (int i = 0; i < part.length(); i++) { + char c = part.charAt(i); + if (Character.isLetter(c)) { + hasLetter = true; + if (Character.isLowerCase(c)) { + return false; + } + } + } + return hasLetter; + } + + private static class IdentifierCollector extends BaseTreeVisitor { + private final Set identifiers = new HashSet<>(); + + @Override + public void visitIdentifier(IdentifierTree tree) { + identifiers.add(tree.name()); + super.visitIdentifier(tree); + } + } + } diff --git a/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java index da39e18fdb4..a60c7459b3b 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java @@ -29,4 +29,36 @@ void test() { .withCheck(new PseudoRandomCheck()) .verifyIssues(); } + + @Test + void test_no_security_context() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/PseudoRandomCheckNoContextSample.java")) + .withCheck(new PseudoRandomCheck()) + .verifyNoIssues(); + } + + @Test + void test_security_keywords() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/PseudoRandomCheckSecurityKeywordsSample.java")) + .withCheck(new PseudoRandomCheck()) + .verifyIssues(); + } + + @Test + void test_crypto_import() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/PseudoRandomCheckCryptoImportSample.java")) + .withCheck(new PseudoRandomCheck()) + .verifyIssues(); + } + + @Test + void test_field_scope() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/PseudoRandomCheckFieldScopeSample.java")) + .withCheck(new PseudoRandomCheck()) + .verifyIssues(); + } } From 624f4757354cbcd0abc49409718840aae1d23515 Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Wed, 10 Jun 2026 13:07:29 +0200 Subject: [PATCH 2/9] SONARJAVA-6440 Address code review and ruling expectations - Use ImportClauseTree as the loop variable type (S4838) - Extract crypto-prefix matching into a helper to remove the multiple continues from hasCryptoImport (S135) - Memoize scopeHasSecurityKeyword results per scope Tree in an IdentityHashMap, cleared on each COMPILATION_UNIT visit - Update sonar-server ruling expectation for S2245: the recovery indexer schedule delay using RandomUtils is now correctly suppressed by the new security-context heuristic (no enclosing security keyword, no crypto imports) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../resources/sonar-server/java-S2245.json | 6 +--- .../sonar/java/checks/PseudoRandomCheck.java | 34 ++++++++++++------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/its/ruling/src/test/resources/sonar-server/java-S2245.json b/its/ruling/src/test/resources/sonar-server/java-S2245.json index ec56a575411..0967ef424bc 100644 --- a/its/ruling/src/test/resources/sonar-server/java-S2245.json +++ b/its/ruling/src/test/resources/sonar-server/java-S2245.json @@ -1,5 +1 @@ -{ -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/RecoveryIndexer.java": [ -92 -] -} +{} diff --git a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java index c92995308c5..bcea73cb376 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java @@ -18,8 +18,10 @@ import java.util.Arrays; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.ExpressionsHelper; @@ -31,6 +33,7 @@ import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.ImportClauseTree; import org.sonar.plugins.java.api.tree.ImportTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -96,6 +99,7 @@ public class PseudoRandomCheck extends IssuableSubscriptionVisitor { ); private boolean cryptoImportPresent = false; + private final Map scopeSecurityContextCache = new IdentityHashMap<>(); @Override public List nodesToVisit() { @@ -106,6 +110,7 @@ public List nodesToVisit() { public void visitNode(Tree tree) { if (tree.is(Tree.Kind.COMPILATION_UNIT)) { cryptoImportPresent = hasCryptoImport((CompilationUnitTree) tree); + scopeSecurityContextCache.clear(); return; } if (tree instanceof MethodInvocationTree mit) { @@ -129,19 +134,20 @@ private static boolean isStaticCallToInsecureRandomMethod(MethodInvocationTree m } private static boolean hasCryptoImport(CompilationUnitTree cut) { - for (Tree importClause : cut.imports()) { - if (!importClause.is(Tree.Kind.IMPORT)) { - continue; - } - Tree qualifiedIdentifier = ((ImportTree) importClause).qualifiedIdentifier(); - if (!(qualifiedIdentifier instanceof ExpressionTree exprTree)) { - continue; + for (ImportClauseTree importClause : cut.imports()) { + if (importClause.is(Tree.Kind.IMPORT) + && ((ImportTree) importClause).qualifiedIdentifier() instanceof ExpressionTree exprTree + && matchesCryptoPrefix(ExpressionsHelper.concatenate(exprTree))) { + return true; } - String importName = ExpressionsHelper.concatenate(exprTree); - for (String prefix : CRYPTO_IMPORT_PREFIXES) { - if (importName.startsWith(prefix)) { - return true; - } + } + return false; + } + + private static boolean matchesCryptoPrefix(String importName) { + for (String prefix : CRYPTO_IMPORT_PREFIXES) { + if (importName.startsWith(prefix)) { + return true; } } return false; @@ -155,6 +161,10 @@ private boolean isInSecurityContext(Tree tree) { if (scope == null) { return false; } + return scopeSecurityContextCache.computeIfAbsent(scope, PseudoRandomCheck::scopeHasSecurityKeyword); + } + + private static boolean scopeHasSecurityKeyword(Tree scope) { IdentifierCollector collector = new IdentifierCollector(); scope.accept(collector); for (String identifier : collector.identifiers) { From d08db1a72ee78229c2a93480570934aaed6151d8 Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Wed, 10 Jun 2026 13:23:36 +0200 Subject: [PATCH 3/9] SONARJAVA-6440 Update ruling expectations for eclipse-jetty and mall Three additional FPs are correctly suppressed by the new security-context heuristic. Inspected sources to confirm each is a legitimate suppression (no crypto imports + no security-keyword identifiers in enclosing method/class scope per the Java keyword set from the ticket): - mall UmsMemberCouponServiceImpl#generateCouponCode (line 91): Random() used to build a 16-digit coupon code; method/class identifiers contain no listed keyword (`coupon`, `member`, `code`, etc. are not in the set). UmsMemberServiceImpl#generateAuthCode (line 112) stays flagged because the file imports org.springframework.security.* -> Part 1 triggers. - eclipse-jetty ShutdownMonitor#setupSocket (line 287): Math.random() used to derive a stop-port key. The local variable is named `key`, which is in the Kotlin spec but not the Java spec (SONARJAVA-6440), so the heuristic correctly suppresses per the ticket's keyword list. DefaultSessionIdManager (line 369) stays flagged because the file imports java.security.SecureRandom -> Part 1 triggers. Applied to both eclipse-jetty and eclipse-jetty-similar-to-main (used by the incremental ruling test). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../resources/eclipse-jetty-similar-to-main/java-S2245.json | 3 --- its/ruling/src/test/resources/eclipse-jetty/java-S2245.json | 3 --- its/ruling/src/test/resources/mall/java-S2245.json | 3 --- 3 files changed, 9 deletions(-) diff --git a/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S2245.json b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S2245.json index d04ce975280..b7aae18f176 100644 --- a/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S2245.json +++ b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S2245.json @@ -1,7 +1,4 @@ { -"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/ShutdownMonitor.java": [ -287 -], "org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java": [ 369 ] diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S2245.json b/its/ruling/src/test/resources/eclipse-jetty/java-S2245.json index d04ce975280..b7aae18f176 100644 --- a/its/ruling/src/test/resources/eclipse-jetty/java-S2245.json +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S2245.json @@ -1,7 +1,4 @@ { -"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/ShutdownMonitor.java": [ -287 -], "org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java": [ 369 ] diff --git a/its/ruling/src/test/resources/mall/java-S2245.json b/its/ruling/src/test/resources/mall/java-S2245.json index 1511b370aaf..8815173f31d 100644 --- a/its/ruling/src/test/resources/mall/java-S2245.json +++ b/its/ruling/src/test/resources/mall/java-S2245.json @@ -1,7 +1,4 @@ { -"com.macro.mall:mall:mall-portal/src/main/java/com/macro/mall/portal/service/impl/UmsMemberCouponServiceImpl.java": [ -91 -], "com.macro.mall:mall:mall-portal/src/main/java/com/macro/mall/portal/service/impl/UmsMemberServiceImpl.java": [ 112 ] From 8954b77b1159ec70f729bac2b756f0dbe5c6636b Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Wed, 10 Jun 2026 14:21:39 +0200 Subject: [PATCH 4/9] SONARJAVA-6440 Update S2245 autoscan FN expectation AutoScanTest reports the total FN count dropped from 4146 to 4142 (-4) and the FP count is unchanged at 18. The only rule modified by this branch is S2245, so the entire delta is attributable to it: 4 of the issues the autoscan version of the rule was previously missing in java-checks-test-sources are now correctly suppressed by the new security-context heuristic in the full analyzer too, removing them from the FN diff. Update diff_S2245.json falseNegatives: 26 -> 22 to match the new analyzer behaviour. The recipe in the failure log (copy target/actual/autoscan-diffs/diff_S2245.json over the checked-in expectation) was not directly available locally, but the metric in the file is just falseNegatives, the only rule touched by this PR is S2245, and the announced delta is exactly -4. Co-Authored-By: Claude Opus 4.7 (1M context) --- its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json index b208b729c10..91a839972ce 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json @@ -1,6 +1,6 @@ { "ruleKey": "S2245", "hasTruePositives": true, - "falseNegatives": 26, + "falseNegatives": 22, "falsePositives": 0 } From 77ebabab34f2de49e94407b7bbe3bfbb7876fd97 Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Wed, 10 Jun 2026 14:35:20 +0200 Subject: [PATCH 5/9] SONARJAVA-6440 Refine autoscan FN expectations: S2245=17, S2440=4 The previous adjustment (S2245 26 -> 22) was based on the reported total FN delta of -4. The next CI iteration revealed two per-rule deltas instead of a single total: - S2245: 26 -> 17 (-9). My new test samples and the heuristic together remove more autoscan-misses than the original total suggested. - S2440 (ClassWithOnlyStaticMethodsInstantiationCheck): 2 -> 4 (+2). The new PseudoRandomCheckNoContextSample.java instantiates RandomUtils and RandomStringUtils (utility classes with only static methods); full mode raises S2440 on each, autoscan misses both without semantics, so the FN diff grows by 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json | 2 +- its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json index 91a839972ce..f4f66daaafd 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2245.json @@ -1,6 +1,6 @@ { "ruleKey": "S2245", "hasTruePositives": true, - "falseNegatives": 22, + "falseNegatives": 17, "falsePositives": 0 } diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json index 0d60d9f0390..a82068be5c1 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json @@ -1,6 +1,6 @@ { "ruleKey": "S2440", "hasTruePositives": true, - "falseNegatives": 2, + "falseNegatives": 4, "falsePositives": 0 } From c1708df86ae7b5c8c40031a6cfdc8eac724673fd Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Thu, 11 Jun 2026 11:59:28 +0200 Subject: [PATCH 6/9] SONARJAVA-6440 Address review feedback from Nils - Style: import java.util.ArrayList and drop the inline FQN. - Lifecycle: move scopeSecurityContextCache.clear() and cryptoImportPresent reset from visitNode(COMPILATION_UNIT) to a new leaveNode(COMPILATION_UNIT) override, so the cache lifecycle is explicit rather than relying on a re-visit of the root. - Defensive: if findDeclarationScope returns null (shouldn't happen in valid Java) fail open (return true / flag) instead of silently suppressing. - Comment: replace the ticket-reference comment on CRYPTO_IMPORT_PREFIXES with one that explains the invariant the constant encodes. - Tests: add four edge cases: * AES256 (all-uppercase with digit suffix; documents that the Dart-faithful tokenization keeps it as a single `aes256` token, which does not match the `aes` keyword). * `import java.security.*;` wildcard crypto import. * `import static javax.crypto.Cipher.ENCRYPT_MODE;` static crypto import. * Inner class with neutral identifiers inside an outer class whose name carries a keyword; the inner class's PRNG call is Compliant because findDeclarationScope returns the inner ClassTree. - Test sample cleanup: drop the unneeded `new RandomUtils()` and `new RandomStringUtils()` instantiations from the no-context sample (they were triggering S2440 as a side effect). Revert diff_S2440.json to its original falseNegatives: 2. Not addressed: Nils's wildcard-import bug claim - verified locally that `"java.security.*".startsWith("java.security.")` returns true, so the wildcard case already works; will reply on the PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../resources/autoscan/diffs/diff_S2440.json | 2 +- .../PseudoRandomCheckNoContextSample.java | 3 --- ...eudoRandomCheckSecurityKeywordsSample.java | 22 +++++++++++++++++++ .../PseudoRandomCheckStaticImportSample.java | 19 ++++++++++++++++ ...PseudoRandomCheckWildcardImportSample.java | 22 +++++++++++++++++++ .../sonar/java/checks/PseudoRandomCheck.java | 20 ++++++++++++----- .../java/checks/PseudoRandomCheckTest.java | 16 ++++++++++++++ 7 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckStaticImportSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckWildcardImportSample.java diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json index a82068be5c1..0d60d9f0390 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2440.json @@ -1,6 +1,6 @@ { "ruleKey": "S2440", "hasTruePositives": true, - "falseNegatives": 4, + "falseNegatives": 2, "falsePositives": 0 } diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java index 62598fa72d1..8e1c5da2ad8 100644 --- a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckNoContextSample.java @@ -20,10 +20,7 @@ void neutralMethod() { double d2 = Math.random(); // Compliant int v2 = ThreadLocalRandom.current().nextInt(); // Compliant - RandomUtils ru = new RandomUtils(); float f1 = RandomUtils.nextFloat(); // Compliant - - RandomStringUtils rsu = new RandomStringUtils(); String s1 = RandomStringUtils.random(1); // Compliant } diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java index 6d8b26b0284..4f39f5c98c8 100644 --- a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java @@ -58,4 +58,26 @@ void splitRandomBytes() { Random r = new Random(); // Compliant r.nextBytes(randomBytes); } + + // --- Digit-suffixed all-uppercase identifier (Dart-faithful behaviour). --- + // `AES256` has no lowercase letter, so isAllUppercaseWithLetter returns true and the + // whole identifier is lowercased to a single token `aes256`. The keyword set holds + // `aes`, not `aes256`, so this does NOT trigger the heuristic. Same Dart behaviour. + // Intentional: documenting that digit-suffixed crypto acronyms do not match. + void digitSuffixedAcronym() { + final int AES256 = 32; + Random r = new Random(); // Compliant + r.nextInt(AES256); + } } + +// --- Inner-class scope isolation. --- +// The outer class name `TokenAware` contains the security keyword `token`, but the +// PRNG call lives in an inner class with neutral identifiers. findDeclarationScope +// returns the inner ClassTree first, so the outer's keyword is NOT in scope. +class TokenAware { + static class NeutralInner { + static final Random R = new Random(); // Compliant + } +} + diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckStaticImportSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckStaticImportSample.java new file mode 100644 index 00000000000..c89a240caa1 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckStaticImportSample.java @@ -0,0 +1,19 @@ +package checks; + +import static javax.crypto.Cipher.ENCRYPT_MODE; + +import java.util.Random; + +// SONARJAVA-6440 Part 1: static import of a crypto class still parses as Tree.Kind.IMPORT, +// so the concatenated name `javax.crypto.Cipher.ENCRYPT_MODE` matches the `javax.crypto.` +// prefix and the file-level crypto-import gate fires. +class PseudoRandomCheckStaticImportSample { + + static final int MODE = ENCRYPT_MODE; // retain the static import + + void noKeywordsInScope() { + int counter = 0; + Random r = new Random(); // Noncompliant {{Make sure that using this pseudorandom number generator is safe here.}} + counter += r.nextInt(); + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckWildcardImportSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckWildcardImportSample.java new file mode 100644 index 00000000000..be87893da26 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckWildcardImportSample.java @@ -0,0 +1,22 @@ +package checks; + +import java.security.*; +import java.util.Random; + +// SONARJAVA-6440 Part 1: wildcard import `java.security.*` -> all PRNG calls flagged. +// ExpressionsHelper.concatenate returns "java.security.*", which still starts with the +// "java.security." prefix, so the file-level crypto-import check fires. +class PseudoRandomCheckWildcardImportSample { + + static final KeyPair PROVIDER_KEYPAIR = null; // forces the wildcard import to be retained + + void noKeywordsInScope() { + int counter = 0; + Random r = new Random(); // Noncompliant {{Make sure that using this pseudorandom number generator is safe here.}} + counter += r.nextInt(); + } + + double anotherNeutralMethod() { + return Math.random(); // Noncompliant + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java index bcea73cb376..e047d8f60a2 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java @@ -16,6 +16,7 @@ */ package org.sonar.java.checks; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.IdentityHashMap; @@ -78,8 +79,8 @@ public class PseudoRandomCheck extends IssuableSubscriptionVisitor { "org.apache.commons.lang.math.JVMRandom" ); - // SONARJAVA-6440: ported from DART-276 security-context heuristic. - // When any of these prefixes appear in the file's imports, all PRNG usages in the file are reported. + // A crypto/auth library import signals the whole file is security-relevant; + // report all PRNG calls without needing per-scope keyword analysis. private static final List CRYPTO_IMPORT_PREFIXES = List.of( "java.security.", "javax.crypto.", @@ -110,7 +111,6 @@ public List nodesToVisit() { public void visitNode(Tree tree) { if (tree.is(Tree.Kind.COMPILATION_UNIT)) { cryptoImportPresent = hasCryptoImport((CompilationUnitTree) tree); - scopeSecurityContextCache.clear(); return; } if (tree instanceof MethodInvocationTree mit) { @@ -126,6 +126,14 @@ && isInSecurityContext(newClass)) { } } + @Override + public void leaveNode(Tree tree) { + if (tree.is(Tree.Kind.COMPILATION_UNIT)) { + cryptoImportPresent = false; + scopeSecurityContextCache.clear(); + } + } + private static boolean isStaticCallToInsecureRandomMethod(MethodInvocationTree mit) { return STATIC_RANDOM_METHODS.matches(mit) && !RANDOM_STRING_UTILS_RANDOM_WITH_RANDOM_SOURCE.matches(mit) @@ -159,7 +167,9 @@ private boolean isInSecurityContext(Tree tree) { } Tree scope = findDeclarationScope(tree); if (scope == null) { - return false; + // Defensive: shouldn't happen in valid Java (every PRNG call has an enclosing + // method or class). Fail open and flag to avoid silent false negatives. + return true; } return scopeSecurityContextCache.computeIfAbsent(scope, PseudoRandomCheck::scopeHasSecurityKeyword); } @@ -196,7 +206,7 @@ private static Tree findDeclarationScope(Tree tree) { // Mirrors Dart's `_splitIntoWords`: split on underscores first; for each part either keep it as // a single lowercase word when all-uppercase, or split further on capital-letter boundaries. static List tokenizeIdentifier(String identifier) { - List words = new java.util.ArrayList<>(); + List words = new ArrayList<>(); for (String part : identifier.split("_")) { if (part.isEmpty()) { continue; diff --git a/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java index a60c7459b3b..1a3c88e119f 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PseudoRandomCheckTest.java @@ -61,4 +61,20 @@ void test_field_scope() { .withCheck(new PseudoRandomCheck()) .verifyIssues(); } + + @Test + void test_wildcard_crypto_import() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/PseudoRandomCheckWildcardImportSample.java")) + .withCheck(new PseudoRandomCheck()) + .verifyIssues(); + } + + @Test + void test_static_crypto_import() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/PseudoRandomCheckStaticImportSample.java")) + .withCheck(new PseudoRandomCheck()) + .verifyIssues(); + } } From 29e9bef88296855a24810b1db789fc42752adfaa Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Thu, 11 Jun 2026 13:16:52 +0200 Subject: [PATCH 7/9] SONARJAVA-6440 Drop CRYPTO_IMPORT_PREFIXES preamble; rename AES256 test - Remove the two-line comment above CRYPTO_IMPORT_PREFIXES. The constant name plus the prefix list are self-explanatory. - Rename the digit-suffixed test variable from AES256 to AES256_KEY to match the exact identifier Nils called out in review; extend the comment to reference APPSEC-3004 as the cross-language follow-up for the tokenizer gap. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...eudoRandomCheckSecurityKeywordsSample.java | 20 ++++++++++++------- .../sonar/java/checks/PseudoRandomCheck.java | 2 -- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java index 4f39f5c98c8..98d24ce8006 100644 --- a/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/PseudoRandomCheckSecurityKeywordsSample.java @@ -59,15 +59,21 @@ void splitRandomBytes() { r.nextBytes(randomBytes); } - // --- Digit-suffixed all-uppercase identifier (Dart-faithful behaviour). --- - // `AES256` has no lowercase letter, so isAllUppercaseWithLetter returns true and the - // whole identifier is lowercased to a single token `aes256`. The keyword set holds - // `aes`, not `aes256`, so this does NOT trigger the heuristic. Same Dart behaviour. - // Intentional: documenting that digit-suffixed crypto acronyms do not match. + // --- Digit-suffixed all-uppercase crypto acronym (Dart-faithful behaviour). --- + // `AES256_KEY` splits on `_` to ["AES256", "KEY"]. The "AES256" part has no lowercase + // letter, so isAllUppercaseWithLetter returns true and it is kept as the single token + // `aes256` (NOT split into `aes` + `256`). The keyword set holds `aes`, not `aes256`, + // so no token matches. `KEY` -> `[key]`, but `key` is not in the Java keyword set + // (it is in the Kotlin set; SONARKT-770 catches this case in Kotlin only). + // This is intentional: the tokenizer mirrors DART-276's _splitIntoWords exactly to + // keep cross-language behaviour aligned. Improving the tokenizer to split letters + // from trailing digits would help here AND in cases like `ChaCha20` (where the keyword + // `chacha20` is also unreachable today) but is out of scope for this faithful port. + // Tracked separately under APPSEC-3004. void digitSuffixedAcronym() { - final int AES256 = 32; + final int AES256_KEY = 32; Random r = new Random(); // Compliant - r.nextInt(AES256); + r.nextInt(AES256_KEY); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java index e047d8f60a2..c15f029b84f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java @@ -79,8 +79,6 @@ public class PseudoRandomCheck extends IssuableSubscriptionVisitor { "org.apache.commons.lang.math.JVMRandom" ); - // A crypto/auth library import signals the whole file is security-relevant; - // report all PRNG calls without needing per-scope keyword analysis. private static final List CRYPTO_IMPORT_PREFIXES = List.of( "java.security.", "javax.crypto.", From 8123b0dbf8b2dd0983e74205e6474d5aec6c885b Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Thu, 11 Jun 2026 14:25:48 +0200 Subject: [PATCH 8/9] SONARJAVA-6440 Update autoscan baselines for S1874 and S2119 CI artifact from run 27343009526 lists only diff_S1874.json and diff_S2119.json as mismatched (autoscan only writes a diff file when it differs from the expectation). The deltas match the +3 total ("expected 4137, actual 4140"): * S1874 ("@Deprecated" code should not be used) 246 -> 248: the new PseudoRandomCheckNoContextSample.java calls deprecated lang3 APIs RandomUtils.nextFloat() and RandomStringUtils.random(int) (deprecated since lang3 3.13). Full mode flags both; autoscan misses both because resolving the @Deprecated annotation needs semantic info. * S2119 ("Random" objects should be reused) 2 -> 3: one extra `new Random()` in the new samples is flagged by full mode and missed by autoscan for the same reason (Random type resolution requires semantics). diff_S2245.json already contains the correct value (17) since 77ebabab and is intentionally untouched here. Co-Authored-By: Claude Opus 4.7 (1M context) --- its/autoscan/src/test/resources/autoscan/diffs/diff_S1874.json | 2 +- its/autoscan/src/test/resources/autoscan/diffs/diff_S2119.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S1874.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S1874.json index dd83ce440c0..f304d559ef1 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S1874.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S1874.json @@ -1,6 +1,6 @@ { "ruleKey": "S1874", "hasTruePositives": true, - "falseNegatives": 246, + "falseNegatives": 248, "falsePositives": 0 } diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2119.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2119.json index e7d7377f547..3f89e139fb0 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2119.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2119.json @@ -1,6 +1,6 @@ { "ruleKey": "S2119", "hasTruePositives": true, - "falseNegatives": 2, + "falseNegatives": 3, "falsePositives": 0 } From 426efc0942633f45f3ec6dc892a30e1092ad9851 Mon Sep 17 00:00:00 2001 From: Luqmansonar Date: Thu, 11 Jun 2026 19:24:01 +0200 Subject: [PATCH 9/9] SONARJAVA-6440 Use leaveFile to reset per-file state --- .../java/org/sonar/java/checks/PseudoRandomCheck.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java index c15f029b84f..8e386da5276 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PseudoRandomCheck.java @@ -28,6 +28,7 @@ import org.sonar.java.checks.helpers.ExpressionsHelper; import org.sonar.java.model.ExpressionUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; @@ -125,11 +126,9 @@ && isInSecurityContext(newClass)) { } @Override - public void leaveNode(Tree tree) { - if (tree.is(Tree.Kind.COMPILATION_UNIT)) { - cryptoImportPresent = false; - scopeSecurityContextCache.clear(); - } + public void leaveFile(JavaFileScannerContext context) { + cryptoImportPresent = false; + scopeSecurityContextCache.clear(); } private static boolean isStaticCallToInsecureRandomMethod(MethodInvocationTree mit) {