This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Security Alerts Review | |
# - ensures that all security alerts from olympix static analysis are properly handled before merging | |
# - enforces a strict review policy where every alert must be either resolved or dismissed with a justification | |
# - prevents merging a PR if any alerts are unresolved or dismissed without a comment | |
# - helps maintain a transparent and collaborative security review process by generating a pr comment summarizing the status of all security alerts | |
# - automatically reverts the PR to draft status if blocking alerts exist | |
# - leaves a summary of all alerts in a comment starting with "π€ GitHub Action: Security Alerts Review π" | |
on: | |
pull_request: | |
types: | |
- ready_for_review | |
paths: | |
- 'src/**/*.sol' | |
workflow_dispatch: | |
jobs: | |
check-security-alerts: | |
runs-on: ubuntu-latest | |
steps: | |
# Ensure that the Olympix Static Analysis workflow has run successfully at least once before proceeding. | |
# This check is necessary because the Security Alerts Review workflow should not proceed unless | |
# a valid Olympix Static Analysis report is available for the current branch. | |
- name: Check if Olympix Static Analysis has run at least once and was successful | |
id: check-analysis | |
env: | |
GITHUB_TOKEN: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} | |
BRANCH_NAME: ${{ github.head_ref }} | |
run: | | |
# Fallback in case BRANCH_NAME is empty. | |
if [ -z "$BRANCH_NAME" ]; then | |
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF##*/}}" | |
echo "BRANCH_NAME was empty, falling back to: $BRANCH_NAME" | |
fi | |
echo "Checking latest Olympix Static Analysis run for branch: $BRANCH_NAME" | |
# Fetch the latest completed runs of the Olympix Static Analysis workflow | |
LATEST_RUN=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ | |
"https://api.github.com/repos/${{ github.repository }}/actions/workflows/olympixStaticAnalysis.yml/runs?status=completed&per_page=10") | |
echo "LATEST_RUN" | |
echo $LATEST_RUN | |
# Filter to find the first run with head_branch matching our branch name | |
WORKFLOW_STATUS=$(echo "$LATEST_RUN" | jq -r --arg branch "$BRANCH_NAME" '.workflow_runs[] | select(.head_branch == $branch) | .conclusion' | head -n1) | |
echo "WORKFLOW_STATUS" | |
echo $WORKFLOW_STATUS | |
if [[ "$WORKFLOW_STATUS" != "success" ]]; then | |
echo "The Olympix Static Analysis workflow has not been successfully completed for branch: $BRANCH_NAME." | |
echo "The Security Alerts Review workflow cannot continue because a valid Olympix Static Analysis report is required." | |
exit 1 | |
fi | |
- uses: actions/checkout@v4 | |
- uses: jwalton/gh-find-current-pr@master | |
id: findPr | |
- name: Validate and set PR Number | |
id: fetch_pr | |
env: | |
GITHUB_TOKEN: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} | |
run: | | |
if [ -z "${{ steps.findPr.outputs.number }}" ]; then | |
echo "Error: No pull request found for this push." >&2 | |
exit 1 | |
fi | |
echo "Found PR number: ${{ steps.findPr.outputs.number }}" | |
PR_NUMBER=${{ steps.findPr.outputs.number }} | |
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV | |
echo "Pull Request Number is: $PR_NUMBER" | |
- name: Fetch Security Alerts for PR | |
env: | |
GITHUB_TOKEN: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} | |
run: | | |
echo "Fetching security alerts for PR #${PR_NUMBER}..." | |
# Fetch security alerts via GitHub API | |
ALERTS=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ | |
"https://api.github.com/repos/${{ github.repository }}/code-scanning/alerts?pr=${PR_NUMBER}") | |
# Log raw API response for debugging | |
echo "Raw API Response:" | |
echo "$ALERTS" | |
# Extract unresolved alerts (open alerts) | |
UNRESOLVED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "open") ]' || echo "[]") | |
# Extract dismissed alerts without comments (empty dismissed_comment) | |
DISMISSED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "dismissed" and (.dismissed_comment == null or .dismissed_comment == ""))]' || echo "[]") | |
# Extract dismissed alerts with comments (successful dismissals) | |
RESOLVED_ALERTS=$(echo "$ALERTS" | jq -c '[.[] | select(.state == "dismissed" and (.dismissed_comment != null and .dismissed_comment != ""))]' || echo "[]") | |
UNRESOLVED_COUNT=$(echo "$UNRESOLVED_ALERTS" | jq -r 'length') | |
DISMISSED_COUNT=$(echo "$DISMISSED_ALERTS" | jq -r 'length') | |
COMMENTED_COUNT=$(echo "$RESOLVED_ALERTS" | jq -r 'length') | |
# Count dismissed alerts with the invalid reason "Used in tests" (invalid because only production code is analyzed) | |
INVALID_REASON_COUNT=$(echo "$RESOLVED_ALERTS" | jq -r '[.[] | select(.dismissed_reason == "used in tests")] | length') | |
# Output for debugging | |
echo "UNRESOLVED_ALERTS: $UNRESOLVED_ALERTS" | |
echo "DISMISSED_ALERTS (without comments): $DISMISSED_ALERTS" | |
echo "RESOLVED_ALERTS (with comments): $RESOLVED_ALERTS" | |
echo "UNRESOLVED_COUNT: $UNRESOLVED_COUNT" | |
echo "DISMISSED_COUNT: $DISMISSED_COUNT" | |
echo "COMMENTED_COUNT: $COMMENTED_COUNT" | |
echo "INVALID_REASON_COUNT: $INVALID_REASON_COUNT" | |
# Save values in the environment as single-line JSON | |
echo "UNRESOLVED_ALERTS=$UNRESOLVED_ALERTS" >> $GITHUB_ENV | |
echo "DISMISSED_ALERTS=$DISMISSED_ALERTS" >> $GITHUB_ENV | |
echo "RESOLVED_ALERTS=$RESOLVED_ALERTS" >> $GITHUB_ENV | |
echo "UNRESOLVED_COUNT=$UNRESOLVED_COUNT" >> $GITHUB_ENV | |
echo "DISMISSED_COUNT=$DISMISSED_COUNT" >> $GITHUB_ENV | |
echo "COMMENTED_COUNT=$COMMENTED_COUNT" >> $GITHUB_ENV | |
echo "INVALID_REASON_COUNT=$INVALID_REASON_COUNT" >> $GITHUB_ENV | |
- name: Find Existing PR Comment | |
id: find_comment | |
env: | |
GITHUB_TOKEN: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} | |
run: | | |
echo "Searching for existing PR comment..." | |
COMMENT_ID=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ | |
"https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" | jq -r \ | |
'.[] | select(.body | startswith("### π€ GitHub Action: Security Alerts Review")) | .id') | |
if [[ -n "$COMMENT_ID" && "$COMMENT_ID" != "null" ]]; then | |
echo "EXISTING_COMMENT_ID=$COMMENT_ID" >> $GITHUB_ENV | |
fi | |
echo "Found comment ID: $COMMENT_ID" | |
- name: Post or Update PR Comment | |
env: | |
GITHUB_TOKEN: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} | |
run: | | |
COMMENT_BODY="### π€ GitHub Action: Security Alerts Review π\n\n" | |
# Add Unresolved Alerts | |
if [[ "$UNRESOLVED_COUNT" -gt 0 ]]; then | |
COMMENT_BODY+="π¨ **Unresolved Security Alerts Found!** π¨\n" | |
COMMENT_BODY+="The following security alerts must be **resolved** before merging:\n\n" | |
while IFS= read -r row; do | |
ALERT_URL=$(echo "$row" | jq -r '.html_url') | |
ALERT_FILE=$(echo "$row" | jq -r '.most_recent_instance.location.path') | |
ALERT_DESCRIPTION=$(echo "$row" | jq -r '.most_recent_instance.message.text') | |
COMMENT_BODY+="π΄ [View Alert]($ALERT_URL) - **File:** \`$ALERT_FILE\`\n" | |
COMMENT_BODY+=" πΉ $ALERT_DESCRIPTION\n\n" | |
done < <(echo "$UNRESOLVED_ALERTS" | jq -c '.[]') | |
fi | |
# Add Dismissed Alerts Without Comments | |
if [[ "$DISMISSED_COUNT" -gt 0 ]]; then | |
COMMENT_BODY+="The following alerts were dismissed but require a dismissal comment:\n\n" | |
while IFS= read -r row; do | |
ALERT_URL=$(echo "$row" | jq -r '.html_url') | |
ALERT_FILE=$(echo "$row" | jq -r '.most_recent_instance.location.path') | |
ALERT_DESCRIPTION=$(echo "$row" | jq -r '.most_recent_instance.message.text') | |
COMMENT_BODY+="π‘ [View Alert]($ALERT_URL) - **File:** \`$ALERT_FILE\`\n" | |
COMMENT_BODY+=" πΉ $ALERT_DESCRIPTION\n\n" | |
done < <(echo "$DISMISSED_ALERTS" | jq -c '.[]') | |
fi | |
# Add alerts dismissed with an invalid reason ("Used in tests") | |
if [[ "$INVALID_REASON_COUNT" -gt 0 ]]; then | |
COMMENT_BODY+="β **Invalid Dismissal Reasons Found!** β\n" | |
COMMENT_BODY+="The following alerts were dismissed with the reason **Used in tests**, which is not allowed for production code. Please provide a valid dismissal reason.\n\n" | |
while IFS= read -r row; do | |
ALERT_URL=$(echo "$row" | jq -r '.html_url') | |
ALERT_FILE=$(echo "$row" | jq -r '.most_recent_instance.location.path') | |
ALERT_DESCRIPTION=$(echo "$row" | jq -r '.most_recent_instance.message.text') | |
DISMISS_REASON=$(echo "$row" | jq -r '.dismissed_reason') | |
CAPITALIZED_REASON=$(echo "$DISMISS_REASON" | sed 's/^\(.\)/\U\1/') | |
COMMENT_BODY+="β [View Alert]($ALERT_URL) - **File:** \`$ALERT_FILE\`\n" | |
COMMENT_BODY+=" πΉ $ALERT_DESCRIPTION\n" | |
COMMENT_BODY+=" πΉ Dismiss Reason: **$CAPITALIZED_REASON** (invalid for production code)\n\n" | |
done < <(echo "$RESOLVED_ALERTS" | jq -c '.[] | select(.dismissed_reason == "used in tests")') | |
fi | |
if [[ "$UNRESOLVED_COUNT" -gt 0 || "$DISMISSED_COUNT" -gt 0 || "$INVALID_REASON_COUNT" -gt 0 ]]; then | |
COMMENT_BODY+="β οΈ **Please resolve the above issues before merging.**\n\n" | |
fi | |
# Add Dismissed alerts With valid comments (successful dismissals) | |
if [[ "$COMMENTED_COUNT" -gt 0 ]]; then | |
COMMENT_BODY+="π’ **Dismissed Security Alerts with Comments**\n" | |
COMMENT_BODY+="The following alerts were dismissed with proper comments:\n\n" | |
while IFS= read -r row; do | |
# Skip alerts that were dismissed with an invalid reason | |
if echo "$row" | jq -e 'select(.dismissed_reason == "used in tests")' > /dev/null; then | |
continue | |
fi | |
ALERT_URL=$(echo "$row" | jq -r '.html_url') | |
ALERT_FILE=$(echo "$row" | jq -r '.most_recent_instance.location.path') | |
ALERT_DESCRIPTION=$(echo "$row" | jq -r '.most_recent_instance.message.text') | |
DISMISS_REASON=$(echo "$row" | jq -r '.dismissed_reason') | |
DISMISS_COMMENT=$(echo "$row" | jq -r '.dismissed_comment') | |
CAPITALIZED_REASON=$(echo "$DISMISS_REASON" | sed 's/^\(.\)/\U\1/') | |
echo "DISMISS_COMMENT" | |
echo $DISMISS_COMMENT | |
DISMISS_COMMENT_ESCAPED=$(jq -Rn --arg c "$DISMISS_COMMENT" '$c | @json') | |
echo "DISMISS_COMMENT_ESCAPED" | |
echo $DISMISS_COMMENT_ESCAPED | |
COMMENT_BODY+="π’ [View Alert]($ALERT_URL) - **File:** \`$ALERT_FILE\`\n" | |
COMMENT_BODY+=" πΉ $ALERT_DESCRIPTION\n" | |
COMMENT_BODY+=" πΉ Dismiss Reason: **$CAPITALIZED_REASON**\n" | |
COMMENT_BODY+=" πΉ Dismiss Comment: $DISMISS_COMMENT_ESCAPED\n\n" | |
done < <(echo "$RESOLVED_ALERTS" | jq -c '.[]') | |
fi | |
# If no unresolved alerts and no dismissed alerts missing comments or with invalid reasons, add overall success message | |
if [[ "$UNRESOLVED_COUNT" -eq 0 && "$DISMISSED_COUNT" -eq 0 && "$INVALID_REASON_COUNT" -eq 0 ]]; then | |
COMMENT_BODY+="β **No unresolved security alerts!** π\n\n" | |
fi | |
echo "COMMENT_BODY" | |
echo $COMMENT_BODY | |
# Update existing comment if found; otherwise, post a new one. | |
if [[ -n "$EXISTING_COMMENT_ID" ]]; then | |
echo "Updating existing comment ID: $EXISTING_COMMENT_ID" | |
curl -s -X PATCH -H "Authorization: token ${GITHUB_TOKEN}" -H "Content-Type: application/json" \ | |
-d "{\"body\": \"$COMMENT_BODY\"}" \ | |
"https://api.github.com/repos/${{ github.repository }}/issues/comments/${EXISTING_COMMENT_ID}" | |
else | |
echo "Posting new comment to PR..." | |
curl -s -X POST -H "Authorization: token ${GITHUB_TOKEN}" -H "Content-Type: application/json" \ | |
-d "{\"body\": \"$COMMENT_BODY\"}" \ | |
"https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" | |
fi | |
- name: Check if Action Should Fail And Revert To Draft | |
env: | |
GITHUB_TOKEN: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} | |
PR_NUMBER: ${{ env.PR_NUMBER }} | |
run: | | |
echo "π Checking if the workflow should fail and revert PR to draft based on security alerts..." | |
# Check if there are any unresolved alerts, dismissed alerts missing comments, or invalid dismissal reasons. | |
if [[ "$UNRESOLVED_COUNT" -gt 0 || "$DISMISSED_COUNT" -gt 0 || "$INVALID_REASON_COUNT" -gt 0 ]]; then | |
echo "β ERROR: Found issues in the PR:" | |
if [[ "$UNRESOLVED_COUNT" -gt 0 ]]; then | |
echo "- $UNRESOLVED_COUNT unresolved security alert(s) found!" | |
fi | |
if [[ "$DISMISSED_COUNT" -gt 0 ]]; then | |
echo "- $DISMISSED_COUNT security alert(s) were dismissed without comments!" | |
fi | |
if [[ "$INVALID_REASON_COUNT" -gt 0 ]]; then | |
echo "- $INVALID_REASON_COUNT alert(s) have an invalid dismissal reason (\"Used in tests\")." | |
fi | |
echo "β οΈ These alerts must be resolved before merging." | |
# Retrieve PR Node ID directly from github event | |
PULL_REQUEST_NODE_ID="${{ github.event.pull_request.node_id }}" | |
echo "PR Node ID: $PULL_REQUEST_NODE_ID" | |
# Revert the PR to draft. | |
echo "Reverting PR #${PR_NUMBER} to draft state due to blocking security issues..." | |
curl -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
-H "Content-Type: application/json" \ | |
-X POST \ | |
-d '{"query": "mutation { convertPullRequestToDraft(input: {pullRequestId: \"'"$PULL_REQUEST_NODE_ID"'\"}) { pullRequest { id isDraft } } }"}' \ | |
https://api.github.com/graphql | |
exit 1 | |
fi | |
echo "β No blocking security issues found. The workflow will pass successfully." |