diff --git a/.github/workflows/sonarcloud-gate.yml b/.github/workflows/sonarcloud-gate.yml new file mode 100644 index 00000000..2e3a2a9b --- /dev/null +++ b/.github/workflows/sonarcloud-gate.yml @@ -0,0 +1,110 @@ +name: SonarCloud Security Gate + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - development + - main + - master + +jobs: + security-gate: + name: SonarCloud Security Gate + runs-on: ubuntu-latest + # Skip fork PRs (no access to SONAR_TOKEN) + if: github.event.pull_request.head.repo.full_name == github.repository + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@v6 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + - name: Check for security BLOCKER issues + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + PROJECT_KEY="NASA-AMMOS_MMGIS" + MAX_ATTEMPTS=20 + SLEEP_SECONDS=30 + + echo "Checking SonarCloud for security BLOCKER issues on PR #${PR_NUMBER}" + + # Poll until SonarCloud has processed the analysis for this PR + for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo "Attempt ${attempt}/${MAX_ATTEMPTS}: Querying SonarCloud..." + + ANALYSIS_RESPONSE=$(curl -s -u "${SONAR_TOKEN}:" \ + "https://sonarcloud.io/api/qualitygates/project_status?projectKey=${PROJECT_KEY}&pullRequest=${PR_NUMBER}") + + ANALYSIS_STATUS=$(echo "$ANALYSIS_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('projectStatus', {}).get('status', 'NONE'))" 2>/dev/null || echo "NONE") + + if [ "$ANALYSIS_STATUS" = "OK" ] || [ "$ANALYSIS_STATUS" = "ERROR" ]; then + echo "SonarCloud analysis complete (gate status: ${ANALYSIS_STATUS})." + break + fi + + if [ "$attempt" -eq "$MAX_ATTEMPTS" ]; then + echo "Timed out after $((MAX_ATTEMPTS * SLEEP_SECONDS))s waiting for SonarCloud analysis." + exit 1 + fi + + echo " Analysis not ready yet. Waiting ${SLEEP_SECONDS}s..." + sleep $SLEEP_SECONDS + done + + # Query for BLOCKER security issues on this PR's new code + ISSUES_RESPONSE=$(curl -s -u "${SONAR_TOKEN}:" \ + "https://sonarcloud.io/api/issues/search?projectKeys=${PROJECT_KEY}&pullRequest=${PR_NUMBER}&severities=BLOCKER&types=VULNERABILITY&tags=security&statuses=OPEN,CONFIRMED,REOPENED&ps=1") + + BLOCKER_COUNT=$(echo "$ISSUES_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('total', -1))" 2>/dev/null || echo "-1") + + echo "" + echo "Security BLOCKER issues found: ${BLOCKER_COUNT}" + + if [ "$BLOCKER_COUNT" = "-1" ]; then + echo "ERROR: Failed to parse SonarCloud response." + echo "$ISSUES_RESPONSE" + exit 1 + elif [ "$BLOCKER_COUNT" = "0" ]; then + echo "No security BLOCKER issues found. Check passed." + exit 0 + else + echo "Security BLOCKER issues detected! Fetching details..." + echo "" + + # Fetch up to 10 blocker issues with details + DETAILS=$(curl -s -u "${SONAR_TOKEN}:" \ + "https://sonarcloud.io/api/issues/search?projectKeys=${PROJECT_KEY}&pullRequest=${PR_NUMBER}&severities=BLOCKER&types=VULNERABILITY&tags=security&statuses=OPEN,CONFIRMED,REOPENED&ps=10") + + echo "$DETAILS" | python3 -c " + import sys, json + data = json.load(sys.stdin) + for issue in data.get('issues', []): + component = issue.get('component', '').replace('${PROJECT_KEY}:', '') + line = issue.get('line', '?') + msg = issue.get('message', 'No message') + rule = issue.get('rule', '?') + tags = ', '.join(issue.get('tags', [])) + print(f' [{rule}] {component}:{line} — {msg} (tags: {tags})') + " + echo "" + echo "Fix these security BLOCKER issues before merging." + exit 1 + fi + + - name: Summary + if: always() + run: | + echo "### SonarCloud Security Gate" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "PR #${{ github.event.pull_request.number }} was checked for security BLOCKER issues." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "[View in SonarCloud](https://sonarcloud.io/summary/new_code?id=NASA-AMMOS_MMGIS&pullRequest=${{ github.event.pull_request.number }})" >> $GITHUB_STEP_SUMMARY diff --git a/configure/package.json b/configure/package.json index d6fb3bdc..9e2c3941 100644 --- a/configure/package.json +++ b/configure/package.json @@ -1,6 +1,6 @@ { "name": "configure", - "version": "4.2.15-20260302", + "version": "4.2.16-20260304", "homepage": "./configure/build", "private": true, "dependencies": { diff --git a/package.json b/package.json index 32db245e..191e325f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "4.2.15-20260302", + "version": "4.2.16-20260304", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "homepage": "build", "repository": { diff --git a/sonar-project.properties b/sonar-project.properties index 13b9841c..2316a777 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -16,7 +16,7 @@ sonar.links.ci=https://github.com/NASA-AMMOS/MMGIS/actions # Source directories to analyze # Note: 'public' directory excluded from sources to reduce LOC - contains mostly static assets (SVGs, images, config files) # Note: 'auxiliary' directory excluded from sources - contains standalone utility scripts, not server code -sonar.sources=src,API,scripts,views,configure +sonar.sources=src,API,scripts,views,configure,spice # Note: sonar.tests is not set because test files are intermixed with source files. # Test files are excluded via sonar.test.exclusions patterns below. @@ -50,7 +50,9 @@ sonar.exclusions=\ **/public/**,\ **/auxiliary/**,\ **/images/**,\ - **/fonts/** + **/fonts/**,\ + **/examples/**,\ + **/.github/** # Test exclusions sonar.test.exclusions=\