mirror of
https://gitlab.com/fdroid/fdroidserver.git
synced 2024-11-19 21:30:10 +01:00
🔍 add scanner_signature_sources
config option
This adds the option to configure which set of signatures `fdroid scanner` should use, by configuring it in `config.yml`. It allows fetching signatures in our custom json format. It also adds 3 additional sources: 'suss', 'exodus', 'etip'
This commit is contained in:
parent
46d077292c
commit
24d88705fa
@ -355,3 +355,19 @@
|
|||||||
# lint_licenses:
|
# lint_licenses:
|
||||||
# - Custom-License-A
|
# - Custom-License-A
|
||||||
# - Another-License
|
# - Another-License
|
||||||
|
|
||||||
|
# `fdroid scanner` can scan for signatures from various sources. By default
|
||||||
|
# it's configured to only use F-Droids official SUSS collection. We have
|
||||||
|
# support for these special collections:
|
||||||
|
# * 'exodus' - official exodus-privacy.org signatures
|
||||||
|
# * 'etip' - exodus privacy investigation platfrom community contributed
|
||||||
|
# signatures
|
||||||
|
# * 'suss' - official F-Droid: Suspicious or Unwanted Software Signatures
|
||||||
|
# You can also configure scanner to use custom collections of signatures here.
|
||||||
|
# They have to follow the format specified in the SUSS readme.
|
||||||
|
# (https://gitlab.com/fdroid/fdroid-suss/#cache-file-data-format)
|
||||||
|
#
|
||||||
|
# scanner_signature_sources:
|
||||||
|
# - suss
|
||||||
|
# - exodus
|
||||||
|
# - https://example.com/signatures.json
|
||||||
|
@ -163,6 +163,7 @@ default_config = {
|
|||||||
'archive_older': 0,
|
'archive_older': 0,
|
||||||
'lint_licenses': fdroidserver.lint.APPROVED_LICENSES, # type: ignore
|
'lint_licenses': fdroidserver.lint.APPROVED_LICENSES, # type: ignore
|
||||||
'git_mirror_size_limit': 10000000000,
|
'git_mirror_size_limit': 10000000000,
|
||||||
|
'scanner_signature_sources': ['suss'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import logging
|
|||||||
import zipfile
|
import zipfile
|
||||||
import itertools
|
import itertools
|
||||||
import traceback
|
import traceback
|
||||||
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@ -141,6 +142,10 @@ class SignatureDataCacheMissException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureDataNoDefaultsException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SignatureDataVersionMismatchException(Exception):
|
class SignatureDataVersionMismatchException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -198,7 +203,7 @@ class SignatureDataController:
|
|||||||
self.check_last_updated()
|
self.check_last_updated()
|
||||||
except SignatureDataCacheMissException:
|
except SignatureDataCacheMissException:
|
||||||
self.load_from_defaults()
|
self.load_from_defaults()
|
||||||
except SignatureDataOutdatedException:
|
except (SignatureDataOutdatedException, SignatureDataNoDefaultsException):
|
||||||
self.fetch_signatures_from_web()
|
self.fetch_signatures_from_web()
|
||||||
self.write_to_cache()
|
self.write_to_cache()
|
||||||
except (SignatureDataMalformedException, SignatureDataVersionMismatchException) as e:
|
except (SignatureDataMalformedException, SignatureDataVersionMismatchException) as e:
|
||||||
@ -208,9 +213,7 @@ class SignatureDataController:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
def load_from_defaults(self):
|
def load_from_defaults(self):
|
||||||
sig_file = (Path(__file__).parent / 'data' / 'scanner' / self.filename).resolve()
|
raise SignatureDataNoDefaultsException()
|
||||||
with open(sig_file) as f:
|
|
||||||
self.set_data(json.load(f))
|
|
||||||
|
|
||||||
def load_from_cache(self):
|
def load_from_cache(self):
|
||||||
sig_file = scanner._scanner_cachedir() / self.filename
|
sig_file = scanner._scanner_cachedir() / self.filename
|
||||||
@ -254,8 +257,9 @@ class SignatureDataController:
|
|||||||
|
|
||||||
class ExodusSignatureDataController(SignatureDataController):
|
class ExodusSignatureDataController(SignatureDataController):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('Exodus signatures', 'exodus.yml', 'https://reports.exodus-privacy.eu.org/api/trackers')
|
super().__init__('Exodus signatures', 'exodus.json', 'https://reports.exodus-privacy.eu.org/api/trackers')
|
||||||
self.cache_duration = timedelta(days=1) # refresh exodus cache after one day
|
self.cache_duration = timedelta(days=1) # refresh exodus cache after one day
|
||||||
|
self.has_trackers_json_key = True
|
||||||
|
|
||||||
def fetch_signatures_from_web(self):
|
def fetch_signatures_from_web(self):
|
||||||
logging.debug(_("downloading '{}'").format(self.url))
|
logging.debug(_("downloading '{}'").format(self.url))
|
||||||
@ -270,8 +274,10 @@ class ExodusSignatureDataController(SignatureDataController):
|
|||||||
if not self.url.startswith("https://"):
|
if not self.url.startswith("https://"):
|
||||||
raise Exception(_("can't open non-https url: '{};".format(self.url)))
|
raise Exception(_("can't open non-https url: '{};".format(self.url)))
|
||||||
with urllib.request.urlopen(self.url) as f: # nosec B310 scheme filtered above
|
with urllib.request.urlopen(self.url) as f: # nosec B310 scheme filtered above
|
||||||
d = json.load(f)
|
trackerlist = json.load(f)
|
||||||
for tracker in d["trackers"].values():
|
if self.has_trackers_json_key:
|
||||||
|
trackerlist = trackerlist["trackers"].values()
|
||||||
|
for tracker in trackerlist:
|
||||||
if tracker.get('code_signature'):
|
if tracker.get('code_signature'):
|
||||||
data["signatures"][tracker["name"]] = {
|
data["signatures"][tracker["name"]] = {
|
||||||
"name": tracker["name"],
|
"name": tracker["name"],
|
||||||
@ -288,6 +294,15 @@ class ExodusSignatureDataController(SignatureDataController):
|
|||||||
self.set_data(data)
|
self.set_data(data)
|
||||||
|
|
||||||
|
|
||||||
|
class EtipSignatureDataController(ExodusSignatureDataController):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.name = 'ETIP signatures'
|
||||||
|
self.filename = 'etip.json'
|
||||||
|
self.url = 'https://etip.exodus-privacy.eu.org/api/trackers/?format=json'
|
||||||
|
self.has_trackers_json_key = False
|
||||||
|
|
||||||
|
|
||||||
class SUSSDataController(SignatureDataController):
|
class SUSSDataController(SignatureDataController):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@ -302,16 +317,42 @@ class SUSSDataController(SignatureDataController):
|
|||||||
|
|
||||||
class ScannerTool():
|
class ScannerTool():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.sdcs = [
|
|
||||||
SUSSDataController(),
|
|
||||||
]
|
|
||||||
|
|
||||||
# we could add support for loading additional signature source
|
# we could add support for loading additional signature source
|
||||||
# definitions from config.yml here
|
# definitions from config.yml here
|
||||||
|
|
||||||
|
self.scanner_data_lookup()
|
||||||
self.load()
|
self.load()
|
||||||
self.compile_regexes()
|
self.compile_regexes()
|
||||||
|
|
||||||
|
def scanner_data_lookup(self):
|
||||||
|
sigsources = common.get_config().get('scanner_signature_sources', [])
|
||||||
|
logging.debug(
|
||||||
|
"scanner is configured to use signature data from: '{}'"
|
||||||
|
.format("', '".join(sigsources))
|
||||||
|
)
|
||||||
|
self.sdcs = []
|
||||||
|
for i, source_url in enumerate(sigsources):
|
||||||
|
if source_url.lower() == 'suss':
|
||||||
|
self.sdcs.append(SUSSDataController())
|
||||||
|
elif source_url.lower() == 'exodus':
|
||||||
|
self.sdcs.append(ExodusSignatureDataController())
|
||||||
|
elif source_url.lower() == 'etip':
|
||||||
|
self.sdcs.append(EtipSignatureDataController())
|
||||||
|
else:
|
||||||
|
u = urllib.parse.urlparse(source_url)
|
||||||
|
if u.scheme != 'https' or u.path == "":
|
||||||
|
raise ConfigurationException(
|
||||||
|
"Invalid 'scanner_signature_sources' configuration: '{}'. "
|
||||||
|
"Has to be a valid HTTPS-URL or match a predefined "
|
||||||
|
"constants: 'suss', 'exodus'".format(source_url)
|
||||||
|
)
|
||||||
|
self.sdcs.append(SignatureDataController(
|
||||||
|
source_url,
|
||||||
|
'{}_{}'.format(i, os.path.basename(u.path)),
|
||||||
|
source_url,
|
||||||
|
))
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
for sdc in self.sdcs:
|
for sdc in self.sdcs:
|
||||||
sdc.load()
|
sdc.load()
|
||||||
@ -697,15 +738,11 @@ def main():
|
|||||||
# initialize/load configuration values
|
# initialize/load configuration values
|
||||||
common.get_config(opts=options)
|
common.get_config(opts=options)
|
||||||
|
|
||||||
|
if options.exodus:
|
||||||
|
if "exodus" not in common.get_config()['scanner_signature_sources']:
|
||||||
|
common.get_config()['scanner_signature_sources'].append('exodus')
|
||||||
if options.refresh:
|
if options.refresh:
|
||||||
scanner._get_tool().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
|
||||||
|
|
||||||
|
@ -656,6 +656,37 @@ class Test_SignatureDataController(unittest.TestCase):
|
|||||||
func_fsfw.assert_called_once_with()
|
func_fsfw.assert_called_once_with()
|
||||||
func_wtc.assert_called_once_with()
|
func_wtc.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_load_try_web_when_no_defaults(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(
|
||||||
|
side_effect=fdroidserver.scanner.SignatureDataNoDefaultsException()
|
||||||
|
)
|
||||||
|
func_fsfw = mock.Mock()
|
||||||
|
func_wtc = mock.Mock()
|
||||||
|
with mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.load_from_cache',
|
||||||
|
func_lfc,
|
||||||
|
), mock.patch(
|
||||||
|
'fdroidserver.scanner.SignatureDataController.load_from_defaults',
|
||||||
|
func_lfd,
|
||||||
|
), 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_lfd.assert_called_once_with()
|
||||||
|
func_fsfw.assert_called_once_with()
|
||||||
|
func_wtc.assert_called_once_with()
|
||||||
|
|
||||||
@unittest.skipIf(
|
@unittest.skipIf(
|
||||||
sys.version_info < (3, 9, 0),
|
sys.version_info < (3, 9, 0),
|
||||||
"mock_open doesn't allow easy access to written data in older python versions",
|
"mock_open doesn't allow easy access to written data in older python versions",
|
||||||
|
Loading…
Reference in New Issue
Block a user