Ubuntu 24.04 Base Image OS Refresh (CVE Monitor) #4
This file contains hidden or 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: Ubuntu 24.04 Base Image OS Refresh (CVE Monitor) | |
| # This workflow monitors Ubuntu 24.04 security notices and automatically triggers | |
| # staging and production rebuilds when new security fixes are detected. | |
| # | |
| # Flow: | |
| # 1. Check Ubuntu Security JSON API for Ubuntu 24.04 (Noble) security notices | |
| # 2. If new notices found in lookback period (default: 12 hours), trigger staging build for each version (with SkipGit=true) | |
| # 3. Wait for staging to complete successfully (up to 2 hours) | |
| # 4. If staging succeeds, trigger production release | |
| # | |
| # Requirements: | |
| # - GITHUB_TOKEN must have workflow dispatch permissions | |
| # - Versions are automatically extracted from git tags (latest N major.minor releases) | |
| # - Uses JSON API: https://ubuntu.com/security/notices.json | |
| # | |
| # Configuration (for manual runs): | |
| # - versions_to_refresh: Number of recent major.minor versions (default: 2) | |
| # - hours_lookback: How many hours to look back for notices (default: 12) | |
| on: | |
| schedule: | |
| # Run every 12 hours | |
| - cron: '0 */12 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| versions_to_refresh: | |
| description: 'Number of recent major.minor versions to refresh (e.g., 2 means latest and n-1)' | |
| default: '2' | |
| type: string | |
| required: false | |
| hours_lookback: | |
| description: 'Number of hours to look back for security notices (default: 12)' | |
| default: '12' | |
| type: string | |
| required: false | |
| permissions: | |
| actions: write # Required to trigger other workflows | |
| contents: read # Required to read repository content | |
| jobs: | |
| check-cves: | |
| name: Check for Ubuntu 24.04 CVEs | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has-cves: ${{ steps.filter-cves.outputs.has_cves }} | |
| versions: ${{ steps.extract-versions.outputs.versions }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 # Fetch all history to get all tags | |
| - name: Fetch Ubuntu Security Notices JSON | |
| id: fetch-json | |
| run: | | |
| echo "Fetching Ubuntu Security Notices JSON..." | |
| curl -s https://ubuntu.com/security/notices.json > notices.json | |
| if [ ! -s notices.json ]; then | |
| echo "Error: Failed to fetch security notices JSON" | |
| exit 1 | |
| fi | |
| # Validate JSON | |
| if ! jq empty notices.json 2>/dev/null; then | |
| echo "Error: Invalid JSON response" | |
| exit 1 | |
| fi | |
| TOTAL_NOTICES=$(jq '.notices | length' notices.json) | |
| echo "✓ Fetched $TOTAL_NOTICES security notices successfully" | |
| - name: Filter for Ubuntu 24.04 Security Notices | |
| id: filter-cves | |
| run: | | |
| # ============================================================================ | |
| # JSON API Parsing Logic | |
| # ============================================================================ | |
| # | |
| # The Ubuntu Security JSON API (https://ubuntu.com/security/notices.json) | |
| # provides structured security notices with the following relevant fields: | |
| # | |
| # { | |
| # "id": "USN-XXXX-X", | |
| # "published": "2026-01-09T19:45:59.741393", | |
| # "releases": [ | |
| # { | |
| # "codename": "noble", | |
| # "version": "24.04", | |
| # "support_tag": "LTS" | |
| # } | |
| # ] | |
| # } | |
| # | |
| # Filtering approach: | |
| # 1. Check if notice was published in the last 12 hours | |
| # 2. Check if releases array contains codename "noble" (Ubuntu 24.04) | |
| # 3. No severity filtering (refresh for any security fix) | |
| # ============================================================================ | |
| # Configuration | |
| AFFECTED_RELEASE="noble" # Ubuntu 24.04 codename | |
| # Use input parameter if provided, otherwise default to 12 | |
| HOURS_LOOKBACK="${{ github.event.inputs.hours_lookback || '12' }}" | |
| # Calculate cutoff timestamp in ISO format | |
| CUTOFF_TIMESTAMP=$(date -u -d "$HOURS_LOOKBACK hours ago" +"%Y-%m-%dT%H:%M:%S") | |
| echo "Checking for security notices published after: $CUTOFF_TIMESTAMP" | |
| echo "Filtering for release: $AFFECTED_RELEASE (Ubuntu 24.04 LTS)" | |
| echo "" | |
| # Filter notices using jq: | |
| # 1. Filter by published date (last 12 hours) | |
| # 2. Filter by releases containing noble | |
| RECENT_NOTICES=$(jq -r --arg cutoff "$CUTOFF_TIMESTAMP" --arg release "$AFFECTED_RELEASE" ' | |
| .notices[] | | |
| select(.published >= $cutoff) | | |
| select(.releases[]? | .codename == $release) | | |
| .id | |
| ' notices.json) | |
| if [ -n "$RECENT_NOTICES" ]; then | |
| # Count notices | |
| NOTICE_COUNT=$(echo "$RECENT_NOTICES" | wc -l) | |
| echo "✓ Found $NOTICE_COUNT security notice(s) affecting Ubuntu 24.04 in the last $HOURS_LOOKBACK hours" | |
| echo "has_cves=true" >> $GITHUB_OUTPUT | |
| # Log details with full notice information | |
| echo "" | |
| echo "Security Notices:" | |
| echo "$RECENT_NOTICES" | while read -r usn_id; do | |
| if [ -n "$usn_id" ]; then | |
| TITLE=$(jq -r --arg id "$usn_id" '.notices[] | select(.id == $id) | .title' notices.json) | |
| PUBLISHED=$(jq -r --arg id "$usn_id" '.notices[] | select(.id == $id) | .published' notices.json) | |
| echo " - $usn_id: $TITLE" | |
| echo " Published: $PUBLISHED" | |
| echo " Link: https://ubuntu.com/security/notices/$usn_id" | |
| fi | |
| done | |
| else | |
| echo "✗ No security notices found for Ubuntu 24.04 in the past $HOURS_LOOKBACK hours" | |
| echo "has_cves=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Extract Versions to Refresh | |
| id: extract-versions | |
| run: | | |
| # Check if we need to extract versions | |
| if [ "${{ steps.filter-cves.outputs.has_cves }}" = "false" ]; then | |
| echo "No CVEs detected, skipping version extraction" | |
| echo 'versions=[]' >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # ============================================================================ | |
| # Dynamic Version Extraction from Git Tags | |
| # ============================================================================ | |
| # | |
| # Version format: | |
| # - Regular: x.x.x (e.g., 25.12.1) | |
| # - Hotfix: x.x.x.x (e.g., 25.12.1.1) | |
| # | |
| # Logic: | |
| # 1. Get all tags matching version pattern | |
| # 2. Group by major.minor (e.g., 25.12.x) | |
| # 3. For each group, get the latest version (including hotfixes) | |
| # 4. Select the latest N releases (configurable) | |
| # | |
| # Example with VERSIONS_TO_REFRESH=2: | |
| # Tags: 25.10.1, 25.11.1, 25.11.1.1, 25.11.1.2, 25.12.1 | |
| # Result: ["25.12.1", "25.11.1.2"] (latest of 25.12 and 25.11) | |
| # ============================================================================ | |
| # Configuration: Number of recent major.minor versions to refresh | |
| # Use input parameter if provided, otherwise default to 2 | |
| VERSIONS_TO_REFRESH="${{ github.event.inputs.versions_to_refresh || '2' }}" | |
| echo "Extracting versions to refresh..." | |
| echo "Configuration: Will refresh last $VERSIONS_TO_REFRESH major.minor releases" | |
| echo "" | |
| # Get all tags matching version pattern (x.x.x or x.x.x.x) | |
| ALL_TAGS=$(git tag | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$' | sort -V) | |
| if [ -z "$ALL_TAGS" ]; then | |
| echo "⚠️ No version tags found in repository" | |
| echo 'versions=[]' >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| echo "Found version tags:" | |
| echo "$ALL_TAGS" | tail -10 | |
| echo "" | |
| # Extract unique major.minor versions and get the latest patch/hotfix for each | |
| # Group by major.minor (e.g., 25.12), then pick the highest version in that group | |
| LATEST_PER_MINOR=$(echo "$ALL_TAGS" | awk -F. '{ | |
| key = $1 "." $2 | |
| if (key in versions) { | |
| if ($0 > versions[key]) { | |
| versions[key] = $0 | |
| } | |
| } else { | |
| versions[key] = $0 | |
| } | |
| } | |
| END { | |
| for (key in versions) { | |
| print versions[key] | |
| } | |
| }' | sort -V) | |
| echo "Latest version per major.minor:" | |
| echo "$LATEST_PER_MINOR" | |
| echo "" | |
| # Get the last N versions | |
| VERSIONS_TO_BUILD=$(echo "$LATEST_PER_MINOR" | tail -n $VERSIONS_TO_REFRESH) | |
| echo "Selected versions to refresh (last $VERSIONS_TO_REFRESH):" | |
| echo "$VERSIONS_TO_BUILD" | |
| echo "" | |
| # Convert to JSON array format | |
| VERSIONS_JSON=$(echo "$VERSIONS_TO_BUILD" | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| echo "versions=$VERSIONS_JSON" >> $GITHUB_OUTPUT | |
| echo "✓ Will refresh versions: $VERSIONS_JSON" | |
| - name: Summary | |
| run: | | |
| echo "### Security Notice Check Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Release:** Ubuntu 24.04 (Noble)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Lookback:** ${{ github.event.inputs.hours_lookback || '12' }} hours" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Security Notices Found:** ${{ steps.filter-cves.outputs.has_cves }}" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.filter-cves.outputs.has_cves }}" = "true" ]; then | |
| echo "- **Action:** Triggering refresh workflows" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Versions to Refresh:** ${{ steps.extract-versions.outputs.versions }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Refresh Policy:** Last ${{ github.event.inputs.versions_to_refresh || '2' }} major.minor releases" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- **Action:** No refresh needed" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| refresh-versions: | |
| name: Refresh Version ${{ matrix.version }} | |
| runs-on: ubuntu-latest | |
| needs: check-cves | |
| if: needs.check-cves.outputs.has-cves == 'true' | |
| strategy: | |
| matrix: | |
| version: ${{ fromJson(needs.check-cves.outputs.versions) }} | |
| fail-fast: false # Continue other versions even if one fails | |
| steps: | |
| - name: Trigger Staging Build for version ${{ matrix.version }} | |
| id: trigger-staging | |
| run: | | |
| echo "Triggering Staging Build (base image refresh) for version ${{ matrix.version }}..." | |
| WORKFLOW_FILE="stg-from-version-build-push-tag-base-image.yaml" | |
| curl -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| https://api.github.com/repos/${{ github.repository }}/actions/workflows/${WORKFLOW_FILE}/dispatches \ | |
| -d "{\"ref\":\"main\",\"inputs\":{\"Tag\":\"${{ matrix.version }}\",\"IsLatest\":\"true\",\"SkipGit\":\"true\",\"CopyVersionScript\":\"true\"}}" | |
| if [ $? -eq 0 ]; then | |
| echo "✓ Successfully triggered Staging Build" | |
| echo "staging_triggered=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "✗ Failed to trigger Staging Build" | |
| echo "staging_triggered=false" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Wait for Staging Build to start | |
| run: | | |
| echo "Waiting for Staging Build to start..." | |
| # Give GitHub Actions time to queue and start the workflow | |
| sleep 30 | |
| - name: Check Staging Build status | |
| id: check-staging | |
| run: | | |
| echo "Monitoring Staging Build completion status..." | |
| WORKFLOW_FILE="stg-from-version-build-push-tag-base-image.yaml" | |
| MAX_WAIT_MINUTES=120 # Wait up to 2 hours for staging build | |
| CHECK_INTERVAL=60 # Check every minute | |
| echo "Will check status every ${CHECK_INTERVAL}s for up to ${MAX_WAIT_MINUTES} minutes" | |
| for i in $(seq 1 $MAX_WAIT_MINUTES); do | |
| echo "Check attempt $i/$MAX_WAIT_MINUTES..." | |
| # Get the latest workflow run on main branch | |
| RESPONSE=$(curl -s \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${{ github.repository }}/actions/workflows/${WORKFLOW_FILE}/runs?per_page=1&branch=main") | |
| STATUS=$(echo "$RESPONSE" | jq -r '.workflow_runs[0].status') | |
| CONCLUSION=$(echo "$RESPONSE" | jq -r '.workflow_runs[0].conclusion') | |
| RUN_ID=$(echo "$RESPONSE" | jq -r '.workflow_runs[0].id') | |
| echo " Run ID: $RUN_ID" | |
| echo " Status: $STATUS" | |
| echo " Conclusion: $CONCLUSION" | |
| if [ "$STATUS" = "completed" ]; then | |
| if [ "$CONCLUSION" = "success" ]; then | |
| echo "✓ Staging Build completed successfully" | |
| echo "staging_success=true" >> $GITHUB_OUTPUT | |
| echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT | |
| exit 0 | |
| else | |
| echo "✗ Staging Build failed with conclusion: $CONCLUSION" | |
| echo "staging_success=false" >> $GITHUB_OUTPUT | |
| echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| fi | |
| echo " Staging Build still running, waiting ${CHECK_INTERVAL}s..." | |
| sleep $CHECK_INTERVAL | |
| done | |
| echo "⚠️ Staging Build did not complete within ${MAX_WAIT_MINUTES} minutes" | |
| echo "staging_success=false" >> $GITHUB_OUTPUT | |
| - name: Trigger Production Release for version ${{ matrix.version }} | |
| if: steps.check-staging.outputs.staging_success == 'true' | |
| id: trigger-production | |
| run: | | |
| echo "Staging Build succeeded. Triggering Production Release for version ${{ matrix.version }}..." | |
| WORKFLOW_FILE="prod-from-version-build-push-tag-base-image.yaml" | |
| curl -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| https://api.github.com/repos/${{ github.repository }}/actions/workflows/${WORKFLOW_FILE}/dispatches \ | |
| -d "{\"ref\":\"main\",\"inputs\":{\"Version\":\"${{ matrix.version }}\",\"IsLatest\":\"true\"}}" | |
| if [ $? -eq 0 ]; then | |
| echo "✓ Successfully triggered Production Release" | |
| echo "production_triggered=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "✗ Failed to trigger Production Release" | |
| echo "production_triggered=false" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Skip Production Release (Staging Build failed) | |
| if: steps.check-staging.outputs.staging_success != 'true' | |
| run: | | |
| echo "⚠️ Skipping Production Release because Staging Build did not complete successfully" | |
| echo "Version ${{ matrix.version }} will not be fully refreshed" | |
| - name: Version Summary | |
| if: always() | |
| run: | | |
| echo "### Version ${{ matrix.version }} Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Staging Build Triggered:** ${{ steps.trigger-staging.outputs.staging_triggered }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Staging Build Success:** ${{ steps.check-staging.outputs.staging_success }}" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.check-staging.outputs.staging_success }}" = "true" ]; then | |
| echo "- **Production Release Triggered:** ${{ steps.trigger-production.outputs.production_triggered }}" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.check-staging.outputs.run_id }}" != "" ]; then | |
| echo "- **Staging Run ID:** ${{ steps.check-staging.outputs.run_id }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "- **Status:** ✅ Refresh Complete" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- **Production Release Triggered:** ✗ No (Staging Build failed)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Status:** ❌ Refresh Incomplete" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| summary: | |
| name: Workflow Summary | |
| runs-on: ubuntu-latest | |
| needs: [check-cves, refresh-versions] | |
| if: always() | |
| steps: | |
| - name: Checkout code for scripts | |
| if: needs.check-cves.outputs.has-cves == 'true' | |
| uses: actions/checkout@v3 | |
| - name: Generate Summary | |
| run: | | |
| echo "# Ubuntu 24.04 CVE Monitor - Execution Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Timestamp:** $(date -u)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ needs.check-cves.outputs.has-cves }}" = "true" ]; then | |
| echo "## ✅ Security Notices Detected" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "New security notices affecting Ubuntu 24.04 were found." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Versions Processed:** ${{ needs.check-cves.outputs.versions }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Refresh Status:** Check individual version summaries above" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "## ✓ No Security Notices Detected" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "No security notices found for Ubuntu 24.04 in the past ${{ github.event.inputs.hours_lookback || '12' }} hours." >> $GITHUB_STEP_SUMMARY | |
| echo "No refresh actions were needed." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "*Next check in ${{ github.event.inputs.hours_lookback || '12' }} hours*" >> $GITHUB_STEP_SUMMARY | |
| - name: Notify Slack - CVE Refresh Complete | |
| if: needs.check-cves.outputs.has-cves == 'true' | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.PRD_SLACK_WEBHOOK_URL }} | |
| run: | | |
| echo "📤 Sending Slack notification for CVE refresh..." | |
| # Parse versions from JSON array | |
| VERSIONS='${{ needs.check-cves.outputs.versions }}' | |
| VERSION_LIST=$(echo "$VERSIONS" | jq -r '.[]' | sed 's/^/• `/' | sed 's/$/`/' | tr '\n' ' ' | sed 's/ $/\n/') | |
| # Determine overall status | |
| if [ "${{ needs.refresh-versions.result }}" = "success" ]; then | |
| STATUS_EMOJI="🔄" | |
| STATUS_MESSAGE="Security Refresh Complete" | |
| COLOR="good" | |
| DETAIL_MESSAGE="All base images have been successfully rebuilt and deployed to address Ubuntu 24.04 security notices." | |
| else | |
| STATUS_EMOJI="⚠️" | |
| STATUS_MESSAGE="Security Refresh Completed with Issues" | |
| COLOR="warning" | |
| DETAIL_MESSAGE="Security refresh completed but some versions may have encountered issues. Check the workflow logs for details." | |
| fi | |
| # Store GitHub Actions variables as bash variables | |
| WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| HOURS_CONFIG="${{ github.event.inputs.hours_lookback || '12' }}" | |
| # Create Slack message line by line | |
| SLACK_MESSAGE=$(printf "%s\n\n%s\n%s\n\n%s\n%s\n\n%s\n\n%s" \ | |
| "$STATUS_EMOJI *Ubuntu 24.04 $STATUS_MESSAGE*" \ | |
| "📋 *Versions Refreshed:*" \ | |
| "$VERSION_LIST" \ | |
| "🔐 *Reason:* New security notices detected for Ubuntu 24.04 (Noble)" \ | |
| "$STATUS_EMOJI $DETAIL_MESSAGE" \ | |
| "🔗 Workflow: <${WORKFLOW_URL}|View Run>" \ | |
| "⏰ Next automatic check in ${HOURS_CONFIG} hours") | |
| echo "Message preview:" | |
| echo "$SLACK_MESSAGE" | |
| # Send to Slack using webhook | |
| if [ -n "$SLACK_WEBHOOK_URL" ]; then | |
| # Build JSON payload | |
| payload=$(jq -n \ | |
| --arg text "$SLACK_MESSAGE" \ | |
| --arg color "$COLOR" \ | |
| --arg status "$STATUS_MESSAGE" \ | |
| '{"text": $text, "attachments": [{"color": $color, "fields": [{"title": "Status", "value": $status, "short": false}]}]}') | |
| # Send to Slack (silent mode to avoid logging webhook URL) | |
| HTTP_STATUS=$(curl -s -w "%{http_code}" -o /dev/null -X POST \ | |
| -H 'Content-type: application/json' \ | |
| --data "$payload" \ | |
| "$SLACK_WEBHOOK_URL") | |
| if [ "$HTTP_STATUS" -eq 200 ]; then | |
| echo "✅ Slack notification sent successfully" | |
| else | |
| echo "❌ Failed to send Slack notification (HTTP $HTTP_STATUS)" | |
| fi | |
| else | |
| echo "⚠️ SLACK_WEBHOOK_URL not set, skipping notification" | |
| fi |