-
Notifications
You must be signed in to change notification settings - Fork 58
fix: resolve intermediary variable detection in Java and Python engines #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ | |
| import org.sonar.plugins.python.api.PythonCheck; | ||
| import org.sonar.plugins.python.api.PythonVisitorContext; | ||
| import org.sonar.plugins.python.api.symbols.Symbol; | ||
| import org.sonar.plugins.python.api.symbols.Usage; | ||
| import org.sonar.plugins.python.api.tree.*; | ||
|
|
||
| public class PythonDetectionEngine implements IDetectionEngine<Tree, Symbol> { | ||
|
|
@@ -324,8 +325,10 @@ public boolean isInvocationOnVariable( | |
|
|
||
| QualifiedExpression qualifiedExpression = (QualifiedExpression) callee; | ||
| if (qualifiedExpression.qualifier() instanceof Name name) { | ||
| Optional<String> nameString = Optional.of(name).map(Name::symbol).map(Symbol::name); | ||
| return nameString.isPresent() && nameString.get().equals(variable.name()); | ||
| Symbol symbol = name.symbol(); | ||
| if (symbol != null) { | ||
| return areSymbolsEquivalent(symbol, variable); | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
|
|
@@ -346,7 +349,44 @@ public boolean isInitForVariable(Tree newClass, TraceSymbol<Symbol> variableSymb | |
|
|
||
| TraceSymbol<Symbol> traceSymbol = symbolOptional.get(); | ||
| Symbol symbol = traceSymbol.getSymbol(); | ||
| return symbol.name().equals(variable.name()); | ||
| if (symbol == null) { | ||
| return false; | ||
| } | ||
| return areSymbolsEquivalent(symbol, variable); | ||
| } | ||
|
|
||
| private boolean areSymbolsEquivalent(@Nonnull Symbol s1, @Nonnull Symbol s2) { | ||
| if (s1.equals(s2)) { | ||
| return true; | ||
| } | ||
|
|
||
| Symbol t1 = traceSymbol(s1); | ||
| Symbol t2 = traceSymbol(s2); | ||
|
|
||
| return t1.equals(t2); | ||
| } | ||
|
|
||
| @Nonnull | ||
| private Symbol traceSymbol(@Nonnull Symbol symbol) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is never executed by any test in the repo. I put a I then ran the whole Python module (46 tests). The probe fires 8 times in total, all from existing key-agreement tests, and never with a chain that needs tracing: So |
||
| // NOTE: symbol.usages() iteration order is not guaranteed. For a variable reassigned more | ||
| // than once (x = y; x = z; use(x)), this returns whichever ASSIGNMENT_LHS appears first in | ||
| // the iteration, which is non-deterministic. Ideally the assignment lexically nearest to | ||
| // the use site should be picked. | ||
| for (Usage usage : symbol.usages()) { | ||
| if (usage.kind() == Usage.Kind.ASSIGNMENT_LHS) { | ||
| Tree parent = usage.tree().parent(); | ||
| if (parent instanceof AssignmentStatement assignment) { | ||
| Expression rhs = assignment.assignedValue(); | ||
| if (rhs instanceof Name name) { | ||
| Symbol rhsSymbol = name.symbol(); | ||
| if (rhsSymbol != null && !rhsSymbol.equals(symbol)) { | ||
| return traceSymbol(rhsSymbol); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return symbol; | ||
| } | ||
|
|
||
| private void analyseExpression( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.ibm.example; | ||
|
|
||
| public class Issue8IntermediaryVariableTestFile { | ||
|
|
||
| public class Car { | ||
| public Car(SeatInterface seat) {} | ||
| } | ||
| public interface SeatInterface {} | ||
| public class LeatherSeats implements SeatInterface {} | ||
| public class HeatedSeats implements SeatInterface {} | ||
|
|
||
| public void test() { | ||
| LeatherSeats s = new LeatherSeats(); | ||
| SeatInterface intermediary = s; | ||
| Car c1 = new Car(s); // Noncompliant {{Car}} | ||
| Car c2 = new Car(intermediary); // Noncompliant {{Car}} | ||
|
|
||
| // chain length > 1: b -> a -> s | ||
| SeatInterface a = s; | ||
| SeatInterface b = a; | ||
| Car c3 = new Car(b); // Noncompliant {{Car}} | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.ibm.example; | ||
|
|
||
| public class Issue8MethodReceiverTestFile { | ||
|
|
||
| public interface SeatInterface { | ||
| String describe(); | ||
| } | ||
|
|
||
| public class Car { | ||
| public Car(SeatInterface seat) {} | ||
| } | ||
|
|
||
| public class LeatherSeats implements SeatInterface { | ||
| public String describe() { | ||
| return "leather"; | ||
| } | ||
| } | ||
|
|
||
| public void test() { | ||
| LeatherSeats s = new LeatherSeats(); | ||
| SeatInterface intermediary = s; | ||
| // s.describe() exercises isInvocationOnVariable: receiver 's' traces to 'intermediary' | ||
| s.describe(); | ||
| Car c = new Car(intermediary); // Noncompliant {{Car}} | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| /* | ||
| * Sonar Cryptography Plugin | ||
| * Copyright (C) 2026 PQCA | ||
| * | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to you under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package com.ibm.plugin.rules.issues; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import com.ibm.engine.detection.DetectionStore; | ||
| import com.ibm.engine.model.IValue; | ||
| import com.ibm.engine.model.ValueAction; | ||
| import com.ibm.engine.model.context.IDetectionContext; | ||
| import com.ibm.engine.model.factory.ValueActionFactory; | ||
| import com.ibm.engine.rule.IDetectionRule; | ||
| import com.ibm.engine.rule.builder.DetectionRuleBuilder; | ||
| import com.ibm.mapper.model.INode; | ||
| import com.ibm.plugin.TestBase; | ||
| import java.util.List; | ||
| import javax.annotation.Nonnull; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.sonar.java.checks.verifier.CheckVerifier; | ||
| import org.sonar.plugins.java.api.JavaCheck; | ||
| import org.sonar.plugins.java.api.JavaFileScannerContext; | ||
| import org.sonar.plugins.java.api.semantic.Symbol; | ||
| import org.sonar.plugins.java.api.tree.Tree; | ||
|
|
||
| class Issue8IntermediaryVariableTest extends TestBase { | ||
|
|
||
| static IDetectionContext detectionContext = | ||
| new IDetectionContext() { | ||
| @Nonnull | ||
| @Override | ||
| public Class<? extends IDetectionContext> type() { | ||
| return IDetectionContext.class; | ||
| } | ||
| }; | ||
|
|
||
| public static List<IDetectionRule<Tree>> seatRules = | ||
| List.of( | ||
| new DetectionRuleBuilder<Tree>() | ||
| .createDetectionRule() | ||
| .forObjectTypes( | ||
| "com.ibm.example.Issue8IntermediaryVariableTestFile$LeatherSeats") | ||
| .forConstructor() | ||
| .shouldBeDetectedAs(new ValueActionFactory<>("LeatherSeats")) | ||
| .withoutParameters() | ||
| .buildForContext(detectionContext) | ||
| .inBundle(() -> "testBundle") | ||
| .withoutDependingDetectionRules()); | ||
|
|
||
| public Issue8IntermediaryVariableTest() { | ||
| super( | ||
| List.of( | ||
| new DetectionRuleBuilder<Tree>() | ||
| .createDetectionRule() | ||
| .forObjectTypes( | ||
| "com.ibm.example.Issue8IntermediaryVariableTestFile$Car") | ||
| .forConstructor() | ||
| .shouldBeDetectedAs(new ValueActionFactory<>("Car")) | ||
| .withMethodParameter( | ||
| "com.ibm.example.Issue8IntermediaryVariableTestFile$SeatInterface") | ||
| .addDependingDetectionRules(seatRules) | ||
| .buildForContext(detectionContext) | ||
| .inBundle(() -> "testBundle") | ||
| .withoutDependingDetectionRules())); | ||
| } | ||
|
|
||
| @Override | ||
| public void asserts( | ||
| int findingId, | ||
| @Nonnull DetectionStore<JavaCheck, Tree, Symbol, JavaFileScannerContext> detectionStore, | ||
| @Nonnull List<INode> nodes) { | ||
| assertThat(detectionStore.getDetectionValues()).hasSize(1); | ||
| IValue<Tree> value0 = detectionStore.getDetectionValues().get(0); | ||
| assertThat(value0).isInstanceOf(ValueAction.class); | ||
| assertThat(value0.asString()).isEqualTo("Car"); | ||
|
|
||
| List<DetectionStore<JavaCheck, Tree, Symbol, JavaFileScannerContext>> stores = | ||
| getStoresOfValueType(ValueAction.class, detectionStore.getChildren()); | ||
|
|
||
| assertThat(stores).hasSize(1); | ||
|
|
||
| DetectionStore<JavaCheck, Tree, Symbol, JavaFileScannerContext> store_1 = stores.get(0); | ||
| assertThat(store_1.getDetectionValues()).hasSize(1); | ||
| IValue<Tree> value0_1 = store_1.getDetectionValues().get(0); | ||
| assertThat(value0_1).isInstanceOf(ValueAction.class); | ||
| assertThat(value0_1.asString()).isEqualTo("LeatherSeats"); | ||
| } | ||
|
|
||
| @Override | ||
| public void update( | ||
| @Nonnull | ||
| com.ibm.engine.detection.Finding< | ||
| JavaCheck, Tree, Symbol, JavaFileScannerContext> | ||
| finding) { | ||
| super.update(finding); | ||
| finding.detectionStore() | ||
| .getDetectionValues() | ||
| .forEach( | ||
| iValue -> { | ||
| this.reportIssue(iValue.getLocation(), iValue.asString()); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| void test() { | ||
| CheckVerifier.newVerifier() | ||
| .onFile("src/test/files/rules/issues/Issue8IntermediaryVariableTestFile.java") | ||
| .withChecks(this) | ||
| .verifyIssues(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reassignment is ignored, and that turns a miss into a wrong answer.
traceSymbolfollows only the initializer of the declaration. Any later assignment to the same variable is invisible. So the trace can end at an object the variable no longer holds.I ran this file against the rules from
Issue8IntermediaryVariableTest:new Car(a)maintodayLeatherSeats, which is wrongToday the engine stays quiet. With this change it reports the wrong algorithm. For a CBOM a wrong entry is worse than a missing one, because nobody can tell it is wrong.
This is the Java twin of the Python ordering problem I raised last time, so please treat both the same way. Minimum: a comment here saying reassignment is not tracked, like the one you added on the Python side. Better: if the variable has more than one assignment, stop and return the original symbol instead of guessing.