diff --git a/README.md b/README.md index a954d57..098f7be 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CodeQL](https://github.com/github/evergreen/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/github/evergreen/actions/workflows/github-code-scanning/codeql) [![Lint Code Base](https://github.com/github/evergreen/actions/workflows/super-linter.yaml/badge.svg)](https://github.com/github/evergreen/actions/workflows/super-linter.yaml) [![Python package](https://github.com/github/evergreen/actions/workflows/python-ci.yml/badge.svg)](https://github.com/github/evergreen/actions/workflows/python-ci.yml) -This is a GitHub Action that given an organization or specified repositories, opens an issue/PR dependabot is not enabled but could be. +This is a GitHub Action that given an organization or specified repositories, opens an issue/PR dependabot is not enabled but could be. It also enables [automated security updates](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates#managing-dependabot-security-updates-for-your-repositories) for the repository. This action was developed by the GitHub OSPO for our own use and developed in a way that we could open source it that it might be useful to you as well! If you want to know more about how we use it, reach out in an issue in this repository. diff --git a/evergreen.py b/evergreen.py index cfbd976..9b57b4f 100644 --- a/evergreen.py +++ b/evergreen.py @@ -5,6 +5,7 @@ import auth import env import github3 +import requests from dependabot_file import build_dependabot_file @@ -74,6 +75,10 @@ def main(): print("\tConfiguration:\n" + dependabot_file) continue + # Get dependabot security updates enabled if possible + if not is_dependabot_security_updates_enabled(repo.owner, repo.name, token): + enable_dependabot_security_updates(repo.owner, repo.name, token) + if follow_up_type == "issue": count_eligible += 1 issue = repo.create_issue(title, body) @@ -87,9 +92,39 @@ def main(): if not skip: pull = commit_changes(title, body, repo, dependabot_file) print("\tCreated pull request " + pull.html_url) + print("Done. " + str(count_eligible) + " repositories were eligible.") +def is_dependabot_security_updates_enabled(owner, repo, access_token): + """Check if Dependabot security updates are enabled at the /repos/:owner/:repo/automated-security-fixes endpoint using the requests library""" + url = f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes" + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github.london-preview+json", + } + + response = requests.get(url, headers=headers, timeout=20) + if response.status_code == 200: + return response.json()["enabled"] + return False + + +def enable_dependabot_security_updates(owner, repo, access_token): + """Enable Dependabot security updates at the /repos/:owner/:repo/automated-security-fixes endpoint using the requests library""" + url = f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes" + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github.london-preview+json", + } + + response = requests.put(url, headers=headers, timeout=20) + if response.status_code == 204: + print("\tDependabot security updates enabled successfully.") + else: + print("\tFailed to enable Dependabot security updates.") + + def get_repos_iterator(organization, repository_list, github_connection): """Get the repositories from the organization or list of repositories""" repos = [] diff --git a/test_evergreen.py b/test_evergreen.py new file mode 100644 index 0000000..e0a33b8 --- /dev/null +++ b/test_evergreen.py @@ -0,0 +1,180 @@ +""" Test the evergreen.py module. """ +import unittest +from unittest.mock import patch + +from evergreen import ( + enable_dependabot_security_updates, + is_dependabot_security_updates_enabled, +) + + +class TestDependabotSecurityUpdates(unittest.TestCase): + """Test the Dependabot security updates functions in evergreen.py""" + + def test_is_dependabot_security_updates_enabled(self): + """ + Test the is_dependabot_security_updates_enabled function. + + This test checks if the is_dependabot_security_updates_enabled function correctly + detects if Dependabot security updates are enabled. + + It mocks the requests.get method to simulate different scenarios. + """ + owner = "my_owner" + repo = "my_repo" + access_token = "my_access_token" + + expected_url = ( + f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes" + ) + expected_headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github.london-preview+json", + } + expected_response = {"enabled": True} + + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = expected_response + + result = is_dependabot_security_updates_enabled(owner, repo, access_token) + + mock_get.assert_called_once_with( + expected_url, headers=expected_headers, timeout=20 + ) + self.assertTrue(result) + + def test_is_dependabot_security_updates_disabled(self): + """ + Test the is_dependabot_security_updates_enabled function when security updates are disabled. + + This test checks if the is_dependabot_security_updates_enabled function correctly + detects if Dependabot security updates are disabled. + + It mocks the requests.get method to simulate different scenarios. + """ + owner = "my_owner" + repo = "my_repo" + access_token = "my_access_token" + + expected_url = ( + f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes" + ) + expected_headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github.london-preview+json", + } + + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = {"enabled": False} + + result = is_dependabot_security_updates_enabled(owner, repo, access_token) + + mock_get.assert_called_once_with( + expected_url, headers=expected_headers, timeout=20 + ) + self.assertFalse(result) + + def test_is_dependabot_security_updates_not_found(self): + """ + Test the is_dependabot_security_updates_enabled function when the endpoint is not found. + + This test checks if the is_dependabot_security_updates_enabled function correctly + handles the case when the endpoint is not found. + + It mocks the requests.get method to simulate different scenarios. + """ + owner = "my_owner" + repo = "my_repo" + access_token = "my_access_token" + + expected_url = ( + f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes" + ) + expected_headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github.london-preview+json", + } + + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 404 + + result = is_dependabot_security_updates_enabled(owner, repo, access_token) + + mock_get.assert_called_once_with( + expected_url, headers=expected_headers, timeout=20 + ) + self.assertFalse(result) + + def test_enable_dependabot_security_updates(self): + """ + Test the enable_dependabot_security_updates function. + + This test checks if the enable_dependabot_security_updates function successfully enables + Dependabot security updates. + + It mocks the requests.put method to simulate different scenarios. + """ + owner = "my_owner" + repo = "my_repo" + access_token = "my_access_token" + + expected_url = ( + f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes" + ) + expected_headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github.london-preview+json", + } + + with patch("requests.put") as mock_put: + mock_put.return_value.status_code = 204 + + with patch("builtins.print") as mock_print: + enable_dependabot_security_updates(owner, repo, access_token) + + mock_put.assert_called_once_with( + expected_url, headers=expected_headers, timeout=20 + ) + mock_print.assert_called_once_with( + "\tDependabot security updates enabled successfully." + ) + + def test_enable_dependabot_security_updates_failed(self): + """ + Test the enable_dependabot_security_updates function when enabling fails. + + This test checks if the enable_dependabot_security_updates function handles the case + when enabling Dependabot security updates fails. + + It mocks the requests.put method to simulate different scenarios. + """ + owner = "my_owner" + repo = "my_repo" + access_token = "my_access_token" + + expected_url = ( + f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes" + ) + expected_headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github.london-preview+json", + } + + with patch("requests.put") as mock_put: + mock_put.return_value.status_code = 500 + + with patch("builtins.print") as mock_print: + enable_dependabot_security_updates(owner, repo, access_token) + + mock_put.assert_called_once_with( + expected_url, headers=expected_headers, timeout=20 + ) + mock_print.assert_called_once_with( + "\tFailed to enable Dependabot security updates." + ) + + +if __name__ == "__main__": + unittest.main()