chiark / gitweb /
scanner: also ignore ttf and otf files
[fdroidserver.git] / fdroidserver / scanner.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # scanner.py - part of the FDroid server tools
5 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Affero General Public License for more details.
16 #
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import os
21 import re
22 import traceback
23 from argparse import ArgumentParser
24 import logging
25
26 import common
27 import metadata
28 from common import BuildException, VCSException
29
30 config = None
31 options = None
32
33
34 # Scan the source code in the given directory (and all subdirectories)
35 # and return the number of fatal problems encountered
36 def scan_source(build_dir, root_dir, thisbuild):
37
38     count = 0
39
40     # Common known non-free blobs (always lower case):
41     usual_suspects = {
42         exp: re.compile(r'.*' + exp, re.IGNORECASE) for exp in [
43             r'flurryagent',
44             r'paypal.*mpl',
45             r'google.*analytics',
46             r'admob.*sdk.*android',
47             r'google.*ad.*view',
48             r'google.*admob',
49             r'google.*play.*services',
50             r'crittercism',
51             r'heyzap',
52             r'jpct.*ae',
53             r'youtube.*android.*player.*api',
54             r'bugsense',
55             r'crashlytics',
56             r'ouya.*sdk',
57             r'libspen23',
58         ]
59     }
60
61     def suspects_found(s):
62         for n, r in usual_suspects.iteritems():
63             if r.match(s):
64                 yield n
65
66     scanignore = common.getpaths(build_dir, thisbuild, 'scanignore')
67     scandelete = common.getpaths(build_dir, thisbuild, 'scandelete')
68
69     scanignore_worked = set()
70     scandelete_worked = set()
71
72     def toignore(fd):
73         for p in scanignore:
74             if fd.startswith(p):
75                 scanignore_worked.add(p)
76                 return True
77         return False
78
79     def todelete(fd):
80         for p in scandelete:
81             if fd.startswith(p):
82                 scandelete_worked.add(p)
83                 return True
84         return False
85
86     def ignoreproblem(what, fd, fp):
87         logging.info('Ignoring %s at %s' % (what, fd))
88         return 0
89
90     def removeproblem(what, fd, fp):
91         logging.info('Removing %s at %s' % (what, fd))
92         os.remove(fp)
93         return 0
94
95     def warnproblem(what, fd):
96         logging.warn('Found %s at %s' % (what, fd))
97
98     def handleproblem(what, fd, fp):
99         if toignore(fd):
100             return ignoreproblem(what, fd, fp)
101         if todelete(fd):
102             return removeproblem(what, fd, fp)
103         logging.error('Found %s at %s' % (what, fd))
104         return 1
105
106     def is_executable(path):
107         return os.path.exists(path) and os.access(path, os.X_OK)
108
109     textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f})
110
111     def is_binary(path):
112         d = None
113         with open(path, 'rb') as f:
114             d = f.read(1024)
115         return bool(d.translate(None, textchars))
116
117     # Iterate through all files in the source code
118     for r, d, f in os.walk(build_dir, topdown=True):
119
120         # It's topdown, so checking the basename is enough
121         for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
122             if ignoredir in d:
123                 d.remove(ignoredir)
124
125         for curfile in f:
126
127             # Path (relative) to the file
128             fp = os.path.join(r, curfile)
129             fd = fp[len(build_dir) + 1:]
130
131             ext = common.get_extension(fd)
132
133             if ext == 'so':
134                 count += handleproblem('shared library', fd, fp)
135             elif ext == 'a':
136                 count += handleproblem('static library', fd, fp)
137             elif ext == 'class':
138                 count += handleproblem('Java compiled class', fd, fp)
139             elif ext == 'apk':
140                 removeproblem('APK file', fd, fp)
141
142             elif ext == 'jar':
143                 for name in suspects_found(curfile):
144                     count += handleproblem('usual supect \'%s\'' % name, fd, fp)
145                 warnproblem('JAR file', fd)
146
147             elif ext == 'java':
148                 if not os.path.isfile(fp):
149                     continue
150                 for line in file(fp):
151                     if 'DexClassLoader' in line:
152                         count += handleproblem('DexClassLoader', fd, fp)
153                         break
154
155             elif ext == 'gradle':
156                 if not os.path.isfile(fp):
157                     continue
158                 for i, line in enumerate(file(fp)):
159                     i = i + 1
160                     for name in suspects_found(line):
161                         count += handleproblem('usual supect \'%s\' at line %d' % (name, i), fd, fp)
162
163             # These files are often found - avoid checking if they are binary
164             # to speed up the scanner
165             elif ext in [
166                     'xml', 'md', 'txt', 'html', 'sh', 'png', 'jpg',
167                     'ttf', 'otf']:
168                 pass
169
170             elif is_binary(fp):
171                 if is_executable(fp):
172                     count += handleproblem('executable binary', fd, fp)
173                 elif ext == '':
174                     count += handleproblem('unknown binary', fd, fp)
175
176     for p in scanignore:
177         if p not in scanignore_worked:
178             logging.error('Unused scanignore path: %s' % p)
179             count += 1
180
181     for p in scandelete:
182         if p not in scandelete_worked:
183             logging.error('Unused scandelete path: %s' % p)
184             count += 1
185
186     # Presence of a jni directory without buildjni=yes might
187     # indicate a problem (if it's not a problem, explicitly use
188     # buildjni=no to bypass this check)
189     if (os.path.exists(os.path.join(root_dir, 'jni')) and
190             not thisbuild['buildjni']):
191         logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
192         count += 1
193
194     return count
195
196
197 def main():
198
199     global config, options
200
201     # Parse command line...
202     parser = ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]")
203     common.setup_global_opts(parser)
204     parser.add_argument("appid", nargs='*', help="app-id with optional versioncode in the form APPID[:VERCODE]")
205     options = parser.parse_args()
206
207     config = common.read_config(options)
208
209     # Read all app and srclib metadata
210     allapps = metadata.read_metadata()
211     apps = common.read_app_args(options.appid, allapps, True)
212
213     probcount = 0
214
215     build_dir = 'build'
216     if not os.path.isdir(build_dir):
217         logging.info("Creating build directory")
218         os.makedirs(build_dir)
219     srclib_dir = os.path.join(build_dir, 'srclib')
220     extlib_dir = os.path.join(build_dir, 'extlib')
221
222     for appid, app in apps.iteritems():
223
224         if app['Disabled']:
225             logging.info("Skipping %s: disabled" % appid)
226             continue
227         if not app['builds']:
228             logging.info("Skipping %s: no builds specified" % appid)
229             continue
230
231         logging.info("Processing " + appid)
232
233         try:
234
235             if app['Repo Type'] == 'srclib':
236                 build_dir = os.path.join('build', 'srclib', app['Repo'])
237             else:
238                 build_dir = os.path.join('build', appid)
239
240             # Set up vcs interface and make sure we have the latest code...
241             vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
242
243             for thisbuild in app['builds']:
244
245                 if thisbuild['disable']:
246                     logging.info("...skipping version %s - %s" % (
247                         thisbuild['version'], thisbuild.get('disable', thisbuild['commit'][1:])))
248                 else:
249                     logging.info("...scanning version " + thisbuild['version'])
250
251                     # Prepare the source code...
252                     root_dir, _ = common.prepare_source(vcs, app, thisbuild,
253                                                         build_dir, srclib_dir,
254                                                         extlib_dir, False)
255
256                     # Do the scan...
257                     count = scan_source(build_dir, root_dir, thisbuild)
258                     if count > 0:
259                         logging.warn('Scanner found %d problems in %s (%s)' % (
260                             count, appid, thisbuild['vercode']))
261                         probcount += count
262
263         except BuildException as be:
264             logging.warn("Could not scan app %s due to BuildException: %s" % (
265                 appid, be))
266             probcount += 1
267         except VCSException as vcse:
268             logging.warn("VCS error while scanning app %s: %s" % (appid, vcse))
269             probcount += 1
270         except Exception:
271             logging.warn("Could not scan app %s due to unknown error: %s" % (
272                 appid, traceback.format_exc()))
273             probcount += 1
274
275     logging.info("Finished:")
276     print "%d problems found" % probcount
277
278 if __name__ == "__main__":
279     main()