|
| 1 | +# Security |
| 2 | + |
| 3 | +This document describes the security posture of `github-insights` for both the upstream repository (`shigechika/github-insights`) and forks created via "Use this template". |
| 4 | + |
| 5 | +## Reporting a vulnerability |
| 6 | + |
| 7 | +If you believe you have found a security issue, please report it privately through GitHub's [private vulnerability reporting](https://github.com/shigechika/github-insights/security/advisories/new) instead of opening a public issue. Public reports are also accepted, but private reports are preferred for anything with non-trivial blast radius. |
| 8 | + |
| 9 | +For forks, replace `shigechika/github-insights` with your own repository in the URL above. |
| 10 | + |
| 11 | +## Trust model |
| 12 | + |
| 13 | +### Who can write what |
| 14 | + |
| 15 | +| Actor | Scope | |
| 16 | +|---|---| |
| 17 | +| Repository owner / collaborators | Push to any branch, modify workflows, settings, secrets | |
| 18 | +| `github-actions[bot]` (GITHUB_TOKEN) | Push to `main` (used by the daily cron to update `data/traffic.json` and `README.md`); bypasses the `Protect main` ruleset | |
| 19 | +| `GH_INSIGHTS_PAT` (Fine-grained PAT) | Read-only Administration on the owner's repos. Used to call `gh api` for `users/<owner>/repos` and `repos/<owner>/<repo>/traffic/{views,clones}`. Cannot push or modify any repository. | |
| 20 | + |
| 21 | +### Who can read what |
| 22 | + |
| 23 | +| Asset | Visibility | |
| 24 | +|---|---| |
| 25 | +| Source code, workflows, `data/traffic.json` | Public (this is a public repo) | |
| 26 | +| Live dashboard at `https://<owner>.github.io/<repo>/` | Public | |
| 27 | +| `GH_INSIGHTS_PAT` | Stored as a repository secret; never echoed by scripts; redacted from Actions logs by GitHub | |
| 28 | +| Aggregated traffic counts (views, clones, uniques) | Public via `data/traffic.json` once the workflow commits — fundamentally what this project publishes | |
| 29 | + |
| 30 | +## Public-only invariant |
| 31 | + |
| 32 | +This tool is designed to **never fetch, store, or publish traffic data for private repositories**, even when the PAT happens to have broader access. |
| 33 | + |
| 34 | +The invariant is enforced in `scripts/collect.sh`: |
| 35 | + |
| 36 | +- The list of repos in scope is built once via `gh api users/<owner>/repos?type=public` (`collect.sh:25`). This is the *only* gate; the `IMPORTANT` comment block above that line warns future contributors not to weaken it. |
| 37 | +- The data-collection loop iterates over that list (`collect.sh:54`), so traffic API calls only target public repos. |
| 38 | +- The rename reconciliation loop (`collect.sh:37–50`) skips probing any repo name that is still in the public list (no rename could have happened), and only honors a detected rename when the resolved new name is also in the public list — so historical data is never reorganized toward a private repo's name even if a private name ever sneaks into `traffic.json`. |
| 39 | + |
| 40 | +The only residual touch on potentially-private repos is the rename probe `gh api repos/<owner>/<old_name>` for names that have left the public list (renamed-away, deleted, or made private). The PAT is read-only Administration, so this is defence-in-depth only — see #9 for the full discussion. |
| 41 | + |
| 42 | +## Workflow trust boundaries |
| 43 | + |
| 44 | +`.github/workflows/collect.yml`: |
| 45 | + |
| 46 | +- Declares `permissions: contents: write` — the minimum needed for the bot's commit-and-push at the end. No other token-scoped permissions. |
| 47 | +- Uses two distinct credentials in separate steps: |
| 48 | + - `GH_INSIGHTS_PAT` (env: `GH_TOKEN`) — passed to `bash scripts/collect.sh` and `bash scripts/generate-charts.sh` for `gh api` calls. Read-only Administration. |
| 49 | + - `GITHUB_TOKEN` (implicit via `actions/checkout`) — used by the final `git push`. Scoped to `contents: write` for this run only. |
| 50 | +- All third-party Actions are pinned to a full commit SHA (currently `actions/checkout@de0fac2e... # v6.0.2` in both `collect.yml` and `lint.yml`). Dependabot watches for updates. |
| 51 | +- A `concurrency: collect-traffic` group prevents overlapping cron and manual runs from racing on `data/traffic.json`. |
| 52 | + |
| 53 | +## Frontend supply chain |
| 54 | + |
| 55 | +`docs/index.html` loads two scripts from `cdn.jsdelivr.net`: |
| 56 | + |
| 57 | +- `chart.js@4.4.1` |
| 58 | +- `chartjs-adapter-date-fns@3.0.0` |
| 59 | + |
| 60 | +Both `<script>` tags carry `integrity="sha384-..."` and `crossorigin="anonymous"` attributes. A jsdelivr or NPM compromise that swapped the file at the same URL would be rejected by the browser's Subresource Integrity check. |
| 61 | + |
| 62 | +When upgrading either dependency, regenerate the hash: |
| 63 | + |
| 64 | +```bash |
| 65 | +curl -sL https://cdn.jsdelivr.net/npm/chart.js@<new-version>/dist/chart.umd.min.js \ |
| 66 | + | openssl dgst -sha384 -binary | openssl base64 -A |
| 67 | +``` |
| 68 | + |
| 69 | +and update both `src` and `integrity` together. |
| 70 | + |
| 71 | +## Branch protection |
| 72 | + |
| 73 | +`main` is protected by a Repository Ruleset (`Protect main`, id 15561427) with: |
| 74 | + |
| 75 | +- `deletion` blocked — `main` cannot be deleted |
| 76 | +- `non_fast_forward` blocked — history rewrite (force push) blocked |
| 77 | + |
| 78 | +Pull-request requirement was deferred because adding `github-actions[bot]` to the bypass list via the API on a personal repo failed validation. It can be added later via the Web UI; see issue #7 for the trail. |
| 79 | + |
| 80 | +## Fork (template) users |
| 81 | + |
| 82 | +When you create a fork via "Use this template", read these notes before going live: |
| 83 | + |
| 84 | +1. **PAT scope**. The setup guide in `README.md` walks through creating a Fine-grained PAT with `Administration: Read-only`. Use **All repositories** or **Only select repositories** — the *Public repositories* preset cannot grant the Administration permission and the workflow will fail. The PAT cannot push or modify anything; it only reads metadata. |
| 85 | +2. **Private-repo safety**. Even if your PAT is scoped to "All repositories", `scripts/collect.sh` filters with `?type=public` and never collects traffic from your private repos. Verify by reading the `IMPORTANT` block in `collect.sh:20–24`. |
| 86 | +3. **Pages visibility**. Once enabled, `data/traffic.json` is public via `raw.githubusercontent.com`. The data is aggregate counts (views, clones, uniques) for your *own* public repos — this is fundamentally what the project publishes. |
| 87 | +4. **Secret name**. The workflow expects the secret to be named `GH_INSIGHTS_PAT`. If you rename it, update `.github/workflows/collect.yml` accordingly. |
| 88 | +5. **Branch protection**. The `Protect main` ruleset is per-repository and does not carry across forks. If you want the same protection, recreate it on your fork (UI: Settings → Rules → Rulesets, or via the API as documented in #7). |
| 89 | +6. **Rename detection**. If you rename a public repo, history is automatically merged into the new name on the next cron run via the GitHub API's 301 redirect (`scripts/collect.sh:27–50`). No manual intervention needed. |
| 90 | + |
| 91 | +## What's intentionally not in scope |
| 92 | + |
| 93 | +- **Authentication, sessions, user data** — none. The dashboard is read-only and ships no auth code. |
| 94 | +- **Server-side state** — none. All persistence lives in `data/traffic.json` in the repo itself; no backend. |
| 95 | +- **Third-party tracking, cookies, analytics on the dashboard** — none. |
0 commit comments