We were drowning in spreadsheets and PDFs. Our audit committee needed a single pane of glass for status, metrics, and evidence, but nothing off-the-shelf fit our specific controls and reporting structure. So I built a custom dashboard. The key was treating it like a CI/CD pipeline: data in, transform, deploy.
The stack is simple and reliable:
* **Backend:** Python (FastAPI) to handle data aggregation and API endpoints.
* **Data:** PostgreSQL. All raw audit findings, control test results, and remediation status live here.
* **Frontend:** Basic React with Recharts for graphs. No fancy UI frameworks that break every six months.
* **Infrastructure:** Docker containers, deployed via a GitLab CI pipeline to a Kubernetes cluster.
The core of the system is the data sync. We pull from our GRC tool, JIRA, and a couple of internal databases. This runs nightly as a Kubernetes CronJob. The pipeline ensures the build and deployment are reproducible.
Here's the heart of the GitLab CI pipeline (.gitlab-ci.yml):
```yaml
stages:
- test
- build
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
test:
stage: test
image: python:3.11-slim
script:
- pip install -r requirements.txt
- pytest
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/audit-dashboard audit-dashboard=$DOCKER_IMAGE --record -n audit
- kubectl rollout status deployment/audit-dashboard -n audit
only:
- main
```
The dashboard itself just consumes the API. The most valuable views we built:
* **Remediation Heatmap:** Control failures by department and over time.
* **Open Finding Age:** Anything older than 90 days turns red.
* **Testing Completion:** Real-time status against the annual plan.
Biggest pitfalls to avoid:
* Don't try real-time sync. Batch it nightly; it's more than enough and prevents API rate limits.
* Build idempotent data ingestion. If the job fails halfway, re-running it shouldn't duplicate records.
* Keep the frontend dumb. All logic and data shaping happens in the backend API.
It's been running for 9 months now with zero flaky deployments. The committee gets a consistent, auto-updating view every morning, and the audit team spends less than an hour a month on status reporting.
Build once, deploy everywhere