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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
cache: maven
- name: Build with Maven
run: mvn -B clean package
- name: Verify analyzer redistribution materials
run: python3 sonar-cryptography-plugin/src/test/python/verify_analyzer_packaging.py sonar-cryptography-plugin/target/sonar-cryptography-plugin-*.jar
# The build must not write into the source tree. It used to: the mapper
# module regenerated ciphersuites.json and JsonCipherSuites.java from a live
# API on every compile. Fail here if that ever comes back.
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,4 +327,21 @@ start a discussion using [GitHub Discussions](https://github.com/cbomkit/sonar-c

## License

[Apache License 2.0](LICENSE.txt)
The independently authored code in this repository is licensed under the
[Apache License 2.0](LICENSE.txt). The packaged plugin also contains third-party
code under its own licences, including sonar-java, sonar-python and sonar-go
under the [Sonar Source-Available License v1.0](https://www.sonarsource.com/license/ssal-1-0-0/),
subject to their individual-file licence notices.

The plugin JAR includes `META-INF/THIRD-PARTY-NOTICES.txt`, the SSAL text, and
each resolved analyser module's original legal files under
`META-INF/third-party/analyzers/`. Matching upstream Maven source JARs are included
under `META-INF/third-party/analyzer-sources/`, including the Java and Python
frontends. A pinned complete sonar-go release source ZIP in the same directory
also provides the native Go bridge, shaded Go modules and build scripts. Its
public commit maps to the plugin's build revision through `GitOrigin-RevId`.
The notices describe the versions, source locations and packaging transformations.

These packaging materials do not establish that every use is permitted by SSAL.
The permitted-purpose restrictions and the provenance and licence notices of
any adapted implementation code still require review before release.
248 changes: 82 additions & 166 deletions engine/src/main/java/com/ibm/engine/detection/MethodMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,227 +21,143 @@

import com.ibm.engine.language.ILanguageTranslation;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.annotation.Nonnull;
import org.sonarsource.analyzer.commons.collections.SetUtils;

/** Matches a call signature against the owner, name and argument choices in a detection rule. */
public final class MethodMatcher<T> {
public static final String ANY = "*";

@Nonnull private final Predicate<IType> invokedObjectTypeString;
@Nonnull private final Predicate<String> methodName;
@Nonnull private final Predicate<List<IType>> parameterTypes;
private final boolean anyOwner;
private final boolean anyName;
private final boolean anyArguments;
private final String[] argumentSignature;

/*
* The following attributes are only used for serializing the MethodMatcher class (see
* `MethodMatcherSerializer.java`). This is indeed required to then obtain a graph representation
* of the detection rules. One can access their value through the public getter functions below.
*/
// Keep the rule's original values available to MethodMatcherSerializer.
@Nonnull private final List<String> invokedObjectTypeStringsSerializable;
@Nonnull private final List<String> methodNamesSerializable;
@Nonnull private final List<String> parameterTypesSerializable;

public MethodMatcher(
@Nonnull String invokedObjectTypeString,
@Nonnull String methodName,
@Nonnull List<String> parameterTypes) {

this.invokedObjectTypeStringsSerializable = List.of(invokedObjectTypeString);
this.methodNamesSerializable = List.of(methodName);
this.parameterTypesSerializable = parameterTypes;

this.invokedObjectTypeString =
createPredicate(invokedObjectTypeString, (type1 -> (iType -> iType.is(type1))));

this.methodName = createPredicate(methodName, (methodName1 -> (methodName1::equals)));

List<Predicate<IType>> types =
parameterTypes.stream()
.<Predicate<IType>>map(
parameterType ->
substituteAny(
type -> type.is(parameterType), parameterType))
.toList();
this.parameterTypes =
(List<IType> actualTypes) -> exactMatchesParameters(types, actualTypes);
@Nonnull String owner, @Nonnull String name, @Nonnull List<String> arguments) {
this(List.of(owner), List.of(name), arguments, false);
}

public MethodMatcher(
@Nonnull String[] invokedObjectTypeStrings,
@Nonnull String[] methodNames,
@Nonnull List<String> parameterTypes) {

this.invokedObjectTypeStringsSerializable = Arrays.asList(invokedObjectTypeStrings);
this.methodNamesSerializable = Arrays.asList(methodNames);
this.parameterTypesSerializable = parameterTypes;

this.invokedObjectTypeString =
createPredicate(
invokedObjectTypeStrings,
types -> (type -> types.stream().anyMatch(type::is)));

this.methodName =
createPredicate(
methodNames, names -> (name -> names.stream().anyMatch(name::equals)));

List<Predicate<IType>> types =
parameterTypes.stream()
.<Predicate<IType>>map(
parameterType ->
substituteAny(
type -> type.is(parameterType), parameterType))
.toList();
this.parameterTypes =
(List<IType> actualTypes) -> exactMatchesParameters(types, actualTypes);
}

public MethodMatcher(
@Nonnull String[] invokedObjectTypeStrings, @Nonnull String[] methodNames) {

this.invokedObjectTypeStringsSerializable = Arrays.asList(invokedObjectTypeStrings);
this.methodNamesSerializable = Arrays.asList(methodNames);
this.parameterTypesSerializable = List.of();

this.invokedObjectTypeString =
createPredicate(
invokedObjectTypeStrings,
types -> (type -> types.stream().anyMatch(type::is)));

this.methodName =
createPredicate(
methodNames, names -> (name -> names.stream().anyMatch(name::equals)));
this.parameterTypes = (List<IType> actualTypes) -> true;
@Nonnull String[] owners, @Nonnull String[] names, @Nonnull List<String> arguments) {
this(Arrays.asList(owners), Arrays.asList(names), arguments, false);
}

private static <E> Predicate<E> substituteAny(Predicate<E> predicate, String... elements) {
if (SetUtils.immutableSetOf(elements).contains(ANY)) {
if (elements.length > 1) {
throw new IllegalStateException(
"Incompatible MethodMatchers.ANY with other predicates.");
}
return e -> true;
}
return predicate;
public MethodMatcher(@Nonnull String[] owners, @Nonnull String[] names) {
this(Arrays.asList(owners), Arrays.asList(names), List.of(), true);
}

private static <E> Predicate<E> createPredicate(
@Nonnull String element,
@Nonnull Function<String, Predicate<E>> singleElementPredicate) {
return substituteAny(singleElementPredicate.apply(element), element);
private MethodMatcher(
List<String> owners, List<String> names, List<String> arguments, boolean anyArguments) {
anyOwner = acceptsEveryChoice(owners);
anyName = acceptsEveryChoice(names);
this.anyArguments = anyArguments;
argumentSignature = arguments.toArray(String[]::new);
invokedObjectTypeStringsSerializable = owners;
methodNamesSerializable = names;
parameterTypesSerializable = arguments;
}

private static <E> Predicate<E> createPredicate(
@Nonnull String[] elements,
@Nonnull Function<List<String>, Predicate<E>> multiElementsPredicate) {
List<String> multiElements = Arrays.asList(elements);
return substituteAny(multiElementsPredicate.apply(multiElements), elements);
}

private boolean exactMatchesParameters(
@Nonnull List<Predicate<IType>> expectedTypes, @Nonnull List<IType> actualTypes) {
return actualTypes.size() == expectedTypes.size()
&& matchesParameters(expectedTypes, actualTypes);
}

private boolean matchesParameters(
@Nonnull List<Predicate<IType>> expectedTypes, @Nonnull List<IType> actualTypes) {
for (int i = 0; i < expectedTypes.size(); i++) {
if (!expectedTypes.get(i).test(actualTypes.get(i))) {
return false;
private static boolean acceptsEveryChoice(List<String> choices) {
for (String choice : choices) {
if (ANY.equals(choice)) {
if (choices.size() != 1) {
throw new IllegalStateException(
"A wildcard must be the only owner or method name choice.");
}
return true;
}
}
return true;
return false;
}

public boolean match(
@Nonnull T expression,
@Nonnull ILanguageTranslation<T> translation,
@Nonnull MatchContext matchContext) {
Optional<IType> invokedObjectType =
translation.getInvokedObjectTypeString(matchContext, expression);
Optional<String> invokedMethodName = translation.getMethodName(matchContext, expression);
List<IType> param = translation.getMethodParameterTypes(matchContext, expression);

if (invokedObjectType.isEmpty() || invokedMethodName.isEmpty()) {
return false;
}

return matchKeysInternal(
invokedObjectType.get(),
invokedMethodName.get(),
param,
translation.supportsSubsetParameterMatching());
@Nonnull MatchContext context) {
var owner = translation.getInvokedObjectTypeString(context, expression);
var name = translation.getMethodName(context, expression);
var arguments = translation.getMethodParameterTypes(context, expression);
return owner.isPresent()
&& name.isPresent()
&& acceptsCall(
owner.get(),
name.get(),
arguments,
translation.supportsSubsetParameterMatching());
}

/**
* Tree-free equivalent of {@link #match}: matches against the invoked-object type, method name
* and parameter types extracted at record time, so a detached recorded call can be matched
* without retaining its AST. Subset (constructor) parameter matching is not applicable to
* detached calls, so it is disabled here.
* Matches a detached call, which has an ordered argument list rather than constructor fields.
*/
public boolean matchKeys(
@Nonnull IType invokedObjectType,
@Nonnull String methodName,
@Nonnull List<IType> parameterTypes) {
return matchKeysInternal(invokedObjectType, methodName, parameterTypes, false);
@Nonnull IType owner, @Nonnull String name, @Nonnull List<IType> arguments) {
return acceptsCall(owner, name, arguments, false);
}

private boolean matchKeysInternal(
@Nonnull IType invokedObjectType,
@Nonnull String invokedMethodName,
@Nonnull List<IType> param,
boolean supportsSubsetParameterMatching) {
boolean typeMatches = this.invokedObjectTypeString.test(invokedObjectType);
boolean nameMatches = this.methodName.test(invokedMethodName);

if (!typeMatches || !nameMatches) {
private boolean acceptsCall(
IType owner, String name, List<IType> arguments, boolean subsetConstructors) {
if (!acceptsOwner(owner) || !(anyName || methodNamesSerializable.contains(name))) {
return false;
}

// For languages supporting subset parameter matching (e.g., Go composite literals),
// the rule matches if at least one expected parameter exists in the actual fields.
if (invokedMethodName.equals("<init>")
&& !parameterTypesSerializable.isEmpty()
&& supportsSubsetParameterMatching) {
return anyParameterMatches(param);
if (subsetConstructors && "<init>".equals(name) && !parameterTypesSerializable.isEmpty()) {
return acceptsConstructorFields(arguments);
}

return this.parameterTypes.test(param);
if (anyArguments) {
return true;
}
if (arguments.size() != argumentSignature.length) {
return false;
}
Iterator<IType> supplied = arguments.iterator();
for (String expected : argumentSignature) {
IType actual = supplied.next();
if (!ANY.equals(expected) && !actual.is(expected)) {
return false;
}
}
return true;
}

/**
* Checks that at least one expected parameter type exists in the actual parameter types. This
* supports named parameter matching (e.g., Go struct field names in composite literals) where
* the composite literal may only specify a subset of the fields the rule can detect.
*/
private boolean anyParameterMatches(@Nonnull List<IType> actualTypes) {
for (String expectedType : parameterTypesSerializable) {
if (ANY.equals(expectedType)) {
return true;
}
boolean found = actualTypes.stream().anyMatch(actual -> actual.is(expectedType));
if (found) {
private boolean acceptsOwner(IType actual) {
if (anyOwner) {
return true;
}
for (String candidate : invokedObjectTypeStringsSerializable) {
if (actual.is(candidate)) {
return true;
}
}
return false;
}

private boolean acceptsConstructorFields(List<IType> supplied) {
return parameterTypesSerializable.stream()
.anyMatch(
expected ->
ANY.equals(expected)
|| supplied.stream()
.anyMatch(actual -> actual.is(expected)));
}

@Nonnull
public List<String> getInvokedObjectTypeStringsSerializable() {
return this.invokedObjectTypeStringsSerializable;
return invokedObjectTypeStringsSerializable;
}

@Nonnull
public List<String> getMethodNamesSerializable() {
return this.methodNamesSerializable;
return methodNamesSerializable;
}

@Nonnull
public List<String> getParameterTypesSerializable() {
return this.parameterTypesSerializable;
return parameterTypesSerializable;
}
}
Loading
Loading