Flutter Engine
The Flutter Engine
scan_deps.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# Copyright 2013 The Flutter Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6#
7# Usage: scan_deps.py --deps <DEPS file> --output <parsed lockfile>
8#
9# This script extracts the dependencies provided from the DEPS file and
10# finds the appropriate git commit hash per dependency for osv-scanner
11# to use in checking for vulnerabilities.
12# It is expected that the lockfile output of this script is then
13# uploaded using GitHub actions to be used by the osv-scanner reusable action.
14
15import argparse
16import json
17import os
18import re
19import shutil
20import subprocess
21import sys
22from compatibility_helper import byte_str_decode
23
24SCRIPT_DIR = os.path.dirname(sys.argv[0])
25CHECKOUT_ROOT = os.path.realpath(os.path.join(SCRIPT_DIR, '..'))
26CHROMIUM_README_FILE = 'third_party/accessibility/README.md'
27CHROMIUM_README_COMMIT_LINE = 4 # The fifth line will always contain the commit hash.
28CHROMIUM = 'https://chromium.googlesource.com/chromium/src'
29DEP_CLONE_DIR = CHECKOUT_ROOT + '/clone-test'
30DEPS = os.path.join(CHECKOUT_ROOT, 'DEPS')
31UPSTREAM_PREFIX = 'upstream_'
32
33
34# Used in parsing the DEPS file.
35class VarImpl:
36 _env_vars = {
37 'host_cpu': 'x64',
38 'host_os': 'linux',
39 }
40
41 def __init__(self, local_scope):
42 self._local_scope = local_scope
43
44 def lookup(self, var_name):
45 """Implements the Var syntax."""
46 if var_name in self._local_scope.get('vars', {}):
47 return self._local_scope['vars'][var_name]
48 # Inject default values for env variables.
49 if var_name in self._env_vars:
50 return self._env_vars[var_name]
51 raise Exception('Var is not defined: %s' % var_name)
52
53
54def extract_deps(deps_file):
55 local_scope = {}
56 var = VarImpl(local_scope)
57 global_scope = {
58 'Var': var.lookup,
59 'deps_os': {},
60 }
61 # Read the content.
62 with open(deps_file, 'r') as file:
63 deps_content = file.read()
64
65 # Eval the content.
66 exec(deps_content, global_scope, local_scope)
67
68 if not os.path.exists(DEP_CLONE_DIR):
69 os.mkdir(DEP_CLONE_DIR) # Clone deps with upstream into temporary dir.
70
71 # Extract the deps and filter.
72 deps = local_scope.get('deps', {})
73 deps_list = local_scope.get('vars')
74 filtered_osv_deps = []
75 for _, dep in deps.items():
76 # We currently do not support packages or cipd which are represented
77 # as dictionaries.
78 if not isinstance(dep, str):
79 continue
80
81 dep_split = dep.rsplit('@', 1)
82 ancestor_result = get_common_ancestor([dep_split[0], dep_split[1]], deps_list)
83 if ancestor_result:
84 filtered_osv_deps.append({
85 'package': {'name': ancestor_result[1], 'commit': ancestor_result[0]}
86 })
87
88 try:
89 # Clean up cloned upstream dependency directory.
90 shutil.rmtree(DEP_CLONE_DIR) # Use shutil.rmtree since dir could be non-empty.
91 except OSError as clone_dir_error:
92 print('Error cleaning up clone directory: %s : %s' % (DEP_CLONE_DIR, clone_dir_error.strerror))
93
94 osv_result = {
95 'packageSource': {'path': deps_file, 'type': 'lockfile'}, 'packages': filtered_osv_deps
96 }
97 return osv_result
98
99
101 """
102 Opens the Flutter Accessibility Library README and uses the commit hash
103 found in the README to check for viulnerabilities.
104 The commit hash in this README will always be in the same format
105 """
106 file_path = os.path.join(CHECKOUT_ROOT, CHROMIUM_README_FILE)
107 with open(file_path) as file:
108 # Read the content of the file opened.
109 content = file.readlines()
110 commit_line = content[CHROMIUM_README_COMMIT_LINE]
111 commit = re.search(r'(?<=\[).*(?=\])', commit_line)
112
113 osv_result = {
114 'packageSource': {'path': file_path, 'type': 'lockfile'},
115 'packages': [{'package': {'name': CHROMIUM, 'commit': commit.group()}}]
116 }
117
118 return osv_result
119
120
121def get_common_ancestor(dep, deps_list):
122 """
123 Given an input of a mirrored dep,
124 compare to the mapping of deps to their upstream
125 in DEPS and find a common ancestor
126 commit SHA value.
127
128 This is done by first cloning the mirrored dep,
129 then a branch which tracks the upstream.
130 From there, git merge-base operates using the HEAD
131 commit SHA of the upstream branch and the pinned
132 SHA value of the mirrored branch
133 """
134 # dep[0] contains the mirror repo.
135 # dep[1] contains the mirror's pinned SHA.
136 # upstream is the origin repo.
137 dep_name = dep[0].split('/')[-1].split('.')[0]
138 if UPSTREAM_PREFIX + dep_name not in deps_list:
139 print('did not find dep: ' + dep_name)
140 return None
141 try:
142 # Get the upstream URL from the mapping in DEPS file.
143 upstream = deps_list.get(UPSTREAM_PREFIX + dep_name)
144 temp_dep_dir = DEP_CLONE_DIR + '/' + dep_name
145 # Clone dependency from mirror.
146 subprocess.check_output(['git', 'clone', '--quiet', '--', dep[0], dep_name], cwd=DEP_CLONE_DIR)
147
148 # Create branch that will track the upstream dep.
149 print('attempting to add upstream remote from: {upstream}'.format(upstream=upstream))
150 subprocess.check_output(['git', 'remote', 'add', 'upstream', upstream], cwd=temp_dep_dir)
151 subprocess.check_output(['git', 'fetch', '--quiet', 'upstream'], cwd=temp_dep_dir)
152 # Get name of the default branch for upstream (e.g. main/master/etc.).
153 default_branch = subprocess.check_output(
154 'git remote show upstream ' + "| sed -n \'/HEAD branch/s/.*: //p\'",
155 cwd=temp_dep_dir,
156 shell=True
157 )
158 default_branch = byte_str_decode(default_branch)
159 default_branch = default_branch.strip()
160
161 # Make upstream branch track the upstream dep.
162 subprocess.check_output([
163 'git', 'checkout', '--force', '-b', 'upstream', '--track', 'upstream/' + default_branch
164 ],
165 cwd=temp_dep_dir)
166 # Get the most recent commit from default branch of upstream.
167 commit = subprocess.check_output(
168 'git for-each-ref ' + "--format=\'%(objectname:short)\' refs/heads/upstream",
169 cwd=temp_dep_dir,
170 shell=True
171 )
172 commit = byte_str_decode(commit)
173 commit = commit.strip()
174
175 # Perform merge-base on most recent default branch commit and pinned mirror commit.
176 ancestor_commit = subprocess.check_output(
177 'git merge-base {commit} {depUrl}'.format(commit=commit, depUrl=dep[1]),
178 cwd=temp_dep_dir,
179 shell=True
180 )
181 ancestor_commit = byte_str_decode(ancestor_commit)
182 ancestor_commit = ancestor_commit.strip()
183 print('Ancestor commit: ' + ancestor_commit)
184 return ancestor_commit, upstream
185 except subprocess.CalledProcessError as error:
186 print(
187 "Subprocess command '{0}' failed with exit code: {1}.".format(
188 error.cmd, str(error.returncode)
189 )
190 )
191 if error.output:
192 print("Subprocess error output: '{0}'".format(error.output))
193 return None
194
195
196def parse_args(args):
197 args = args[1:]
198 parser = argparse.ArgumentParser(description='A script to find common ancestor commit SHAs')
199
200 parser.add_argument(
201 '--deps',
202 '-d',
203 type=str,
204 help='Input DEPS file to extract.',
205 default=os.path.join(CHECKOUT_ROOT, 'DEPS')
206 )
207 parser.add_argument(
208 '--output',
209 '-o',
210 type=str,
211 help='Output osv-scanner compatible deps file.',
212 default=os.path.join(CHECKOUT_ROOT, 'osv-lockfile.json')
213 )
214
215 return parser.parse_args(args)
216
217
218def write_manifest(deps, manifest_file):
219 output = {'results': deps}
220 print(json.dumps(output, indent=2))
221 with open(manifest_file, 'w') as manifest:
222 json.dump(output, manifest, indent=2)
223
224
225def main(argv):
226 args = parse_args(argv)
227 deps = extract_deps(args.deps)
228 readme_deps = parse_readme()
229 write_manifest([deps, readme_deps], args.output)
230 return 0
231
232
233if __name__ == '__main__':
234 sys.exit(main(sys.argv))
def lookup(self, var_name)
Definition: scan_deps.py:44
dictionary _env_vars
Definition: scan_deps.py:36
def __init__(self, local_scope)
Definition: scan_deps.py:41
uint32_t uint32_t * format
def byte_str_decode(str_or_bytes)
Definition: main.py:1
const myers::Point & get(const myers::Segment &)
def print(*args, **kwargs)
Definition: run_tests.py:49
def parse_args(args)
Definition: scan_deps.py:196
def main(argv)
Definition: scan_deps.py:225
def parse_readme()
Definition: scan_deps.py:100
def get_common_ancestor(dep, deps_list)
Definition: scan_deps.py:121
def extract_deps(deps_file)
Definition: scan_deps.py:54
def write_manifest(deps, manifest_file)
Definition: scan_deps.py:218