Merge branch '10-create-plugins' into 'master'

Resolve "Create plugins"

Closes #10

See merge request bartlomiej.pluta/esa-tool!10
This commit is contained in:
Bartłomiej Pluta
2019-05-30 16:12:48 +00:00
40 changed files with 1457 additions and 26 deletions

View File

@@ -23,7 +23,8 @@ dependencies {
compile "com.github.javaparser:javaparser-core:${javaParserVersion}"
compile "commons-io:commons-io:${commonsIoVersion}"
compile "org.fusesource.jansi:jansi:${jansiVersion}"
compile "org.apache.commons:commons-lang3:${commonsLangVersion}"
compile "org.apache.commons:commons-text:${commonsTextVersion}"
}
jar {

View File

@@ -6,4 +6,6 @@ ext {
javaParserVersion = '3.13.4'
commonsIoVersion = '2.6'
jansiVersion = '1.17.1'
commonsLangVersion = '3.8.1'
commonsTextVersion = '1.6'
}

3
esa Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
java -jar build/libs/esa-1.0-SNAPSHOT.jar $@

View File

@@ -9,6 +9,7 @@ import com.bartek.esa.file.provider.FileProvider;
import java.io.File;
import java.util.Collections;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -76,7 +77,7 @@ public abstract class Analyser {
outputPlugins = plugins.stream()
.filter(plugin -> pluginCodes
.stream()
.anyMatch(pluginCode -> plugin.getClass().getCanonicalName().equals(pluginCode))
.anyMatch(doesNameMatchPlugin(plugin))
)
.collect(Collectors.toSet());
}
@@ -85,11 +86,16 @@ public abstract class Analyser {
outputPlugins = outputPlugins.stream()
.filter(plugin -> excludeCodes
.stream()
.noneMatch(pluginCode -> plugin.getClass().getCanonicalName().equals(pluginCode))
.noneMatch(doesNameMatchPlugin(plugin))
)
.collect(Collectors.toSet());
}
return outputPlugins;
}
private Predicate<String> doesNameMatchPlugin(Plugin plugin) {
return pluginCode -> plugin.getClass().getCanonicalName().equals(pluginCode)
|| plugin.getClass().getSimpleName().equals(pluginCode);
}
}

View File

@@ -5,7 +5,9 @@ import com.bartek.esa.core.model.object.Issue;
import org.w3c.dom.Document;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public abstract class BasePlugin implements Plugin {
@@ -32,11 +34,21 @@ public abstract class BasePlugin implements Plugin {
addIssue(severity, "", lineNumber, line);
}
protected void addIssue(Severity severity, Map<String, String> descriptionModel, Integer lineNumber, String line) {
addIssue(severity, "", descriptionModel, lineNumber, line);
}
protected void addIssue(Severity severity, String descriptionCode, Integer lineNumber, String line) {
addIssue(severity, descriptionCode, new HashMap<>(), lineNumber, line);
}
protected void addIssue(Severity severity, String descriptionCode, Map<String, String> descriptionModel, Integer lineNumber, String line) {
Issue issue = Issue.builder()
.severity(severity)
.issuer(this.getClass())
.descriptionCode(descriptionCode)
.descriptionModel(descriptionModel)
.file(file)
.lineNumber(lineNumber)
.line(line)

View File

@@ -3,16 +3,21 @@ package com.bartek.esa.core.archetype;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.model.object.Issue;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.error.EsaException;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ParseProblemException;
import com.github.javaparser.Problem;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.TokenRange;
import com.github.javaparser.ast.CompilationUnit;
import io.vavr.control.Try;
import com.github.javaparser.ast.expr.Expression;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.xml.xpath.XPathConstants;
import java.io.File;
import java.util.HashMap;
import static java.lang.String.format;
public abstract class JavaPlugin extends BasePlugin {
private final GlobMatcher globMatcher;
@@ -34,8 +39,27 @@ public abstract class JavaPlugin extends BasePlugin {
return;
}
CompilationUnit compilationUnit = Try.of(() -> StaticJavaParser.parse(file)).getOrElseThrow(EsaException::new);
run(compilationUnit);
try {
CompilationUnit compilationUnit = StaticJavaParser.parse(file);
run(compilationUnit);
} catch (ParseProblemException e) {
printParsingErrorToStderr(e, file);
} catch (Exception e) {
e.printStackTrace();
}
}
private void printParsingErrorToStderr(ParseProblemException e, File file) {
e.getProblems().stream()
.map(p -> format("%s%s:\n%s\nIgnoring file...\n", file.getAbsolutePath(), getRange(p), p.getMessage()))
.forEach(System.err::println);
}
private String getRange(Problem problem) {
return problem.getLocation()
.flatMap(TokenRange::toRange)
.map(range -> format(" (line %d, col %d)", range.begin.line, range.begin.column))
.orElse("");
}
private boolean isApplicationPackageFile(File file) {
@@ -47,6 +71,7 @@ public abstract class JavaPlugin extends BasePlugin {
Issue issue = Issue.builder()
.issuer(JavaPlugin.class)
.descriptionCode(".NO_PACKAGE")
.descriptionModel(new HashMap<>())
.severity(Severity.ERROR)
.build();
@@ -56,7 +81,11 @@ public abstract class JavaPlugin extends BasePlugin {
}
String path = packageValue.getNodeValue().replaceAll("\\.", "/");
return globMatcher.fileMatchesGlobPattern(file, String.format("**/%s/**", path));
return globMatcher.fileMatchesGlobPattern(file, format("**/%s/**", path));
}
protected Integer getLineNumberFromExpression(Expression expression) {
return expression.getRange().map(r -> r.begin.line).orElse(null);
}
public abstract void run(CompilationUnit compilationUnit);

View File

@@ -3,9 +3,16 @@ package com.bartek.esa.core.archetype;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.namespace.QName;
import java.io.File;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.lang.String.format;
public abstract class XmlPlugin extends BasePlugin {
private final GlobMatcher globMatcher;
@@ -32,4 +39,21 @@ public abstract class XmlPlugin extends BasePlugin {
protected Object xPath(Document xml, String expression, QName returnType) {
return xmlHelper.xPath(xml, expression, returnType);
}
protected Stream<Node> stream(NodeList nodeList) {
return xmlHelper.stream(nodeList);
}
protected String tagString(Node node) {
Node[] attributes = new Node[node.getAttributes().getLength()];
for(int i=0; i<attributes.length; ++i) {
attributes[i] = node.getAttributes().item(i);
}
String attributesString = Arrays.stream(attributes)
.map(n -> format("%s=\"%s\"", n.getNodeName(), n.getNodeValue()))
.collect(Collectors.joining(" "));
return format("<%s %s ...", node.getNodeName(), attributesString);
}
}

View File

@@ -3,6 +3,7 @@ package com.bartek.esa.core.desc.provider;
import com.bartek.esa.core.model.object.Issue;
import com.bartek.esa.error.EsaException;
import io.vavr.control.Try;
import org.apache.commons.text.StringSubstitutor;
import javax.inject.Inject;
import java.util.Optional;
@@ -20,7 +21,7 @@ public class DescriptionProvider {
public String getDescriptionForIssue(Issue issue) {
String code = issue.getIssuer().getCanonicalName() + issue.getDescriptionCode();
String description = descriptions.getProperty(code);
String description = StringSubstitutor.replace(descriptions.getProperty(code), issue.getDescriptionModel());
return description != null && !description.isEmpty() ? description : "No description provided.";
}
}

View File

@@ -2,6 +2,8 @@ package com.bartek.esa.core.di;
import com.bartek.esa.core.desc.provider.DescriptionProvider;
import com.bartek.esa.core.executor.PluginExecutor;
import com.bartek.esa.core.helper.ParentNodeFinder;
import com.bartek.esa.core.helper.StaticScopeHelper;
import com.bartek.esa.core.java.JavaSyntaxRegexProvider;
import com.bartek.esa.core.xml.XmlHelper;
import dagger.Module;
@@ -16,17 +18,27 @@ public class CoreModule {
}
@Provides
public JavaSyntaxRegexProvider javaSyntaxRegexProvider() {
return new JavaSyntaxRegexProvider();
public DescriptionProvider descriptionProvider() {
return new DescriptionProvider();
}
@Provides
public DescriptionProvider descriptionProvider() {
return new DescriptionProvider();
public JavaSyntaxRegexProvider javaSyntaxRegexProvider() {
return new JavaSyntaxRegexProvider();
}
@Provides
public XmlHelper xmlHelper() {
return new XmlHelper();
}
@Provides
public StaticScopeHelper staticScopeHelper() {
return new StaticScopeHelper();
}
@Provides
public ParentNodeFinder parentNodeFinder() {
return new ParentNodeFinder();
}
}

View File

@@ -1,9 +1,16 @@
package com.bartek.esa.core.di;
import com.bartek.esa.core.archetype.Plugin;
import com.bartek.esa.core.helper.ParentNodeFinder;
import com.bartek.esa.core.helper.StaticScopeHelper;
import com.bartek.esa.core.java.JavaSyntaxRegexProvider;
import com.bartek.esa.core.plugin.*;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
import dagger.multibindings.IntoSet;
import java.util.HashSet;
import java.util.Set;
@@ -16,4 +23,130 @@ public class PluginModule {
public Set<Plugin> plugins() {
return new HashSet<>();
}
@Provides
@IntoSet
public Plugin loggingPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, StaticScopeHelper staticScopeHelper) {
return new LoggingPlugin(globMatcher, xmlHelper, staticScopeHelper);
}
@Provides
@IntoSet
public Plugin debuggablePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new DebuggablePlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin allowBackupPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new AllowBackupPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin permissionRaceConditionPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new PermissionsRaceConditionPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin secureRandomPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new SecureRandomPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin implicitIntentsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, JavaSyntaxRegexProvider javaSyntaxRegexProvider) {
return new ImplicitIntentsPlugin(globMatcher, xmlHelper, javaSyntaxRegexProvider);
}
@Provides
@IntoSet
public Plugin sharedUidPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new SharedUidPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin usesSdkPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new UsesSdkPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin cipherInstancePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, StaticScopeHelper staticScopeHelper) {
return new CipherInstancePlugin(globMatcher, xmlHelper, staticScopeHelper);
}
@Provides
@IntoSet
public Plugin strictModePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, StaticScopeHelper staticScopeHelper) {
return new StrictModePlugin(globMatcher, xmlHelper, staticScopeHelper);
}
@Provides
@IntoSet
public Plugin externalStoragePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, ParentNodeFinder parentNodeFinder) {
return new ExternalStoragePlugin(globMatcher, xmlHelper, parentNodeFinder);
}
@Provides
@IntoSet
public Plugin suppressWarningsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new SuppressWarningsPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin exportedComponentsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new ExportedComponentsPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin dangerousPermissionPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new DangerousPermissionPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin textInputValidationPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new TextInputValidationPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin intentFilterPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new IntentFilterPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin sqlInjectionPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new SqlInjectionPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin worldAccessPermissionsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new WorldAccessPermissionsPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin orderedAndStickyBroadcastPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new OrderedBroadcastPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin webViewPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new WebViewPlugin(globMatcher, xmlHelper);
}
@Provides
@IntoSet
public Plugin telephonyManagerPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
return new TelephonyManagerPlugin(globMatcher, xmlHelper);
}
}

View File

@@ -21,7 +21,7 @@ public class PluginExecutor {
public Set<Issue> executeForFiles(File manifest, Set<File> files, Set<Plugin> plugins, boolean debug) {
return files.stream()
.peek(file -> { if(debug) System.out.printf("File: %s", file.getAbsolutePath()); })
.peek(file -> { if(debug) System.out.printf("File: %s\n", file.getAbsolutePath()); })
.map(file -> executeForFile(manifest, file, plugins, debug))
.flatMap(Set::stream)
.collect(toSet());
@@ -29,8 +29,8 @@ public class PluginExecutor {
private Set<Issue> executeForFile(File manifest, File file, Set<Plugin> plugins, boolean debug) {
Document xmlManifest = xmlHelper.parseXml(manifest);
return plugins.parallelStream()
.peek(plugin -> { if(debug) System.out.printf(" Plugin: %s", plugin.getClass().getCanonicalName()); })
return plugins.stream()
.peek(plugin -> { if(debug) System.out.printf(" Plugin: %s\n", plugin.getClass().getCanonicalName()); })
.peek(plugin -> plugin.update(file, xmlManifest))
.filter(plugin -> plugin.supports(file))
.map(Plugin::runForIssues)

View File

@@ -0,0 +1,25 @@
package com.bartek.esa.core.helper;
import com.github.javaparser.ast.Node;
import javax.inject.Inject;
import java.util.Optional;
public class ParentNodeFinder {
@Inject
public ParentNodeFinder() {
}
public <T extends Node> Optional<T> findParentNode(Node child, Class<T> nodeType) {
Node parent = child.getParentNode().orElse(null);
while(parent != null && !parent.getClass().equals(nodeType)) {
parent = parent.getParentNode().orElse(null);
}
return Optional.ofNullable(parent).map(nodeType::cast);
}
}

View File

@@ -0,0 +1,56 @@
package com.bartek.esa.core.helper;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.expr.*;
import javax.inject.Inject;
import java.util.Optional;
import java.util.function.Predicate;
import static java.lang.String.format;
public class StaticScopeHelper {
@Inject
public StaticScopeHelper() {
}
public Predicate<MethodCallExpr> isFromScope(CompilationUnit compilationUnit, String methodName, String scope, String importScope) {
return expr -> {
boolean isFromScope = expr.getScope()
.filter(Expression::isNameExpr)
.map(Expression::asNameExpr)
.map(NameExpr::getName)
.map(SimpleName::getIdentifier)
.map(s -> s.equals(scope))
.orElse(false);
if(!isFromScope) {
isFromScope = compilationUnit.findAll(ImportDeclaration.class).stream()
.filter(ImportDeclaration::isStatic)
.filter(e -> e.getName().getIdentifier().matches(methodName))
.map(ImportDeclaration::getName)
.map(Name::getQualifier)
.flatMap(Optional::stream)
.map(Node::toString)
.anyMatch(q -> q.equals(format("%s.%s", importScope, scope)));
}
if(!isFromScope) {
isFromScope = compilationUnit.findAll(ImportDeclaration.class).stream()
.filter(ImportDeclaration::isStatic)
.filter(ImportDeclaration::isAsterisk)
.map(ImportDeclaration::getName)
.map(Name::getQualifier)
.flatMap(Optional::stream)
.map(Node::toString)
.anyMatch(q -> q.equals(importScope));
}
return isFromScope;
};
}
}

View File

@@ -1,8 +1,9 @@
package com.bartek.esa.core.java;
import javax.inject.Inject;
import com.github.javaparser.ast.expr.Expression;
import static java.lang.String.format;
import javax.inject.Inject;
import java.util.regex.Pattern;
public class JavaSyntaxRegexProvider {
@@ -11,7 +12,20 @@ public class JavaSyntaxRegexProvider {
}
public String methodInvocation(String methodName) {
return format("^%s\\s*\\($", methodName);
public Pattern constant() {
return Pattern.compile("^[A-Z0-9_$]*$");
}
public boolean isConstant(Expression expression) {
String value = expression.toString();
if(expression.isNameExpr() && constant().matcher(value).matches()) {
return true;
}
if(expression.isFieldAccessExpr()) {
return constant().matcher(expression.asFieldAccessExpr().getName().getIdentifier()).matches();
}
return false;
}
}

View File

@@ -5,6 +5,8 @@ import lombok.Builder;
import lombok.Data;
import java.io.File;
import java.util.Map;
import java.util.Optional;
@Data
@Builder
@@ -12,6 +14,7 @@ public class Issue implements Comparable {
private final Class<?> issuer;
private final Severity severity;
private final String descriptionCode;
private final Map<String, String> descriptionModel;
private final File file;
private final Integer lineNumber;
private final String line;
@@ -25,6 +28,6 @@ public class Issue implements Comparable {
return compByFile;
}
return lineNumber - another.lineNumber;
return Optional.ofNullable(lineNumber).orElse(0) - Optional.ofNullable(another.lineNumber).orElse(0);
}
}

View File

@@ -0,0 +1,30 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.inject.Inject;
import javax.xml.xpath.XPathConstants;
import java.util.Optional;
public class AllowBackupPlugin extends AndroidManifestPlugin {
@Inject
public AllowBackupPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
Node applicationNode = (Node) xPath(xml, "/manifest/application", XPathConstants.NODE);
Optional.ofNullable(applicationNode.getAttributes().getNamedItem("android:allowBackup")).ifPresentOrElse(n -> {
if (!n.getNodeValue().equals("false")) {
addIssue(Severity.WARNING, ".NO_FALSE", null, n.toString());
}
}, () -> addIssue(Severity.ERROR, ".NO_ATTR", null, null));
}
}

View File

@@ -0,0 +1,37 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.helper.StaticScopeHelper;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.MethodCallExpr;
import javax.inject.Inject;
import java.util.regex.Pattern;
public class CipherInstancePlugin extends JavaPlugin {
private static final Pattern ALGORITHM_QUALIFIER = Pattern.compile("^\"\\w+/\\w+/\\w+\"$");
private final StaticScopeHelper staticScopeHelper;
@Inject
public CipherInstancePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, StaticScopeHelper staticScopeHelper) {
super(globMatcher, xmlHelper);
this.staticScopeHelper = staticScopeHelper;
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().equals("getInstance"))
.filter(staticScopeHelper.isFromScope(compilationUnit, "getInstance", "Cipher", "javax.crypto"))
.filter(expr -> expr.getArguments().isNonEmpty())
.filter(expr -> !isFullCipherQualifier(expr.getArguments().get(0).toString()))
.forEach(expr -> addIssue(Severity.ERROR, getLineNumberFromExpression(expr), expr.toString()));
}
private boolean isFullCipherQualifier(String qualifier) {
return ALGORITHM_QUALIFIER.matcher(qualifier).matches();
}
}

View File

@@ -0,0 +1,47 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.inject.Inject;
import javax.xml.xpath.XPathConstants;
import java.util.Optional;
public class DangerousPermissionPlugin extends AndroidManifestPlugin {
@Inject
public DangerousPermissionPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
NodeList customPermissions = (NodeList) xPath(xml, "/manifest/permission", XPathConstants.NODESET);
stream(customPermissions)
.filter(this::isDangerousPermission)
.filter(this::doesNotHaveDescription)
.forEach(permission -> addIssue(Severity.WARNING, null, tagString(permission)));
}
private boolean isDangerousPermission(Node permission) {
return Optional.ofNullable(permission.getAttributes().getNamedItem("android:protectionLevel"))
.map(Node::getNodeValue)
.map(v -> v.equals("dangerous"))
.orElse(false);
}
private boolean doesNotHaveDescription(Node permission) {
Boolean doesHaveDescription = Optional.ofNullable(permission.getAttributes().getNamedItem("android:description"))
.map(Node::getNodeValue)
.map(v -> !v.isEmpty())
.orElse(false);
return !doesHaveDescription;
}
}

View File

@@ -0,0 +1,30 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.inject.Inject;
import javax.xml.xpath.XPathConstants;
import java.util.Optional;
public class DebuggablePlugin extends AndroidManifestPlugin {
@Inject
public DebuggablePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
Node applicationNode = (Node) xPath(xml, "/manifest/application", XPathConstants.NODE);
Optional.ofNullable(applicationNode.getAttributes().getNamedItem("android:debuggable")).ifPresentOrElse(n -> {
if(!n.getNodeValue().equals("false")) {
addIssue(Severity.WARNING, ".NO_FALSE", null, n.toString());
}
}, () -> addIssue(Severity.ERROR, ".NO_ATTR",null, null));
}
}

View File

@@ -0,0 +1,80 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.inject.Inject;
import javax.xml.xpath.XPathConstants;
import java.util.Map;
import java.util.Optional;
import static java.lang.String.format;
public class ExportedComponentsPlugin extends AndroidManifestPlugin {
@Inject
public ExportedComponentsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
findExportedComponents(xml, "activity");
findExportedComponents(xml, "service");
findExportedComponents(xml, "receiver");
findExportedProviders(xml);
}
private void findExportedComponents(Document xml, String component) {
NodeList exportedActivities = (NodeList) xPath(xml, format("/manifest/application/%s", component), XPathConstants.NODESET);
stream(exportedActivities)
.filter(this::isExported)
.filter(node -> doesNotHavePermission(node, "android:permission"))
.forEach(node -> addIssue(Severity.WARNING, ".NO_PERMISSION", getModel(node), null, null));
}
private Map<String, String> getModel(Node node) {
return Map.of(
"componentName", node.getAttributes().getNamedItem("android:name").getNodeValue(),
"componentType", node.getNodeName()
);
}
private void findExportedProviders(Document xml) {
NodeList exportedProviders = (NodeList) xPath(xml, "/manifest/application/provider", XPathConstants.NODESET);
stream(exportedProviders)
.filter(this::isExported)
.filter(node -> doesNotHavePermission(node, "android:writePermission")
|| doesNotHavePermission(node, "android:readPermission"))
.forEach(node -> addIssue(Severity.WARNING, ".NO_PERMISSION", getModel(node), null, null));
}
private boolean doesNotHavePermission(Node node, String permissionAttribute) {
Boolean doesHavePermission = Optional.ofNullable(node.getAttributes().getNamedItem(permissionAttribute))
.map(Node::getNodeValue)
.map(s -> !s.isEmpty())
.orElse(false);
return !doesHavePermission;
}
private boolean isExported(Node node) {
return Optional.ofNullable(node.getAttributes().getNamedItem("android:exported"))
.map(Node::getNodeValue)
.map(v -> v.equals("true"))
.orElse(false);
}
private String nodeToString(Node node) {
String nodeName = Optional.ofNullable(node.getAttributes().getNamedItem("android:name"))
.map(Node::getNodeValue)
.map(name -> format(" android:name=\"%s\"", name))
.orElse("");
return format("<%s%s ...", node.getNodeName(), nodeName);
}
}

View File

@@ -0,0 +1,44 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.helper.ParentNodeFinder;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.MethodCallExpr;
import javax.inject.Inject;
public class ExternalStoragePlugin extends JavaPlugin {
private final ParentNodeFinder parentNodeFinder;
@Inject
public ExternalStoragePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, ParentNodeFinder parentNodeFinder) {
super(globMatcher, xmlHelper);
this.parentNodeFinder = parentNodeFinder;
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("getExternalStorageDirectory|getExternalStoragePublicDirectory"))
.forEach(this::findCheckingStorageStateForAccessingExternalStorage);
}
private void findCheckingStorageStateForAccessingExternalStorage(MethodCallExpr accessingToExternalStorage) {
parentNodeFinder.findParentNode(accessingToExternalStorage, MethodDeclaration.class).ifPresent(methodDeclaration ->
findCheckingStorageStateInMethodDeclaration(accessingToExternalStorage, methodDeclaration)
);
}
private void findCheckingStorageStateInMethodDeclaration(MethodCallExpr accessingToExternalStorage, MethodDeclaration methodDeclaration) {
boolean isStateBeingChecked = methodDeclaration.findAll(MethodCallExpr.class).stream()
.anyMatch(e -> e.getName().getIdentifier().equals("getExternalStorageState"));
if (!isStateBeingChecked) {
addIssue(Severity.WARNING, getLineNumberFromExpression(accessingToExternalStorage), accessingToExternalStorage.toString());
}
}
}

View File

@@ -0,0 +1,174 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.java.JavaSyntaxRegexProvider;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.*;
import javax.inject.Inject;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class ImplicitIntentsPlugin extends JavaPlugin {
private final JavaSyntaxRegexProvider java;
@Inject
public ImplicitIntentsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, JavaSyntaxRegexProvider javaSyntaxRegexProvider) {
super(globMatcher, xmlHelper);
this.java = javaSyntaxRegexProvider;
}
@Override
public void run(CompilationUnit compilationUnit) {
checkCreatingImplicitIntents(compilationUnit);
checkCreatingPendingIntentsWithoutIntentVariable(compilationUnit);
checkCreatingPendingIntentsWithIntentVariables(compilationUnit);
checkCreatingPendingIntentsWithIntentsArraysVariables(compilationUnit);
}
// Works for:
// Intent[] myIntents = { new Intent(...), ... }
// getActivities(this, 0, myIntents, 0);
private void checkCreatingPendingIntentsWithIntentsArraysVariables(CompilationUnit compilationUnit) {
List<String> implicitIntentsArraysVariables = compilationUnit.findAll(ObjectCreationExpr.class).stream()
.filter(expr -> expr.getType().getName().getIdentifier().equals("Intent"))
.filter(this::isCreatingImplicitIntent)
.map(Node::getParentNode)
.flatMap(Optional::stream)
.map(Node::getParentNode)
.flatMap(Optional::stream)
.filter(node -> node instanceof VariableDeclarator)
.map(node -> (VariableDeclarator) node)
.map(VariableDeclarator::getName)
.map(SimpleName::getIdentifier)
.collect(Collectors.toList());
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("getActivities"))
.filter(expr -> expr.getArguments().size() >= 4)
.filter(expr -> expr.getArguments().get(2).isNameExpr())
.filter(expr -> implicitIntentsArraysVariables.contains(expr.getArguments().get(2).asNameExpr().getName().getIdentifier()))
.forEach(expr -> addIssue(Severity.VULNERABILITY, ".PENDING_INTENT", getLineNumberFromExpression(expr), expr.toString()));
}
private void checkCreatingImplicitIntents(CompilationUnit compilationUnit) {
List<String> intentVariables = getIntentVariables(compilationUnit);
checkAllSetActionMethodInvocations(compilationUnit, intentVariables);
checkCreatingImplicitIntentsUsingConstructor(compilationUnit);
}
// Works for: new Intent(Intent.ABC), new Intent(ABC)
private void checkCreatingImplicitIntentsUsingConstructor(CompilationUnit compilationUnit) {
compilationUnit.findAll(ObjectCreationExpr.class).stream()
.filter(expr -> expr.getType().getName().getIdentifier().equals("Intent"))
.filter(this::isCreatingImplicitIntent)
.forEach(objectCreation -> addIssue(Severity.INFO, ".IMPLICIT_INTENT", getLineNumberFromExpression(objectCreation), objectCreation.toString()));
}
// Returns: i for: Intent i = new Intent(...)
private List<String> getIntentVariables(CompilationUnit compilationUnit) {
return compilationUnit.findAll(VariableDeclarationExpr.class).stream()
.filter(expr -> expr.getElementType().toString().equals("Intent"))
.map(VariableDeclarationExpr::getVariables)
.flatMap(NodeList::stream)
.map(VariableDeclarator::getName)
.map(SimpleName::getIdentifier)
.collect(Collectors.toList());
}
// Checks if: new Intent(Intent.ABC), new Intent(ABC)
private boolean isCreatingImplicitIntent(ObjectCreationExpr objectCreation) {
NodeList<Expression> arguments = objectCreation.getArguments();
boolean isImplicit = false;
// Works for: new Intent(CONSTANT, ...)
if (arguments.size() == 1) {
Expression argument = arguments.get(0);
isImplicit = java.isConstant(argument);
}
// Not works for: new Intent(this, ...)
if(arguments.size() == 2) {
Expression firstArg = arguments.get(0);
Expression secondArg = arguments.get(1);
boolean isThisExpr = firstArg.isThisExpr();
boolean isTryingToGetClass = secondArg.isClassExpr();
boolean isExplicit = isThisExpr || isTryingToGetClass;
isImplicit = !isExplicit;
}
return isImplicit;
}
// Works for: i.setAction(...)
private void checkAllSetActionMethodInvocations(CompilationUnit compilationUnit, List<String> intentVariables) {
compilationUnit.findAll(MethodCallExpr.class).forEach(methodCall -> {
boolean isCalledOnIntentObject = methodCall.getScope()
.map(Expression::toString)
.filter(intentVariables::contains)
.isPresent();
if(isCalledOnIntentObject && methodCall.getName().getIdentifier().equals("setAction")) {
addIssue(Severity.INFO, ".IMPLICIT_INTENT", getLineNumberFromExpression(methodCall), methodCall.toString());
}
});
}
// Works for:
// Intent myIntent = new Intent(...)
// getActivity(this, 0, myIntent, 0);
private void checkCreatingPendingIntentsWithIntentVariables(CompilationUnit compilationUnit) {
List<String> implicitIntentsVariables = compilationUnit.findAll(ObjectCreationExpr.class).stream()
.filter(expr -> expr.getType().getName().getIdentifier().equals("Intent"))
.filter(this::isCreatingImplicitIntent)
.map(Node::getParentNode)
.flatMap(Optional::stream)
.filter(node -> node instanceof VariableDeclarator)
.map(node -> (VariableDeclarator) node)
.map(VariableDeclarator::getName)
.map(SimpleName::getIdentifier)
.collect(Collectors.toList());
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("getActivity|getBroadcast|getService"))
.filter(expr -> expr.getArguments().size() >= 4)
.filter(expr -> expr.getArguments().get(2).isNameExpr())
.filter(expr -> implicitIntentsVariables.contains(expr.getArguments().get(2).asNameExpr().getName().getIdentifier()))
.forEach(expr -> addIssue(Severity.VULNERABILITY, ".PENDING_INTENT", getLineNumberFromExpression(expr), expr.toString()));
}
private void checkCreatingPendingIntentsWithoutIntentVariable(CompilationUnit compilationUnit) {
// Works for: getActivity(this, 0, new Intent(...), 0)
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("getActivity|getBroadcast|getService"))
.filter(expr -> expr.getArguments().size() >= 4)
.filter(expr -> expr.getArguments().get(2).isObjectCreationExpr())
.filter(expr -> isCreatingImplicitIntent(expr.getArguments().get(2).asObjectCreationExpr()))
.forEach(expr -> addIssue(Severity.VULNERABILITY, ".PENDING_INTENT", getLineNumberFromExpression(expr), expr.toString()));
// Works for: getActivities(this, 0, new Intent[] { new Intent(...), ...}, 0)
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("getActivities"))
.filter(expr -> expr.getArguments().size() >= 4)
.filter(expr -> expr.getArguments().get(2).isArrayCreationExpr())
.filter(expr -> isCreatingImplicitIntentsArray(expr.getArguments().get(2).asArrayCreationExpr()))
.forEach(expr -> addIssue(Severity.VULNERABILITY, ".PENDING_INTENT", getLineNumberFromExpression(expr), expr.toString()));
}
private boolean isCreatingImplicitIntentsArray(ArrayCreationExpr arrayCreationExpr) {
return arrayCreationExpr.getInitializer()
.map(this::isCreatingImplicitIntentsArray)
.orElse(false);
}
private boolean isCreatingImplicitIntentsArray(ArrayInitializerExpr arrayInitializerExpr) {
return arrayInitializerExpr.getValues().stream()
.filter(Expression::isObjectCreationExpr)
.map(Expression::asObjectCreationExpr)
.anyMatch(this::isCreatingImplicitIntent);
}
}

View File

@@ -0,0 +1,51 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.inject.Inject;
import java.util.Map;
public class IntentFilterPlugin extends AndroidManifestPlugin {
@Inject
public IntentFilterPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
NodeList filters = xml.getElementsByTagName("intent-filter");
stream(filters)
.filter(this::isNotMainActivity)
.map(Node::getParentNode)
.forEach(n -> addIssue(Severity.INFO, getModel(n), null, null));
}
private Map<String, String> getModel(Node node) {
return Map.of(
"componentType", node.getNodeName(),
"componentName", node.getAttributes().getNamedItem("android:name").getNodeValue()
);
}
private boolean isNotMainActivity(Node filter) {
long mainActivityIntentFilters = stream(filter.getChildNodes())
.filter(n -> n.getNodeName().matches("action|category"))
.map(n -> n.getAttributes().getNamedItem("android:name"))
.map(Node::getNodeValue)
.filter(v -> v.equals("android.intent.action.MAIN") || v.equals("android.intent.category.LAUNCHER"))
.count();
long currentIntentFilters = stream(filter.getChildNodes())
.filter(n -> n.getNodeName().matches("action|category"))
.count();
return mainActivityIntentFilters != currentIntentFilters;
}
}

View File

@@ -0,0 +1,29 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.helper.StaticScopeHelper;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.MethodCallExpr;
import javax.inject.Inject;
public class LoggingPlugin extends JavaPlugin {
private final StaticScopeHelper staticScopeHelper;
@Inject
public LoggingPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, StaticScopeHelper staticScopeHelper) {
super(globMatcher, xmlHelper);
this.staticScopeHelper = staticScopeHelper;
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("v|d|i|w|e|wtf"))
.filter(staticScopeHelper.isFromScope(compilationUnit, "v|d|i|w|e|wtf", "Log", "android.util"))
.forEach(expr -> addIssue(Severity.INFO, getLineNumberFromExpression(expr), expr.toString()));
}
}

View File

@@ -0,0 +1,25 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.MethodCallExpr;
import javax.inject.Inject;
public class OrderedBroadcastPlugin extends JavaPlugin {
@Inject
public OrderedBroadcastPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("sendOrderedBroadcast|sendOrderedBroadcastAsUser|sendStickyOrderedBroadcast|sendStickyOrderedBroadcastAsUser"))
.forEach(expr -> addIssue(Severity.WARNING, getLineNumberFromExpression(expr), expr.toString()));
}
}

View File

@@ -0,0 +1,40 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.inject.Inject;
import javax.xml.xpath.XPathConstants;
import java.util.Map;
import static java.lang.Integer.parseInt;
public class PermissionsRaceConditionPlugin extends AndroidManifestPlugin {
@Inject
public PermissionsRaceConditionPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
boolean isAnyPermissionDefined = ((NodeList) xPath(xml, "/manifest/permission", XPathConstants.NODESET)).getLength() > 0;
if(isAnyPermissionDefined) {
Node usesSdkNode = (Node) xPath(xml, "/manifest/uses-sdk", XPathConstants.NODE);
Node minSdkVersionNode = usesSdkNode.getAttributes().getNamedItem("android:minSdkVersion");
int minSdkVersion = parseInt(minSdkVersionNode.getNodeValue());
if(minSdkVersion < 21) {
addIssue(Severity.VULNERABILITY, getModel(minSdkVersion), null, minSdkVersionNode.toString());
}
}
}
private Map<String, String> getModel(int minSdkVersion) {
return Map.of("minSdkVersion", Integer.toString(minSdkVersion));
}
}

View File

@@ -0,0 +1,26 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import javax.inject.Inject;
public class SecureRandomPlugin extends JavaPlugin {
@Inject
public SecureRandomPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(ObjectCreationExpr.class).stream()
.filter(expr -> expr.getType().getName().getIdentifier().equals("SecureRandom"))
.filter(expr -> expr.getArguments().isNonEmpty())
.forEach(expr -> addIssue(Severity.VULNERABILITY, getLineNumberFromExpression(expr), expr.toString()));
}
}

View File

@@ -0,0 +1,28 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.inject.Inject;
import javax.xml.xpath.XPathConstants;
import java.util.Optional;
public class SharedUidPlugin extends AndroidManifestPlugin {
@Inject
public SharedUidPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
Node manifestNode = (Node) xPath(xml, "/manifest", XPathConstants.NODE);
Optional.ofNullable(manifestNode.getAttributes().getNamedItem("android:sharedUserId")).ifPresent(node -> {
addIssue(Severity.VULNERABILITY, null, node.toString());
});
}
}

View File

@@ -0,0 +1,25 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.MethodCallExpr;
import javax.inject.Inject;
public class SqlInjectionPlugin extends JavaPlugin {
@Inject
public SqlInjectionPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().equals("rawQuery"))
.forEach(expr -> addIssue(Severity.VULNERABILITY, getLineNumberFromExpression(expr), expr.toString()));
}
}

View File

@@ -0,0 +1,29 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.helper.StaticScopeHelper;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.MethodCallExpr;
import javax.inject.Inject;
public class StrictModePlugin extends JavaPlugin {
private final StaticScopeHelper staticScopeHelper;
@Inject
public StrictModePlugin(GlobMatcher globMatcher, XmlHelper xmlHelper, StaticScopeHelper staticScopeHelper) {
super(globMatcher, xmlHelper);
this.staticScopeHelper = staticScopeHelper;
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().equals("setThreadPolicy"))
.filter(staticScopeHelper.isFromScope(compilationUnit, "setThreadPolicy", "StrictMode", "android.os"))
.forEach(expr -> addIssue(Severity.WARNING, getLineNumberFromExpression(expr), expr.toString()));
}
}

View File

@@ -0,0 +1,25 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.AnnotationExpr;
import javax.inject.Inject;
public class SuppressWarningsPlugin extends JavaPlugin {
@Inject
public SuppressWarningsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(AnnotationExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().equals("SuppressWarnings"))
.forEach(expr -> addIssue(Severity.WARNING, getLineNumberFromExpression(expr), expr.toString()));
}
}

View File

@@ -0,0 +1,26 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.CastExpr;
import javax.inject.Inject;
public class TelephonyManagerPlugin extends JavaPlugin {
@Inject
public TelephonyManagerPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(CastExpr.class).stream()
.filter(expr -> expr.getType().isClassOrInterfaceType())
.filter(expr -> expr.getType().asClassOrInterfaceType().getName().getIdentifier().equals("TelephonyManager"))
.forEach(expr -> addIssue(Severity.INFO, getLineNumberFromExpression(expr), expr.toString()));
}
}

View File

@@ -0,0 +1,36 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.ResourceLayoutPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.inject.Inject;
import java.util.Optional;
public class TextInputValidationPlugin extends ResourceLayoutPlugin {
@Inject
public TextInputValidationPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
NodeList editTextNodes = xml.getElementsByTagName("EditText");
stream(editTextNodes)
.filter(this::doesNotHaveInputType)
.forEach(n -> addIssue(Severity.WARNING, null, tagString(n)));
}
private boolean doesNotHaveInputType(Node editText) {
Boolean doesHaveInputType = Optional.ofNullable(editText.getAttributes().getNamedItem("android:inputType"))
.map(Node::getNodeValue)
.map(v -> !v.isEmpty())
.orElse(false);
return !doesHaveInputType;
}
}

View File

@@ -0,0 +1,34 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.AndroidManifestPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.inject.Inject;
import javax.xml.xpath.XPathConstants;
import java.util.Map;
import java.util.Optional;
public class UsesSdkPlugin extends AndroidManifestPlugin {
@Inject
public UsesSdkPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
protected void run(Document xml) {
Optional.ofNullable((Node) xPath(xml, "/manifest/uses-sdk", XPathConstants.NODE)).ifPresentOrElse(usesSdkNode -> {
if(usesSdkNode.getAttributes().getNamedItem("android:minSdkVersion") == null) {
addIssue(Severity.ERROR, ".USES_SDK.NO_MIN_SDK_VERSION", null, null);
}
Optional.ofNullable(usesSdkNode.getAttributes().getNamedItem("android:maxSdkVersion")).ifPresent(maxSdkVersion ->
addIssue(Severity.ERROR, ".USES_SDK.MAX_SDK_VERSION", Map.of("maxSdkVersion", maxSdkVersion.getNodeValue()),null, maxSdkVersion.toString())
);
}, () -> addIssue(Severity.ERROR, ".NO_USES_SDK", null, null));
}
}

View File

@@ -0,0 +1,51 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import javax.inject.Inject;
public class WebViewPlugin extends JavaPlugin {
private static final String SETTINGS_METHODS = "addJavascriptInterface|setJavaScriptEnabled|setWebContentsDebuggingEnabled|setAllowFileAccess|setDomStorageEnabled";
@Inject
public WebViewPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches(SETTINGS_METHODS))
.forEach(this::issueMethod);
}
private void issueMethod(MethodCallExpr methodCall) {
switch (methodCall.getName().getIdentifier()) {
case "addJavascriptInterface":
addIssue(Severity.VULNERABILITY, ".JS_INTERFACE", getLineNumberFromExpression(methodCall), methodCall.toString());
break;
case "setJavaScriptEnabled":
issueSettingsMethod(methodCall, ".JS_ENABLED");
break;
case "setWebContentsDebuggingEnabled":
issueSettingsMethod(methodCall, ".DEBUGGING_ENABLED");
break;
case "setAllowFileAccess":
issueSettingsMethod(methodCall, ".ALLOW_FILE_ACCESS");
break;
}
}
private void issueSettingsMethod(MethodCallExpr methodCall, String descriptionCode) {
Expression firstArg = methodCall.getArguments().get(0);
if (firstArg.isBooleanLiteralExpr() && firstArg.asBooleanLiteralExpr().getValue()) {
addIssue(Severity.WARNING, descriptionCode, getLineNumberFromExpression(methodCall), methodCall.toString());
}
}
}

View File

@@ -0,0 +1,39 @@
package com.bartek.esa.core.plugin;
import com.bartek.esa.core.archetype.JavaPlugin;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.xml.XmlHelper;
import com.bartek.esa.file.matcher.GlobMatcher;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.NameExpr;
import javax.inject.Inject;
import java.util.Map;
public class WorldAccessPermissionsPlugin extends JavaPlugin {
@Inject
public WorldAccessPermissionsPlugin(GlobMatcher globMatcher, XmlHelper xmlHelper) {
super(globMatcher, xmlHelper);
}
@Override
public void run(CompilationUnit compilationUnit) {
compilationUnit.findAll(NameExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("MODE_WORLD_(READABLE|WRITEABLE)"))
.forEach(expr -> addIssue(Severity.ERROR, getModel(expr), getLineNumberFromExpression(expr), expr.toString()));
compilationUnit.findAll(FieldAccessExpr.class).stream()
.filter(expr -> expr.getName().getIdentifier().matches("MODE_WORLD_(READABLE|WRITEABLE)"))
.forEach(expr -> addIssue(Severity.ERROR, getModel(expr), getLineNumberFromExpression(expr), expr.toString()));
}
private Map<String, String> getModel(NameExpr expression) {
return Map.of("exprName", expression.getName().getIdentifier());
}
private Map<String, String> getModel(FieldAccessExpr expression) {
return Map.of("exprName", expression.getName().getIdentifier());
}
}

View File

@@ -3,6 +3,8 @@ package com.bartek.esa.core.xml;
import com.bartek.esa.error.EsaException;
import io.vavr.control.Try;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.inject.Inject;
import javax.xml.namespace.QName;
@@ -10,6 +12,8 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.util.Arrays;
import java.util.stream.Stream;
public class XmlHelper {
@@ -28,4 +32,13 @@ public class XmlHelper {
return Try.of(() -> XPathFactory.newDefaultInstance().newXPath().evaluate(expression, xml, returnType))
.getOrElseThrow(EsaException::new);
}
public Stream<Node> stream(NodeList nodeList) {
Node[] nodes = new Node[nodeList.getLength()];
for (int i=0; i<nodeList.getLength(); ++i) {
nodes[i] = nodeList.item(i);
}
return Arrays.stream(nodes);
}
}

View File

@@ -1,6 +1,7 @@
package com.bartek.esa.formatter.formatter;
import com.bartek.esa.core.desc.provider.DescriptionProvider;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.model.object.Issue;
import com.bartek.esa.formatter.archetype.Formatter;
import org.fusesource.jansi.Ansi;
@@ -16,6 +17,10 @@ import static org.fusesource.jansi.Ansi.Color.*;
import static org.fusesource.jansi.Ansi.ansi;
public class ColorFormatter implements Formatter {
private static final Ansi.Color INFO_COLOR = GREEN;
private static final Ansi.Color WARNING_COLOR = YELLOW;
private static final Ansi.Color ERROR_COLOR = MAGENTA;
private static final Ansi.Color VULNERABILITY_COLOR = RED;
private final DescriptionProvider descriptionProvider;
@@ -39,6 +44,7 @@ public class ColorFormatter implements Formatter {
.collect(Collectors.joining());
System.out.println(format.substring(0, format.length() - 2));
System.out.println(printSummary(issues));
AnsiConsole.systemUninstall();
}
@@ -73,8 +79,9 @@ public class ColorFormatter implements Formatter {
.map(file -> ansi
.fg(BLUE)
.a("File: ")
.reset()
.fg(CYAN)
.a(file.getAbsolutePath())
.reset()
.a("\n"))
.orElse(ansi);
}
@@ -86,16 +93,40 @@ public class ColorFormatter implements Formatter {
.fg(CYAN)
.a("Line");
Optional.ofNullable(issue.getLineNumber()).ifPresentOrElse(
number -> ansi.a(" ").a(number).a(": "),
number -> ansi.a(" ").fg(MAGENTA).a(number).fg(CYAN).a(": "),
() -> ansi.a(": ")
);
ansi.reset().a(line).a("\n");
ansi.fg(BLUE).a(line).reset().a("\n");
});
return ansi;
}
private String printSummary(Set<Issue> issues) {
Ansi ansi = ansi();
ansi.a("\n--- Total:\n");
Arrays.stream(Severity.values())
.forEach(severity -> ansi.fg(getColorForSeverity(severity))
.a(severity.name())
.a(": ")
.reset()
.a(countIssuesBySeverity(issues, severity))
.a("\n"));
return ansi.toString();
}
private long countIssuesBySeverity(Set<Issue> issues, Severity severity) {
return issues.stream()
.map(Issue::getSeverity)
.filter(severity::equals)
.count();
}
private Ansi.Color getColorForSeverity(Issue issue) {
switch (issue.getSeverity()) {
return getColorForSeverity(issue.getSeverity());
}
private Ansi.Color getColorForSeverity(Severity severity) {
switch (severity) {
case INFO:
return GREEN;
case WARNING:

View File

@@ -1,10 +1,12 @@
package com.bartek.esa.formatter.formatter;
import com.bartek.esa.core.desc.provider.DescriptionProvider;
import com.bartek.esa.core.model.enumeration.Severity;
import com.bartek.esa.core.model.object.Issue;
import com.bartek.esa.formatter.archetype.Formatter;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -30,6 +32,7 @@ public class SimpleFormatter implements Formatter {
.collect(Collectors.joining());
System.out.println(format.substring(0, format.length() - 2));
System.out.println(printSummary(issues));
}
private String format(Issue issue) {
@@ -71,4 +74,23 @@ public class SimpleFormatter implements Formatter {
format.append(line).append("\n");
});
}
private String printSummary(Set<Issue> issues) {
StringBuilder format = new StringBuilder();
format.append("\n--- Total:\n");
Arrays.stream(Severity.values())
.forEach(severity -> format
.append(severity.name())
.append(": ")
.append(countIssuesBySeverity(issues, severity))
.append("\n"));
return format.toString();
}
private long countIssuesBySeverity(Set<Issue> issues, Severity severity) {
return issues.stream()
.map(Issue::getSeverity)
.filter(severity::equals)
.count();
}
}

View File

@@ -1,4 +1,142 @@
com.bartek.esa.core.archetype.JavaPlugin.NO_PACKAGE=There is no package defined in AndroidManifest.xml file. \n\
Package should be defined as attribute of <manifest> tag.\n\
For example: <manifest package="com.bartek.esa.test">\n\
Please fix it to use this tool.
Please fix it to use this tool.
com.bartek.esa.core.plugin.LoggingPlugin=Potential data leakage in logs. \n\
Logging method was detected. Please check if no sensitive data is logged there.
com.bartek.esa.core.plugin.DebuggablePlugin.NO_ATTR=There is no android:debuggable option. Potential data leakage. \n\
The android:debuggable option was not found in the AndroidManifest.xml file. \n\
To avoid any potential data leakage in the future, please explicitly set this flag to false. \n\
The attribute should be placed in <application> tag.\n\
For example: <application android:debuggable="false">
com.bartek.esa.core.plugin.DebuggablePlugin.NO_FALSE=The android:debuggable is set to 'true'. Potential data leakage. \n\
The android:debuggable option in AndroidManifest.xml is set to 'true'. \n\
This will cause application to be debuggable and can result in \
security issues and data leakage on the production environment. \n\
Consider setting it to 'false'.
com.bartek.esa.core.plugin.AllowBackupPlugin.NO_ATTR=There is no android:allowBackup option. Potential data leakage. \n\
The android:allowBackup option was not found in the AndroidManifest.xml file. \n\
To avoid any potential data theft in the future, please explicitly set this flag to false. \n\
The attribute should be placed in <application> tag.\n\
For example: <application android:allowBackup="false">
com.bartek.esa.core.plugin.AllowBackupPlugin.NO_FALSE=The android:allowBackup is set to 'true'. Potential data leakage. \n\
The android:allowBackup option in AndroidManifest.xml is set to 'true'. \n\
This will allow accessing the backups via adb if device has USB debugging enabled.\n\
Consider setting it to 'false'.
com.bartek.esa.core.plugin.PermissionsRaceConditionPlugin=Potential permissions race condition vulnerability. \n\
There are declared custom permissions in AndroidManifest.xml and the minimal API version is set to ${minSdkVersion} that is less than 21.\n\
It means that declared permissions can be obtained by malicious application installed before and without need of having proper signature.\n\
Consider setting minimal API version to 21 at least.
com.bartek.esa.core.plugin.SecureRandomPlugin=Initializing SecureRandom object with custom seed. \n\
Specifying custom seed for SecureRandom can produce predictable sequence of numbers. \n\
Please create SecureRandom object without any arguments instead.
com.bartek.esa.core.plugin.ImplicitIntentsPlugin.IMPLICIT_INTENT=Creating implicit intent. Potential data leakage. \n\
Implicit intents can be abused in man-in-the-middle attack. Malicious application can hijack intent and start its\n\
activity/send service etc. to steal sent data. \n\
Also make sure that no sensitive information is passing to this intent.
com.bartek.esa.core.plugin.ImplicitIntentsPlugin.PENDING_INTENT=Creating pending intent from implicit intent. Potential permission escalation vulnerability\n\
As far as pending intents contains UID of issuing application and its permissions, they should be fed only\n\
with explicit intents to avoid permission escalation vulnerability.
com.bartek.esa.core.plugin.SharedUidPlugin=Making use of shared UserID.\n\
Shared UserID violates a sandbox nature of Android system. All applications working with the same UID work also \n\
within the same process and share granted permissions, resources and so on.\n\
Remember, that if you really want to use this feature, after publishing your app, you won't be able to change it anymore.
com.bartek.esa.core.plugin.UsesSdkPlugin.NO_USES_SDK=There is no <uses-sdk> defined in AndroidManifest.xml file.\n\
In order to use this tool, <uses-sdk> should be defined in AndroidManifest.xml with android:minSdkVersion attribute at least.\n\
This element should be placed below the root (<manifest>) level.\n\
For example:\n\
<manifest>\n\
\t<uses-sdk android:minSdkVersion="23">\n\
\t...\n\
</manifest>
com.bartek.esa.core.plugin.UsesSdkPlugin.USES_SDK.NO_MIN_SDK_VERSION=There is no minSdkVersion defined in AndroidManifest.xml file.\n\
In order to use this tool, minimal SDK version should be provided as the attribute of <uses-sdk> element.\n\
For example: <uses-sdk android:minSdkVersion="23">
com.bartek.esa.core.plugin.UsesSdkPlugin.USES_SDK.MAX_SDK_VERSION=Application defines an upper limit for API version.\n\
The android:maxSdkVersion is set to ${maxSdkVersion} in AndroidManifest.xml.\n\
There is no need to limit available platforms for application.\n\
Furthermore it can cause unexpected application uninstall\n\
on upgrading Android version (along with API which can exceed defined maximal API version).
com.bartek.esa.core.plugin.CipherInstancePlugin=Not fully-qualified algorithm name provided in Cipher.getInstance() method.\n\
Passing a shortcut instead of fully-qualified algorithm name in Cipher.getInstance() method is not portable across providers\n\
and can impact the system low secure than intended to be.\n\
Fully-qualified name matches the pattern: algorithm/mode/pattern\n\
For example: AES/CBC/PKCS5Padding
com.bartek.esa.core.plugin.StrictModePlugin=Strict mode is turned on.\n\
Strict mode was found in the file. Remember to delete it before publishing.
com.bartek.esa.core.plugin.ExternalStoragePlugin=External storage state is not checked.\n\
There is attempt to access to external storage without checking its state.\n\
External storage state should be checked through 'Environment.getExternalStorageState()' method.
com.bartek.esa.core.plugin.SuppressWarningsPlugin=@SuppressWarnings annotation was found.\n\
The @SuppressWarnings annotation might be hiding useful warnings.\n\
Consider removing it.
com.bartek.esa.core.plugin.ExportedComponentsPlugin.NO_PERMISSION=Exported ${componentType}.\n\
The ${componentType} with name '${componentName}' is exported but not protected by any permission. \n\
It means any malicious application could make use of the component. \n\
Consider using 'android:permission' tag and adding custom permission to protect it.
com.bartek.esa.core.plugin.DangerousPermissionPlugin=Custom permission without description.\n\
Custom permission with 'dangerous' protection level was found and it doesn't have any description.\n\
As long as the permission requires user attention, he should have provided a meaningful description about\n\
permission.
com.bartek.esa.core.plugin.TextInputValidationPlugin=Input type is no selected.\n\
The EditText view doesn't have a input type selected.\n\
Consider associating a input type with this view.\n\
For example: <EditText android:inputType="number" ...
com.bartek.esa.core.plugin.IntentFilterPlugin=Implemented intent filter.\n\
The ${componentType} with name '${componentName}' does have a intent filter declared. \n\
It means, that the component is implicitly exposed to public.\n\
Consider removing intent filter.\n\
Also be aware, that intent filter is not a security tool. It can be easily omitted.
com.bartek.esa.core.plugin.SqlInjectionPlugin='rawQuery' method detected. Potential SQL injection attack.\n\
'rawQuery' method should be avoided because of possibility to inject SQL code.
com.bartek.esa.core.plugin.WorldAccessPermissionsPlugin=World access permissions detected. Potential data leakage.\n\
The deprecated '${exprName}' constant has been found and it can be risky to use.\n\
It grants world access permission to selected resource.\n\
Consider using less permissive mode.
com.bartek.esa.core.plugin.OrderedBroadcastPlugin=Sending ordered broadcast. Potential broadcast theft.\n\
Malicious applications can intercept ordered broadcasts, stop their propagation and resend with malicious data.
com.bartek.esa.core.plugin.WebViewPlugin.JS_INTERFACE=WebView with JavaScript interface. Potential malicious code injection.\n\
The WebView uses 'addJavascriptInterface' method which exposes public methods to JavaScript code. Loading JavaScript code \n\
from untrusted sources is a major security violation and should never be used.
com.bartek.esa.core.plugin.WebViewPlugin.JS_ENABLED=JavaScript enabled in WebView.\n\
The WebView has enabled JavaScript code execution. This can effect in XSS attack.\n\
Consider disabling JavaScript in WebView.
com.bartek.esa.core.plugin.WebViewPlugin.DEBUGGING_ENABLED=JavaScript debugging enabled in WebView.\n\
The WebView has enabled JavaScript code debugging. This can effect in data leakage from WebView component.\n\
Consider disabling JavaScript debugging in WebView.
com.bartek.esa.core.plugin.WebViewPlugin.ALLOW_FILE_ACCESS=Access to file system from WebView.\n\
The WebView has granted access to private files. Loading content from untrusted source may effect with \n\
accessing private files by malicious site/application.\n\
Consider disabling this option.
com.bartek.esa.core.plugin.TelephonyManagerPlugin=Usage of TelephonyManager.\n\
The TelephonyManager service is detected to be used.\n\
Make sure that no sensitive data (like IMEI, phone number etc.) exits the application.