diff --git a/.github/workflows/third_party_scan.yml b/.github/workflows/third_party_scan.yml new file mode 100644 index 0000000000000..3e4a6b25cb3f7 --- /dev/null +++ b/.github/workflows/third_party_scan.yml @@ -0,0 +1,56 @@ +name: Third party dependency scan +on: + # Only the default branch is supported. + branch_protection_rule: + branches: [ main ] + schedule: + - cron: "0 8 * * *" # runs daily at 08:00 + + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Third party dependency scan + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + actions: read + contents: read + + steps: + - name: "Checkout code" + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + with: + persist-credentials: false + + - name: setup python + uses: actions/setup-python@98f2ad02fd48d057ee3b4d4f66525b231c3e52b6 + with: + python-version: '3.7.7' # install the python version needed + + - name: install dependency + run: pip install git+https://github.com/psf/requests.git@4d394574f5555a8ddcc38f707e0c9f57f55d9a3b + + - name: execute py script + run: python ci/deps_parser.py + + - name: parse deps_parser output.txt + run: python ci/scan_flattened_deps.py + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 + with: + name: SARIF file + path: osvReport.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@a3a6c128d771b6b9bdebb1c9d0583ebd2728a108 + with: + sarif_file: osvReport.sarif diff --git a/DEPS b/DEPS index fe49a95084178..a4be8cf8a22ca 100644 --- a/DEPS +++ b/DEPS @@ -97,6 +97,122 @@ vars = { # Setup Git hooks by default. "setup_githooks": True, + + # Upstream URLs for third party dependencies, used in + # determining common ancestor commit for vulnerability scanning + # prefixed with 'upstream_' in order to be identified by parsing tool. + # The vulnerabiity database being used in this scan can be browsed + # using this UI https://osv.dev/list + # If a new dependency needs to be added, the upstream (non-mirrored) + # git URL for that dependency should be added to this list + # with the key-value pair being: + # 'upstream_[dep name from last slash and before .git in URL]':'[git URL]' + # example: + "upstream_abseil-cpp": "https://github.com/abseil/abseil-cpp.git", + "upstream_angle": "https://github.com/google/angle.git", + "upstream_archive": "https://github.com/brendan-duncan/archive.git", + "upstream_args": "https://github.com/dart-lang/args.git", + "upstream_async": "https://github.com/dart-lang/async.git", + "upstream_bazel_worker": "https://github.com/dart-lang/bazel_worker.git", + "upstream_benchmark": "https://github.com/google/benchmark.git", + "upstream_boolean_selector": "https://github.com/dart-lang/boolean_selector.git", + "upstream_boringssl_gen": "https://github.com/dart-lang/boringssl_gen.git", + "upstream_boringssl": "https://github.com/openssl/openssl.git", + "upstream_browser_launcher": "https://github.com/dart-lang/browser_launcher.git", + "upstream_buildroot": "https://github.com/flutter/buildroot.git", + "upstream_cli_util": "https://github.com/dart-lang/cli_util.git", + "upstream_clock": "https://github.com/dart-lang/clock.git", + "upstream_collection": "https://github.com/dart-lang/collection.git", + "upstream_colorama": "https://github.com/tartley/colorama.git", + "upstream_convert": "https://github.com/dart-lang/convert.git", + "upstream_crypto": "https://github.com/dart-lang/crypto.git", + "upstream_csslib": "https://github.com/dart-lang/csslib.git", + "upstream_dart_style": "https://github.com/dart-lang/dart_style.git", + "upstream_dartdoc": "https://github.com/dart-lang/dartdoc.git", + "upstream_equatable": "https://github.com/felangel/equatable.git", + "upstream_ffi": "https://github.com/dart-lang/ffi.git", + "upstream_file": "https://github.com/google/file.dart.git", + "upstream_fixnum": "https://github.com/dart-lang/fixnum.git", + "upstream_flatbuffers": "https://github.com/google/flatbuffers.git", + "upstream_fontconfig": "https://gitlab.freedesktop.org/fontconfig/fontconfig.git", + "upstream_freetype2": "https://gitlab.freedesktop.org/freetype/freetype.git", + "upstream_gcloud": "https://github.com/dart-lang/gcloud.git", + "upstream_glfw": "https://github.com/glfw/glfw.git", + "upstream_glob": "https://github.com/dart-lang/glob.git", + "upstream_googleapis": "https://github.com/google/googleapis.dart.git", + "upstream_googletest": "https://github.com/google/googletest.git", + "upstream_gtest-parallel": "https://github.com/google/gtest-parallel.git", + "upstream_harfbuzz": "https://github.com/harfbuzz/harfbuzz.git", + "upstream_html": "https://github.com/dart-lang/html.git", + "upstream_http_multi_server": "https://github.com/dart-lang/http_multi_server.git", + "upstream_http_parser": "https://github.com/dart-lang/http_parser.git", + "upstream_http": "https://github.com/dart-lang/http.git", + "upstream_icu": "https://github.com/unicode-org/icu.git", + "upstream_imgui": "https://github.com/ocornut/imgui.git", + "upstream_inja": "https://github.com/pantor/inja.git", + "upstream_json": "https://github.com/nlohmann/json.git", + "upstream_json_rpc_2": "https://github.com/dart-lang/json_rpc_2.git", + "upstream_libcxx": "https://github.com/llvm-mirror/libcxx.git", + "upstream_libcxxabi": "https://github.com/llvm-mirror/libcxxabi.git", + "upstream_libexpat": "https://github.com/libexpat/libexpat.git", + "upstream_libjpeg-turbo": "https://github.com/libjpeg-turbo/libjpeg-turbo.git", + "upstream_libpng": "https://github.com/glennrp/libpng.git", + "upstream_libtess2": "https://github.com/memononen/libtess2.git", + "upstream_libwebp": "https://chromium.googlesource.com/webm/libwebp.git", + "upstream_libxml": "https://gitlab.gnome.org/GNOME/libxml2.git", + "upstream_linter": "https://github.com/dart-lang/linter.git", + "upstream_logging": "https://github.com/dart-lang/logging.git", + "upstream_markdown": "https://github.com/dart-lang/markdown.git", + "upstream_matcher": "https://github.com/dart-lang/matcher.git", + "upstream_mime": "https://github.com/dart-lang/mime.git", + "upstream_mockito": "https://github.com/dart-lang/mockito.git", + "upstream_oauth2": "https://github.com/dart-lang/oauth2.git", + "upstream_ocmock": "https://github.com/erikdoe/ocmock.git", + "upstream_package_config": "https://github.com/dart-lang/package_config.git", + "upstream_packages": "https://github.com/flutter/packages.git", + "upstream_path": "https://github.com/dart-lang/path.git", + "upstream_platform": "https://github.com/google/platform.dart.git", + "upstream_pool": "https://github.com/dart-lang/pool.git", + "upstream_process_runner": "https://github.com/google/process_runner.git", + "upstream_process": "https://github.com/google/process.dart.git", + "upstream_protobuf": "https://github.com/google/protobuf.dart.git", + "upstream_pub_semver": "https://github.com/dart-lang/pub_semver.git", + "upstream_pub": "https://github.com/dart-lang/pub.git", + "upstream_pyyaml": "https://github.com/yaml/pyyaml.git", + "upstream_quiver-dart": "https://github.com/google/quiver-dart.git", + "upstream_rapidjson": "https://github.com/Tencent/rapidjson.git", + "upstream_root_certificates": "https://github.com/dart-lang/root_certificates.git", + "upstream_sdk": "https://github.com/dart-lang/sdk.git", + "upstream_shaderc": "https://github.com/google/shaderc.git", + "upstream_shelf": "https://github.com/dart-lang/shelf.git", + "upstream_skia": "https://skia.googlesource.com/skia.git", + "upstream_source_map_stack_trace": "https://github.com/dart-lang/source_map_stack_trace.git", + "upstream_source_maps": "https://github.com/dart-lang/source_maps.git", + "upstream_source_span": "https://github.com/dart-lang/source_span.git", + "upstream_sqlite": "https://github.com/sqlite/sqlite.git", + "upstream_sse": "https://github.com/dart-lang/sse.git", + "upstream_stack_trace": "https://github.com/dart-lang/stack_trace.git", + "upstream_stream_channel": "https://github.com/dart-lang/stream_channel.git", + "upstream_string_scanner": "https://github.com/dart-lang/string_scanner.git", + "upstream_SwiftShader": "https://swiftshader.googlesource.com/SwiftShader.git", + "upstream_term_glyph": "https://github.com/dart-lang/term_glyph.git", + "upstream_test_reflective_loader": "https://github.com/dart-lang/test_reflective_loader.git", + "upstream_test": "https://github.com/dart-lang/test.git", + "upstream_tinygltf": "https://github.com/syoyo/tinygltf.git", + "upstream_typed_data": "https://github.com/dart-lang/typed_data.git", + "upstream_usage": "https://github.com/dart-lang/usage.git", + "upstream_vector_math": "https://github.com/google/vector_math.dart.git", + "upstream_Vulkan-Headers": "https://github.com/KhronosGroup/Vulkan-Headers.git", + "upstream_VulkanMemoryAllocator": "https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git", + "upstream_watcher": "https://github.com/dart-lang/watcher.git", + "upstream_web_socket_channel": "https://github.com/dart-lang/web_socket_channel.git", + "upstream_webdev": "https://github.com/dart-lang/webdev.git", + "upstream_webkit_inspection_protocol": "https://github.com/google/webkit_inspection_protocol.dart.git", + "upstream_wuffs-mirror-release-c": "https://github.com/google/wuffs-mirror-release-c.git", + "upstream_yaml_edit": "https://github.com/dart-lang/yaml_edit.git", + "upstream_yaml": "https://github.com/dart-lang/yaml.git", + "upstream_yapf": "https://github.com/google/yapf.git", + "upstream_zlib": "https://github.com/madler/zlib.git", } gclient_gn_args_file = 'src/third_party/dart/build/config/gclient_args.gni' diff --git a/ci/deps_parser.py b/ci/deps_parser.py index 4e46c8d9cb75c..bf4f6d8b72049 100644 --- a/ci/deps_parser.py +++ b/ci/deps_parser.py @@ -3,7 +3,7 @@ # Copyright 2013 The Flutter Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. - +# # Usage: deps_parser.py --deps --output # # This script parses the DEPS file, extracts the fully qualified dependencies @@ -12,11 +12,16 @@ import argparse import os +import re import sys SCRIPT_DIR = os.path.dirname(sys.argv[0]) CHECKOUT_ROOT = os.path.realpath(os.path.join(SCRIPT_DIR, '..')) +CHROMIUM_README_FILE = 'third_party/accessibility/README.md' +CHROMIUM_README_COMMIT_LINE = 4 # the fifth line will always contain the commit hash +CHROMIUM = 'https://chromium.googlesource.com/chromium/src' + # Used in parsing the DEPS file. class VarImpl: @@ -55,15 +60,30 @@ def parse_deps_file(deps_file): # Extract the deps and filter. deps = local_scope.get('deps', {}) filtered_deps = [] - for val in deps.values(): + for _, dep in deps.items(): # We currently do not support packages or cipd which are represented # as dictionaries. - if isinstance(val, str): - filtered_deps.append(val) - + if isinstance(dep, str): + filtered_deps.append(dep) return filtered_deps +def parse_readme(deps): + """ + Opens the Flutter Accessibility Library README and uses the commit hash + found in the README to check for viulnerabilities. + The commit hash in this README will always be in the same format + """ + file_path = os.path.join(CHECKOUT_ROOT, CHROMIUM_README_FILE) + with open(file_path) as file: + # read the content of the file opened + content = file.readlines() + commit_line = content[CHROMIUM_README_COMMIT_LINE] + commit = re.search(r'(?<=\[).*(?=\])', commit_line) + deps.append(CHROMIUM + '@' + commit.group()) + return deps + + def write_manifest(deps, manifest_file): print('\n'.join(sorted(deps))) with open(manifest_file, 'w') as manifest: @@ -97,6 +117,7 @@ def parse_args(args): def main(argv): args = parse_args(argv) deps = parse_deps_file(args.deps) + deps = parse_readme(deps) write_manifest(deps, args.output) return 0 diff --git a/ci/deps_parser_tests.py b/ci/deps_parser_tests.py new file mode 100644 index 0000000000000..dd96d163499f4 --- /dev/null +++ b/ci/deps_parser_tests.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import os +import sys +import unittest +from deps_parser import VarImpl + +SCRIPT_DIR = os.path.dirname(sys.argv[0]) +CHECKOUT_ROOT = os.path.realpath(os.path.join(SCRIPT_DIR, '..')) +DEPS = os.path.join(CHECKOUT_ROOT, 'DEPS') +UPSTREAM_PREFIX = 'upstream_' + + +class TestDepsParserMethods(unittest.TestCase): + # Extract both mirrored dep names and URLs & + # upstream names and URLs from DEPs file. + def setUp(self): # lower-camel-case for the python unittest framework + # Read the content. + with open(DEPS, 'r') as file: + deps_content = file.read() + + local_scope_mirror = {} + var = VarImpl(local_scope_mirror) + global_scope_mirror = { + 'Var': var.lookup, + 'deps_os': {}, + } + + # Eval the content. + exec(deps_content, global_scope_mirror, local_scope_mirror) + + # Extract the upstream URLs + # vars contains more than just upstream URLs + # however the upstream URLs are prefixed with 'upstream_' + upstream = local_scope_mirror.get('vars') + self.upstream_urls = upstream + + # Extract the deps and filter. + deps = local_scope_mirror.get('deps', {}) + filtered_deps = [] + for _, dep in deps.items(): + # We currently do not support packages or cipd which are represented + # as dictionaries. + if isinstance(dep, str): + filtered_deps.append(dep) + self.deps = filtered_deps + + def test_each_dep_has_upstream_url(self): + # For each DEP in the deps file, check for an associated upstream URL in deps file. + for dep in self.deps: + dep_repo = dep.split('@')[0] + dep_name = dep_repo.split('/')[-1].split('.')[0] + # vulkan-deps and khronos do not have one upstream URL + # all other deps should have an associated upstream URL for vuln scanning purposes + if dep_name not in ('vulkan-deps', 'khronos'): + # Add the prefix on the dep name when searching for the upstream entry. + self.assertTrue( + UPSTREAM_PREFIX + dep_name in self.upstream_urls, + msg=dep_name + ' not found in upstream URL list. ' + + 'Each dep in the "deps" section of DEPS file must have associated upstream URL' + ) + + def test_each_upstream_url_has_dep(self): + # Parse DEPS into dependency names. + deps_names = [] + for dep in self.deps: + dep_repo = dep.split('@')[0] + dep_name = dep_repo.split('/')[-1].split('.')[0] + deps_names.append(dep_name) + + # For each upstream URL dep, check it exists as in DEPS. + for upsream_dep in self.upstream_urls: + # Only test on upstream deps in vars section which start with the upstream prefix + if upsream_dep.startswith(UPSTREAM_PREFIX): + # Strip the prefix to check that it has a corresponding dependency in the DEPS file + self.assertTrue( + upsream_dep[len(UPSTREAM_PREFIX):] in deps_names, + msg=upsream_dep + ' from upstream list not found in DEPS. ' + + 'Each upstream URL in DEPS file must have an associated dep in the "deps" section' + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/ci/scan_flattened_deps.py b/ci/scan_flattened_deps.py new file mode 100644 index 0000000000000..ece1154fb97a4 --- /dev/null +++ b/ci/scan_flattened_deps.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +# +# Usage: scan_flattened_deps.py --flat-deps --output +# +# This script parses the flattened, fully qualified dependencies, +# and uses the OSV API to check for known vulnerabilities +# for the given hash of the dependency + +import argparse +import json +import os +import shutil +import subprocess +import sys +from urllib import request + +SCRIPT_DIR = os.path.dirname(sys.argv[0]) +CHECKOUT_ROOT = os.path.realpath(os.path.join(SCRIPT_DIR, '..')) +DEP_CLONE_DIR = CHECKOUT_ROOT + '/clone-test' +DEPS = os.path.join(CHECKOUT_ROOT, 'DEPS') +HELP_STR = 'To find complete information on this vulnerability, navigate to ' +OSV_VULN_DB_URL = 'https://osv.dev/vulnerability/' +SECONDS_PER_YEAR = 31556952 +UPSTREAM_PREFIX = 'upstream_' + +failed_deps = [] # deps which fail to be be cloned or git-merge based + +sarif_log = { + '$schema': + 'https://json.schemastore.org/sarif-2.1.0.json', 'version': + '2.1.0', 'runs': [{ + 'tool': {'driver': {'name': 'OSV Scan', 'rules': []}}, + 'results': [] + }] +} + + +def sarif_result(): + """ + Returns the template for a result entry in the Sarif log, + which is populated with CVE findings from OSV API + """ + return { + 'ruleId': + 'N/A', 'message': {'text': 'OSV Scan Finding'}, 'locations': [{ + 'physicalLocation': { + 'artifactLocation': { + 'uri': 'No location associated with this finding' + }, + 'region': {'startLine': 1, 'startColumn': 1, 'endColumn': 1} + } + }] + } + + +def sarif_rule(): + """ + Returns the template for a rule entry in the Sarif log, + which is populated with CVE findings from OSV API + """ + return { + 'id': 'OSV Scan', 'name': 'OSV Scan Finding', + 'shortDescription': {'text': 'Insert OSV id'}, 'fullDescription': { + 'text': 'Vulnerability found by scanning against the OSV API' + }, 'help': { + 'text': + 'More details in the OSV DB at: https://osv.dev/vulnerability/' + }, 'defaultConfiguration': {'level': 'error'}, + 'properties': {'tags': ['supply-chain', 'dependency']} + } + + +def parse_deps_file(deps_flat_file): + """ + Takes input of fully qualified dependencies, + for each dep find the common ancestor commit SHA + from the upstream and query OSV API using that SHA + + If the commit cannot be found or the dep cannot be + compared to an upstream, prints list of those deps + """ + deps_list = [] + with open(DEPS, 'r') as file: + local_scope = {} + global_scope = {'Var': lambda x: x} # dummy lambda + # Read the content. + deps_content = file.read() + + # Eval the content. + exec(deps_content, global_scope, local_scope) + + # Extract the deps and filter. + deps_list = local_scope.get('vars') + queries = [] # list of queries to submit in bulk request to OSV API + with open(deps_flat_file, 'r') as file: + lines = file.readlines() + + headers = { + 'Content-Type': 'application/json', + } + osv_url = 'https://api.osv.dev/v1/querybatch' + + if not os.path.exists(DEP_CLONE_DIR): + os.mkdir(DEP_CLONE_DIR) #clone deps with upstream into temporary dir + + # Extract commit hash, save in dictionary + for line in lines: + dep = line.strip().split( + '@' + ) # separate fully qualified dep into name + pinned hash + + common_commit = get_common_ancestor_commit(dep, deps_list) + if isinstance(common_commit, str): + queries.append({'commit': common_commit}) + else: + failed_deps.append(dep[0]) + + print( + 'Dependencies that could not be parsed for ancestor commits: ' + + ', '.join(failed_deps) + ) + try: + # clean up cloned upstream dependency directory + shutil.rmtree( + DEP_CLONE_DIR + ) # use shutil.rmtree since dir could be non-empty + except OSError as clone_dir_error: + print( + 'Error cleaning up clone directory: %s : %s' % + (DEP_CLONE_DIR, clone_dir_error.strerror) + ) + # Query OSV API using common ancestor commit for each dep + # return any vulnerabilities found. + data = json.dumps({'queries': queries}).encode('utf-8') + req = request.Request(osv_url, data, headers=headers) + with request.urlopen(req) as resp: + res_body = resp.read() + results_json = json.loads(res_body.decode('utf-8')) + if resp.status != 200: + print('Request error') + elif results_json['results'] == [{}]: + print('Found no vulnerabilities') + else: + results = results_json['results'] + filtered_results = list(filter(lambda vuln: vuln != {}, results)) + if len(filtered_results) > 0: + print( + 'Found vulnerability on {vuln_count} dependenc(y/ies), adding to report' + .format(vuln_count=str(len(filtered_results))) + ) + print(*filtered_results) + return filtered_results + print('Found no vulnerabilities') + return {} + + +def get_common_ancestor_commit(dep, deps_list): + """ + Given an input of a mirrored dep, + compare to the mapping of deps to their upstream + in DEPS and find a common ancestor + commit SHA value. + + This is done by first cloning the mirrored dep, + then a branch which tracks the upstream. + From there, git merge-base operates using the HEAD + commit SHA of the upstream branch and the pinned + SHA value of the mirrored branch + """ + # dep[0] contains the mirror repo + # dep[1] contains the mirror's pinned SHA + # upstream is the origin repo + dep_name = dep[0].split('/')[-1].split('.')[0] + if UPSTREAM_PREFIX + dep_name not in deps_list: + print('did not find dep: ' + dep_name) + return {} + try: + # get the upstream URL from the mapping in DEPS file + upstream = deps_list.get(UPSTREAM_PREFIX + dep_name) + temp_dep_dir = DEP_CLONE_DIR + '/' + dep_name + # clone dependency from mirror + subprocess.check_output([ + 'git', 'clone', '--quiet', '--', dep[0], temp_dep_dir + ]) + + # create branch that will track the upstream dep + print( + 'attempting to add upstream remote from: {upstream}'.format( + upstream=upstream + ) + ) + subprocess.check_output([ + 'git', '--git-dir', temp_dep_dir + '/.git', 'remote', 'add', 'upstream', + upstream + ]) + subprocess.check_output([ + 'git', '--git-dir', temp_dep_dir + '/.git', 'fetch', '--quiet', + 'upstream' + ]) + # get name of the default branch for upstream (e.g. main/master/etc.) + default_branch = subprocess.check_output( + 'git --git-dir ' + temp_dep_dir + '/.git remote show upstream ' + + "| sed -n \'/HEAD branch/s/.*: //p\'", + shell=True + ).decode().strip() + print( + 'default_branch found: {default_branch}'.format( + default_branch=default_branch + ) + ) + # make upstream branch track the upstream dep + subprocess.check_output([ + 'git', '--git-dir', temp_dep_dir + '/.git', 'checkout', '-b', + 'upstream', '--track', 'upstream/' + default_branch + ]) + # get the most recent commit from default branch of upstream + commit = subprocess.check_output( + 'git --git-dir ' + temp_dep_dir + '/.git for-each-ref ' + + "--format=\'%(objectname:short)\' refs/heads/upstream", + shell=True + ) + commit = commit.decode().strip() + + # perform merge-base on most recent default branch commit and pinned mirror commit + ancestor_commit = subprocess.check_output( + 'git --git-dir {temp_dep_dir}/.git merge-base {commit} {depUrl}'.format( + temp_dep_dir=temp_dep_dir, commit=commit, depUrl=dep[1] + ), + shell=True + ) + ancestor_commit = ancestor_commit.decode().strip() + print('Ancestor commit: ' + ancestor_commit) + return ancestor_commit + except subprocess.CalledProcessError as error: + print("Subprocess error '{0}' occured.".format(error.output)) + return {} + + +def write_sarif(responses, manifest_file): + """ + Creates a full SARIF report based on the OSV API response which + may contain several vulnerabilities + + Combines a rule with a result in order to construct the report + """ + data = sarif_log + for response in responses: + for vuln in response['vulns']: + new_rule = create_rule_entry(vuln) + data['runs'][0]['tool']['driver']['rules'].append(new_rule) + data['runs'][0]['results'].append(create_result_entry(vuln)) + with open(manifest_file, 'w') as out: + json.dump(data, out) + + +def create_rule_entry(vuln): + """ + Creates a Sarif rule entry from an OSV finding. + Vuln object follows OSV Schema and is required to have 'id' and 'modified' + """ + rule = sarif_rule() + rule['id'] = vuln['id'] + rule['shortDescription']['text'] = vuln['id'] + rule['help']['text'] += vuln['id'] + return rule + + +def create_result_entry(vuln): + """ + Creates a Sarif res entry from an OSV entry. + Rule finding linked to the associated rule metadata via ruleId + """ + result = sarif_result() + result['ruleId'] = vuln['id'] + return result + + +def parse_args(args): + args = args[1:] + parser = argparse.ArgumentParser( + description='A script to scan a flattened DEPS file using OSV API.' + ) + + parser.add_argument( + '--flat-deps', + '-d', + type=str, + help='Input flattened DEPS file.', + default=os.path.join(CHECKOUT_ROOT, 'deps_flatten.txt') + ) + parser.add_argument( + '--output', + '-o', + type=str, + help='Output SARIF log of vulnerabilities found in OSV database.', + default=os.path.join(CHECKOUT_ROOT, 'osvReport.sarif') + ) + + return parser.parse_args(args) + + +def main(argv): + args = parse_args(argv) + osv_scans = parse_deps_file(args.flat_deps) + write_sarif(osv_scans, args.output) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv))