Skip to content
Open
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
110 changes: 110 additions & 0 deletions .github/workflows/sonarcloud-gate.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion configure/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "configure",
"version": "4.2.15-20260302",
"version": "4.2.16-20260304",
"homepage": "./configure/build",
"private": true,
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
6 changes: 4 additions & 2 deletions sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -50,7 +50,9 @@ sonar.exclusions=\
**/public/**,\
**/auxiliary/**,\
**/images/**,\
**/fonts/**
**/fonts/**,\
**/examples/**,\
**/.github/**

# Test exclusions
sonar.test.exclusions=\
Expand Down