[workflows] Split pr-code-format into two parts to make it more secure (#78216)
Actions triggered by pull_request_target events have access to all repository secrets, so it is unsafe to use them when executing untrusted code. The pr-code-format workflow does not execute any untrusted code, but it passes untrused input into clang-format. An attacker could use this to exploit a flaw in clang-format and potentially gain access to the repository secrets. By splitting the workflow, we can use the pull_request target which is more secure and isolate the issue write permissions in a separate job. The pull_request target also makes it easier to test changes to the code-format-helepr.py script, because the version of the script from the pull request will be used rather than the version of the script from main. Fixes #77142
This commit is contained in:
parent
ad0acf9ef6
commit
bc06cd5cbc
89
.github/workflows/issue-write.yml
vendored
Normal file
89
.github/workflows/issue-write.yml
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
name: Comment on an issue
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Check code formatting"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pr-comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
steps:
|
||||
- name: 'Download artifact'
|
||||
# v7.0.1
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "workflow-args"
|
||||
})[0];
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/workflow-args.zip`, Buffer.from(download.data));
|
||||
|
||||
- run: unzip workflow-args.zip
|
||||
|
||||
- name: 'Comment on PR'
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
const comments = JSON.parse(fs.readFileSync('./comments'));
|
||||
if (!comments) {
|
||||
return;
|
||||
}
|
||||
|
||||
let runInfo = await github.actions.getWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id
|
||||
});
|
||||
|
||||
if (runInfo.data.pull_requests.length != 1) {
|
||||
console.log("Error retrieving pull request number");
|
||||
console.log(runInfo);
|
||||
return;
|
||||
}
|
||||
const prNumber = runInfo.data.pull_requests[0].number;
|
||||
|
||||
await comments.forEach(function (comment) {
|
||||
if (comment.id) {
|
||||
github.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
comment_id: comment.id,
|
||||
body: comment.body
|
||||
});
|
||||
} else {
|
||||
github.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: comment.body
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
- name: Dump comments file
|
||||
if: always()
|
||||
run: cat comments
|
||||
30
.github/workflows/pr-code-format.yml
vendored
30
.github/workflows/pr-code-format.yml
vendored
@ -1,7 +1,5 @@
|
||||
name: "Check code formatting"
|
||||
on: pull_request_target
|
||||
permissions:
|
||||
pull-requests: write
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
code_formatter:
|
||||
@ -27,18 +25,6 @@ jobs:
|
||||
separator: ","
|
||||
skip_initial_fetch: true
|
||||
|
||||
# We need to make sure that we aren't executing/using any code from the
|
||||
# PR for security reasons as we're using pull_request_target. Checkout
|
||||
# the target branch with the necessary files.
|
||||
- name: Fetch code formatting utils
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
llvm/utils/git/requirements_formatting.txt
|
||||
llvm/utils/git/code-format-helper.py
|
||||
sparse-checkout-cone-mode: false
|
||||
path: code-format-tools
|
||||
|
||||
- name: "Listed files"
|
||||
env:
|
||||
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
|
||||
@ -56,10 +42,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'code-format-tools/llvm/utils/git/requirements_formatting.txt'
|
||||
cache-dependency-path: 'llvm/utils/git/requirements_formatting.txt'
|
||||
|
||||
- name: Install python dependencies
|
||||
run: pip install -r code-format-tools/llvm/utils/git/requirements_formatting.txt
|
||||
run: pip install -r llvm/utils/git/requirements_formatting.txt
|
||||
|
||||
- name: Run code formatter
|
||||
env:
|
||||
@ -72,9 +58,17 @@ jobs:
|
||||
# explicitly in code-format-helper.py and not have to diff starting at
|
||||
# the merge base.
|
||||
run: |
|
||||
python ./code-format-tools/llvm/utils/git/code-format-helper.py \
|
||||
python ./llvm/utils/git/code-format-helper.py \
|
||||
--write-comment-to-file \
|
||||
--token ${{ secrets.GITHUB_TOKEN }} \
|
||||
--issue-number $GITHUB_PR_NUMBER \
|
||||
--start-rev $(git merge-base $START_REV $END_REV) \
|
||||
--end-rev $END_REV \
|
||||
--changed-files "$CHANGED_FILES"
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: workflow-args
|
||||
path: |
|
||||
comments
|
||||
|
||||
@ -44,6 +44,7 @@ class FormatArgs:
|
||||
token: str = None
|
||||
verbose: bool = True
|
||||
issue_number: int = 0
|
||||
write_comment_to_file: bool = False
|
||||
|
||||
def __init__(self, args: argparse.Namespace = None) -> None:
|
||||
if not args is None:
|
||||
@ -53,12 +54,14 @@ class FormatArgs:
|
||||
self.token = args.token
|
||||
self.changed_files = args.changed_files
|
||||
self.issue_number = args.issue_number
|
||||
self.write_comment_to_file = args.write_comment_to_file
|
||||
|
||||
|
||||
class FormatHelper:
|
||||
COMMENT_TAG = "<!--LLVM CODE FORMAT COMMENT: {fmt}-->"
|
||||
name: str
|
||||
friendly_name: str
|
||||
comment: dict = None
|
||||
|
||||
@property
|
||||
def comment_tag(self) -> str:
|
||||
@ -119,6 +122,13 @@ View the diff from {self.name} here.
|
||||
comment_text = self.comment_tag + "\n\n" + comment_text
|
||||
|
||||
existing_comment = self.find_comment(pr)
|
||||
|
||||
if args.write_comment_to_file:
|
||||
self.comment = {"body": comment_text}
|
||||
if existing_comment:
|
||||
self.comment["id"] = existing_comment.id
|
||||
return
|
||||
|
||||
if existing_comment:
|
||||
existing_comment.edit(comment_text)
|
||||
elif create_new:
|
||||
@ -309,6 +319,8 @@ def hook_main():
|
||||
if fmt.has_tool():
|
||||
if not fmt.run(args.changed_files, args):
|
||||
failed_fmts.append(fmt.name)
|
||||
if fmt.comment:
|
||||
comments.append(fmt.comment)
|
||||
else:
|
||||
print(f"Couldn't find {fmt.name}, can't check " + fmt.friendly_name.lower())
|
||||
|
||||
@ -349,6 +361,11 @@ if __name__ == "__main__":
|
||||
type=str,
|
||||
help="Comma separated list of files that has been changed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--write-comment-to-file",
|
||||
action="store_true",
|
||||
help="Don't post comments on the PR, instead write the comments and metadata a file called 'comment'",
|
||||
)
|
||||
|
||||
args = FormatArgs(parser.parse_args())
|
||||
|
||||
@ -357,9 +374,18 @@ if __name__ == "__main__":
|
||||
changed_files = args.changed_files.split(",")
|
||||
|
||||
failed_formatters = []
|
||||
comments = []
|
||||
for fmt in ALL_FORMATTERS:
|
||||
if not fmt.run(changed_files, args):
|
||||
failed_formatters.append(fmt.name)
|
||||
if fmt.comment:
|
||||
comments.append(fmt.comment)
|
||||
|
||||
if len(comments):
|
||||
with open("comments", "w") as f:
|
||||
import json
|
||||
|
||||
json.dump(comments, f)
|
||||
|
||||
if len(failed_formatters) > 0:
|
||||
print(f"error: some formatters failed: {' '.join(failed_formatters)}")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user