| 1 | #!/usr/bin/env bash |
| 2 | # Common helpers for coverity-audit scripts. |
| 3 | # Sourced from the per-action scripts; not executed directly. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | |
| 7 | # ANSI colors for transparent output (per the project's run() pattern). |
| 8 | # |
| 9 | # IMPORTANT: define with $'...' so the variables contain real ESC bytes, |
| 10 | # not the literal four-character string "\033". This way both `echo -e |
| 11 | # "${COV_RED}..."` and `printf '%s' "${COV_RED}..."` render correctly -- |
| 12 | # without forcing every printf format string to be the variable itself |
| 13 | # (which trips shellcheck SC2059) or %b (which adds inconsistency). |
| 14 | # |
| 15 | # Color vars are referenced by sourcing scripts; shellcheck cannot see that. |
| 16 | # shellcheck disable=SC2034 |
| 17 | COV_RED=$'\033[0;31m' |
| 18 | # shellcheck disable=SC2034 |
| 19 | COV_GREEN=$'\033[0;32m' |
| 20 | # shellcheck disable=SC2034 |
| 21 | COV_YELLOW=$'\033[1;33m' |
| 22 | # shellcheck disable=SC2034 |
| 23 | COV_GRAY=$'\033[0;90m' |
| 24 | # shellcheck disable=SC2034 |
| 25 | COV_NC=$'\033[0m' |
| 26 | |
| 27 | # Locate the repo root by walking up from the script directory. |
| 28 | # This way the scripts work no matter where the user runs them from. |
| 29 | cov_repo_root() { |
| 30 | git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel |
| 31 | } |
| 32 | |
| 33 | # Source `<repo-root>/.env` if it exists. .env is the user's local-only |
| 34 | # secrets file (gitignored). It must export at least: |
| 35 | # COVERITY_COOKIE — the full Cookie header value pasted from a curl |
| 36 | # copy-as-cURL captured in DevTools |
| 37 | # COVERITY_PROJECT_ID — Coverity Scan numeric projectId (constant per project) |
| 38 | # COVERITY_HOST — defaults to https://scan4.scan.coverity.com |
| 39 | # Optional: |
| 40 | # COVERITY_VIEW_OUTSTANDING — viewId of the Outstanding view |
| 41 | # COVERITY_USER_AGENT — overridable UA string |
| 42 | cov_load_env() { |
| 43 | local root env |
| 44 | root="$(cov_repo_root)" |
| 45 | env="${root}/.env" |
| 46 | if [[ ! -f "${env}" || ! -r "${env}" ]]; then |
| 47 | echo -e "${COV_RED}[ERROR]${COV_NC} Missing ${env}. See SKILL.md for the .env template." >&2 |
| 48 | return 1 |
| 49 | fi |
| 50 | set -a |
| 51 | # shellcheck disable=SC1090 |
| 52 | source "${env}" |
| 53 | set +a |
| 54 | |
| 55 | : "${COVERITY_COOKIE:?COVERITY_COOKIE is empty in .env — paste a fresh cookie from the browser}" |
| 56 | : "${COVERITY_PROJECT_ID:?COVERITY_PROJECT_ID is empty in .env}" |
| 57 | if [[ ! "${COVERITY_PROJECT_ID}" =~ ^[1-9][0-9]*$ ]]; then |
| 58 | echo -e "${COV_RED}[ERROR]${COV_NC} COVERITY_PROJECT_ID must be a positive integer, got: '${COVERITY_PROJECT_ID}'" >&2 |
| 59 | return 1 |
| 60 | fi |
| 61 | : "${COVERITY_HOST:=https://scan4.scan.coverity.com}" |
| 62 | : "${COVERITY_USER_AGENT:=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36}" |
| 63 | export COVERITY_COOKIE COVERITY_PROJECT_ID COVERITY_HOST COVERITY_USER_AGENT |
| 64 | |
| 65 | # Extract XSRF-TOKEN from the cookie string. |
| 66 | COVERITY_XSRF="$(printf '%s' "${COVERITY_COOKIE}" | sed -n 's/.*XSRF-TOKEN=\([^;]*\).*/\1/p')" |
| 67 | if [[ -z "${COVERITY_XSRF}" ]]; then |
| 68 | echo -e "${COV_RED}[ERROR]${COV_NC} XSRF-TOKEN not found inside COVERITY_COOKIE. Did you paste the full Cookie header?" >&2 |
| 69 | return 1 |
| 70 | fi |
| 71 | export COVERITY_XSRF |
| 72 | } |
| 73 | |
| 74 | # Audit artifacts go under .local/audits/coverity/ at the repo root. |
| 75 | # .local/ is gitignored -- see AGENTS.md for the convention. |
| 76 | # Creates the directory on first call so callers can redirect output into |
| 77 | # subpaths without thinking about it. |
| 78 | cov_audit_dir() { |
| 79 | local root dir |
| 80 | root="$(cov_repo_root)" |
| 81 | dir="${root}/.local/audits/coverity" |
| 82 | mkdir -p "${dir}" |
| 83 | echo "${dir}" |
| 84 | } |
| 85 | |
| 86 | # Reject non-ASCII bytes in a string. Coverity's edge (Cloudflare) rejects |
| 87 | # em-dashes and smart quotes with a 403 challenge; far better to fail before |
| 88 | # the network round-trip than to debug a Cloudflare block. |
| 89 | # |
| 90 | # `tr -d '\000-\177'` deletes ALL ASCII bytes; anything left is non-ASCII. |
| 91 | # This is portable across GNU and BSD/macOS (unlike `grep -P`, which is GNU-only). |
| 92 | cov_require_ascii() { |
| 93 | local s="$1" |
| 94 | if LC_ALL=C printf '%s' "${s}" | LC_ALL=C tr -d '\000-\177' | grep -q .; then |
| 95 | echo -e "${COV_RED}[ERROR]${COV_NC} Comment contains non-ASCII characters. Cloudflare blocks them. Replace em-dashes with '--' and curly quotes with straight quotes." >&2 |
| 96 | return 1 |
| 97 | fi |
| 98 | } |
| 99 | |
| 100 | # CID validation: Coverity CIDs are positive integers (>= 1). Reject anything |
| 101 | # else before interpolating into jq filters, URLs, or paths. |
| 102 | cov_require_numeric_cid() { |
| 103 | local cid="$1" |
| 104 | if [[ ! "${cid}" =~ ^[1-9][0-9]*$ ]]; then |
| 105 | echo -e "${COV_RED}[ERROR]${COV_NC} CID must be a positive integer, got: '${cid}'" >&2 |
| 106 | return 1 |
| 107 | fi |
| 108 | } |