Compare commits
5 Commits
0109df6963
...
docs/ci-so
| Author | SHA1 | Date | |
|---|---|---|---|
| ec468e9983 | |||
| 2257df09eb | |||
| 17f9207a17 | |||
| 28c5c228f7 | |||
| 6e0ff2a821 |
64
ci/SONARQUBE.md
Normal file
64
ci/SONARQUBE.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# SonarQube static analysis
|
||||
|
||||
Each code repo in the Runic Gateway org reports static-analysis results to the
|
||||
self-hosted **SonarQube** server for review. Analysis is **non-blocking**: it
|
||||
runs on push to `main` (i.e. *after* merge), never on pull requests, so it never
|
||||
gates a PR. It complements each repo's PR gate and release pipeline — it only
|
||||
feeds the dashboard.
|
||||
|
||||
## Server
|
||||
|
||||
- **URL:** `https://sonar.whitlocktech.com`
|
||||
- Each repo is a separate SonarQube project, keyed as below.
|
||||
|
||||
## Projects
|
||||
|
||||
| Repo | Project key | Sources analysed | Language |
|
||||
|---|---|---|---|
|
||||
| `website` | `runic-gateway-website` | `server/src`, `client/src`, `bot/src` | JS/TS |
|
||||
| `link` | `Runic-Gateway-link` | `sidecar/src` | Rust |
|
||||
| `Android-app` | `Runic-Gateway-Android-app` | `app/src/main` | Kotlin |
|
||||
|
||||
> Project keys are **case-sensitive** and must match what already exists on the
|
||||
> server — SonarQube refuses to create a key that differs only in case from an
|
||||
> existing one. `link` and `Android-app` reuse the pre-existing capitalised keys
|
||||
> above; `website` predates this note with its lower-case key.
|
||||
|
||||
## How it's wired
|
||||
|
||||
Each repo carries two files, identical in shape across repos:
|
||||
|
||||
- **`sonar-project.properties`** (repo root) — declares the project key, sources,
|
||||
tests, and exclusions. The Sonar scanner reads this.
|
||||
- **`.gitea/workflows/sonarqube.yml`** — a `SonarQube` workflow that, on push to
|
||||
`main` (and via manual `workflow_dispatch`), checks out with full history
|
||||
(`fetch-depth: 0`, needed for accurate blame + "new code") and runs
|
||||
`sonarsource/sonarqube-scan-action@v4`.
|
||||
|
||||
The scan is **source-based** — it does not build the project or run a language
|
||||
toolchain, so the workflows are lightweight (checkout + scan only). Richer
|
||||
signals (Rust Clippy, Android Lint, JaCoCo coverage) are left as documented,
|
||||
commented-out enrichment in each repo's `sonar-project.properties`; enable them
|
||||
per repo when wanted.
|
||||
|
||||
## One-time setup per repo (Gitea UI → Repo → Settings → Actions)
|
||||
|
||||
Both are consumed by the scan action via `env:` in the workflow:
|
||||
|
||||
- **Secret `SONAR_TOKEN`** — a SonarQube *Analysis* token (My Account →
|
||||
Security in SonarQube; project-scoped or global).
|
||||
- **Variable `SONAR_HOST_URL`** — the SonarQube base URL reachable from the
|
||||
self-hosted runner. Kept as a **variable, not committed**, so the internal
|
||||
address stays out of git.
|
||||
|
||||
The self-hosted `ubuntu-latest` runner must be able to reach `SONAR_HOST_URL` on
|
||||
the network. Nothing waits on the SonarQube Quality Gate, so a failing gate does
|
||||
not fail the job — check the dashboard.
|
||||
|
||||
## Adding a new repo
|
||||
|
||||
1. Create the project in SonarQube; note its key.
|
||||
2. Add `sonar-project.properties` (copy an existing repo's, adjust key + sources).
|
||||
3. Add `.gitea/workflows/sonarqube.yml` (copy verbatim — it's language-agnostic).
|
||||
4. Set the `SONAR_TOKEN` secret and `SONAR_HOST_URL` variable in the repo's
|
||||
Gitea Actions settings.
|
||||
@@ -409,7 +409,19 @@ who"; `activity_log` provides the history feed.
|
||||
- **bcrypt** hashing (cost 10+); plaintext passwords never stored, logged, or returned.
|
||||
- **Rate limiting** (`express-rate-limit`) on `/auth/login` and `/public/contact`.
|
||||
- **Validation** (`express-validator`) on all writes; centralized error handler.
|
||||
- **helmet** with a CSP suited to the SPA (self + inline styles as needed; image sources for uploads/hero).
|
||||
- **helmet** with a Content-Security-Policy tuned for the built React SPA (see `server/src/app.js`):
|
||||
`default-src 'self'`; `script-src 'self'` (the Vite build emits only external module chunks — the
|
||||
inline module-preload polyfill is disabled in `client/vite.config.js` to keep this valid);
|
||||
`style-src 'self' 'unsafe-inline' https://fonts.googleapis.com` (React's pervasive inline
|
||||
`style={{…}}` attributes can't be nonce'd, plus the Google Fonts stylesheet); `font-src 'self'
|
||||
https://fonts.gstatic.com` (Cinzel); `img-src 'self' data: https:` (same-origin uploads, plus
|
||||
external https images embedded in wiki/news bodies or `BRAND_*` logo/hero/favicon); `connect-src
|
||||
'self'` (REST + SSE are same-origin); `frame-ancestors 'self'`; `object-src 'none'`; `base-uri
|
||||
'self'`. `upgrade-insecure-requests` is intentionally **not** set (TLS terminates at the proxy, there
|
||||
are no mixed-content subresources, and it would break a local `npm start` over plain http). The
|
||||
`/api/docs` Swagger UI route gets a **looser** policy that additionally allows inline script/style,
|
||||
since swagger-ui-express injects an inline bootstrap. helmet also strips `X-Powered-By`; the two
|
||||
internal-only listeners (`internalApp.js`, `bot/src/app.js`) disable it explicitly too.
|
||||
- **Admin not indexed**: `X-Robots-Tag: noindex, nofollow` on `/api/v1/admin` and the admin SPA routes; `robots.txt` disallows `/admin`.
|
||||
- **No directory browsing** (express.static doesn't list; no `serve-index`).
|
||||
- **No hardcoded credentials**: first admin via `seed.js` reading `ADMIN_USERNAME`/`ADMIN_PASSWORD` from env (created only if no users exist); `.env` git-ignored, `.env.example` committed.
|
||||
|
||||
Reference in New Issue
Block a user