| 1 | #!/usr/bin/env bash |
| 2 | # Search SonarCloud findings for the configured project. |
| 3 | # |
| 4 | # Usage: |
| 5 | # sonar-search.sh issues [--rule RULE_ID] [--resolved=false|true] |
| 6 | # sonar-search.sh hotspots [--status=TO_REVIEW|REVIEWED] |
| 7 | # sonar-search.sh summary # rule + count for open issues + hotspots |
| 8 | # |
| 9 | # Output: a single merged JSON object on stdout (.issues / .hotspots is the |
| 10 | # concatenation of all pages). Always paginated to .paging.total -- a `--ps` |
| 11 | # arg is no longer accepted because it was a footgun (only the first page |
| 12 | # was ever returned). |
| 13 | # |
| 14 | # This is a READ-ONLY script -- it does not mutate Sonar state. Safe to |
| 15 | # run anytime to inspect what's outstanding. |
| 16 | |
| 17 | set -euo pipefail |
| 18 | |
| 19 | # shellcheck source=./_lib.sh |
| 20 | # shellcheck disable=SC1091 |
| 21 | source "$(dirname "$0")/_lib.sh" |
| 22 | sq_load_env |
| 23 | |
| 24 | cmd="${1:-summary}"; shift || true |
| 25 | |
| 26 | # Whitelist common URL-param values to avoid raw user input ending up in |
| 27 | # the URL. Sonar would reject malformed params anyway, but the error |
| 28 | # messages are confusing -- fail-fast locally instead. |
| 29 | _validate_resolved() { |
| 30 | local v="$1" |
| 31 | case "${v}" in true|false) ;; *) |
| 32 | echo -e "${SQ_RED}[ERROR]${SQ_NC} --resolved must be 'true' or 'false', got: '${v}'" >&2 |
| 33 | return 1 ;; |
| 34 | esac |
| 35 | } |
| 36 | _validate_status() { |
| 37 | local v="$1" |
| 38 | case "${v}" in TO_REVIEW|REVIEWED) ;; *) |
| 39 | echo -e "${SQ_RED}[ERROR]${SQ_NC} --status must be 'TO_REVIEW' or 'REVIEWED', got: '${v}'" >&2 |
| 40 | return 1 ;; |
| 41 | esac |
| 42 | } |
| 43 | |
| 44 | case "${cmd}" in |
| 45 | issues) |
| 46 | rule="" |
| 47 | resolved="false" |
| 48 | while [[ $# -gt 0 ]]; do |
| 49 | arg="$1" |
| 50 | case "${arg}" in |
| 51 | --rule) |
| 52 | if [[ $# -lt 2 ]]; then |
| 53 | echo -e "${SQ_RED}[ERROR]${SQ_NC} --rule requires a value (e.g. --rule c:S2245)" >&2 |
| 54 | exit 2 |
| 55 | fi |
| 56 | rule="$2" |
| 57 | shift 2 |
| 58 | ;; |
| 59 | --resolved=*) resolved="${arg#*=}"; shift ;; |
| 60 | *) echo "Unknown arg: ${arg}" >&2; exit 2 ;; |
| 61 | esac |
| 62 | done |
| 63 | _validate_resolved "${resolved}" |
| 64 | path="/api/issues/search?componentKeys=${SONAR_PROJECT}&resolved=${resolved}" |
| 65 | # URL-encode the rule id so values containing `:` (always), |
| 66 | # spaces, or other reserved chars don't inject extra params. |
| 67 | [[ -n "${rule}" ]] && path="${path}&rules=$(sq_url_encode "${rule}")" |
| 68 | sq_paginate "${path}" \ |
| 69 | | jq -s '{paging: .[0].paging, issues: [.[].issues[]]}' |
| 70 | ;; |
| 71 | |
| 72 | hotspots) |
| 73 | status="TO_REVIEW" |
| 74 | while [[ $# -gt 0 ]]; do |
| 75 | arg="$1" |
| 76 | case "${arg}" in |
| 77 | --status=*) status="${arg#*=}"; shift ;; |
| 78 | *) echo "Unknown arg: ${arg}" >&2; exit 2 ;; |
| 79 | esac |
| 80 | done |
| 81 | _validate_status "${status}" |
| 82 | path="/api/hotspots/search?projectKey=${SONAR_PROJECT}&status=${status}" |
| 83 | sq_paginate "${path}" \ |
| 84 | | jq -s '{paging: .[0].paging, hotspots: [.[].hotspots[]]}' |
| 85 | ;; |
| 86 | |
| 87 | summary) |
| 88 | echo "=== Open issues by rule ===" >&2 |
| 89 | # Issue facets are computed server-side and returned on every page; |
| 90 | # the first page's facet totals reflect ALL matching issues, so a |
| 91 | # single fetch is correct here. |
| 92 | sq_run_read curl --fail --silent --show-error -u "${SONAR_TOKEN}:" \ |
| 93 | "${SONAR_HOST_URL}/api/issues/search?componentKeys=${SONAR_PROJECT}&resolved=false&ps=1&facets=rules" \ |
| 94 | | jq -r '.facets[] | select(.property=="rules") | .values[] | " \(.val) (\(.count))"' \ |
| 95 | | sort -k2 -t'(' -nr | head -30 |
| 96 | |
| 97 | echo >&2 |
| 98 | echo "=== Open hotspots by rule ===" >&2 |
| 99 | # Hotspot search has no facets, so we have to walk every page. |
| 100 | sq_paginate "/api/hotspots/search?projectKey=${SONAR_PROJECT}&status=TO_REVIEW" \ |
| 101 | | jq -s '[.[].hotspots[].ruleKey] |
| 102 | | group_by(.) | map({rule: .[0], count: length}) |
| 103 | | sort_by(-.count) | .[] | " \(.rule) (\(.count))"' -r \ |
| 104 | | head -30 |
| 105 | ;; |
| 106 | |
| 107 | "") |
| 108 | echo "usage: $0 issues|hotspots|summary [args...]" >&2 |
| 109 | exit 2 |
| 110 | ;; |
| 111 | *) |
| 112 | echo "Unknown command: ${cmd}" >&2 |
| 113 | exit 2 |
| 114 | ;; |
| 115 | esac |