blob: 5fa7695a0a952801ce998379adfb5afa0aa0b7c7 [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
Mike Frysingera942aee2020-03-20 03:53:37 -040014import sys
Don Garrettf8bf7842014-03-20 17:03:42 -070015
Chris McDonalda4fb7fe2019-08-19 15:16:57 -060016from chromite.lib.depgraph import DepGraphGenerator
Don Garrettf8bf7842014-03-20 17:03:42 -070017
Mike Frysingerd8d25cd2021-04-06 11:39:17 -040018from chromite.lib import build_target_lib
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
Mike Frysingerd0960812020-06-09 01:53:32 -040022from chromite.lib import pformat
Alex Klein5cdd57f2020-11-23 11:53:28 -070023from chromite.lib import sysroot_lib
Alex Klein18a60af2020-06-11 12:08:47 -060024from chromite.lib.parser import package_info
Mike Frysingercc838832014-05-24 13:10:30 -040025
Chris McDonaldd8a7f112019-11-01 10:35:07 -060026
Mike Frysingera942aee2020-03-20 03:53:37 -040027assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
28
29
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070030def FlattenDepTree(deptree, pkgtable=None, parentcpv=None, get_cpe=False):
Don Garrett25f309a2014-03-19 14:02:12 -070031 """Simplify dependency json.
32
33Turn something like this (the parallel_emerge DepsTree format):
Jim Hebert91c052c2011-03-11 11:00:53 -080034{
35 "app-admin/eselect-1.2.9": {
36 "action": "merge",
37 "deps": {
38 "sys-apps/coreutils-7.5-r1": {
39 "action": "merge",
40 "deps": {},
41 "deptype": "runtime"
42 },
43 ...
44 }
45 }
46}
47 ...into something like this (the cros_extract_deps format):
48{
49 "app-admin/eselect-1.2.9": {
50 "deps": ["coreutils-7.5-r1"],
51 "rev_deps": [],
52 "name": "eselect",
53 "category": "app-admin",
54 "version": "1.2.9",
55 "full_name": "app-admin/eselect-1.2.9",
56 "action": "merge"
57 },
58 "sys-apps/coreutils-7.5-r1": {
59 "deps": [],
60 "rev_deps": ["app-admin/eselect-1.2.9"],
61 "name": "coreutils",
62 "category": "sys-apps",
63 "version": "7.5-r1",
64 "full_name": "sys-apps/coreutils-7.5-r1",
65 "action": "merge"
66 }
67}
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070068
69 Args:
70 deptree: The dependency tree.
71 pkgtable: The package table to update. If None, create a new one.
72 parentcpv: The parent CPV.
73 get_cpe: If set True, include CPE in the flattened dependency tree.
74
75 Returns:
76 A flattened dependency tree.
Jim Hebert91c052c2011-03-11 11:00:53 -080077 """
David James1b363582012-12-17 11:53:11 -080078 if pkgtable is None:
79 pkgtable = {}
Mike Frysinger0bdbc102019-06-13 15:27:29 -040080 for cpv, record in deptree.items():
Jim Hebert91c052c2011-03-11 11:00:53 -080081 if cpv not in pkgtable:
Alex Klein18a60af2020-06-11 12:08:47 -060082 split = package_info.SplitCPV(cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070083 pkgtable[cpv] = {'deps': [],
84 'rev_deps': [],
Xuewei Zhang656f9932017-09-15 16:15:05 -070085 'name': split.package,
86 'category': split.category,
87 'version': '%s' % split.version,
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070088 'full_name': cpv,
89 'cpes': [],
90 'action': record['action']}
91 if get_cpe:
Xuewei Zhang656f9932017-09-15 16:15:05 -070092 pkgtable[cpv]['cpes'].extend(GetCPEFromCPV(
93 split.category, split.package, split.version_no_rev))
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070094
Jim Hebert91c052c2011-03-11 11:00:53 -080095 # If we have a parent, that is a rev_dep for the current package.
96 if parentcpv:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070097 pkgtable[cpv]['rev_deps'].append(parentcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080098 # If current package has any deps, record those.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070099 for childcpv in record['deps']:
100 pkgtable[cpv]['deps'].append(childcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -0800101 # Visit the subtree recursively as well.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700102 FlattenDepTree(record['deps'], pkgtable=pkgtable, parentcpv=cpv,
103 get_cpe=get_cpe)
Ned Nguyen3bbd2072019-01-28 19:33:41 -0700104 # Sort 'deps' & 'rev_deps' alphabetically to make them more readable.
105 pkgtable[cpv]['deps'].sort()
106 pkgtable[cpv]['rev_deps'].sort()
Jim Hebert91c052c2011-03-11 11:00:53 -0800107 return pkgtable
108
109
Jim Hebertcf870d72013-06-12 15:33:34 -0700110def GetCPEFromCPV(category, package, version):
111 """Look up the CPE for a specified Portage package.
Jim Hebert91c052c2011-03-11 11:00:53 -0800112
Jim Hebertcf870d72013-06-12 15:33:34 -0700113 Args:
114 category: The Portage package's category, e.g. "net-misc"
115 package: The Portage package's name, e.g. "curl"
116 version: The Portage version, e.g. "7.30.0"
117
Mike Frysinger02e1e072013-11-10 22:11:34 -0500118 Returns:
119 A list of CPE Name strings, e.g.
120 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
Jim Hebertcf870d72013-06-12 15:33:34 -0700121 """
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700122 equery_cmd = ['equery', 'm', '-U', '%s/%s' % (category, package)]
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500123 lines = cros_build_lib.run(equery_cmd, check=False, print_cmd=False,
Mike Frysinger44b83812019-12-10 00:09:30 -0500124 stdout=True, encoding='utf-8').stdout.splitlines()
Jim Hebertcf870d72013-06-12 15:33:34 -0700125 # Look for lines like "Remote-ID: cpe:/a:kernel:linux-pam ID: cpe"
126 # and extract the cpe URI.
127 cpes = []
128 for line in lines:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700129 if 'ID: cpe' not in line:
Jim Hebertcf870d72013-06-12 15:33:34 -0700130 continue
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700131 cpes.append('%s:%s' % (line.split()[1], version.replace('_', '')))
Jim Hebertcf870d72013-06-12 15:33:34 -0700132 # Note that we're assuming we can combine the root of the CPE, taken
133 # from metadata.xml, and tack on the version number as used by
134 # Portage, and come up with a legitimate CPE. This works so long as
135 # Portage and CPE agree on the precise formatting of the version
Jim Hebert96aff9c2013-07-16 15:43:17 -0700136 # number, which they almost always do. The major exception we've
137 # identified thus far is that our ebuilds have a pattern of inserting
138 # underscores prior to patchlevels, that neither upstream nor CPE
139 # use. For example, our code will decide we have
Jim Hebertcf870d72013-06-12 15:33:34 -0700140 # cpe:/a:todd_miller:sudo:1.8.6_p7 yet the advisories use a format
141 # like cpe:/a:todd_miller:sudo:1.8.6p7, without the underscore. (CPE
142 # is "right" in this example, in that it matches www.sudo.ws.)
143 #
Jim Hebert96aff9c2013-07-16 15:43:17 -0700144 # Removing underscores seems to improve our chances of correctly
145 # arriving at the CPE used by NVD. However, at the end of the day,
146 # ebuild version numbers are rev'd by people who don't have "try to
147 # match NVD" as one of their goals, and there is always going to be
148 # some risk of minor formatting disagreements at the version number
149 # level, if not from stray underscores then from something else.
150 #
Jim Hebertcf870d72013-06-12 15:33:34 -0700151 # This is livable so long as you do some fuzzy version number
152 # comparison in your vulnerability monitoring, between what-we-have
153 # and what-the-advisory-says-is-affected.
154 return cpes
155
156
Chris McDonalde69db662018-11-15 12:50:18 -0700157def GenerateCPEList(deps_list, sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700158 """Generate all CPEs for the packages included in deps_list and SDK packages
159
160 Args:
161 deps_list: A flattened dependency tree (cros_extract_deps format).
Chris McDonalde69db662018-11-15 12:50:18 -0700162 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700163
164 Returns:
165 A list of CPE info for packages in deps_list and SDK packages, e.g.
166 [
167 {
168 "ComponentName": "app-admin/sudo",
169 "Repository": "cros",
170 "Targets": [
171 "cpe:/a:todd_miller:sudo:1.8.19p2"
172 ]
173 },
174 {
175 "ComponentName": "sys-libs/glibc",
176 "Repository": "cros",
177 "Targets": [
178 "cpe:/a:gnu:glibc:2.23"
179 ]
180 }
181 ]
182 """
Jim Hebertcf870d72013-06-12 15:33:34 -0700183 cpe_dump = []
Xuewei Zhang656f9932017-09-15 16:15:05 -0700184
Alex Klein5cdd57f2020-11-23 11:53:28 -0700185 # Generate CPEs for SDK packages.
186 for pkg_info in sorted(
187 sysroot_lib.get_sdk_provided_packages(sysroot), key=lambda x: x.cpvr):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700188 # Only add CPE for SDK CPVs missing in deps_list.
Alex Klein5cdd57f2020-11-23 11:53:28 -0700189 if deps_list.get(pkg_info.cpvr) is not None:
Xuewei Zhang656f9932017-09-15 16:15:05 -0700190 continue
191
Alex Klein5cdd57f2020-11-23 11:53:28 -0700192 cpes = GetCPEFromCPV(pkg_info.category, pkg_info.package, pkg_info.version)
Xuewei Zhang656f9932017-09-15 16:15:05 -0700193 if cpes:
Alex Klein5cdd57f2020-11-23 11:53:28 -0700194 cpe_dump.append({'ComponentName': '%s' % pkg_info.atom,
Xuewei Zhang656f9932017-09-15 16:15:05 -0700195 'Repository': 'cros',
196 'Targets': sorted(cpes)})
197 else:
Alex Klein5cdd57f2020-11-23 11:53:28 -0700198 logging.warning('No CPE entry for %s', pkg_info.cpvr)
Xuewei Zhang656f9932017-09-15 16:15:05 -0700199
200 # Generage CPEs for packages in deps_list.
Mike Frysinger1457e482019-01-03 04:04:02 -0500201 for cpv, record in sorted(deps_list.items()):
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700202 if record['cpes']:
203 name = '%s/%s' % (record['category'], record['name'])
204 cpe_dump.append({'ComponentName': name,
205 'Repository': 'cros',
206 'Targets': sorted(record['cpes'])})
Jim Hebertcf870d72013-06-12 15:33:34 -0700207 else:
Ralph Nathan446aee92015-03-23 14:44:56 -0700208 logging.warning('No CPE entry for %s', cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700209 return sorted(cpe_dump, key=lambda k: k['ComponentName'])
Jim Hebert91c052c2011-03-11 11:00:53 -0800210
211
Chris McDonalde69db662018-11-15 12:50:18 -0700212def ParseArgs(argv):
213 """Parse command line arguments."""
Mike Frysinger012371e2019-01-03 03:42:32 -0500214 parser = commandline.ArgumentParser(description=__doc__)
Chris McDonalde69db662018-11-15 12:50:18 -0700215 target = parser.add_mutually_exclusive_group()
216 target.add_argument('--sysroot', type='path', help='Path to the sysroot.')
217 target.add_argument('--board', help='Board name.')
218
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700219 parser.add_argument('--format', default='deps',
220 choices=['deps', 'cpe'],
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700221 help='Output either traditional deps or CPE-only JSON.')
222 parser.add_argument('--output-path', default=None,
223 help='Write output to the given path.')
Mike Frysinger012371e2019-01-03 03:42:32 -0500224 parser.add_argument('pkgs', nargs='*')
225 opts = parser.parse_args(argv)
226 opts.Freeze()
227 return opts
Jim Hebert91c052c2011-03-11 11:00:53 -0800228
Chris McDonalde69db662018-11-15 12:50:18 -0700229
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600230def FilterObsoleteDeps(package_deps):
231 """Remove all the packages that are to be uninstalled from |package_deps|.
232
233 Returns:
234 None since this method mutates |package_deps| directly.
235 """
236 obsolete_package_deps = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400237 for k, v in package_deps.items():
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600238 if v['action'] in ('merge', 'nomerge'):
239 continue
240 elif v['action'] == 'uninstall':
241 obsolete_package_deps.append(k)
242 else:
243 assert False, (
244 'Unrecognized action. Package dep data: %s' % v)
245 for p in obsolete_package_deps:
246 del package_deps[p]
247
248
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600249def ExtractDeps(sysroot,
250 package_list,
251 formatting='deps',
Chris McDonald76608422020-06-15 11:42:20 -0600252 include_bdepend=True,
253 backtrack=True):
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600254 """Returns the set of dependencies for the packages in package_list.
255
256 For calculating dependencies graph, this should only consider packages
Chris McDonaldebc7ae72019-10-03 14:58:46 -0600257 that are DEPENDS, RDEPENDS, or BDEPENDS. Essentially, this should answer the
258 question "which are all the packages which changing them may change the
259 execution of any binaries produced by packages in |package_list|."
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600260
261 Args:
262 sysroot: the path (string) to the root directory into which the package is
263 pretend to be merged. This value is also used for setting
264 PORTAGE_CONFIGROOT.
265 package_list: the list of packages (CP string) to extract their dependencies
266 from.
267 formatting: can either be 'deps' or 'cpe'. For 'deps', see the return
268 format in docstring of FlattenDepTree, for 'cpe', see the return format in
269 docstring of GenerateCPEList.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600270 include_bdepend: Controls whether BDEPEND packages that would be installed
271 to BROOT (usually "/" instead of ROOT) are included in the output.
Chris McDonald76608422020-06-15 11:42:20 -0600272 backtrack: Setting to False disables backtracking in Portage's dependency
273 solver. If the highest available version of dependencies doesn't produce
274 a solvable graph Portage will give up and return an error instead of
275 trying other candidates.
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600276
277 Returns:
278 A JSON-izable object that either follows 'deps' or 'cpe' format.
279 """
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600280 lib_argv = ['--quiet', '--pretend', '--emptytree']
281 if include_bdepend:
282 lib_argv += ['--include-bdepend']
Chris McDonald76608422020-06-15 11:42:20 -0600283 if not backtrack:
284 lib_argv += ['--backtrack=0']
Chris McDonalde69db662018-11-15 12:50:18 -0700285 lib_argv += ['--sysroot=%s' % sysroot]
286 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800287
288 deps = DepGraphGenerator()
Jim Hebertcf870d72013-06-12 15:33:34 -0700289 deps.Initialize(lib_argv)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600290
291 deps_tree, _deps_info, bdeps_tree = deps.GenDependencyTree()
292 trees = (deps_tree, bdeps_tree)
293
294 flattened_trees = tuple(
295 FlattenDepTree(tree, get_cpe=(formatting == 'cpe')) for tree in trees)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600296
297 # Workaround: since emerge doesn't honor the --emptytree flag, for now we need
298 # to manually filter out packages that are obsolete (meant to be
299 # uninstalled by emerge)
300 # TODO(crbug.com/938605): remove this work around once
301 # https://bugs.gentoo.org/681308 is addressed.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600302 for tree in flattened_trees:
303 FilterObsoleteDeps(tree)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600304
Chris McDonalde69db662018-11-15 12:50:18 -0700305 if formatting == 'cpe':
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600306 flattened_trees = tuple(
307 GenerateCPEList(tree, sysroot) for tree in flattened_trees)
308 return flattened_trees
Chris McDonalde69db662018-11-15 12:50:18 -0700309
310
311def main(argv):
Mike Frysinger012371e2019-01-03 03:42:32 -0500312 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700313
Mike Frysingerd8d25cd2021-04-06 11:39:17 -0400314 sysroot = (opts.sysroot or
315 build_target_lib.get_default_sysroot_path(opts.board))
Chris McDonald486508d2019-11-04 09:20:07 -0700316 deps_list, _ = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700317
Mike Frysingerd0960812020-06-09 01:53:32 -0400318 pformat.json(deps_list,
319 fp=opts.output_path if opts.output_path else sys.stdout)