2 # -*- coding: utf-8 -*-
4 # scanner.py - part of the FDroid server tools
5 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
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.
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.
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/>.
23 from argparse import ArgumentParser
28 from common import BuildException, VCSException
36 There are two incompatible versions of the 'magic' module, one
37 that comes as part of libmagic, which is what Debian includes as
38 python-magic, then another called python-magic that is a separate
39 project that wraps libmagic. The second is 'magic' on pypi, so
40 both need to be supported. Then on platforms where libmagic is
41 not easily included, e.g. OSX and Windows, fallback to the
42 built-in 'mimetypes' module so this will work without
43 libmagic. Hence this function with the following hacks:
50 def mime_from_file(path):
52 return magic.from_file(path, mime=True)
62 def mime_guess_type(path):
63 return mimetypes.guess_type(path, strict=False)
68 ms = magic.open(magic.MIME_TYPE)
70 magic.from_file(init_path, mime=True)
72 except AttributeError:
80 logging.info("Using magic method " + method)
81 if method == 'from_file':
85 if method == 'guess_type':
86 return mime_guess_type
88 logging.critical("unknown magic method!")
91 # Scan the source code in the given directory (and all subdirectories)
92 # and return the number of fatal problems encountered
93 def scan_source(build_dir, root_dir, thisbuild):
97 # Common known non-free blobs (always lower case):
99 re.compile(r'.*flurryagent', re.IGNORECASE),
100 re.compile(r'.*paypal.*mpl', re.IGNORECASE),
101 re.compile(r'.*google.*analytics', re.IGNORECASE),
102 re.compile(r'.*admob.*sdk.*android', re.IGNORECASE),
103 re.compile(r'.*google.*ad.*view', re.IGNORECASE),
104 re.compile(r'.*google.*admob', re.IGNORECASE),
105 re.compile(r'.*google.*play.*services', re.IGNORECASE),
106 re.compile(r'.*crittercism', re.IGNORECASE),
107 re.compile(r'.*heyzap', re.IGNORECASE),
108 re.compile(r'.*jpct.*ae', re.IGNORECASE),
109 re.compile(r'.*youtube.*android.*player.*api', re.IGNORECASE),
110 re.compile(r'.*bugsense', re.IGNORECASE),
111 re.compile(r'.*crashlytics', re.IGNORECASE),
112 re.compile(r'.*ouya.*sdk', re.IGNORECASE),
113 re.compile(r'.*libspen23', re.IGNORECASE),
116 scanignore = common.getpaths(build_dir, thisbuild, 'scanignore')
117 scandelete = common.getpaths(build_dir, thisbuild, 'scandelete')
119 scanignore_worked = set()
120 scandelete_worked = set()
125 scanignore_worked.add(p)
132 scandelete_worked.add(p)
136 def ignoreproblem(what, fd, fp):
137 logging.info('Ignoring %s at %s' % (what, fd))
140 def removeproblem(what, fd, fp):
141 logging.info('Removing %s at %s' % (what, fd))
145 def warnproblem(what, fd):
146 logging.warn('Found %s at %s' % (what, fd))
148 def handleproblem(what, fd, fp):
150 return ignoreproblem(what, fd, fp)
152 return removeproblem(what, fd, fp)
153 logging.error('Found %s at %s' % (what, fd))
156 get_mime_type = init_mime_type()
158 # Iterate through all files in the source code
159 for r, d, f in os.walk(build_dir, topdown=True):
161 # It's topdown, so checking the basename is enough
162 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
168 # Path (relative) to the file
169 fp = os.path.join(r, curfile)
170 fd = fp[len(build_dir) + 1:]
172 mime = get_mime_type(fp)
174 if mime == 'application/x-sharedlib':
175 count += handleproblem('shared library', fd, fp)
177 elif mime == 'application/x-archive':
178 count += handleproblem('static library', fd, fp)
180 elif mime == 'application/x-executable' or mime == 'application/x-mach-binary':
181 count += handleproblem('binary executable', fd, fp)
183 elif mime == 'application/x-java-applet':
184 count += handleproblem('Java compiled class', fd, fp)
189 'application/java-archive',
190 'application/octet-stream',
193 if common.has_extension(fp, 'apk'):
194 removeproblem('APK file', fd, fp)
196 elif common.has_extension(fp, 'jar'):
198 if any(suspect.match(curfile) for suspect in usual_suspects):
199 count += handleproblem('usual supect', fd, fp)
201 warnproblem('JAR file', fd)
203 elif common.has_extension(fp, 'zip'):
204 warnproblem('ZIP file', fd)
207 warnproblem('unknown compressed or binary file', fd)
209 elif common.has_extension(fp, 'java'):
210 if not os.path.isfile(fp):
212 for line in file(fp):
213 if 'DexClassLoader' in line:
214 count += handleproblem('DexClassLoader', fd, fp)
217 elif common.has_extension(fp, 'gradle'):
218 if not os.path.isfile(fp):
220 for i, line in enumerate(file(fp)):
222 if any(suspect.match(line) for suspect in usual_suspects):
223 count += handleproblem('usual suspect at line %d' % i, fd, fp)
227 if p not in scanignore_worked:
228 logging.error('Unused scanignore path: %s' % p)
232 if p not in scandelete_worked:
233 logging.error('Unused scandelete path: %s' % p)
236 # Presence of a jni directory without buildjni=yes might
237 # indicate a problem (if it's not a problem, explicitly use
238 # buildjni=no to bypass this check)
239 if (os.path.exists(os.path.join(root_dir, 'jni')) and
240 not thisbuild['buildjni']):
241 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
249 global config, options
251 # Parse command line...
252 parser = ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]")
253 common.setup_global_opts(parser)
254 parser.add_argument("appid", nargs='*', help="app-id with optional versioncode in the form APPID[:VERCODE]")
255 options = parser.parse_args()
257 config = common.read_config(options)
259 # Read all app and srclib metadata
260 allapps = metadata.read_metadata()
261 apps = common.read_app_args(options.appid, allapps, True)
266 if not os.path.isdir(build_dir):
267 logging.info("Creating build directory")
268 os.makedirs(build_dir)
269 srclib_dir = os.path.join(build_dir, 'srclib')
270 extlib_dir = os.path.join(build_dir, 'extlib')
272 for appid, app in apps.iteritems():
275 logging.info("Skipping %s: disabled" % appid)
277 if not app['builds']:
278 logging.info("Skipping %s: no builds specified" % appid)
281 logging.info("Processing " + appid)
285 if app['Repo Type'] == 'srclib':
286 build_dir = os.path.join('build', 'srclib', app['Repo'])
288 build_dir = os.path.join('build', appid)
290 # Set up vcs interface and make sure we have the latest code...
291 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
293 for thisbuild in app['builds']:
295 if thisbuild['disable']:
296 logging.info("...skipping version %s - %s" % (
297 thisbuild['version'], thisbuild.get('disable', thisbuild['commit'][1:])))
299 logging.info("...scanning version " + thisbuild['version'])
301 # Prepare the source code...
302 root_dir, _ = common.prepare_source(vcs, app, thisbuild,
303 build_dir, srclib_dir,
307 count = scan_source(build_dir, root_dir, thisbuild)
309 logging.warn('Scanner found %d problems in %s (%s)' % (
310 count, appid, thisbuild['vercode']))
313 except BuildException as be:
314 logging.warn("Could not scan app %s due to BuildException: %s" % (
317 except VCSException as vcse:
318 logging.warn("VCS error while scanning app %s: %s" % (appid, vcse))
321 logging.warn("Could not scan app %s due to unknown error: %s" % (
322 appid, traceback.format_exc()))
325 logging.info("Finished:")
326 print "%d app(s) with problems" % probcount
328 if __name__ == "__main__":