2 # 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.
6 """Command to extract the dependancy tree for a given package."""
9 import portage # pylint: disable=F0401
11 from parallel_emerge import DepGraphGenerator
13 from chromite.lib import commandline
14 from chromite.lib import cros_build_lib
16 def FlattenDepTree(deptree, pkgtable=None, parentcpv=None):
17 """Simplify dependency json.
19 Turn something like this (the parallel_emerge DepsTree format):
21 "app-admin/eselect-1.2.9": {
24 "sys-apps/coreutils-7.5-r1": {
33 ...into something like this (the cros_extract_deps format):
35 "app-admin/eselect-1.2.9": {
36 "deps": ["coreutils-7.5-r1"],
39 "category": "app-admin",
41 "full_name": "app-admin/eselect-1.2.9",
44 "sys-apps/coreutils-7.5-r1": {
46 "rev_deps": ["app-admin/eselect-1.2.9"],
48 "category": "sys-apps",
50 "full_name": "sys-apps/coreutils-7.5-r1",
57 for cpv, record in deptree.items():
58 if cpv not in pkgtable:
59 cat, nam, ver, rev = portage.versions.catpkgsplit(cpv)
60 pkgtable[cpv] = {"deps": [],
64 "version": "%s-%s" % (ver, rev),
66 "cpes": GetCPEFromCPV(cat, nam, ver),
67 "action": record["action"]}
68 # If we have a parent, that is a rev_dep for the current package.
70 pkgtable[cpv]["rev_deps"].append(parentcpv)
71 # If current package has any deps, record those.
72 for childcpv in record["deps"]:
73 pkgtable[cpv]["deps"].append(childcpv)
74 # Visit the subtree recursively as well.
75 FlattenDepTree(record["deps"], pkgtable=pkgtable, parentcpv=cpv)
79 def GetCPEFromCPV(category, package, version):
80 """Look up the CPE for a specified Portage package.
83 category: The Portage package's category, e.g. "net-misc"
84 package: The Portage package's name, e.g. "curl"
85 version: The Portage version, e.g. "7.30.0"
88 A list of CPE Name strings, e.g.
89 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
91 equery_cmd = ["equery", "m", "-U", "%s/%s" % (category, package)]
92 lines = cros_build_lib.RunCommand(equery_cmd, error_code_ok=True,
94 redirect_stdout=True).output.splitlines()
95 # Look for lines like "Remote-ID: cpe:/a:kernel:linux-pam ID: cpe"
96 # and extract the cpe URI.
99 if "ID: cpe" not in line:
101 cpes.append("%s:%s" % (line.split()[1], version.replace("_", "")))
102 # Note that we're assuming we can combine the root of the CPE, taken
103 # from metadata.xml, and tack on the version number as used by
104 # Portage, and come up with a legitimate CPE. This works so long as
105 # Portage and CPE agree on the precise formatting of the version
106 # number, which they almost always do. The major exception we've
107 # identified thus far is that our ebuilds have a pattern of inserting
108 # underscores prior to patchlevels, that neither upstream nor CPE
109 # use. For example, our code will decide we have
110 # cpe:/a:todd_miller:sudo:1.8.6_p7 yet the advisories use a format
111 # like cpe:/a:todd_miller:sudo:1.8.6p7, without the underscore. (CPE
112 # is "right" in this example, in that it matches www.sudo.ws.)
114 # Removing underscores seems to improve our chances of correctly
115 # arriving at the CPE used by NVD. However, at the end of the day,
116 # ebuild version numbers are rev'd by people who don't have "try to
117 # match NVD" as one of their goals, and there is always going to be
118 # some risk of minor formatting disagreements at the version number
119 # level, if not from stray underscores then from something else.
121 # This is livable so long as you do some fuzzy version number
122 # comparison in your vulnerability monitoring, between what-we-have
123 # and what-the-advisory-says-is-affected.
127 def ExtractCPEList(deps_list):
129 for cpv, record in deps_list.items():
131 cpe_dump.append({"Name": cpv, "Targets": sorted(record["cpes"]),
132 "Repository": "cros"})
134 cros_build_lib.Warning("No CPE entry for %s", cpv)
135 return sorted(cpe_dump, key=lambda k: k["Name"])
139 parser = commandline.ArgumentParser(description="""
140 This extracts the dependency tree for the specified package, and outputs it
141 to stdout, in a serialized JSON format.""")
142 parser.add_argument("--board", required=True,
143 help="The board to use when computing deps.")
144 parser.add_argument("--format", default="deps",
145 choices=["deps", "cpe"],
146 help="Output either traditional deps or CPE-only JSON")
147 # Even though this is really just a pass-through to DepGraphGenerator,
148 # handling it as a known arg here allows us to specify a default more
149 # elegantly than testing for its presence in the unknown_args later.
150 parser.add_argument("--root-deps", default="rdeps",
151 help="Which deps to report (defaults to rdeps)")
152 known_args, unknown_args = parser.parse_known_args(argv)
154 lib_argv = ["--board=%s" % known_args.board,
155 "--root-deps=%s" % known_args.root_deps,
156 "--quiet", "--pretend", "--emptytree"]
157 lib_argv.extend(unknown_args)
159 deps = DepGraphGenerator()
160 deps.Initialize(lib_argv)
161 deps_tree, _deps_info = deps.GenDependencyTree()
162 deps_list = FlattenDepTree(deps_tree)
163 if known_args.format == "cpe":
164 deps_list = ExtractCPEList(deps_list)
165 print json.dumps(deps_list, sort_keys=True, indent=2)