{"id":51472348,"url":"https://github.com/rube-de/hermes-security-dashboard","last_synced_at":"2026-07-06T19:01:03.602Z","repository":{"id":365483650,"uuid":"1272066735","full_name":"rube-de/hermes-security-dashboard","owner":"rube-de","description":"Dashboard for Hermes, an agent that runs periodic security reviews on Oasis Protocol repos — SvelteKit + node:sqlite, with an agent-facing HTTP API.","archived":false,"fork":false,"pushed_at":"2026-06-17T15:43:34.000Z","size":95,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-17T16:35:52.305Z","etag":null,"topics":["dashboard","oasis-protocol","security","security-dashboard","sqlite","svelte","sveltekit","typescript","web3"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rube-de.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-17T08:59:40.000Z","updated_at":"2026-06-17T15:44:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rube-de/hermes-security-dashboard","commit_stats":null,"previous_names":["rube-de/hermes-security-dashboard"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/rube-de/hermes-security-dashboard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rube-de%2Fhermes-security-dashboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rube-de%2Fhermes-security-dashboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rube-de%2Fhermes-security-dashboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rube-de%2Fhermes-security-dashboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rube-de","download_url":"https://codeload.github.com/rube-de/hermes-security-dashboard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rube-de%2Fhermes-security-dashboard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35202786,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","oasis-protocol","security","security-dashboard","sqlite","svelte","sveltekit","typescript","web3"],"created_at":"2026-07-06T19:01:03.070Z","updated_at":"2026-07-06T19:01:03.581Z","avatar_url":"https://github.com/rube-de.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hermes Security Dashboard\n\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n\nDashboard for **Hermes**, an agent that periodically runs security reviews on\nOasis Protocol GitHub repositories. It shows an overview of all repos and their\nfindings by severity, a live \"active run\" indicator, and per-repo review history\nwith full agent-generated review reports — including a run-over-run **diff**\n(new / still-open / resolved findings).\n\nThe Hermes agent feeds the dashboard over a small HTTP API: it registers repos,\nstreams active-scan progress, and submits finished reports.\n\nBuilt from a Claude Design prototype (Oasis-inspired: deep dark theme + teal\naccent, Space Grotesk / IBM Plex Sans / IBM Plex Mono). Dark and light themes.\n\n## Stack\n\n- **SvelteKit 2 + Svelte 5** (runes), TypeScript, Vite — `adapter-node` standalone server\n- **`node:sqlite`** — built-in synchronous SQLite, no native build step\n- **`sanitize-html`** — sanitizes optional agent-submitted HTML report bodies\n- CSS custom properties for the dark/light token system\n\n## Run\n\n```sh\nbun install\nbun run dev            # http://localhost:5173\n\nbun run build          # production build (adapter-node)\nnode build             # serve build/ (PORT, default 3000)\n```\n\nOn first boot the database is seeded with demo data (10 Oasis repos, realistic\nfindings, review history, a live scan). It's a no-op once real data exists.\n\nConfig via env (see `.env.example`): `HERMES_DB` (db path), `PORT`,\n`HERMES_API_TOKEN` (optional write auth). For production — Docker, sub-path\nhosting, and durable storage — see [Deploy](#deploy).\n\nDrive the live UI like the real agent would:\n\n```sh\nHERMES_URL=http://localhost:5173 node scripts/simulate-scan.mjs sapphire-paratime\n```\n\n## Deploy\n\n### Docker\n\nMulti-stage build → a non-root Node 26 image (`node:sqlite` is built in) serving\nthe `adapter-node` server. Only production dependencies are installed; the rest\nof the build output is self-contained.\n\n```sh\n# root-path deploy (default)\ndocker build -t hermes-dashboard .\n\n# served under a sub-path behind a path-routing reverse proxy\ndocker build --build-arg BASE_PATH=/security -t hermes-dashboard .\n\n# /data is the database volume; runs as UID:GID 10000:10000\ndocker run -p 3000:3000 -v hermes-data:/data hermes-dashboard\n```\n\n### Compose\n\n`compose.yaml` is a ready-to-use reference (service, `/data` volume, env). Build\nfrom source — the base path is set at build time:\n\n```sh\ndocker compose up --build -d                 # root\nBASE_PATH=/security docker compose up --build -d   # under /security\n```\n\nOr pull a published image instead by editing the `dashboard` service (comment\nout `build:`, point `image:` at a tag below). The file also includes a\ncommented rclone sidecar sketch for the snapshot sync.\n\n### Published images\n\nCI publishes two variants to GHCR (the base path is baked, so each is a separate\ntag). Make the package public in the repo's Packages settings for anonymous pulls.\n\n```sh\n# root-path\ndocker pull ghcr.io/rube-de/hermes-security-dashboard:latest\n# served under /security\ndocker pull ghcr.io/rube-de/hermes-security-dashboard:latest-security\n```\n\nAlso tagged per branch, short SHA, and semver (`vX.Y.Z` releases also produce the\n`-security` variant). A published image's base path **cannot** be changed at\nruntime — pick the matching tag, or build from source with your own `BASE_PATH`.\n\n### Base path\n\n`BASE_PATH` is baked at **build time** (SvelteKit `paths.base`); it must start\nwith `/` and not end with `/`. With it set, the **entire app — UI *and*\n`/api/*` — responds only under that prefix, and the root 404s.** Every caller\nmust include the prefix, including the Hermes agent's push API:\n\n```sh\n# built with BASE_PATH=/security → push to the prefixed URL\ncurl -X POST localhost:3000/security/api/repos/sapphire-paratime/reviews ...\n# or with the simulator:\nHERMES_URL=http://localhost:3000/security node scripts/simulate-scan.mjs\n```\n\nLeft unset, the app serves at the root exactly as before — `bun run dev` and\nroot-path deploys are unchanged.\n\n### Durability (externally file-synced volume)\n\nIn production `HERMES_DB` lives on a volume that a separate sidecar periodically\nfile-copies to object storage and restores on redeploy. Copying a live SQLite\nfile with an external tool is unsafe, so the live DB (WAL mode) is **never**\ncopied directly. Instead the server:\n\n- emits a consistent snapshot (`VACUUM INTO` a temp file + atomic rename) on an\n  interval (`HERMES_SNAPSHOT_INTERVAL` seconds, default 300) **and** on graceful\n  shutdown (SIGTERM/SIGINT → final snapshot → exit 0);\n- checkpoints the WAL (`TRUNCATE`) each cycle so `-wal` stays bounded;\n- on startup, if the live DB is missing/empty but the snapshot exists, restores\n  it into place **before** seeding — so real synced data suppresses demo data.\n\nPoint the sync sidecar at the snapshot file (default `${HERMES_DB}.snapshot`),\n**never** the live `.db` / `-wal` / `-shm`.\n\n### Environment\n\n| Var | Default | Purpose |\n| --- | --- | --- |\n| `HERMES_DB` | `hermes.db` (`/data/hermes.db` in Docker) | Live SQLite path |\n| `PORT` / `HOST` | `3000` / `0.0.0.0` | adapter-node bind |\n| `HERMES_API_TOKEN` | _unset_ | Bearer auth for writes; unset = open (loud startup warning in production) |\n| `HERMES_SEED_DEMO` | `true` | Seed demo data on an empty DB; set `false` in production to start empty |\n| `HERMES_DB_SNAPSHOT` | `${HERMES_DB}.snapshot` | Snapshot file the sync layer copies |\n| `HERMES_SNAPSHOT_INTERVAL` | `300` | Seconds between snapshot/checkpoint cycles |\n| `BASE_PATH` | `''` | Sub-path prefix — **build arg**, baked at build time |\n\n## Pages\n\n| Route                          | What                                                        |\n| ------------------------------ | ---------------------------------------------------------- |\n| `/`                            | Overview — totals by severity, run strip, trend, repo list with search + status/severity filters |\n| `/repo/[id]`                   | Repo detail — metric summary, live scan banner, review history |\n| `/repo/[id]/review/[reviewId]` | Review report — severity band, summary, diff vs previous run, findings with code + remediation, resolved section |\n\nUI pages read directly from the database via server `load`. The active-run\nbanner additionally polls `GET /api/scan` so the agent's progress shows live.\n\n## Agent API\n\nA machine-readable **OpenAPI 3.1** spec lives at\n[`src/lib/server/openapi.yaml`](src/lib/server/openapi.yaml). The app serves it\nand renders browsable docs:\n\n- **`/docs`** — rendered API reference ([Scalar](https://github.com/scalar/scalar)).\n- **`/openapi.yaml`** — the raw spec.\n- **`/api/openapi.json`** — the spec as JSON, for codegen/tooling.\n\n(Under a sub-path deploy each sits beneath the base path, e.g. `/security/docs`.)\nThe tables below mirror the spec; the route handlers remain the source of truth.\n\nBase path `/api`. Reads are open; **writes** honour `HERMES_API_TOKEN` if set\n(`Authorization: Bearer \u003ctoken\u003e`), otherwise are unauthenticated.\n\nThe producer is the Hermes agent itself: it runs the security review on a\nschedule (a cron-style task configured agent-side) and POSTs each result to the\nwrite endpoints below with `HERMES_API_TOKEN`. Nothing here schedules anything —\nthe dashboard only stores and renders what the agent pushes. When the dashboard\nsits behind a reverse proxy that gates the UI (e.g. a SIWE wallet gateway), the\nscheduled job can't authenticate through that gate; it should reach the dashboard\n**directly on the internal network**, including the base-path prefix if one is\nbaked in:\n\n```sh\n# push directly to the internal host, with the /security prefix and the token;\n# body is the same shape as \"Submit a review report\" below\ncurl -X POST http://hermes-security-dashboard:3000/security/api/repos/:id/reviews \\\n  -H \"Authorization: Bearer $HERMES_API_TOKEN\" \\\n  -H 'content-type: application/json' -d '{ \"commit\": \"…\", \"findings\": [ … ] }'\n```\n\n| Method | Endpoint                     | Purpose                                |\n| ------ | ---------------------------- | -------------------------------------- |\n| GET    | `/api/health`                | Liveness check                         |\n| GET    | `/api/overview`              | Aggregate metrics + repo summaries     |\n| GET    | `/api/repos`                 | List repositories                      |\n| POST   | `/api/repos`                 | Register / update a repository         |\n| GET    | `/api/repos/:id`             | Repo detail + review history           |\n| GET    | `/api/repos/:id/reviews`     | Reviews for a repo                     |\n| POST   | `/api/repos/:id/reviews`     | **Submit a review report**             |\n| GET    | `/api/reviews`               | List reviews across repos (trend source) |\n| GET    | `/api/reviews/:id`           | Single review (findings + diff)        |\n| GET    | `/api/trends`                | Daily new/resolved/review aggregates   |\n| GET    | `/api/scan`                  | Current active-run state               |\n| PUT    | `/api/scan`                  | Update active-run state                |\n\n### Register a repo\n\n```sh\ncurl -X POST localhost:3000/api/repos -H 'content-type: application/json' -d '{\n  \"id\": \"sapphire-paratime\",\n  \"lang\": \"Solidity\",\n  \"description\": \"Confidential EVM ParaTime\",\n  \"path\": \"oasisprotocol/sapphire-paratime\",\n  \"lines\": 19200\n}'\n```\n\n### Submit a review report\n\n`findings` is the structured form the dashboard renders into the report layout.\n`html` is **optional** — a pre-rendered report body that is sanitized\nserver-side and shown below the structured findings. The diff (new / carried /\nresolved) is a property of the commit: only a commit's first scan carries a delta\n(computed against the previous commit's unioned findings), and re-scans of the same\ncommit report a zero delta — so re-scanning one commit never fabricates churn.\n\n```sh\ncurl -X POST localhost:3000/api/repos/sapphire-paratime/reviews \\\n  -H 'content-type: application/json' -d '{\n  \"commit\": \"a3f9c21\",\n  \"model\": \"claude-opus-4-8\",\n  \"trigger\": \"Scheduled\",\n  \"engine\": \"slither+semgrep+llm\",\n  \"durationSecs\": 231,\n  \"lines\": 19200,\n  \"filesScanned\": 80,\n  \"nextRunAt\": \"2026-06-18T18:00:00Z\",\n  \"findings\": [\n    {\n      \"severity\": \"crit\",\n      \"title\": \"Reentrancy in withdraw()\",\n      \"file\": \"contracts/ConfidentialVault.sol\",\n      \"line\": 142,\n      \"cwe\": \"CWE-841\",\n      \"description\": \"Balance updated after an external call.\",\n      \"code\": \"(bool ok,) = msg.sender.call{value: amt}(\\\"\\\");\\nbal[msg.sender] -= amt;\",\n      \"recommendation\": \"Apply checks-effects-interactions or a nonReentrant guard.\"\n    }\n  ],\n  \"html\": \"\u003ch3\u003eNotes\u003c/h3\u003e\u003cp\u003eOptional narrative…\u003c/p\u003e\"\n}'\n```\n\n`severity` is one of `crit` | `high` | `med` | `low`. `commit` and each\nfinding's `severity` + `title` are required; everything else is optional.\n\nA commit can be scanned more than once — LLM reviews are non-deterministic, and you\nmay run several models against the same code. Submits are therefore idempotent on\nscan **content**, not on `(repo, commit)`. The content key is `commit` + `model` +\n`engine` + the finding set, where each finding contributes only its `severity` +\n`file` + `title` (the same identity used for the new/carried/resolved diff). A\nresubmit with the same key returns the existing review (`duplicate: true`, HTTP 200);\na re-run that finds a different issue, drops one, changes a severity, or runs a\ndifferent `model` is stored as its own review. Note the key ignores a finding's\n`line`/`description`/`recommendation`/`code`, so a retry that only rewords those (or\nmoves a line) dedups to the first report. Each scan shows up as its own row, newest\nfirst, with its `model`. A repo's headline status unions the findings across **all**\nscans of its current commit, so an issue one model flagged isn't hidden because a\nlater model missed it.\n\n`nextRunAt` (epoch-ms or ISO-8601) tells the dashboard when the agent plans to run\nnext; it's rendered as **Next run** on the overview. The schedule is agent-driven —\nthere is no fixed cadence. The dashboard shows \"unscheduled\" until the agent first\nreports a value. The last reported next run then **persists**: omitting `nextRunAt`\non a later submit keeps the previous value, and sending `nextRunAt: 0` clears it back\nto \"unscheduled\".\n\n### Read reviews / trends\n\n```sh\n# flat review list — the raw material for custom trends (newest first)\ncurl 'localhost:3000/api/reviews?since=2026-06-01\u0026limit=500'\ncurl 'localhost:3000/api/reviews?repo=sapphire-paratime'\n\n# pre-aggregated daily buckets: new / resolved / reviews per day\ncurl 'localhost:3000/api/trends?days=30'\ncurl 'localhost:3000/api/trends?days=14\u0026repo=sapphire-paratime'\n```\n\n`GET /api/reviews` accepts `repo`, `since`/`until` (epoch-ms or ISO-8601), and\n`limit` (1..1000, default 200). `GET /api/trends` accepts `days` (1..365,\ndefault 14) and optional `repo`; each bucket is\n`{ day, date, newFindings, resolvedFindings, reviews }`.\n\n### Update the active run\n\n```sh\n# progress update during a scan\ncurl -X PUT localhost:3000/api/scan -H 'content-type: application/json' -d '{\n  \"active\": true, \"repoId\": \"sapphire-paratime\", \"commit\": \"a3f9c21\",\n  \"currentFile\": \"contracts/FeeManager.sol\", \"progress\": 62\n}'\n\n# clear when finished\ncurl -X PUT localhost:3000/api/scan -H 'content-type: application/json' -d '{ \"active\": false }'\n```\n\n## Data model\n\n`node:sqlite` tables: `repos`, `reviews`, `findings`, `scan` (singleton live\nrun), `meta` (next run, org label, etc.). Findings carry a stable `fingerprint`\n(`file` + `title`) so the same issue is tracked run-over-run — that's what powers\nthe new/carried/resolved diff and per-finding age (\"open N runs\"). Server data\naccess lives in `src/lib/server/` (`db.ts`, `store.ts`, `seed.ts`,\n`sanitize.ts`, `auth.ts`).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frube-de%2Fhermes-security-dashboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frube-de%2Fhermes-security-dashboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frube-de%2Fhermes-security-dashboard/lists"}