blob: 4c0d755d789847b3cf8f1833e7fa88cb5feefc6c [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
12
Jim Hebert91c052c2011-03-11 11:00:53 -080013import json
Xuewei Zhang656f9932017-09-15 16:15:05 -070014import os
Don Garrettf8bf7842014-03-20 17:03:42 -070015
Jim Hebert91c052c2011-03-11 11:00:53 -080016from parallel_emerge import DepGraphGenerator
Don Garrettf8bf7842014-03-20 17:03:42 -070017
Jim Hebertcf870d72013-06-12 15:33:34 -070018from chromite.lib import commandline
19from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070020from chromite.lib import cros_logging as logging
Xuewei Zhang656f9932017-09-15 16:15:05 -070021from chromite.lib import osutils
22from chromite.lib import portage_util
Mike Frysingercc838832014-05-24 13:10:30 -040023
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070024def FlattenDepTree(deptree, pkgtable=None, parentcpv=None, get_cpe=False):
Don Garrett25f309a2014-03-19 14:02:12 -070025 """Simplify dependency json.
26
27Turn something like this (the parallel_emerge DepsTree format):
Jim Hebert91c052c2011-03-11 11:00:53 -080028{
29 "app-admin/eselect-1.2.9": {
30 "action": "merge",
31 "deps": {
32 "sys-apps/coreutils-7.5-r1": {
33 "action": "merge",
34 "deps": {},
35 "deptype": "runtime"
36 },
37 ...
38 }
39 }
40}
41 ...into something like this (the cros_extract_deps format):
42{
43 "app-admin/eselect-1.2.9": {
44 "deps": ["coreutils-7.5-r1"],
45 "rev_deps": [],
46 "name": "eselect",
47 "category": "app-admin",
48 "version": "1.2.9",
49 "full_name": "app-admin/eselect-1.2.9",
50 "action": "merge"
51 },
52 "sys-apps/coreutils-7.5-r1": {
53 "deps": [],
54 "rev_deps": ["app-admin/eselect-1.2.9"],
55 "name": "coreutils",
56 "category": "sys-apps",
57 "version": "7.5-r1",
58 "full_name": "sys-apps/coreutils-7.5-r1",
59 "action": "merge"
60 }
61}
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070062
63 Args:
64 deptree: The dependency tree.
65 pkgtable: The package table to update. If None, create a new one.
66 parentcpv: The parent CPV.
67 get_cpe: If set True, include CPE in the flattened dependency tree.
68
69 Returns:
70 A flattened dependency tree.
Jim Hebert91c052c2011-03-11 11:00:53 -080071 """
David James1b363582012-12-17 11:53:11 -080072 if pkgtable is None:
73 pkgtable = {}
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070074 for cpv, record in deptree.iteritems():
Jim Hebert91c052c2011-03-11 11:00:53 -080075 if cpv not in pkgtable:
Xuewei Zhang656f9932017-09-15 16:15:05 -070076 split = portage_util.SplitCPV(cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070077 pkgtable[cpv] = {'deps': [],
78 'rev_deps': [],
Xuewei Zhang656f9932017-09-15 16:15:05 -070079 'name': split.package,
80 'category': split.category,
81 'version': '%s' % split.version,
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070082 'full_name': cpv,
83 'cpes': [],
84 'action': record['action']}
85 if get_cpe:
Xuewei Zhang656f9932017-09-15 16:15:05 -070086 pkgtable[cpv]['cpes'].extend(GetCPEFromCPV(
87 split.category, split.package, split.version_no_rev))
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070088
Jim Hebert91c052c2011-03-11 11:00:53 -080089 # If we have a parent, that is a rev_dep for the current package.
90 if parentcpv:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070091 pkgtable[cpv]['rev_deps'].append(parentcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080092 # If current package has any deps, record those.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070093 for childcpv in record['deps']:
94 pkgtable[cpv]['deps'].append(childcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080095 # Visit the subtree recursively as well.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070096 FlattenDepTree(record['deps'], pkgtable=pkgtable, parentcpv=cpv,
97 get_cpe=get_cpe)
Ned Nguyen3bbd2072019-01-28 19:33:41 -070098 # Sort 'deps' & 'rev_deps' alphabetically to make them more readable.
99 pkgtable[cpv]['deps'].sort()
100 pkgtable[cpv]['rev_deps'].sort()
Jim Hebert91c052c2011-03-11 11:00:53 -0800101 return pkgtable
102
103
Jim Hebertcf870d72013-06-12 15:33:34 -0700104def GetCPEFromCPV(category, package, version):
105 """Look up the CPE for a specified Portage package.
Jim Hebert91c052c2011-03-11 11:00:53 -0800106
Jim Hebertcf870d72013-06-12 15:33:34 -0700107 Args:
108 category: The Portage package's category, e.g. "net-misc"
109 package: The Portage package's name, e.g. "curl"
110 version: The Portage version, e.g. "7.30.0"
111
Mike Frysinger02e1e072013-11-10 22:11:34 -0500112 Returns:
113 A list of CPE Name strings, e.g.
114 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
Jim Hebertcf870d72013-06-12 15:33:34 -0700115 """
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700116 equery_cmd = ['equery', 'm', '-U', '%s/%s' % (category, package)]
Jim Hebertcf870d72013-06-12 15:33:34 -0700117 lines = cros_build_lib.RunCommand(equery_cmd, error_code_ok=True,
118 print_cmd=False,
119 redirect_stdout=True).output.splitlines()
120 # Look for lines like "Remote-ID: cpe:/a:kernel:linux-pam ID: cpe"
121 # and extract the cpe URI.
122 cpes = []
123 for line in lines:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700124 if 'ID: cpe' not in line:
Jim Hebertcf870d72013-06-12 15:33:34 -0700125 continue
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700126 cpes.append('%s:%s' % (line.split()[1], version.replace('_', '')))
Jim Hebertcf870d72013-06-12 15:33:34 -0700127 # Note that we're assuming we can combine the root of the CPE, taken
128 # from metadata.xml, and tack on the version number as used by
129 # Portage, and come up with a legitimate CPE. This works so long as
130 # Portage and CPE agree on the precise formatting of the version
Jim Hebert96aff9c2013-07-16 15:43:17 -0700131 # number, which they almost always do. The major exception we've
132 # identified thus far is that our ebuilds have a pattern of inserting
133 # underscores prior to patchlevels, that neither upstream nor CPE
134 # use. For example, our code will decide we have
Jim Hebertcf870d72013-06-12 15:33:34 -0700135 # cpe:/a:todd_miller:sudo:1.8.6_p7 yet the advisories use a format
136 # like cpe:/a:todd_miller:sudo:1.8.6p7, without the underscore. (CPE
137 # is "right" in this example, in that it matches www.sudo.ws.)
138 #
Jim Hebert96aff9c2013-07-16 15:43:17 -0700139 # Removing underscores seems to improve our chances of correctly
140 # arriving at the CPE used by NVD. However, at the end of the day,
141 # ebuild version numbers are rev'd by people who don't have "try to
142 # match NVD" as one of their goals, and there is always going to be
143 # some risk of minor formatting disagreements at the version number
144 # level, if not from stray underscores then from something else.
145 #
Jim Hebertcf870d72013-06-12 15:33:34 -0700146 # This is livable so long as you do some fuzzy version number
147 # comparison in your vulnerability monitoring, between what-we-have
148 # and what-the-advisory-says-is-affected.
149 return cpes
150
151
Chris McDonalde69db662018-11-15 12:50:18 -0700152def GenerateSDKCPVList(sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700153 """Find all SDK packages from package.provided
154
155 Args:
Chris McDonalde69db662018-11-15 12:50:18 -0700156 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700157
158 Returns:
159 A list of CPV Name strings, e.g.
160 ["sys-libs/glibc-2.23-r9", "dev-lang/go-1.8.3-r1"]
161 """
162 # Look at packages in package.provided.
Chris McDonalde69db662018-11-15 12:50:18 -0700163 sdk_file_path = os.path.join(sysroot, 'etc', 'portage',
Xuewei Zhang656f9932017-09-15 16:15:05 -0700164 'profile', 'package.provided')
165 for line in osutils.ReadFile(sdk_file_path).splitlines():
166 # Skip comments and empty lines.
167 line = line.split('#', 1)[0].strip()
168 if not line:
169 continue
170 yield line
171
172
Chris McDonalde69db662018-11-15 12:50:18 -0700173def GenerateCPEList(deps_list, sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700174 """Generate all CPEs for the packages included in deps_list and SDK packages
175
176 Args:
177 deps_list: A flattened dependency tree (cros_extract_deps format).
Chris McDonalde69db662018-11-15 12:50:18 -0700178 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700179
180 Returns:
181 A list of CPE info for packages in deps_list and SDK packages, e.g.
182 [
183 {
184 "ComponentName": "app-admin/sudo",
185 "Repository": "cros",
186 "Targets": [
187 "cpe:/a:todd_miller:sudo:1.8.19p2"
188 ]
189 },
190 {
191 "ComponentName": "sys-libs/glibc",
192 "Repository": "cros",
193 "Targets": [
194 "cpe:/a:gnu:glibc:2.23"
195 ]
196 }
197 ]
198 """
Jim Hebertcf870d72013-06-12 15:33:34 -0700199 cpe_dump = []
Xuewei Zhang656f9932017-09-15 16:15:05 -0700200
201 # Generage CPEs for SDK packages.
Mike Frysinger1457e482019-01-03 04:04:02 -0500202 for sdk_cpv in sorted(GenerateSDKCPVList(sysroot)):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700203 # Only add CPE for SDK CPVs missing in deps_list.
204 if deps_list.get(sdk_cpv) is not None:
205 continue
206
207 split = portage_util.SplitCPV(sdk_cpv)
208 cpes = GetCPEFromCPV(split.category, split.package, split.version_no_rev)
209 if cpes:
Alex Klein9f93b482018-10-01 09:26:51 -0600210 cpe_dump.append({'ComponentName': '%s' % split.cp,
Xuewei Zhang656f9932017-09-15 16:15:05 -0700211 'Repository': 'cros',
212 'Targets': sorted(cpes)})
213 else:
214 logging.warning('No CPE entry for %s', sdk_cpv)
215
216 # Generage CPEs for packages in deps_list.
Mike Frysinger1457e482019-01-03 04:04:02 -0500217 for cpv, record in sorted(deps_list.items()):
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700218 if record['cpes']:
219 name = '%s/%s' % (record['category'], record['name'])
220 cpe_dump.append({'ComponentName': name,
221 'Repository': 'cros',
222 'Targets': sorted(record['cpes'])})
Jim Hebertcf870d72013-06-12 15:33:34 -0700223 else:
Ralph Nathan446aee92015-03-23 14:44:56 -0700224 logging.warning('No CPE entry for %s', cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700225 return sorted(cpe_dump, key=lambda k: k['ComponentName'])
Jim Hebert91c052c2011-03-11 11:00:53 -0800226
227
Chris McDonalde69db662018-11-15 12:50:18 -0700228def ParseArgs(argv):
229 """Parse command line arguments."""
Mike Frysinger012371e2019-01-03 03:42:32 -0500230 parser = commandline.ArgumentParser(description=__doc__)
Chris McDonalde69db662018-11-15 12:50:18 -0700231 target = parser.add_mutually_exclusive_group()
232 target.add_argument('--sysroot', type='path', help='Path to the sysroot.')
233 target.add_argument('--board', help='Board name.')
234
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700235 parser.add_argument('--format', default='deps',
236 choices=['deps', 'cpe'],
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700237 help='Output either traditional deps or CPE-only JSON.')
238 parser.add_argument('--output-path', default=None,
239 help='Write output to the given path.')
Mike Frysinger012371e2019-01-03 03:42:32 -0500240 parser.add_argument('pkgs', nargs='*')
241 opts = parser.parse_args(argv)
242 opts.Freeze()
243 return opts
Jim Hebert91c052c2011-03-11 11:00:53 -0800244
Chris McDonalde69db662018-11-15 12:50:18 -0700245
246def ExtractDeps(sysroot, package_list, formatting='deps'):
247 """Returns the set of dependencies for the packages in package_list"""
248 lib_argv = ['--quiet', '--pretend', '--emptytree']
249 lib_argv += ['--sysroot=%s' % sysroot]
250 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800251
252 deps = DepGraphGenerator()
Jim Hebertcf870d72013-06-12 15:33:34 -0700253 deps.Initialize(lib_argv)
David James1b363582012-12-17 11:53:11 -0800254 deps_tree, _deps_info = deps.GenDependencyTree()
Chris McDonalde69db662018-11-15 12:50:18 -0700255 flattened_deps = FlattenDepTree(deps_tree, get_cpe=(formatting == 'cpe'))
256 if formatting == 'cpe':
257 flattened_deps = GenerateCPEList(flattened_deps, sysroot)
258 return flattened_deps
259
260
261def main(argv):
Mike Frysinger012371e2019-01-03 03:42:32 -0500262 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700263
Mike Frysinger012371e2019-01-03 03:42:32 -0500264 sysroot = opts.sysroot or cros_build_lib.GetSysroot(opts.board)
265 deps_list = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700266
267 deps_output = json.dumps(deps_list, sort_keys=True, indent=2)
Mike Frysinger012371e2019-01-03 03:42:32 -0500268 if opts.output_path:
269 with open(opts.output_path, 'w') as f:
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700270 f.write(deps_output)
271 else:
272 print(deps_output)