Vulnerability Scanning on Third Party Deps (#36506)
* initial flatten deps scan
* move 3rd party scan to separate action
* allow fork to run
* install requests
* use packages
* pip install
* rename
* conditional vuln report
* trailing whitespace
* trailing whitespace
* detailed print
* add testing file
* add upload test sarif
* results sarif
* move sarif
* upload modified sarif
* test flow
* test with results.sarif
* formatting
* test naming convention
* description with text in artifactLocation
* don't use locations
* use template sarif
* just use template
* add one field mod
* add another field mod
* use actual osvReport
* add field
* add field
* test
* no information uri
* no information uri
* add name
* template NA data for results
* back to minimal template
* dynamic rules
* template update
* no results
* only use template
* test
* new test
* new test
* add back locations
* descriptive fields
* test
* use package name
* variable commit hash
* add chromium accessibility readme support
* use batch query test
* clean up
* use variables for sarif template
* initial automating ancestor commit
* allow for workflow on testing
* install gitpython in workflow
* wrap in try
* expand try
* check commit is not none
* quiet clone
* fix commit newline
* proper print for failed deps
* remove gitpython
* remove import
* fix origin source
* remove .dart from dep names
* update dep
* typo
* update
* clone into controlled name repo now
* fix github upstream clone url
* test CVE finding
* use templated rule and result
* typo
* remove test CVE
* add link straight to OSV DB
* comments
* use os mkdir
* check time of pinned commit
* quiet git
* print osv api query results if vulns found
* move upstream mapping into DEPS file
* add testing for DEPS file
* add khronos exception
* add basic ancestor commit test
* no vulns message
* do not produce empty sarif
* add yaml
* remove unused python dep
* no change?
* no more print, causing recipe issues
* string test
* string test
* no more fstrings
* convert to .format
* syntax
* remove unused dep
* test
* switch test script
* no encoding
* add back test
* typo
* remove scan flat deps tests again
* update
* fix tests
* typo
* newline
* use checkout dir
* prefix
* update to use prefix
* lint
* runhook attempt
* lint
* lint
* lint
* lint
* no license blurb
* cleanup
* enable for main
* do not raise error
* run on branch
* data indentation
* check file existence
* workflow updates
* add push for testing
* syntax
* workflow test
* test github action
* syntax
* allow empty report
* update cron
* pin hash
* newline
* sort by key with prefix omitted
* alphabetize, copyright header
* pylint tests
* lint
* lint
* trailing whitespace?
* lint
* update
* get error types
* allow test
* use output
* only main branch
* licenses check
* results.sarif
* revert
* license updates
* add upstream
* replace Requests library with urllib, remove pylint wrapper
* lint
* undo license
* clone test nit
* isinstance
* DEPS formatting
Co-authored-by: Zachary Anderson <zanderso@users.noreply.github.com>
* use subprocess.check_output
* lint
* lint
* review syntax from comments
* remove line
* more description in error
* lint
* fix checkout path
* remove duplicate eval
* lint
* lint
* lint
* clone-test mkdir and cleanup
* use shutil.rmtree for non-empty dir
* lint
* linting
* linting
* var name
* Update ci/deps_parser_tests.py
Co-authored-by: Zachary Anderson <zanderso@users.noreply.github.com>
* Update ci/deps_parser_tests.py
Co-authored-by: Zachary Anderson <zanderso@users.noreply.github.com>
* more description
* lint
* refactor deps file parsing
* early return
* lint
Co-authored-by: Zachary Anderson <zanderso@users.noreply.github.com>
diff --git a/.github/workflows/third_party_scan.yml b/.github/workflows/third_party_scan.yml
new file mode 100644
index 0000000..3e4a6b2
--- /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 14b8300..0664cda 100644
--- a/DEPS
+++ b/DEPS
@@ -97,6 +97,122 @@
# 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 4e46c8d..bf4f6d8 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 <DEPS file> --output <flattened deps>
#
# 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 @@
# 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 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 0000000..dd96d16
--- /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 0000000..ece1154
--- /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 <flat DEPS file> --output <vulnerability report>
+#
+# 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))