10: Create ExportedComponentsPlugin
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,4 +95,10 @@ public class PluginModule {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
|
||||
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, format(".%s.NO_PERMISSION", component.toUpperCase()), null, tagString(node)));
|
||||
}
|
||||
|
||||
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, ".PROVIDER.NO_PERMISSION", null, tagString(node)));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +86,24 @@ com.bartek.esa.core.plugin.ExternalStoragePlugin=External storage state is not c
|
||||
|
||||
com.bartek.esa.core.plugin.SuppressWarningsPlugin=@SuppressWarnings annotation was found.\n\
|
||||
The @SuppressWarnings annotation might be hiding useful warnings.\n\
|
||||
Consider removing it.
|
||||
Consider removing it.
|
||||
|
||||
com.bartek.esa.core.plugin.ExportedComponentsPlugin.ACTIVITY.NO_PERMISSION=Exported activity.\n\
|
||||
The activity 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.ExportedComponentsPlugin.SERVICE.NO_PERMISSION=Exported service.\n\
|
||||
The service 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.ExportedComponentsPlugin.RECEIVER.NO_PERMISSION=Exported broadcast receiver.\n\
|
||||
The broadcast receiver 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.ExportedComponentsPlugin.PROVIDER.NO_PERMISSION=Exported content provider. Potential data leakage.\n\
|
||||
The content provider is exported but not protected by any permissions. \n\
|
||||
It means any malicious application could make use of data provided by the component and/or insert some new data. \n\
|
||||
Consider using 'android:readPermission' and 'android:writePermission' tags and adding custom permission to protect it.
|
||||
|
||||
Reference in New Issue
Block a user