Merge branch 'unified-scanner-signatures' into 'master'
cached scanner signatures Closes #1008 See merge request fdroid/fdroidserver!1198
This commit is contained in:
commit
7accb96b9e
@ -987,7 +987,7 @@ def main():
|
|||||||
if not options.appid and not options.all:
|
if not options.appid and not options.all:
|
||||||
parser.error("option %s: If you really want to build all the apps, use --all" % "all")
|
parser.error("option %s: If you really want to build all the apps, use --all" % "all")
|
||||||
|
|
||||||
config = common.read_config(options)
|
config = common.read_config(opts=options)
|
||||||
|
|
||||||
if config['build_server_always']:
|
if config['build_server_always']:
|
||||||
options.server = True
|
options.server = True
|
||||||
|
@ -76,7 +76,7 @@ from fdroidserver.exception import FDroidException, VCSException, NoSubmodulesEx
|
|||||||
BuildException, VerificationException, MetaDataException
|
BuildException, VerificationException, MetaDataException
|
||||||
from .asynchronousfilereader import AsynchronousFileReader
|
from .asynchronousfilereader import AsynchronousFileReader
|
||||||
|
|
||||||
from . import apksigcopier
|
from . import apksigcopier, common
|
||||||
|
|
||||||
|
|
||||||
# The path to this fdroidserver distribution
|
# The path to this fdroidserver distribution
|
||||||
@ -130,7 +130,6 @@ default_config = {
|
|||||||
'ant': "ant",
|
'ant': "ant",
|
||||||
'mvn3': "mvn",
|
'mvn3': "mvn",
|
||||||
'gradle': os.path.join(FDROID_PATH, 'gradlew-fdroid'),
|
'gradle': os.path.join(FDROID_PATH, 'gradlew-fdroid'),
|
||||||
'gradle_version_dir': str(Path.home() / '.cache/fdroidserver/gradle'),
|
|
||||||
'sync_from_local_copy_dir': False,
|
'sync_from_local_copy_dir': False,
|
||||||
'allow_disabled_algorithms': False,
|
'allow_disabled_algorithms': False,
|
||||||
'per_app_repos': False,
|
'per_app_repos': False,
|
||||||
@ -319,6 +318,31 @@ def fill_config_defaults(thisconfig):
|
|||||||
ndk_paths[ndkdict['release']] = ndk_paths.pop(k)
|
ndk_paths[ndkdict['release']] = ndk_paths.pop(k)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if 'cachedir_scanner' not in thisconfig:
|
||||||
|
thisconfig['cachedir_scanner'] = str(Path(thisconfig['cachedir']) / 'scanner')
|
||||||
|
if 'gradle_version_dir' not in thisconfig:
|
||||||
|
thisconfig['gradle_version_dir'] = str(Path(thisconfig['cachedir']) / 'gradle')
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(opts=None):
|
||||||
|
"""Get config instace. This function takes care of initializing config data before returning it."""
|
||||||
|
global config, options
|
||||||
|
|
||||||
|
if config is not None:
|
||||||
|
return config
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
common.fill_config_defaults(config)
|
||||||
|
common.read_config(opts=opts)
|
||||||
|
|
||||||
|
# make sure these values are available in common.py even if they didn't
|
||||||
|
# declare global in a scope
|
||||||
|
common.config = config
|
||||||
|
if opts is not None:
|
||||||
|
common.options = opts
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def regsub_file(pattern, repl, path):
|
def regsub_file(pattern, repl, path):
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
|
@ -45,3 +45,10 @@ class BuildException(FDroidException):
|
|||||||
|
|
||||||
class VerificationException(FDroidException):
|
class VerificationException(FDroidException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationException(FDroidException):
|
||||||
|
def __init__(self, value=None, detail=None):
|
||||||
|
super().__init__()
|
||||||
|
self.value = value
|
||||||
|
self.detail = detail
|
||||||
|
@ -16,29 +16,28 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import imghdr
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import json
|
||||||
|
import imghdr
|
||||||
|
import logging
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import itertools
|
||||||
|
import traceback
|
||||||
|
import urllib.request
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from collections import namedtuple
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
import logging
|
from pathlib import Path
|
||||||
import itertools
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from . import _
|
from . import _
|
||||||
from . import common
|
from . import common
|
||||||
from . import metadata
|
from . import metadata
|
||||||
from .exception import BuildException, VCSException
|
from .exception import BuildException, VCSException, ConfigurationException
|
||||||
from . import scanner
|
from . import scanner
|
||||||
|
|
||||||
config = None
|
|
||||||
options = None
|
options = None
|
||||||
|
|
||||||
DEFAULT_JSON_PER_BUILD = {'errors': [], 'warnings': [], 'infos': []} # type: ignore
|
DEFAULT_JSON_PER_BUILD = {'errors': [], 'warnings': [], 'infos': []} # type: ignore
|
||||||
@ -47,66 +46,8 @@ json_per_build = deepcopy(DEFAULT_JSON_PER_BUILD)
|
|||||||
MAVEN_URL_REGEX = re.compile(r"""\smaven\s*(?:{.*?(?:setUrl|url)|\((?:url)?)\s*=?\s*(?:uri)?\(?\s*["']?([^\s"']+)["']?[^})]*[)}]""",
|
MAVEN_URL_REGEX = re.compile(r"""\smaven\s*(?:{.*?(?:setUrl|url)|\((?:url)?)\s*=?\s*(?:uri)?\(?\s*["']?([^\s"']+)["']?[^})]*[)}]""",
|
||||||
re.DOTALL)
|
re.DOTALL)
|
||||||
|
|
||||||
CODE_SIGNATURES = {
|
|
||||||
exp: re.compile(r'.*' + exp, re.IGNORECASE) for exp in [
|
|
||||||
r'com/google/firebase',
|
|
||||||
r'com/google/android/gms',
|
|
||||||
r'com/google/android/play/core',
|
|
||||||
r'com/google/tagmanager',
|
|
||||||
r'com/google/analytics',
|
|
||||||
r'com/android/billing',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Common known non-free blobs (always lower case):
|
SCANNER_CACHE_VERSION = 1
|
||||||
NON_FREE_GRADLE_LINES = {
|
|
||||||
exp: re.compile(r'.*' + exp, re.IGNORECASE) for exp in [
|
|
||||||
r'flurryagent',
|
|
||||||
r'paypal.*mpl',
|
|
||||||
r'admob.*sdk.*android',
|
|
||||||
r'google.*ad.*view',
|
|
||||||
r'google.*admob',
|
|
||||||
r'google.*play.*services',
|
|
||||||
r'com.google.android.play:core.*',
|
|
||||||
r'com.google.android.play:app-update',
|
|
||||||
r'com.google.android.libraries.places:places',
|
|
||||||
r'com.google.mlkit',
|
|
||||||
r'com.android.billingclient',
|
|
||||||
r'androidx.work:work-gcm',
|
|
||||||
r'crittercism',
|
|
||||||
r'heyzap',
|
|
||||||
r'jpct.*ae',
|
|
||||||
r'youtube.*android.*player.*api',
|
|
||||||
r'bugsense',
|
|
||||||
r'crashlytics',
|
|
||||||
r'ouya.*sdk',
|
|
||||||
r'libspen23',
|
|
||||||
r'firebase',
|
|
||||||
r'''["']com.facebook.android['":]''',
|
|
||||||
r'cloudrail',
|
|
||||||
r'com.tencent.bugly',
|
|
||||||
r'appcenter-push',
|
|
||||||
r'com.github.junrar:junrar',
|
|
||||||
r'androidx.navigation:navigation-dynamic-features',
|
|
||||||
r'xyz.belvi.mobilevision:barcodescanner',
|
|
||||||
r'org.jetbrains.kotlinx:kotlinx-coroutines-play-services',
|
|
||||||
r'me.pushy:sdk',
|
|
||||||
r'io.github.sinaweibosdk',
|
|
||||||
r'com.umeng.umsdk',
|
|
||||||
r'com.google.android.exoplayer:extension-cast',
|
|
||||||
r'io.objectbox:objectbox-gradle-plugin',
|
|
||||||
r'com.evernote:android-job',
|
|
||||||
r'com.yayandroid:LocationManager',
|
|
||||||
r'com.onesignal:OneSignal',
|
|
||||||
r'com.cloudinary:cloudinary-android',
|
|
||||||
r'com.google.android.exoplayer:extension-cronet',
|
|
||||||
r'com.anjlab.android.iab.v3:library',
|
|
||||||
r'com.github.penn5:donations',
|
|
||||||
r'com.mapbox',
|
|
||||||
r'com.yandex.android',
|
|
||||||
r'com.hypertrack',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_gradle_compile_commands(build):
|
def get_gradle_compile_commands(build):
|
||||||
@ -171,78 +112,272 @@ def get_embedded_classes(apkfile, depth=0):
|
|||||||
return classes
|
return classes
|
||||||
|
|
||||||
|
|
||||||
# taken from exodus_core
|
def _datetime_now():
|
||||||
def _exodus_compile_signatures(signatures):
|
"""Get datetime.now(), using this funciton allows mocking it for testing."""
|
||||||
"""
|
return datetime.utcnow()
|
||||||
Compiles the regex associated to each signature, in order to speed up the trackers detection.
|
|
||||||
|
|
||||||
:return: A compiled list of signatures.
|
|
||||||
"""
|
|
||||||
compiled_tracker_signature = []
|
|
||||||
try:
|
|
||||||
compiled_tracker_signature = [
|
|
||||||
re.compile(track.code_signature) for track in signatures
|
|
||||||
]
|
|
||||||
except TypeError:
|
|
||||||
print("signatures is not iterable")
|
|
||||||
return compiled_tracker_signature
|
|
||||||
|
|
||||||
|
|
||||||
# taken from exodus_core
|
def _scanner_cachedir():
|
||||||
def load_exodus_trackers_signatures():
|
"""Get `Path` to fdroidserver cache dir."""
|
||||||
"""
|
cfg = common.get_config()
|
||||||
Load trackers signatures from the official Exodus database.
|
if not cfg:
|
||||||
|
raise ConfigurationException('config not initialized')
|
||||||
|
if "cachedir_scanner" not in cfg:
|
||||||
|
raise ConfigurationException("could not load 'cachedir_scanner' from config")
|
||||||
|
cachedir = Path(cfg["cachedir_scanner"])
|
||||||
|
cachedir.mkdir(exist_ok=True, parents=True)
|
||||||
|
return cachedir
|
||||||
|
|
||||||
:return: a dictionary containing signatures.
|
|
||||||
"""
|
class SignatureDataMalformedException(Exception):
|
||||||
signatures = []
|
pass
|
||||||
exodus_url = "https://reports.exodus-privacy.eu.org/api/trackers"
|
|
||||||
r = requests.get(exodus_url, timeout=300)
|
|
||||||
data = r.json()
|
class SignatureDataOutdatedException(Exception):
|
||||||
for e in data['trackers']:
|
pass
|
||||||
signatures.append(
|
|
||||||
namedtuple('tracker', data['trackers'][e].keys())(
|
|
||||||
*data['trackers'][e].values()
|
class SignatureDataCacheMissException(Exception):
|
||||||
)
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureDataVersionMismatchException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureDataController:
|
||||||
|
def __init__(self, name, filename, url):
|
||||||
|
self.name = name
|
||||||
|
self.filename = filename
|
||||||
|
self.url = url
|
||||||
|
# by default we assume cache is valid indefinitely
|
||||||
|
self.cache_duration = timedelta(days=999999)
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
def check_data_version(self):
|
||||||
|
if self.data.get("version") != SCANNER_CACHE_VERSION:
|
||||||
|
raise SignatureDataVersionMismatchException()
|
||||||
|
|
||||||
|
def check_last_updated(self):
|
||||||
|
"""
|
||||||
|
Check if the last_updated value is ok and raise an exception if expired or inaccessible.
|
||||||
|
|
||||||
|
:raises SignatureDataMalformedException: when timestamp value is
|
||||||
|
inaccessible or not parse-able
|
||||||
|
:raises SignatureDataOutdatedException: when timestamp is older then
|
||||||
|
`self.cache_duration`
|
||||||
|
"""
|
||||||
|
last_updated = self.data.get("last_updated", None)
|
||||||
|
if last_updated:
|
||||||
|
try:
|
||||||
|
last_updated = datetime.fromtimestamp(last_updated)
|
||||||
|
except ValueError as e:
|
||||||
|
raise SignatureDataMalformedException() from e
|
||||||
|
except TypeError as e:
|
||||||
|
raise SignatureDataMalformedException() from e
|
||||||
|
delta = (last_updated + self.cache_duration) - scanner._datetime_now()
|
||||||
|
if delta > timedelta(seconds=0):
|
||||||
|
logging.debug(_('next {name} cache update due in {time}').format(
|
||||||
|
name=self.filename, time=delta
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
raise SignatureDataOutdatedException()
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
try:
|
||||||
|
self.fetch_signatures_from_web()
|
||||||
|
self.write_to_cache()
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(_("downloading scanner signatures from '{}' failed").format(self.url)) from e
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self.load_from_cache()
|
||||||
|
self.verify_data()
|
||||||
|
self.check_last_updated()
|
||||||
|
except SignatureDataCacheMissException:
|
||||||
|
self.load_from_defaults()
|
||||||
|
except SignatureDataOutdatedException:
|
||||||
|
self.fetch_signatures_from_web()
|
||||||
|
self.write_to_cache()
|
||||||
|
except (SignatureDataMalformedException, SignatureDataVersionMismatchException) as e:
|
||||||
|
logging.critical(_("scanner cache is malformed! You can clear it with: '{clear}'").format(
|
||||||
|
clear='rm -r {}'.format(common.get_config()['cachedir_scanner'])
|
||||||
|
))
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def load_from_defaults(self):
|
||||||
|
sig_file = (Path(__file__).parent / 'data' / 'scanner' / self.filename).resolve()
|
||||||
|
with open(sig_file) as f:
|
||||||
|
self.set_data(json.load(f))
|
||||||
|
|
||||||
|
def load_from_cache(self):
|
||||||
|
sig_file = scanner._scanner_cachedir() / self.filename
|
||||||
|
if not sig_file.exists():
|
||||||
|
raise SignatureDataCacheMissException()
|
||||||
|
with open(sig_file) as f:
|
||||||
|
self.set_data(json.load(f))
|
||||||
|
|
||||||
|
def write_to_cache(self):
|
||||||
|
sig_file = scanner._scanner_cachedir() / self.filename
|
||||||
|
with open(sig_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.data, f, indent=2)
|
||||||
|
logging.debug("write '{}' to cache".format(self.filename))
|
||||||
|
|
||||||
|
def verify_data(self):
|
||||||
|
"""
|
||||||
|
Clean and validate `self.data`.
|
||||||
|
|
||||||
|
Right now this function does just a basic key sanitation.
|
||||||
|
"""
|
||||||
|
self.check_data_version()
|
||||||
|
valid_keys = ['timestamp', 'last_updated', 'version', 'signatures', 'cache_duration']
|
||||||
|
|
||||||
|
for k in list(self.data.keys()):
|
||||||
|
if k not in valid_keys:
|
||||||
|
del self.data[k]
|
||||||
|
|
||||||
|
def set_data(self, new_data):
|
||||||
|
self.data = new_data
|
||||||
|
if 'cache_duration' in new_data:
|
||||||
|
self.cache_duration = timedelta(seconds=new_data['cache_duration'])
|
||||||
|
|
||||||
|
def fetch_signatures_from_web(self):
|
||||||
|
if not self.url.startswith("https://"):
|
||||||
|
raise Exception(_("can't open non-https url: '{};".format(self.url)))
|
||||||
|
logging.debug(_("downloading '{}'").format(self.url))
|
||||||
|
with urllib.request.urlopen(self.url) as f: # nosec B310 scheme filtered above
|
||||||
|
self.set_data(json.load(f))
|
||||||
|
self.data['last_updated'] = scanner._datetime_now().timestamp()
|
||||||
|
|
||||||
|
|
||||||
|
class ExodusSignatureDataController(SignatureDataController):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('Exodus signatures', 'exodus.yml', 'https://reports.exodus-privacy.eu.org/api/trackers')
|
||||||
|
self.cache_duration = timedelta(days=1) # refresh exodus cache after one day
|
||||||
|
|
||||||
|
def fetch_signatures_from_web(self):
|
||||||
|
logging.debug(_("downloading '{}'").format(self.url))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"signatures": {},
|
||||||
|
"timestamp": scanner._datetime_now().timestamp(),
|
||||||
|
"last_updated": scanner._datetime_now().timestamp(),
|
||||||
|
"version": SCANNER_CACHE_VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self.url.startswith("https://"):
|
||||||
|
raise Exception(_("can't open non-https url: '{};".format(self.url)))
|
||||||
|
with urllib.request.urlopen(self.url) as f: # nosec B310 scheme filtered above
|
||||||
|
d = json.load(f)
|
||||||
|
for tracker in d["trackers"].values():
|
||||||
|
if tracker.get('code_signature'):
|
||||||
|
data["signatures"][tracker["name"]] = {
|
||||||
|
"name": tracker["name"],
|
||||||
|
"warn_code_signatures": [tracker["code_signature"]],
|
||||||
|
# exodus also provides network signatures, unused atm.
|
||||||
|
# "network_signatures": [tracker["network_signature"]],
|
||||||
|
"AntiFeatures": ["Tracking"],
|
||||||
|
"license": "NonFree" # We assume all trackers in exodus
|
||||||
|
# are non-free, although free
|
||||||
|
# trackers like piwik, acra,
|
||||||
|
# etc. might be listed by exodus
|
||||||
|
# too.
|
||||||
|
}
|
||||||
|
self.set_data(data)
|
||||||
|
|
||||||
|
|
||||||
|
class SUSSDataController(SignatureDataController):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
'SUSS',
|
||||||
|
'suss.json',
|
||||||
|
'https://fdroid.gitlab.io/fdroid-suss/suss.json'
|
||||||
)
|
)
|
||||||
logging.debug('{} trackers signatures loaded'.format(len(signatures)))
|
|
||||||
return signatures, scanner._exodus_compile_signatures(signatures)
|
def load_from_defaults(self):
|
||||||
|
self.set_data(json.loads(SUSS_DEFAULT))
|
||||||
|
|
||||||
|
|
||||||
def scan_binary(apkfile, extract_signatures=None):
|
class ScannerTool():
|
||||||
|
def __init__(self):
|
||||||
|
self.sdcs = [
|
||||||
|
SUSSDataController(),
|
||||||
|
]
|
||||||
|
|
||||||
|
# we could add support for loading additional signature source
|
||||||
|
# definitions from config.yml here
|
||||||
|
|
||||||
|
self.load()
|
||||||
|
self.compile_regexes()
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
for sdc in self.sdcs:
|
||||||
|
sdc.load()
|
||||||
|
|
||||||
|
def compile_regexes(self):
|
||||||
|
self.regexs = {
|
||||||
|
'err_code_signatures': {},
|
||||||
|
'err_gradle_signatures': {},
|
||||||
|
'warn_code_signatures': {},
|
||||||
|
'warn_gradle_signatures': {},
|
||||||
|
}
|
||||||
|
for sdc in self.sdcs:
|
||||||
|
for signame, sigdef in sdc.data.get('signatures', {}).items():
|
||||||
|
for sig in sigdef.get('code_signatures', []):
|
||||||
|
self.regexs['err_code_signatures'][sig] = re.compile('.*' + sig, re.IGNORECASE)
|
||||||
|
for sig in sigdef.get('gradle_signatures', []):
|
||||||
|
self.regexs['err_gradle_signatures'][sig] = re.compile('.*' + sig, re.IGNORECASE)
|
||||||
|
for sig in sigdef.get('warn_code_signatures', []):
|
||||||
|
self.regexs['warn_code_signatures'][sig] = re.compile('.*' + sig, re.IGNORECASE)
|
||||||
|
for sig in sigdef.get('warn_gradle_signatures', []):
|
||||||
|
self.regexs['warn_gradle_signatures'][sig] = re.compile('.*' + sig, re.IGNORECASE)
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
for sdc in self.sdcs:
|
||||||
|
sdc.fetch_signatures_from_web()
|
||||||
|
sdc.write_to_cache()
|
||||||
|
|
||||||
|
def add(self, new_controller: SignatureDataController):
|
||||||
|
self.sdcs.append(new_controller)
|
||||||
|
self.compile_regexes()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: change this from singleton instance to dependency injection
|
||||||
|
# use `_get_tool()` instead of accessing this directly
|
||||||
|
_SCANNER_TOOL = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tool():
|
||||||
|
"""
|
||||||
|
Lazy loading function for getting a ScannerTool instance.
|
||||||
|
|
||||||
|
ScannerTool initialization need to access `common.config` values. Those are only available after initialization through `common.read_config()`. So this factory assumes config was called at an erlier point in time.
|
||||||
|
"""
|
||||||
|
if not scanner._SCANNER_TOOL:
|
||||||
|
scanner._SCANNER_TOOL = ScannerTool()
|
||||||
|
return scanner._SCANNER_TOOL
|
||||||
|
|
||||||
|
|
||||||
|
def scan_binary(apkfile):
|
||||||
"""Scan output of dexdump for known non-free classes."""
|
"""Scan output of dexdump for known non-free classes."""
|
||||||
logging.info(_('Scanning APK with dexdump for known non-free classes.'))
|
logging.info(_('Scanning APK with dexdump for known non-free classes.'))
|
||||||
result = get_embedded_classes(apkfile)
|
result = get_embedded_classes(apkfile)
|
||||||
problems = 0
|
problems, warnings = 0, 0
|
||||||
for classname in result:
|
for classname in result:
|
||||||
for suspect, regexp in CODE_SIGNATURES.items():
|
for suspect, regexp in _get_tool().regexs['warn_code_signatures'].items():
|
||||||
if regexp.match(classname):
|
if regexp.match(classname):
|
||||||
logging.debug("Found class '%s'" % classname)
|
logging.debug("Warning: found class '%s'" % classname)
|
||||||
|
warnings += 1
|
||||||
|
for suspect, regexp in _get_tool().regexs['err_code_signatures'].items():
|
||||||
|
if regexp.match(classname):
|
||||||
|
logging.debug("Problem: found class '%s'" % classname)
|
||||||
problems += 1
|
problems += 1
|
||||||
|
if warnings:
|
||||||
if extract_signatures:
|
logging.warning(_("Found {count} warnings in {filename}").format(count=warnings, filename=apkfile))
|
||||||
|
|
||||||
def _detect_tracker(sig, tracker, class_list):
|
|
||||||
for clazz in class_list:
|
|
||||||
if sig.search(clazz):
|
|
||||||
logging.debug("Found tracker, class {} matching {}".format(clazz, tracker.code_signature))
|
|
||||||
return tracker
|
|
||||||
return None
|
|
||||||
|
|
||||||
results = []
|
|
||||||
args = [(extract_signatures[1][index], tracker, result)
|
|
||||||
for (index, tracker) in enumerate(extract_signatures[0]) if
|
|
||||||
len(tracker.code_signature) > 3]
|
|
||||||
|
|
||||||
for res in itertools.starmap(_detect_tracker, args):
|
|
||||||
if res:
|
|
||||||
results.append(res)
|
|
||||||
|
|
||||||
trackers = [t for t in results if t is not None]
|
|
||||||
problems += len(trackers)
|
|
||||||
|
|
||||||
if problems:
|
if problems:
|
||||||
logging.critical("Found problems in %s" % apkfile)
|
logging.critical(_("Found {count} problems in {filename}").format(count=problems, filename=apkfile))
|
||||||
return problems
|
return problems
|
||||||
|
|
||||||
|
|
||||||
@ -255,20 +390,9 @@ def scan_source(build_dir, build=metadata.Build()):
|
|||||||
"""
|
"""
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
allowlisted = [
|
|
||||||
'firebase-jobdispatcher', # https://github.com/firebase/firebase-jobdispatcher-android/blob/master/LICENSE
|
|
||||||
'com.firebaseui', # https://github.com/firebase/FirebaseUI-Android/blob/master/LICENSE
|
|
||||||
'geofire-android', # https://github.com/firebase/geofire-java/blob/master/LICENSE
|
|
||||||
'com.yandex.android:authsdk', # https://github.com/yandexmobile/yandex-login-sdk-android/blob/master/LICENSE.txt
|
|
||||||
'com.hypertrack:hyperlog', # https://github.com/hypertrack/hyperlog-android#license
|
|
||||||
]
|
|
||||||
|
|
||||||
def is_allowlisted(s):
|
|
||||||
return any(al in s for al in allowlisted)
|
|
||||||
|
|
||||||
def suspects_found(s):
|
def suspects_found(s):
|
||||||
for n, r in NON_FREE_GRADLE_LINES.items():
|
for n, r in _get_tool().regexs['err_gradle_signatures'].items():
|
||||||
if r.match(s) and not is_allowlisted(s):
|
if r.match(s):
|
||||||
yield n
|
yield n
|
||||||
|
|
||||||
allowed_repos = [re.compile(r'^https://' + re.escape(repo) + r'/*') for repo in [
|
allowed_repos = [re.compile(r'^https://' + re.escape(repo) + r'/*') for repo in [
|
||||||
@ -540,7 +664,7 @@ def scan_source(build_dir, build=metadata.Build()):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global config, options, json_per_build
|
global options, json_per_build
|
||||||
|
|
||||||
# Parse command line...
|
# Parse command line...
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
@ -557,6 +681,8 @@ def main():
|
|||||||
help=_("Force scan of disabled apps and builds."))
|
help=_("Force scan of disabled apps and builds."))
|
||||||
parser.add_argument("--json", action="store_true", default=False,
|
parser.add_argument("--json", action="store_true", default=False,
|
||||||
help=_("Output JSON to stdout."))
|
help=_("Output JSON to stdout."))
|
||||||
|
parser.add_argument("--refresh", "-r", action="store_true", default=False,
|
||||||
|
help=_("fetch the latest version of signatures from the web"))
|
||||||
metadata.add_metadata_arguments(parser)
|
metadata.add_metadata_arguments(parser)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
metadata.warnings_action = options.W
|
metadata.warnings_action = options.W
|
||||||
@ -568,21 +694,28 @@ def main():
|
|||||||
else:
|
else:
|
||||||
logging.getLogger().setLevel(logging.ERROR)
|
logging.getLogger().setLevel(logging.ERROR)
|
||||||
|
|
||||||
config = common.read_config(options)
|
# initialize/load configuration values
|
||||||
|
common.get_config(opts=options)
|
||||||
|
|
||||||
|
if options.refresh:
|
||||||
|
scanner._get_tool().refresh()
|
||||||
|
if options.exodus:
|
||||||
|
c = ExodusSignatureDataController()
|
||||||
|
if options.refresh:
|
||||||
|
c.fetch_signatures_from_web()
|
||||||
|
else:
|
||||||
|
c.fetch()
|
||||||
|
scanner._get_tool().add(c)
|
||||||
|
|
||||||
probcount = 0
|
probcount = 0
|
||||||
|
|
||||||
exodus = []
|
|
||||||
if options.exodus:
|
|
||||||
exodus = load_exodus_trackers_signatures()
|
|
||||||
|
|
||||||
appids = []
|
appids = []
|
||||||
for apk in options.appid:
|
for apk in options.appid:
|
||||||
if os.path.isfile(apk):
|
if os.path.isfile(apk):
|
||||||
count = scanner.scan_binary(apk, exodus)
|
count = scanner.scan_binary(apk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
_('Scanner found {count} problems in {apk}:').format(
|
_('Scanner found {count} problems in {apk}').format(
|
||||||
count=count, apk=apk
|
count=count, apk=apk
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -683,3 +816,299 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
SUSS_DEFAULT = '''{
|
||||||
|
"cache_duration": 86400,
|
||||||
|
"signatures": {
|
||||||
|
"admob": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"admob.*sdk.*android"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"androidx": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"androidx.navigation:navigation-dynamic-features",
|
||||||
|
"androidx.work:work-gcm"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"appcenter-push": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"appcenter-push"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"bugsense": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"bugsense"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"cloudrail": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"cloudrail"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.android.billing": {
|
||||||
|
"code_signatures": [
|
||||||
|
"com/android/billing"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.android.billingclient": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.android.billingclient"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.anjlab.android.iab.v3": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.anjlab.android.iab.v3:library"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.cloudinary": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.cloudinary:cloudinary-android"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.evernote": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.evernote:android-job"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.facebook": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"[\\"']com.facebook.android['\\":]"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.github.junrar": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.github.junrar:junrar"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.github.penn5": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.github.penn5:donations"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.analytics": {
|
||||||
|
"code_signatures": [
|
||||||
|
"com/google/analytics"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.android.exoplayer": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.google.android.exoplayer:extension-cast",
|
||||||
|
"com.google.android.exoplayer:extension-cronet"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.android.gms": {
|
||||||
|
"code_signatures": [
|
||||||
|
"com/google/android/gms"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.android.libraries.places": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.google.android.libraries.places:places"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.android.play": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.google.android.play:app-update",
|
||||||
|
"com.google.android.play:core.*"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.android.play.core": {
|
||||||
|
"code_signatures": [
|
||||||
|
"com/google/android/play/core"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.firebase": {
|
||||||
|
"code_signatures": [
|
||||||
|
"com/google/firebase"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.mlkit": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.google.mlkit"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.google.tagmanager": {
|
||||||
|
"code_signatures": [
|
||||||
|
"com/google/tagmanager"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.hypertrack": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com\\\\.hypertrack(?!:hyperlog)"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.mapbox": {
|
||||||
|
"MaintainerNotes": "com.mapbox.mapboxsdk:mapbox-sdk-services seems to be fully under this license:\\nhttps://github.com/mapbox/mapbox-java/blob/main/LICENSE\\n",
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com\\\\.mapbox(?!\\\\.mapboxsdk:mapbox-sdk-services)"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.onesignal": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.onesignal:OneSignal"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.tencent.bugly": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.tencent.bugly"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.umeng.umsdk": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.umeng.umsdk"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.yandex.android": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com\\\\.yandex\\\\.android(?!:authsdk)"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"com.yayandroid": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com.yayandroid:LocationManager"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"crashlytics": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"crashlytics"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"crittercism": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"crittercism"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"firebase": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"com(\\\\.google)?\\\\.firebase[.:](?!firebase-jobdispatcher|geofire-java)"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"flurryagent": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"flurryagent"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"google-ad": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"google.*ad.*view"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"google.admob": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"google.*admob"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"google.play.services": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"google.*play.*services"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"heyzap": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"heyzap"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"io.github.sinaweibosdk": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"io.github.sinaweibosdk"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"io.objectbox": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"io.objectbox:objectbox-gradle-plugin"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"jpct": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"jpct.*ae"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"libspen23": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"libspen23"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"me.pushy": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"me.pushy:sdk"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"org.jetbrains.kotlinx": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"org.jetbrains.kotlinx:kotlinx-coroutines-play-services"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"ouya": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"ouya.*sdk"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"paypal": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"paypal.*mpl"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"xyz.belvi.mobilevision": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"xyz.belvi.mobilevision:barcodescanner"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
},
|
||||||
|
"youtube": {
|
||||||
|
"gradle_signatures": [
|
||||||
|
"youtube.*android.*player.*api"
|
||||||
|
],
|
||||||
|
"license": "NonFree"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": 1664480104.875586,
|
||||||
|
"version": 1,
|
||||||
|
"last_updated": 1664480104.875586
|
||||||
|
}'''
|
||||||
|
@ -402,7 +402,7 @@ class BuildTest(unittest.TestCase):
|
|||||||
os.chdir(testdir)
|
os.chdir(testdir)
|
||||||
os.mkdir("build")
|
os.mkdir("build")
|
||||||
|
|
||||||
config = dict()
|
config = fdroidserver.common.get_config()
|
||||||
config['sdk_path'] = os.getenv('ANDROID_HOME')
|
config['sdk_path'] = os.getenv('ANDROID_HOME')
|
||||||
config['ndk_paths'] = {'r10d': os.getenv('ANDROID_NDK_HOME')}
|
config['ndk_paths'] = {'r10d': os.getenv('ANDROID_NDK_HOME')}
|
||||||
fdroidserver.common.config = config
|
fdroidserver.common.config = config
|
||||||
|
@ -17,6 +17,7 @@ import zipfile
|
|||||||
import collections
|
import collections
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
localmodule = os.path.realpath(
|
localmodule = os.path.realpath(
|
||||||
os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')
|
os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')
|
||||||
@ -29,7 +30,7 @@ import fdroidserver.build
|
|||||||
import fdroidserver.common
|
import fdroidserver.common
|
||||||
import fdroidserver.metadata
|
import fdroidserver.metadata
|
||||||
import fdroidserver.scanner
|
import fdroidserver.scanner
|
||||||
from testcommon import TmpCwd
|
from testcommon import TmpCwd, mock_open_to_str
|
||||||
|
|
||||||
|
|
||||||
class ScannerTest(unittest.TestCase):
|
class ScannerTest(unittest.TestCase):
|
||||||
@ -53,8 +54,8 @@ class ScannerTest(unittest.TestCase):
|
|||||||
'com.integreight.onesheeld': 11,
|
'com.integreight.onesheeld': 11,
|
||||||
'com.jens.automation2': 2,
|
'com.jens.automation2': 2,
|
||||||
'firebase-suspect': 1,
|
'firebase-suspect': 1,
|
||||||
'org.mozilla.rocket': 3,
|
'org.mozilla.rocket': 1,
|
||||||
'org.tasks': 3,
|
'org.tasks': 2,
|
||||||
'realm': 1,
|
'realm': 1,
|
||||||
'se.manyver': 2,
|
'se.manyver': 2,
|
||||||
}
|
}
|
||||||
@ -446,21 +447,27 @@ class Test_scan_binary(unittest.TestCase):
|
|||||||
fdroidserver.common.config = config
|
fdroidserver.common.config = config
|
||||||
fdroidserver.common.options = mock.Mock()
|
fdroidserver.common.options = mock.Mock()
|
||||||
|
|
||||||
def test_code_signature_match(self):
|
fdroidserver.scanner._SCANNER_TOOL = mock.Mock()
|
||||||
apkfile = os.path.join(self.basedir, 'no_targetsdk_minsdk1_unsigned.apk')
|
fdroidserver.scanner._SCANNER_TOOL.regexs = {}
|
||||||
mock_code_signatures = {
|
fdroidserver.scanner._SCANNER_TOOL.regexs['err_code_signatures'] = {
|
||||||
"java/lang/Object": re.compile(
|
"java/lang/Object": re.compile(
|
||||||
r'.*java/lang/Object', re.IGNORECASE | re.UNICODE
|
r'.*java/lang/Object', re.IGNORECASE | re.UNICODE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
with mock.patch("fdroidserver.scanner.CODE_SIGNATURES", mock_code_signatures):
|
fdroidserver.scanner._SCANNER_TOOL.regexs['warn_code_signatures'] = {}
|
||||||
self.assertEqual(
|
|
||||||
1,
|
def test_code_signature_match(self):
|
||||||
fdroidserver.scanner.scan_binary(apkfile),
|
apkfile = os.path.join(self.basedir, 'no_targetsdk_minsdk1_unsigned.apk')
|
||||||
"Did not find expected code signature '{}' in binary '{}'".format(
|
self.assertEqual(
|
||||||
fdroidserver.scanner.CODE_SIGNATURES.values(), apkfile
|
1,
|
||||||
),
|
fdroidserver.scanner.scan_binary(apkfile),
|
||||||
)
|
"Did not find expected code signature '{}' in binary '{}'".format(
|
||||||
|
fdroidserver.scanner._SCANNER_TOOL.regexs[
|
||||||
|
'err_code_signatures'
|
||||||
|
].values(),
|
||||||
|
apkfile,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@unittest.skipIf(
|
@unittest.skipIf(
|
||||||
sys.version_info < (3, 9),
|
sys.version_info < (3, 9),
|
||||||
@ -470,115 +477,213 @@ class Test_scan_binary(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
def test_bottom_level_embedded_apk_code_signature(self):
|
def test_bottom_level_embedded_apk_code_signature(self):
|
||||||
apkfile = os.path.join(self.basedir, 'apk.embedded_1.apk')
|
apkfile = os.path.join(self.basedir, 'apk.embedded_1.apk')
|
||||||
mock_code_signatures = {
|
fdroidserver.scanner._SCANNER_TOOL.regexs['err_code_signatures'] = {
|
||||||
"org/bitbucket/tickytacky/mirrormirror/MainActivity": re.compile(
|
"org/bitbucket/tickytacky/mirrormirror/MainActivity": re.compile(
|
||||||
r'.*org/bitbucket/tickytacky/mirrormirror/MainActivity',
|
r'.*org/bitbucket/tickytacky/mirrormirror/MainActivity',
|
||||||
re.IGNORECASE | re.UNICODE,
|
re.IGNORECASE | re.UNICODE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
with mock.patch("fdroidserver.scanner.CODE_SIGNATURES", mock_code_signatures):
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
1,
|
1,
|
||||||
fdroidserver.scanner.scan_binary(apkfile),
|
fdroidserver.scanner.scan_binary(apkfile),
|
||||||
"Did not find expected code signature '{}' in binary '{}'".format(
|
"Did not find expected code signature '{}' in binary '{}'".format(
|
||||||
fdroidserver.scanner.CODE_SIGNATURES.values(), apkfile
|
fdroidserver.scanner._SCANNER_TOOL.regexs[
|
||||||
),
|
'err_code_signatures'
|
||||||
)
|
].values(),
|
||||||
|
apkfile,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_top_level_signature_embedded_apk_present(self):
|
def test_top_level_signature_embedded_apk_present(self):
|
||||||
apkfile = os.path.join(self.basedir, 'apk.embedded_1.apk')
|
apkfile = os.path.join(self.basedir, 'apk.embedded_1.apk')
|
||||||
mock_code_signatures = {
|
fdroidserver.scanner._SCANNER_TOOL.regexs['err_code_signatures'] = {
|
||||||
"org/fdroid/ci/BuildConfig": re.compile(
|
"org/fdroid/ci/BuildConfig": re.compile(
|
||||||
r'.*org/fdroid/ci/BuildConfig', re.IGNORECASE | re.UNICODE
|
r'.*org/fdroid/ci/BuildConfig', re.IGNORECASE | re.UNICODE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
with mock.patch("fdroidserver.scanner.CODE_SIGNATURES", mock_code_signatures):
|
|
||||||
self.assertEqual(
|
|
||||||
1,
|
|
||||||
fdroidserver.scanner.scan_binary(apkfile),
|
|
||||||
"Did not find expected code signature '{}' in binary '{}'".format(
|
|
||||||
fdroidserver.scanner.CODE_SIGNATURES.values(), apkfile
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_no_match(self):
|
|
||||||
apkfile = os.path.join(self.basedir, 'no_targetsdk_minsdk1_unsigned.apk')
|
|
||||||
result = fdroidserver.scanner.scan_binary(apkfile)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
0, result, "Found false positives in binary '{}'".format(apkfile)
|
1,
|
||||||
|
fdroidserver.scanner.scan_binary(apkfile),
|
||||||
|
"Did not find expected code signature '{}' in binary '{}'".format(
|
||||||
|
fdroidserver.scanner._SCANNER_TOOL.regexs[
|
||||||
|
'err_code_signatures'
|
||||||
|
].values(),
|
||||||
|
apkfile,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Test__exodus_compile_signatures(unittest.TestCase):
|
class Test_SignatureDataController(unittest.TestCase):
|
||||||
def setUp(self):
|
# __init__
|
||||||
self.m1 = mock.Mock()
|
def test_init(self):
|
||||||
self.m1.code_signature = r"^random\sregex$"
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
self.m2 = mock.Mock()
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
self.m2.code_signature = r"^another.+regex$"
|
|
||||||
self.mock_sigs = [self.m1, self.m2]
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
result = fdroidserver.scanner._exodus_compile_signatures(self.mock_sigs)
|
|
||||||
self.assertListEqual(
|
|
||||||
result,
|
|
||||||
[
|
|
||||||
re.compile(self.m1.code_signature),
|
|
||||||
re.compile(self.m2.code_signature),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
self.assertEqual(sdc.name, 'nnn')
|
||||||
|
self.assertEqual(sdc.filename, 'fff.yml')
|
||||||
|
self.assertEqual(sdc.cache_duration, timedelta(999999))
|
||||||
|
self.assertDictEqual(sdc.data, {})
|
||||||
|
|
||||||
def test_not_iterable(self):
|
# check_last_updated
|
||||||
result = fdroidserver.scanner._exodus_compile_signatures(123)
|
def test_check_last_updated_ok(self):
|
||||||
self.assertListEqual(result, [])
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
|
||||||
class Test_load_exodus_trackers_signatures(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.requests_ret = mock.Mock()
|
|
||||||
self.requests_ret.json = mock.Mock(
|
|
||||||
return_value={
|
|
||||||
"trackers": {
|
|
||||||
"1": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "Steyer Puch 1",
|
|
||||||
"description": "blah blah blah",
|
|
||||||
"creation_date": "1956-01-01",
|
|
||||||
"code_signature": "com.puch.|com.steyer.",
|
|
||||||
"network_signature": "pst\\.com",
|
|
||||||
"website": "https://pst.com",
|
|
||||||
"categories": ["tracker"],
|
|
||||||
"documentation": [],
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "Steyer Puch 2",
|
|
||||||
"description": "blah blah blah",
|
|
||||||
"creation_date": "1956-01-01",
|
|
||||||
"code_signature": "com.puch.|com.steyer.",
|
|
||||||
"network_signature": "pst\\.com",
|
|
||||||
"website": "https://pst.com",
|
|
||||||
"categories": ["tracker"],
|
|
||||||
"documentation": [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
self.requests_func = mock.Mock(return_value=self.requests_ret)
|
sdc.data['last_updated'] = datetime.utcnow().timestamp()
|
||||||
self.compilesig_func = mock.Mock(return_value="mocked return value")
|
sdc.check_last_updated()
|
||||||
|
|
||||||
def test_ok(self):
|
def test_check_last_updated_exception_cache_outdated(self):
|
||||||
with mock.patch("requests.get", self.requests_func), mock.patch(
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
"fdroidserver.scanner._exodus_compile_signatures", self.compilesig_func
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
sdc.cache_duration = timedelta(days=7)
|
||||||
|
sdc.data['last_updated'] = (datetime.utcnow() - timedelta(days=30)).timestamp()
|
||||||
|
with self.assertRaises(fdroidserver.scanner.SignatureDataOutdatedException):
|
||||||
|
sdc.check_last_updated()
|
||||||
|
|
||||||
|
def test_check_last_updated_exception_not_string(self):
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
sdc.data['last_updated'] = 'sepp'
|
||||||
|
with self.assertRaises(fdroidserver.scanner.SignatureDataMalformedException):
|
||||||
|
sdc.check_last_updated()
|
||||||
|
|
||||||
|
def test_check_last_updated_exception_not_iso_formatted_string(self):
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
sdc.data['last_updated'] = '01/09/2002 10:11'
|
||||||
|
with self.assertRaises(fdroidserver.scanner.SignatureDataMalformedException):
|
||||||
|
sdc.check_last_updated()
|
||||||
|
|
||||||
|
def test_check_last_updated_no_exception_missing_when_last_updated_not_set(self):
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
sdc.check_last_updated()
|
||||||
|
|
||||||
|
# check_data_version
|
||||||
|
def test_check_data_version_ok(self):
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
sdc.data['version'] = fdroidserver.scanner.SCANNER_CACHE_VERSION
|
||||||
|
sdc.check_data_version()
|
||||||
|
|
||||||
|
def test_check_data_version_exception(self):
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
with self.assertRaises(
|
||||||
|
fdroidserver.scanner.SignatureDataVersionMismatchException
|
||||||
):
|
):
|
||||||
sigs, regex = fdroidserver.scanner.load_exodus_trackers_signatures()
|
sdc.check_data_version()
|
||||||
self.requests_func.assert_called_once_with(
|
|
||||||
"https://reports.exodus-privacy.eu.org/api/trackers", timeout=300
|
|
||||||
)
|
|
||||||
self.assertEqual(len(sigs), 2)
|
|
||||||
self.assertListEqual([1, 2], sorted([x.id for x in sigs]))
|
|
||||||
|
|
||||||
self.compilesig_func.assert_called_once_with(sigs)
|
def test_load_ok(self):
|
||||||
self.assertEqual(regex, "mocked return value")
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
func_lfc = mock.Mock()
|
||||||
|
func_vd = mock.Mock()
|
||||||
|
func_clu = mock.Mock()
|
||||||
|
with mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.load_from_cache',
|
||||||
|
func_lfc,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.verify_data',
|
||||||
|
func_vd,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.check_last_updated',
|
||||||
|
func_clu,
|
||||||
|
):
|
||||||
|
sdc.load()
|
||||||
|
func_lfc.assert_called_once_with()
|
||||||
|
func_vd.assert_called_once_with()
|
||||||
|
func_clu.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_load_initial_cache_miss(self):
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
func_lfc = mock.Mock(
|
||||||
|
side_effect=fdroidserver.scanner.SignatureDataCacheMissException
|
||||||
|
)
|
||||||
|
func_lfd = mock.Mock()
|
||||||
|
with mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.load_from_cache',
|
||||||
|
func_lfc,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.load_from_defaults',
|
||||||
|
func_lfd,
|
||||||
|
):
|
||||||
|
sdc.load()
|
||||||
|
func_lfc.assert_called_once_with()
|
||||||
|
func_lfd.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_load_cache_auto_refresh(self):
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
func_lfc = mock.Mock()
|
||||||
|
func_vd = mock.Mock()
|
||||||
|
func_clu = mock.Mock(
|
||||||
|
side_effect=fdroidserver.scanner.SignatureDataOutdatedException()
|
||||||
|
)
|
||||||
|
func_fsfw = mock.Mock()
|
||||||
|
func_wtc = mock.Mock()
|
||||||
|
with mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.load_from_cache',
|
||||||
|
func_lfc,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.verify_data',
|
||||||
|
func_vd,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.check_last_updated',
|
||||||
|
func_clu,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.fetch_signatures_from_web',
|
||||||
|
func_fsfw,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.write_to_cache',
|
||||||
|
func_wtc,
|
||||||
|
):
|
||||||
|
sdc.load()
|
||||||
|
func_lfc.assert_called_once_with()
|
||||||
|
func_vd.assert_called_once_with()
|
||||||
|
func_clu.assert_called_once_with()
|
||||||
|
func_fsfw.assert_called_once_with()
|
||||||
|
func_wtc.assert_called_once_with()
|
||||||
|
|
||||||
|
@unittest.skipIf(
|
||||||
|
sys.version_info < (3, 9, 0),
|
||||||
|
"mock_open doesn't allow easy access to written data in older python versions",
|
||||||
|
)
|
||||||
|
def test_write_to_cache(self):
|
||||||
|
open_func = mock.mock_open()
|
||||||
|
sdc = fdroidserver.scanner.SignatureDataController(
|
||||||
|
'nnn', 'fff.yml', 'https://example.com/test.json'
|
||||||
|
)
|
||||||
|
sdc.data = {"mocked": "data"}
|
||||||
|
|
||||||
|
with mock.patch("builtins.open", open_func), mock.patch(
|
||||||
|
"fdroidserver.scanner._scanner_cachedir",
|
||||||
|
return_value=pathlib.Path('.'),
|
||||||
|
):
|
||||||
|
sdc.write_to_cache()
|
||||||
|
|
||||||
|
open_func.assert_called_with(pathlib.Path('fff.yml'), 'w', encoding="utf-8")
|
||||||
|
self.assertEqual(mock_open_to_str(open_func), """{\n "mocked": "data"\n}""")
|
||||||
|
|
||||||
|
|
||||||
|
class Test_ScannerTool(unittest.TestCase):
|
||||||
|
def test_load(self):
|
||||||
|
st = mock.Mock()
|
||||||
|
st.sdcs = [mock.Mock(), mock.Mock()]
|
||||||
|
fdroidserver.scanner.ScannerTool.load(st)
|
||||||
|
st.sdcs[0].load.assert_called_once_with()
|
||||||
|
st.sdcs[1].load.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
class Test_main(unittest.TestCase):
|
class Test_main(unittest.TestCase):
|
||||||
@ -627,7 +732,7 @@ class Test_main(unittest.TestCase):
|
|||||||
|
|
||||||
self.exit_func.assert_not_called()
|
self.exit_func.assert_not_called()
|
||||||
self.read_app_args_func.assert_not_called()
|
self.read_app_args_func.assert_not_called()
|
||||||
self.scan_binary_func.assert_called_once_with('local.application.apk', [])
|
self.scan_binary_func.assert_called_once_with('local.application.apk')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@ -648,8 +753,7 @@ if __name__ == "__main__":
|
|||||||
[
|
[
|
||||||
unittest.makeSuite(ScannerTest),
|
unittest.makeSuite(ScannerTest),
|
||||||
unittest.makeSuite(Test_scan_binary),
|
unittest.makeSuite(Test_scan_binary),
|
||||||
unittest.makeSuite(Test__exodus_compile_signatures),
|
unittest.makeSuite(Test_SignatureDataController),
|
||||||
unittest.makeSuite(Test_load_exodus_trackers_signatures),
|
|
||||||
unittest.makeSuite(Test_main),
|
unittest.makeSuite(Test_main),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -36,8 +36,7 @@ class TmpCwd():
|
|||||||
|
|
||||||
|
|
||||||
class TmpPyPath():
|
class TmpPyPath():
|
||||||
"""Context-manager for temporarily changing the current working
|
"""Context-manager for temporarily adding a direcory to python path
|
||||||
directory.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, additional_path):
|
def __init__(self, additional_path):
|
||||||
@ -48,3 +47,14 @@ class TmpPyPath():
|
|||||||
|
|
||||||
def __exit__(self, a, b, c):
|
def __exit__(self, a, b, c):
|
||||||
sys.path.remove(self.additional_path)
|
sys.path.remove(self.additional_path)
|
||||||
|
|
||||||
|
|
||||||
|
def mock_open_to_str(mock):
|
||||||
|
"""
|
||||||
|
helper function for accessing all data written into a
|
||||||
|
unittest.mock.mock_open() instance as a string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "".join([
|
||||||
|
x.args[0] for x in mock.mock_calls if str(x).startswith("call().write(")
|
||||||
|
])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user