| 1 | #!/usr/bin/env bash |
| 2 | # Fetch all pages of a Coverity Scan view table. |
| 3 | # |
| 4 | # Usage: |
| 5 | # fetch-table.sh <viewId> <pages> <output-prefix> |
| 6 | # Example: |
| 7 | # fetch-table.sh 41549 7 .local/audits/coverity/raw/outstanding |
| 8 | # |
| 9 | # Coverity's UI uses two steps per page: |
| 10 | # 1) POST /views/table.json {projectId, viewId, pageNum} -- updates server view state |
| 11 | # 2) GET /reports/table.json?projectId=X&viewId=Y -- fetches rows for current state |
| 12 | # |
| 13 | # The script combines all pages into <prefix>-all.json (a flat array). |
| 14 | # Pages already on disk are skipped (idempotent). |
| 15 | |
| 16 | set -euo pipefail |
| 17 | |
| 18 | # shellcheck source=./_lib.sh |
| 19 | # shellcheck disable=SC1091 |
| 20 | source "$(dirname "$0")/_lib.sh" |
| 21 | cov_load_env |
| 22 | |
| 23 | VIEW_ID="${1:?usage: $0 <viewId> <pages> <output-prefix>}" |
| 24 | PAGES="${2:?usage: $0 <viewId> <pages> <output-prefix>}" |
| 25 | PREFIX="${3:?usage: $0 <viewId> <pages> <output-prefix>}" |
| 26 | |
| 27 | # View IDs and page counts are positive integers. Reject anything else |
| 28 | # before the loop -- otherwise non-numeric PAGES would make `seq` produce |
| 29 | # no output, and the final jq merge would block waiting on stdin. |
| 30 | if [[ ! "${VIEW_ID}" =~ ^[1-9][0-9]*$ ]]; then |
| 31 | echo -e "${COV_RED}[ERROR]${COV_NC} viewId must be a positive integer, got: '${VIEW_ID}'" >&2 |
| 32 | exit 1 |
| 33 | fi |
| 34 | if [[ ! "${PAGES}" =~ ^[1-9][0-9]*$ ]]; then |
| 35 | echo -e "${COV_RED}[ERROR]${COV_NC} pages must be a positive integer, got: '${PAGES}'" >&2 |
| 36 | exit 1 |
| 37 | fi |
| 38 | |
| 39 | mkdir -p "$(dirname "${PREFIX}")" |
| 40 | |
| 41 | for page in $(seq 1 "${PAGES}"); do |
| 42 | out="${PREFIX}-page${page}.json" |
| 43 | if [[ -s "${out}" ]]; then |
| 44 | echo -e "${COV_GRAY}[page ${page}/${PAGES}] cached ${out}${COV_NC}" >&2 |
| 45 | continue |
| 46 | fi |
| 47 | |
| 48 | # Step 1: set server-side page state. |
| 49 | post_body="{\"projectId\":${COVERITY_PROJECT_ID},\"viewId\":${VIEW_ID},\"pageNum\":${page}}" |
| 50 | post_http=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ |
| 51 | -H "accept: application/json, text/plain, */*" \ |
| 52 | -H "content-type: application/json" \ |
| 53 | -H "origin: ${COVERITY_HOST}" \ |
| 54 | -H "referer: ${COVERITY_HOST}/" \ |
| 55 | -H "user-agent: ${COVERITY_USER_AGENT}" \ |
| 56 | -H "x-xsrf-token: ${COVERITY_XSRF}" \ |
| 57 | -b "${COVERITY_COOKIE}" \ |
| 58 | --data-raw "${post_body}" \ |
| 59 | "${COVERITY_HOST}/views/table.json") |
| 60 | if [[ "${post_http}" != "200" ]]; then |
| 61 | echo -e "${COV_RED}[page ${page}] POST failed HTTP=${post_http}${COV_NC}" >&2 |
| 62 | exit 2 |
| 63 | fi |
| 64 | |
| 65 | # Step 2: fetch the now-current page. |
| 66 | # Capture exit status of curl explicitly so a transport failure (timeout, |
| 67 | # connection reset, DNS) doesn't leave a non-empty partial file behind |
| 68 | # that the next run would treat as cached. |
| 69 | get_http=$(curl -sS -o "${out}" -w '%{http_code}' \ |
| 70 | -H "accept: application/json, text/plain, */*" \ |
| 71 | -H "referer: ${COVERITY_HOST}/" \ |
| 72 | -H "user-agent: ${COVERITY_USER_AGENT}" \ |
| 73 | -b "${COVERITY_COOKIE}" \ |
| 74 | "${COVERITY_HOST}/reports/table.json?projectId=${COVERITY_PROJECT_ID}&viewId=${VIEW_ID}" \ |
| 75 | || echo "000") |
| 76 | if [[ "${get_http}" != "200" ]]; then |
| 77 | echo -e "${COV_RED}[page ${page}] GET failed HTTP=${get_http}${COV_NC}" >&2 |
| 78 | rm -f "${out}" |
| 79 | exit 3 |
| 80 | fi |
| 81 | # Validate response is JSON with the expected shape -- a Cloudflare |
| 82 | # challenge or a 200-with-HTML-body would otherwise be cached as |
| 83 | # 'valid' and confuse subsequent runs. |
| 84 | if ! jq -e '.resultSet.results | type == "array"' "${out}" >/dev/null 2>&1; then |
| 85 | echo -e "${COV_RED}[page ${page}] response not in expected shape; first 200 chars:${COV_NC}" >&2 |
| 86 | head -c 200 "${out}" >&2; echo >&2 |
| 87 | rm -f "${out}" |
| 88 | exit 3 |
| 89 | fi |
| 90 | |
| 91 | count=$(jq '.resultSet.results | length' "${out}") |
| 92 | total=$(jq '.resultSet.totalCount' "${out}") |
| 93 | echo -e "${COV_GRAY}[page ${page}/${PAGES}] fetched ${count} rows (total=${total})${COV_NC}" >&2 |
| 94 | sleep 0.3 |
| 95 | done |
| 96 | |
| 97 | combined="${PREFIX}-all.json" |
| 98 | # Build the explicit page-file list. A glob (`-page*.json`) would pick up |
| 99 | # stale page files from a previous larger fetch (e.g. running with PAGES=5 |
| 100 | # after a prior run with PAGES=7 would merge 7 pages into "all"). Enumerate |
| 101 | # only the pages we asked for in this invocation. |
| 102 | page_files=() |
| 103 | for page in $(seq 1 "${PAGES}"); do |
| 104 | page_files+=("${PREFIX}-page${page}.json") |
| 105 | done |
| 106 | jq -s '[.[].resultSet.results[]]' "${page_files[@]}" > "${combined}" |
| 107 | echo -e "${COV_GREEN}Combined $(jq 'length' "${combined}") defects into ${combined}${COV_NC}" >&2 |