Skip to content

Commit d16bdf3

Browse files
authored
Merge pull request #1600 from aboutcode-org/commit-vuln-export
Add management command to commit exported data
2 parents 433f2a9 + 3718965 commit d16bdf3

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Release notes
22
=============
33

4+
5+
Version (next)
6+
-------------------
7+
8+
- Add management command to commit exported vulnerability data (#1600)
9+
10+
411
Version v34.0.1
512
-------------------
613

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import logging
11+
import os
12+
import shutil
13+
import tempfile
14+
import textwrap
15+
from datetime import datetime
16+
from pathlib import Path
17+
from urllib.parse import urlparse
18+
19+
import requests
20+
from django.core.management.base import BaseCommand
21+
from django.core.management.base import CommandError
22+
from git import Repo
23+
24+
from vulnerablecode.settings import ALLOWED_HOSTS
25+
from vulnerablecode.settings import VULNERABLECODE_VERSION
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
class Command(BaseCommand):
31+
help = """Commit the exported vulnerability data in the backing GitHub repository.
32+
33+
This command takes the path to the exported vulnerability data and creates a pull
34+
request in the backing GitHub repository with the changes.
35+
"""
36+
37+
def add_arguments(self, parser):
38+
parser.add_argument(
39+
"path",
40+
help="Path to exported data.",
41+
)
42+
43+
def handle(self, *args, **options):
44+
if path := options["path"]:
45+
base_path = Path(path)
46+
47+
if not path or not base_path.is_dir():
48+
raise CommandError("Enter a valid directory path to the exported data.")
49+
50+
vcio_export_repo_url = os.environ.get("VULNERABLECODE_EXPORT_REPO_URL")
51+
vcio_github_service_token = os.environ.get("VULNERABLECODE_GITHUB_SERVICE_TOKEN")
52+
vcio_github_service_name = os.environ.get("VULNERABLECODE_GITHUB_SERVICE_NAME")
53+
vcio_github_service_email = os.environ.get("VULNERABLECODE_GITHUB_SERVICE_EMAIL")
54+
55+
# Check for missing environment variables
56+
missing_vars = []
57+
if not vcio_export_repo_url:
58+
missing_vars.append("VULNERABLECODE_EXPORT_REPO_URL")
59+
if not vcio_github_service_token:
60+
missing_vars.append("VULNERABLECODE_GITHUB_SERVICE_TOKEN")
61+
if not vcio_github_service_name:
62+
missing_vars.append("VULNERABLECODE_GITHUB_SERVICE_NAME")
63+
if not vcio_github_service_email:
64+
missing_vars.append("VULNERABLECODE_GITHUB_SERVICE_EMAIL")
65+
66+
if missing_vars:
67+
raise CommandError(f'Missing environment variables: {", ".join(missing_vars)}')
68+
69+
local_dir = tempfile.mkdtemp()
70+
current_date = datetime.now().strftime("%Y-%m-%d")
71+
72+
branch_name = f"export-update-{current_date}"
73+
pr_title = "Update package vulnerabilities from VulnerableCode"
74+
pr_body = f"""\
75+
Tool: pkg:github/aboutcode-org/vulnerablecode@v{VULNERABLECODE_VERSION}
76+
Reference: https://{ALLOWED_HOSTS[0]}/
77+
"""
78+
commit_message = f"""\
79+
Update package vulnerabilities from VulnerableCode
80+
81+
Tool: pkg:github/aboutcode-org/vulnerablecode@v{VULNERABLECODE_VERSION}
82+
Reference: https://{ALLOWED_HOSTS[0]}/
83+
84+
Signed-off-by: {vcio_github_service_name} <{vcio_github_service_email}>
85+
"""
86+
87+
self.stdout.write("Committing VulnerableCode package and vulnerability data.")
88+
repo = self.clone_repository(
89+
repo_url=vcio_export_repo_url,
90+
local_path=local_dir,
91+
token=vcio_github_service_token,
92+
)
93+
94+
repo.config_writer().set_value("user", "name", vcio_github_service_name).release()
95+
repo.config_writer().set_value("user", "email", vcio_github_service_email).release()
96+
97+
self.add_changes(repo=repo, content_path=path)
98+
99+
if self.commit_and_push_changes(
100+
repo=repo,
101+
branch=branch_name,
102+
commit_message=textwrap.dedent(commit_message),
103+
):
104+
self.create_pull_request(
105+
repo_url=vcio_export_repo_url,
106+
branch=branch_name,
107+
title=pr_title,
108+
body=textwrap.dedent(pr_body),
109+
token=vcio_github_service_token,
110+
)
111+
shutil.rmtree(local_dir)
112+
113+
def clone_repository(self, repo_url, local_path, token):
114+
"""Clone repository to local_path."""
115+
116+
if os.path.exists(local_path):
117+
shutil.rmtree(local_path)
118+
119+
authenticated_repo_url = repo_url.replace("https://", f"https://{token}@")
120+
return Repo.clone_from(authenticated_repo_url, local_path)
121+
122+
def add_changes(self, repo, content_path):
123+
"""Copy changes from the ``content_path`` to ``repo``."""
124+
125+
source_path = Path(content_path)
126+
destination_path = Path(repo.working_dir)
127+
128+
for item in source_path.iterdir():
129+
if not item.is_dir():
130+
continue
131+
target_item = destination_path / item.name
132+
if target_item.exists():
133+
shutil.rmtree(target_item)
134+
shutil.copytree(item, target_item)
135+
136+
def commit_and_push_changes(self, repo, branch, commit_message, remote_name="origin"):
137+
"""Commit changes and push to remote repository, return name of changed files."""
138+
139+
repo.git.checkout("HEAD", b=branch)
140+
files_changed = repo.git.diff("HEAD", name_only=True)
141+
142+
if not files_changed:
143+
self.stderr.write(self.style.SUCCESS("No changes to commit."))
144+
return
145+
146+
repo.git.add(A=True)
147+
repo.index.commit(commit_message)
148+
repo.git.push(remote_name, branch)
149+
return files_changed
150+
151+
def create_pull_request(self, repo_url, branch, title, body, token):
152+
"""Create a pull request in the GitHub repository."""
153+
154+
url_parts = urlparse(repo_url).path
155+
path_parts = url_parts.strip("/").rstrip(".git").split("/")
156+
157+
if len(path_parts) >= 2:
158+
repo_owner = path_parts[0]
159+
repo_name = path_parts[1]
160+
else:
161+
raise ValueError("Invalid GitHub repo URL")
162+
163+
url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls"
164+
headers = {"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"}
165+
data = {"title": title, "head": branch, "base": "main", "body": body}
166+
167+
response = requests.post(url, headers=headers, json=data)
168+
169+
if response.status_code == 201:
170+
pr_response = response.json()
171+
self.stdout.write(
172+
self.style.SUCCESS(
173+
f"Pull request created successfully: {pr_response.get('html_url')}."
174+
)
175+
)
176+
else:
177+
self.stderr.write(
178+
self.style.ERROR(f"Failed to create pull request: {response.content}")
179+
)

0 commit comments

Comments
 (0)