From dd71f74a710eb37dca601b0f2ae90bd9390a704d Mon Sep 17 00:00:00 2001 From: Daryl Walleck Date: Mon, 6 Nov 2023 15:29:38 -0500 Subject: [PATCH] Add GitHub workflow to protect certain files from modifications (#8927) * Adds github action workflow to protect certain files from modifications * Added empty line to the end of the git-protect script * Added preliminary list of files to protect * Cleared empty line of spaces --- .github/scripts/git_protect.py | 103 ++++++++++++++++++++ .github/workflows/check-protected-files.yml | 45 +++++++++ .gitprotect | 91 +++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 .github/scripts/git_protect.py create mode 100644 .github/workflows/check-protected-files.yml create mode 100644 .gitprotect diff --git a/.github/scripts/git_protect.py b/.github/scripts/git_protect.py new file mode 100644 index 00000000000..83b3d88da2b --- /dev/null +++ b/.github/scripts/git_protect.py @@ -0,0 +1,103 @@ +import argparse +import logging +import re +import subprocess +from pathlib import Path + +log = logging.getLogger(__name__) + + +def gitignore_to_regex(pattern) -> str: + # Replace .gitignore-style patterns with regex equivalents + pattern = pattern.replace("*", ".*") # * -> .* + pattern = pattern.replace("?", ".") # ? -> . + pattern = pattern.replace("[!", "[^") # [!abc] -> [^abc] + + # If the pattern ends with '/', it matches directories + if pattern.endswith("/"): + pattern = f"{pattern}.*" + + return rf"^{pattern}" + + +def get_protected_files(file_name: str) -> list[str]: + # Check to see if the .gitprotect file exists + config_path = Path(file_name) + if not config_path.exists(): + log.error(f"ERROR: Could not find .gitprotect at {config_path.absolute()}") + exit(1) + + # Open the file and read in file paths + with open(file_name, "r") as file: + return [gitignore_to_regex(line.strip()) for line in file] + + +def get_changed_files(base_ref: str, head_ref: str) -> list[str]: + result = subprocess.run( + [ + "git", + "diff", + "--name-only", + base_ref, + head_ref, + ], + capture_output=True, + text=True, + ) + return result.stdout.splitlines() + + +def check_changes_against_protect_list( + changed_files: list[str], protected_files: list[str], comment_only: bool +): + violations = set() + + # If any modified file is one in the protect list, add the files to the violations list + for protected_file in protected_files: + pattern = re.compile(protected_file) + files_with_pattern = [f for f in changed_files if pattern.search(f)] + violations.update(files_with_pattern) + + violations_list = "\n".join(violations) + if violations: + log.error( + f"The following files are protected and cannot be modified:\n{violations_list}" + ) + if comment_only: + exit_code = 0 + else: + exit_code = 1 + exit(exit_code) + else: + log.debug("No changes to protected files were detected.") + + +def main(args): + changed_files = get_changed_files( + args.base_ref, + args.head_ref, + ) + protected_files = get_protected_files(".gitprotect") + check_changes_against_protect_list( + protected_files=protected_files, + changed_files=changed_files, + comment_only=args.comment_only + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="A utility function to check if protected files have been modified." + ) + parser.add_argument( + "base_ref", help="The git SHA for the most recent merged commit." + ) + parser.add_argument("head_ref", help="The git SHA for the incoming commit") + parser.add_argument( + "--comment-only", + action="store_true", + help="Sets git-protect to not exit with an error code", + ) + + args = parser.parse_args() + main(args) diff --git a/.github/workflows/check-protected-files.yml b/.github/workflows/check-protected-files.yml new file mode 100644 index 00000000000..2c4a530611b --- /dev/null +++ b/.github/workflows/check-protected-files.yml @@ -0,0 +1,45 @@ +name: Check For Modifications to Protected Files + +on: + pull_request_target: + +jobs: + check-if-protected-files-are-modified: + permissions: write-all + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Check for file changes using git-protect + run: | + python .github/scripts/git_protect.py ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} --comment-only &> output.txt + + - name: Post a comment back to the PR if protected files have changed + if: ${{ always() }} + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + + fs.readFile('output.txt', 'utf8', (err, data) => { + if (err) { + console.error('Error reading the file:', err); + return; + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: data + }) + }); \ No newline at end of file diff --git a/.gitprotect b/.gitprotect new file mode 100644 index 00000000000..418309637f2 --- /dev/null +++ b/.gitprotect @@ -0,0 +1,91 @@ +docker-compose.yml +requirements.txt +wsgi.py +Dockerfile.nginx-debian +Dockerfile.nginx-alpine +Dockerfile.django-debian +Dockerfile.django-alpine + +dojo/announcement/ +dojo/api_v2/ +dojo/authorization/ +dojo/banner/ +dojo/benchmark/ +dojo/components/ +dojo/cred/ +dojo/db_migrations/ +dojo/development_environment/ +dojo/endpoint/ +dojo/engagement/ +dojo/finding/ +dojo/finding_group/ +dojo/github_issue_link/ +dojo/group/ +dojo/importers/ +dojo/jira_link/ +dojo/management/ +dojo/metrics/ +dojo/note_type/ +dojo/notes/ +dojo/notifications/ +dojo/object/ +dojo/product/ +dojo/product_type/ +dojo/regulations/ +dojo/reports/ +dojo/risk_acceptance/ +dojo/rules/ +dojo/scans/ +dojo/search/ +dojo/settings/ +dojo/sla_config/ +dojo/survey/ +dojo/system_settings/ +dojo/templates/ +dojo/templatetags/ +dojo/test/ +dojo/test_type/ +dojo/tool_config/ +dojo/tool_product/ +dojo/tool_type/ +dojo/user/ +dojo/apps.py +dojo/celery.py +dojo/checks.py +dojo/context_processors.py +dojo/decorators.py +dojo/filters.py +dojo/forms.py +dojo/github.py +dojo/middleware.py +dojo/models.py +dojo/okta.py +dojo/pipeline.py +dojo/remote_user.py +dojo/tasks.py +dojo/urls.py +dojo/utils.py +dojo/views.py +dojo/wsgi.py + +helm/defectdojo/values.yaml +helm/defectdojo/templates/ + +docker/certs/ +docker/environments/ +docker/extra_fixtures/ +docker/extra_settings/ +docker/docker-compose-check.sh +docker/entrypoint-celery-beat.sh +docker/entrypoint-celery-worker.sh +docker/entrypoint-initializer.sh +docker/entrypoint-integration-tests.sh +docker/entrypoint-nginx.sh +docker/entrypoint-unit-tests-devDocker.sh +docker/entrypoint-unit-tests.sh +docker/entrypoint-uwsgi-dev.sh +docker/entrypoint-uwsgi.sh +docker/entrypoint.sh +docker/setEnv.sh +docker/unit-tests.sh +docker/wait-for-it.sh \ No newline at end of file