Verify listed CVEs in the release notes.
This code makes sure that CVEs listed in the release notes
do not exist in the released images.
BUG=b/297072262
TEST=Builds with different release notes in https://cos-review.googlesource.com/c/cos/tools/+/56629
RELEASE_NOTE=none
Change-Id: Ia79d23e3cf2186a7057973c9b6d4fb6369f44620
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/56491
Tested-by: Anil Altinay <aaltinay@google.com>
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Reviewed-by: Arnav Kansal <rnv@google.com>
diff --git a/release/tests/cloudbuild.yaml b/release/tests/cloudbuild.yaml
new file mode 100644
index 0000000..15438cd
--- /dev/null
+++ b/release/tests/cloudbuild.yaml
@@ -0,0 +1,7 @@
+steps:
+- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
+ entrypoint: 'bash'
+ args: ['-c',
+ 'pip3 install -r requirements.txt && python3 release-note-cve-verifier.py ${_BUILD_GCR}'
+ ]
+timeout: 1800s
diff --git a/release/tests/release-note-cve-verifier.py b/release/tests/release-note-cve-verifier.py
new file mode 100644
index 0000000..784060f
--- /dev/null
+++ b/release/tests/release-note-cve-verifier.py
@@ -0,0 +1,117 @@
+"""
+Release-note-cve-verifier is invoked from git trigger for every CL.
+It verifies that CVEs listed in the release note are fixed.
+This ensures that release CL will not be submitted with a wrong release note.
+"""
+
+import sys
+import yaml
+import subprocess
+import os
+import git
+import re
+import logging
+from google.cloud.devtools import containeranalysis_v1
+
+def validate_config(release_config):
+ for release_container in release_config:
+ for key in ["staging_container_name", "release_container_name", "build_commit", "release_tags"]:
+ assert key in release_container, "missing {} in entry {}".format(key, release_container)
+
+def validate_src_gcr_path(path):
+ # path format: gcr.io/cos-infra-prod
+ return len(path) > len("gcr.io/") and path[:len("gcr.io/")] == "gcr.io/"
+
+def verify_release_note(src_bucket):
+ assert validate_src_gcr_path(src_bucket), "cannot use address {}, only gcr.io/ addresses are supported".format(src_bucket)
+ with open('release/release-versions.yaml', 'r') as file:
+ try:
+ release_config = yaml.safe_load(file)
+ validate_config(release_config)
+
+ # Get the project id from src bucket.
+ # We already verified the format of the src bucket.
+ project_id = src_bucket[len("gcr.io/"):]
+ cves = release_note_cves()
+ logging.info("CVEs listed on the release notes: %s", cves)
+ verify_result = True
+ for release_container in release_config:
+ staging_container_name = release_container["staging_container_name"]
+ build_tag = release_container["build_commit"]
+ src_path = os.path.join(src_bucket, staging_container_name)
+ container_tag_url = src_path + ":" + build_tag
+ # We need digest URL for occurences.
+ digest = container_digest(container_tag_url)
+ container_digest_url = "https://" + src_path + "@" + digest
+ logging.info("container URL: %s", container_digest_url)
+ if not verify_fixed_cves(project_id, container_digest_url, cves):
+ verify_result = False
+ return verify_result
+
+ except yaml.YAMLError as ex:
+ raise Exception("Invalid YAML config: %s" % str(ex))
+
+def container_digest(container_url):
+ """Returns digest of the container."""
+ # contaiener_url = 'gcr.io/my-project/my-image:abc'
+
+ digest = subprocess.run(
+ ['gcloud', 'container', 'images', 'describe', container_url, '--format', 'value(image_summary.digest)'],
+ capture_output = True,
+ check = True
+ )
+ return digest.stdout.decode('utf-8').strip('\"').rstrip()
+
+def verify_fixed_cves(project_id, container_digest_url, cves):
+ """Retrieves all the occurrences associated with a specified image.
+ Returns false if any of the cves among the occurrences.
+ Otherwise, return true."""
+ # container_digest_url = 'https://gcr.io/my-project/my-image@sha256:123'
+ # project_id = 'my-gcp-project'
+ # cves = [CVE-2020-1111, CVE-2021-1111]
+
+ client = containeranalysis_v1.ContainerAnalysisClient()
+ grafeas_client = client.get_grafeas_client()
+ project_name = f"projects/{project_id}"
+ for cve in cves:
+ filter_str = f'kind="VULNERABILITY" AND resourceUrl="{container_digest_url}" AND noteId="{cve}"'
+ logging.info(filter_str)
+ occurrences = grafeas_client.list_occurrences(parent=project_name, filter=filter_str)
+ for o in occurrences:
+ logging.info("%s has not been fixed in the %s", cve, container_digest_url)
+ return False
+ logging.info("%s does not affect the image", cve)
+ return True
+
+def commit_message():
+ """Returns the last commit message"""
+ repo = git.Repo(os.getcwd())
+ main = repo.head.reference
+ return main.commit.message
+
+def release_note_cves():
+ """Finds the CVEs listed in the release note of the commit message."""
+ commit_msg = commit_message()
+ cves = []
+ reg = re.compile('CVE-\d{4}-\d{4,7}')
+ lines = commit_msg.splitlines()
+ for i in range(len(lines)):
+ line = lines[i]
+ # Find start of RELEASE_NOTE and scan the following lines until
+ # there is an empty line.
+ if line.startswith('RELEASE_NOTE='):
+ while line: #Until we hit to empty line
+ cves = cves + reg.findall(line)
+ i = i + 1
+ line = lines[i].strip()
+ return cves
+
+def main():
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
+ src_bucket = sys.argv[1]
+ logging.info("source bucket: %s", src_bucket)
+ if not verify_release_note(src_bucket):
+ sys.exit("Release note verification failed. Check the logs above for details.")
+
+if __name__ == '__main__':
+ main()
diff --git a/release/tests/requirements.txt b/release/tests/requirements.txt
new file mode 100644
index 0000000..fbb2e20
--- /dev/null
+++ b/release/tests/requirements.txt
@@ -0,0 +1,4 @@
+PyYAML
+gitpython
+google-api-python-client
+google-cloud-containeranalysis