Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Olympix static analysis with alerts review #1001

Merged
merged 7 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/olympixStaticAnalysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Olympix Static Analysis

# - runs the olympix static analyzer on newly added or modified solidity contracts inside the src/ folder in a pull request
# - detects potential security vulnerabilities and uploads the results to github code scanning
# - only scans diff (added, renamed, modified) solidity files in src/ instead of the whole repository
# - ensures security issues are identified before merging, allowing the team to review and discuss findings within the PR

on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- 'src/**/*.sol'

jobs:
static-analysis:
name: Static Analysis Security Check
runs-on: ubuntu-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Get added, renamed, modified Solidity Files
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: |
src/**/*.sol

- name: Convert Changed Files to Args
if: steps.changed-files.outputs.any_changed == 'true'
id: format-args
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
run: |
args=$(echo $ALL_CHANGED_FILES | xargs -n 1 -I {} printf -- "-p %s " "{}")
echo "ARGS=$args" >> $GITHUB_ENV

- name: Run Olympix Integrated Security
if: steps.changed-files.outputs.any_changed == 'true'
uses: olympix/integrated-security@main
env:
OLYMPIX_API_TOKEN: ${{ secrets.OLYMPIX_API_TOKEN }}
with:
args: --output-format sarif --output-path ./ ${{ env.ARGS }}

- name: Upload Result to GitHub Code Scanning
if: steps.changed-files.outputs.any_changed == 'true'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: olympix.sarif
269 changes: 269 additions & 0 deletions .github/workflows/securityAlertsReview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
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
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.GITHUB_TOKEN }}
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")

# 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)

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.GITHUB_TOKEN }}
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.GITHUB_TOKEN }}
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.GITHUB_TOKEN }}
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.GITHUB_TOKEN }}
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/')

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\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

# 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.GITHUB_TOKEN }}
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."
Loading