blob: 4bacb82d94bde06b0cda7a29fe25aae0a1eb3f84 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Jim Hebert91c052c2011-03-11 11:00:53 -08002# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger012371e2019-01-03 03:42:32 -05006"""Command to extract the dependancy tree for a given package.
7
8This produces JSON output for other tools to process.
9"""
Don Garrett25f309a2014-03-19 14:02:12 -070010
Mike Frysinger383367e2014-09-16 15:06:17 -040011from __future__ import print_function
Chris McDonaldd2fa6162019-07-30 15:30:58 -060012from __future__ import absolute_import
Mike Frysinger383367e2014-09-16 15:06:17 -040013
Jim Hebert91c052c2011-03-11 11:00:53 -080014import json
Xuewei Zhang656f9932017-09-15 16:15:05 -070015import os
Don Garrettf8bf7842014-03-20 17:03:42 -070016
Chris McDonalda4fb7fe2019-08-19 15:16:57 -060017from chromite.lib.depgraph import DepGraphGenerator
Don Garrettf8bf7842014-03-20 17:03:42 -070018
Jim Hebertcf870d72013-06-12 15:33:34 -070019from chromite.lib import commandline
20from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070021from chromite.lib import cros_logging as logging
Xuewei Zhang656f9932017-09-15 16:15:05 -070022from chromite.lib import osutils
23from chromite.lib import portage_util
Mike Frysingercc838832014-05-24 13:10:30 -040024
Chris McDonaldd8a7f112019-11-01 10:35:07 -060025
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070026def FlattenDepTree(deptree, pkgtable=None, parentcpv=None, get_cpe=False):
Don Garrett25f309a2014-03-19 14:02:12 -070027 """Simplify dependency json.
28
29Turn something like this (the parallel_emerge DepsTree format):
Jim Hebert91c052c2011-03-11 11:00:53 -080030{
31 "app-admin/eselect-1.2.9": {
32 "action": "merge",
33 "deps": {
34 "sys-apps/coreutils-7.5-r1": {
35 "action": "merge",
36 "deps": {},
37 "deptype": "runtime"
38 },
39 ...
40 }
41 }
42}
43 ...into something like this (the cros_extract_deps format):
44{
45 "app-admin/eselect-1.2.9": {
46 "deps": ["coreutils-7.5-r1"],
47 "rev_deps": [],
48 "name": "eselect",
49 "category": "app-admin",
50 "version": "1.2.9",
51 "full_name": "app-admin/eselect-1.2.9",
52 "action": "merge"
53 },
54 "sys-apps/coreutils-7.5-r1": {
55 "deps": [],
56 "rev_deps": ["app-admin/eselect-1.2.9"],
57 "name": "coreutils",
58 "category": "sys-apps",
59 "version": "7.5-r1",
60 "full_name": "sys-apps/coreutils-7.5-r1",
61 "action": "merge"
62 }
63}
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070064
65 Args:
66 deptree: The dependency tree.
67 pkgtable: The package table to update. If None, create a new one.
68 parentcpv: The parent CPV.
69 get_cpe: If set True, include CPE in the flattened dependency tree.
70
71 Returns:
72 A flattened dependency tree.
Jim Hebert91c052c2011-03-11 11:00:53 -080073 """
David James1b363582012-12-17 11:53:11 -080074 if pkgtable is None:
75 pkgtable = {}
Mike Frysinger0bdbc102019-06-13 15:27:29 -040076 for cpv, record in deptree.items():
Jim Hebert91c052c2011-03-11 11:00:53 -080077 if cpv not in pkgtable:
Xuewei Zhang656f9932017-09-15 16:15:05 -070078 split = portage_util.SplitCPV(cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070079 pkgtable[cpv] = {'deps': [],
80 'rev_deps': [],
Xuewei Zhang656f9932017-09-15 16:15:05 -070081 'name': split.package,
82 'category': split.category,
83 'version': '%s' % split.version,
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070084 'full_name': cpv,
85 'cpes': [],
86 'action': record['action']}
87 if get_cpe:
Xuewei Zhang656f9932017-09-15 16:15:05 -070088 pkgtable[cpv]['cpes'].extend(GetCPEFromCPV(
89 split.category, split.package, split.version_no_rev))
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070090
Jim Hebert91c052c2011-03-11 11:00:53 -080091 # If we have a parent, that is a rev_dep for the current package.
92 if parentcpv:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070093 pkgtable[cpv]['rev_deps'].append(parentcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080094 # If current package has any deps, record those.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070095 for childcpv in record['deps']:
96 pkgtable[cpv]['deps'].append(childcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080097 # Visit the subtree recursively as well.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070098 FlattenDepTree(record['deps'], pkgtable=pkgtable, parentcpv=cpv,
99 get_cpe=get_cpe)
Ned Nguyen3bbd2072019-01-28 19:33:41 -0700100 # Sort 'deps' & 'rev_deps' alphabetically to make them more readable.
101 pkgtable[cpv]['deps'].sort()
102 pkgtable[cpv]['rev_deps'].sort()
Jim Hebert91c052c2011-03-11 11:00:53 -0800103 return pkgtable
104
105
Jim Hebertcf870d72013-06-12 15:33:34 -0700106def GetCPEFromCPV(category, package, version):
107 """Look up the CPE for a specified Portage package.
Jim Hebert91c052c2011-03-11 11:00:53 -0800108
Jim Hebertcf870d72013-06-12 15:33:34 -0700109 Args:
110 category: The Portage package's category, e.g. "net-misc"
111 package: The Portage package's name, e.g. "curl"
112 version: The Portage version, e.g. "7.30.0"
113
Mike Frysinger02e1e072013-11-10 22:11:34 -0500114 Returns:
115 A list of CPE Name strings, e.g.
116 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
Jim Hebertcf870d72013-06-12 15:33:34 -0700117 """
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700118 equery_cmd = ['equery', 'm', '-U', '%s/%s' % (category, package)]
Mike Frysinger45602c72019-09-22 02:15:11 -0400119 lines = cros_build_lib.run(equery_cmd, error_code_ok=True, print_cmd=False,
Mike Frysinger44b83812019-12-10 00:09:30 -0500120 stdout=True, encoding='utf-8').stdout.splitlines()
Jim Hebertcf870d72013-06-12 15:33:34 -0700121 # Look for lines like "Remote-ID: cpe:/a:kernel:linux-pam ID: cpe"
122 # and extract the cpe URI.
123 cpes = []
124 for line in lines:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700125 if 'ID: cpe' not in line:
Jim Hebertcf870d72013-06-12 15:33:34 -0700126 continue
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700127 cpes.append('%s:%s' % (line.split()[1], version.replace('_', '')))
Jim Hebertcf870d72013-06-12 15:33:34 -0700128 # Note that we're assuming we can combine the root of the CPE, taken
129 # from metadata.xml, and tack on the version number as used by
130 # Portage, and come up with a legitimate CPE. This works so long as
131 # Portage and CPE agree on the precise formatting of the version
Jim Hebert96aff9c2013-07-16 15:43:17 -0700132 # number, which they almost always do. The major exception we've
133 # identified thus far is that our ebuilds have a pattern of inserting
134 # underscores prior to patchlevels, that neither upstream nor CPE
135 # use. For example, our code will decide we have
Jim Hebertcf870d72013-06-12 15:33:34 -0700136 # cpe:/a:todd_miller:sudo:1.8.6_p7 yet the advisories use a format
137 # like cpe:/a:todd_miller:sudo:1.8.6p7, without the underscore. (CPE
138 # is "right" in this example, in that it matches www.sudo.ws.)
139 #
Jim Hebert96aff9c2013-07-16 15:43:17 -0700140 # Removing underscores seems to improve our chances of correctly
141 # arriving at the CPE used by NVD. However, at the end of the day,
142 # ebuild version numbers are rev'd by people who don't have "try to
143 # match NVD" as one of their goals, and there is always going to be
144 # some risk of minor formatting disagreements at the version number
145 # level, if not from stray underscores then from something else.
146 #
Jim Hebertcf870d72013-06-12 15:33:34 -0700147 # This is livable so long as you do some fuzzy version number
148 # comparison in your vulnerability monitoring, between what-we-have
149 # and what-the-advisory-says-is-affected.
150 return cpes
151
152
Chris McDonalde69db662018-11-15 12:50:18 -0700153def GenerateSDKCPVList(sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700154 """Find all SDK packages from package.provided
155
156 Args:
Chris McDonalde69db662018-11-15 12:50:18 -0700157 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700158
159 Returns:
160 A list of CPV Name strings, e.g.
161 ["sys-libs/glibc-2.23-r9", "dev-lang/go-1.8.3-r1"]
162 """
163 # Look at packages in package.provided.
Chris McDonalde69db662018-11-15 12:50:18 -0700164 sdk_file_path = os.path.join(sysroot, 'etc', 'portage',
Xuewei Zhang656f9932017-09-15 16:15:05 -0700165 'profile', 'package.provided')
166 for line in osutils.ReadFile(sdk_file_path).splitlines():
167 # Skip comments and empty lines.
168 line = line.split('#', 1)[0].strip()
169 if not line:
170 continue
171 yield line
172
173
Chris McDonalde69db662018-11-15 12:50:18 -0700174def GenerateCPEList(deps_list, sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700175 """Generate all CPEs for the packages included in deps_list and SDK packages
176
177 Args:
178 deps_list: A flattened dependency tree (cros_extract_deps format).
Chris McDonalde69db662018-11-15 12:50:18 -0700179 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700180
181 Returns:
182 A list of CPE info for packages in deps_list and SDK packages, e.g.
183 [
184 {
185 "ComponentName": "app-admin/sudo",
186 "Repository": "cros",
187 "Targets": [
188 "cpe:/a:todd_miller:sudo:1.8.19p2"
189 ]
190 },
191 {
192 "ComponentName": "sys-libs/glibc",
193 "Repository": "cros",
194 "Targets": [
195 "cpe:/a:gnu:glibc:2.23"
196 ]
197 }
198 ]
199 """
Jim Hebertcf870d72013-06-12 15:33:34 -0700200 cpe_dump = []
Xuewei Zhang656f9932017-09-15 16:15:05 -0700201
202 # Generage CPEs for SDK packages.
Mike Frysinger1457e482019-01-03 04:04:02 -0500203 for sdk_cpv in sorted(GenerateSDKCPVList(sysroot)):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700204 # Only add CPE for SDK CPVs missing in deps_list.
205 if deps_list.get(sdk_cpv) is not None:
206 continue
207
208 split = portage_util.SplitCPV(sdk_cpv)
209 cpes = GetCPEFromCPV(split.category, split.package, split.version_no_rev)
210 if cpes:
Alex Klein9f93b482018-10-01 09:26:51 -0600211 cpe_dump.append({'ComponentName': '%s' % split.cp,
Xuewei Zhang656f9932017-09-15 16:15:05 -0700212 'Repository': 'cros',
213 'Targets': sorted(cpes)})
214 else:
215 logging.warning('No CPE entry for %s', sdk_cpv)
216
217 # Generage CPEs for packages in deps_list.
Mike Frysinger1457e482019-01-03 04:04:02 -0500218 for cpv, record in sorted(deps_list.items()):
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700219 if record['cpes']:
220 name = '%s/%s' % (record['category'], record['name'])
221 cpe_dump.append({'ComponentName': name,
222 'Repository': 'cros',
223 'Targets': sorted(record['cpes'])})
Jim Hebertcf870d72013-06-12 15:33:34 -0700224 else:
Ralph Nathan446aee92015-03-23 14:44:56 -0700225 logging.warning('No CPE entry for %s', cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700226 return sorted(cpe_dump, key=lambda k: k['ComponentName'])
Jim Hebert91c052c2011-03-11 11:00:53 -0800227
228
Chris McDonalde69db662018-11-15 12:50:18 -0700229def ParseArgs(argv):
230 """Parse command line arguments."""
Mike Frysinger012371e2019-01-03 03:42:32 -0500231 parser = commandline.ArgumentParser(description=__doc__)
Chris McDonalde69db662018-11-15 12:50:18 -0700232 target = parser.add_mutually_exclusive_group()
233 target.add_argument('--sysroot', type='path', help='Path to the sysroot.')
234 target.add_argument('--board', help='Board name.')
235
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700236 parser.add_argument('--format', default='deps',
237 choices=['deps', 'cpe'],
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700238 help='Output either traditional deps or CPE-only JSON.')
239 parser.add_argument('--output-path', default=None,
240 help='Write output to the given path.')
Mike Frysinger012371e2019-01-03 03:42:32 -0500241 parser.add_argument('pkgs', nargs='*')
242 opts = parser.parse_args(argv)
243 opts.Freeze()
244 return opts
Jim Hebert91c052c2011-03-11 11:00:53 -0800245
Chris McDonalde69db662018-11-15 12:50:18 -0700246
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600247def FilterObsoleteDeps(package_deps):
248 """Remove all the packages that are to be uninstalled from |package_deps|.
249
250 Returns:
251 None since this method mutates |package_deps| directly.
252 """
253 obsolete_package_deps = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400254 for k, v in package_deps.items():
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600255 if v['action'] in ('merge', 'nomerge'):
256 continue
257 elif v['action'] == 'uninstall':
258 obsolete_package_deps.append(k)
259 else:
260 assert False, (
261 'Unrecognized action. Package dep data: %s' % v)
262 for p in obsolete_package_deps:
263 del package_deps[p]
264
265
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600266def ExtractDeps(sysroot,
267 package_list,
268 formatting='deps',
269 include_bdepend=True):
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600270 """Returns the set of dependencies for the packages in package_list.
271
272 For calculating dependencies graph, this should only consider packages
Chris McDonaldebc7ae72019-10-03 14:58:46 -0600273 that are DEPENDS, RDEPENDS, or BDEPENDS. Essentially, this should answer the
274 question "which are all the packages which changing them may change the
275 execution of any binaries produced by packages in |package_list|."
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600276
277 Args:
278 sysroot: the path (string) to the root directory into which the package is
279 pretend to be merged. This value is also used for setting
280 PORTAGE_CONFIGROOT.
281 package_list: the list of packages (CP string) to extract their dependencies
282 from.
283 formatting: can either be 'deps' or 'cpe'. For 'deps', see the return
284 format in docstring of FlattenDepTree, for 'cpe', see the return format in
285 docstring of GenerateCPEList.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600286 include_bdepend: Controls whether BDEPEND packages that would be installed
287 to BROOT (usually "/" instead of ROOT) are included in the output.
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600288
289 Returns:
290 A JSON-izable object that either follows 'deps' or 'cpe' format.
291 """
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600292 lib_argv = ['--quiet', '--pretend', '--emptytree']
293 if include_bdepend:
294 lib_argv += ['--include-bdepend']
Chris McDonalde69db662018-11-15 12:50:18 -0700295 lib_argv += ['--sysroot=%s' % sysroot]
296 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800297
298 deps = DepGraphGenerator()
Jim Hebertcf870d72013-06-12 15:33:34 -0700299 deps.Initialize(lib_argv)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600300
301 deps_tree, _deps_info, bdeps_tree = deps.GenDependencyTree()
302 trees = (deps_tree, bdeps_tree)
303
304 flattened_trees = tuple(
305 FlattenDepTree(tree, get_cpe=(formatting == 'cpe')) for tree in trees)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600306
307 # Workaround: since emerge doesn't honor the --emptytree flag, for now we need
308 # to manually filter out packages that are obsolete (meant to be
309 # uninstalled by emerge)
310 # TODO(crbug.com/938605): remove this work around once
311 # https://bugs.gentoo.org/681308 is addressed.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600312 for tree in flattened_trees:
313 FilterObsoleteDeps(tree)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600314
Chris McDonalde69db662018-11-15 12:50:18 -0700315 if formatting == 'cpe':
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600316 flattened_trees = tuple(
317 GenerateCPEList(tree, sysroot) for tree in flattened_trees)
318 return flattened_trees
Chris McDonalde69db662018-11-15 12:50:18 -0700319
320
321def main(argv):
Mike Frysinger012371e2019-01-03 03:42:32 -0500322 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700323
Mike Frysinger012371e2019-01-03 03:42:32 -0500324 sysroot = opts.sysroot or cros_build_lib.GetSysroot(opts.board)
Chris McDonald486508d2019-11-04 09:20:07 -0700325 deps_list, _ = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700326
327 deps_output = json.dumps(deps_list, sort_keys=True, indent=2)
Mike Frysinger012371e2019-01-03 03:42:32 -0500328 if opts.output_path:
329 with open(opts.output_path, 'w') as f:
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700330 f.write(deps_output)
331 else:
332 print(deps_output)