Governance, Risk & Compliance (GRC) CLI for managing security compliance as code. Designed for the SirosID wallet platform but applicable to any project that tracks controls, audit findings, and framework mappings in YAML.
grc automates the full compliance lifecycle:
- Sync — create and update GitHub issues for each audit finding; discover linked PRs and collect evidence.
- Derive — compute control and mapping statuses bottom-up from findings and evidence (verified → in progress → to-do).
- Render — generate a complete Docusaurus compliance site with per-control pages, per-requirement pages for each framework, dashboards, finding summaries, risk register, and year-cycle calendar.
- Serve — run an HTTP server for the rendered site with optional GitHub webhook listener for automatic re-rendering on push.
- Export — produce an auditor-ready JSON evidence package.
- Status — print a read-only overview of current compliance posture.
grc works on a compliance repository with the following layout:
catalog/
technical/ # Platform security controls (YAML)
organizational/ # Operator controls (YAML)
frameworks/ # Normative requirement text per framework
mappings/ # Framework → control cross-references (YAML)
eudi-secreq.yaml # EUDI Wallet Security Requirements
iso27001-annexa.yaml # ISO/IEC 27001:2022 Annex A
gdpr.yaml # GDPR checklist
audits/ # Structured audit findings with evidence (YAML)
risk-register/ # Accepted risks with compensating controls (YAML)
Each control has an ID (e.g. SID-AUTH-01), a status (verified,
in_progress, to_do), and references to source code, PRs,
deployed endpoints, or external reports that serve as evidence.
Mapping files cross-reference external requirements to internal controls. Supported frameworks:
| Framework | Standard | Requirements |
|---|---|---|
| EUDI | ENISA Wallet Security Requirements v0.5 | 85 |
| FitCEM | Nordic EUDIW Certification System – Wallet Instance FitCEM PP | — |
| ISO 27001 | ISO/IEC 27001:2022 Annex A | 93 |
| GDPR | Regulation (EU) 2016/679 | 19 |
| OWASP ASVS | OWASP ASVS 4.0.3 Level 3 | — |
| STRIDE | Threat model mapping | — |
Risks (accepted, transferred, monitoring, or draft proposals) are tracked in YAML files under risk-register/. Each risk
links to a finding, documents compensating controls, residual severity, and
review intervals. The rendered site shows an overview with severity badges
and per-risk detail pages.
Every risk must have an owner: the person or role accountable for it, who
approves the treatment plan and accepts the residual risk (ISO/IEC 27001:2022
6.1.2 c) 2), 6.1.3 e)). This is distinct from decision.reviewer (who
assesses the analysis) and from the register-level owner (the team that owns
the register file). grc validate and grc risk validate fail on a risk
without one; grc risk list --owner NAME filters by it.
Projects can name their risk assessment methodology document with
risk_register.methodology in .grc.yaml. It is treated as authoritative
for the register: it is served to MCP clients as grc://risk/methodology, the
MCP server instructions and risk_review prompt tell agents to check the
register against it, and agents are told never to change the register's
schema or assessment rules without the matching change to the document.
grc validate fails if the configured file is missing. An accepted risk must
record decision.owner_accepted_date (YYYY-MM-DD): when its owner accepted the
residual risk and approved the treatment plan, distinct from the decision date
and from decision.reviewer (the approving CISO). When a risk records
consequence, likelihood and residual_likelihood, validation also checks
that severity and residual_severity equal the level derived from them
(consequence × likelihood: e.g. medium × possible = medium, medium × unlikely
= low), so the recorded levels cannot drift from the assessment.
Every risk that is not a draft must also record a treatment_action: what is
done to treat it, who does it, and its status. A draft may omit it but is
validated if present.
treatment_action:
action: "what is being done" # required
responsible: "person or role doing it" # required
status: open | in_progress | done # required
due_date: "YYYY-MM-DD" # optional target date
completed_date: "YYYY-MM-DD" # required when status is done; must not be set otherwiseIt is rendered on the risk page (with the status in the index), included in
grc risk list --format json and the grc://risk/register MCP resource, and
grc risk summary counts risks by treatment status.
Recurring GRC activities (reviews, audits, penetration tests) can be loaded from an iCal (.ics) feed and projected onto a single canonical year. The rendered page shows a circular SVG year diagram and a month-by-month schedule.
Controls can reference calendar activities by a stable lookup key derived
from the event title (e.g. DPIA Review → dpia-review) rather than
calendar UIDs that may change over time.
Two different things, kept in separate directories, one directory each:
- Assessments (
assessments.dir, defaultassessments/) are internal. They exist to generate findings (and so tracking issues, privately) and to update the control catalog throughgrc derive. They are never rendered on the public profile.Without aassessments/<name>/assessment.yaml findings; header key `assessment:` assessments/<name>/soa.yaml optional explicit statement of applicability assessments/<name>/references/ supporting material, never renderedsoa.yaml, an assessment's statement of applicability is implicit: the requirement catalogs it lists inassessment.catalogs(framework IDs from.grc.yaml). Flatassessments/<name>.yamlfiles work too. - Audits (
audits.dir, defaultaudits/) are conducted by an external auditor, usually after an assessment served as the pre-audit (audit.pre_audit: <assessment name>). A real audit has a more detailed, scoped statement of applicability and a published outcome:audits/<cycle>/audit.yaml findings; header key `audit:` (auditor, pre_audit, ...) audits/<cycle>/soa.yaml statement of applicability audits/<cycle>/result.yaml the compliance result (certificate) audits/<cycle>/references/ supporting documents
Statement of applicability (soa: header plus requirements: rows of
annex_a, title, applicable (true/false/partial), status
(implemented/partial/gap/not_applicable), justification, evidence),
rendered as a table. soa.status is draft, in_review or approved (with
approved_date); an audit's statement is published only once approved.
Statements of assessments are never public.
Result (result.yaml):
result:
status: issued # planned | pre_audit | in_audit | issued | not_issued | withdrawn
scheme: ISO/IEC 27001:2022
body: Certification Body AB
certificate_id: CERT-123
scope: Development process
issued: "2026-11-01"
valid_until: "2029-10-31"
documents:
- {title: Certificate (PDF), file: references/certificate.pdf}
- {title: Auditor report, url: "https://example.org/report"}A result is published only when status: issued; any other state,
including a missing or unknown one, is internal. Documents given as file
(pdf, png, jpg, svg, txt, inside the audit directory, no symlinks out of it)
are copied to static/grc-audits/<cycle>/; url documents (https) are linked.
The public site shows, for each audit, only its issued result and approved
statement, never findings, pre-audit links or assessments. The private site
shows everything, marks unpublished parts with a banner, and lists assessments
under Audits > Internal assessments. Pages and documents from an earlier
render are removed when nothing is publishable any more. The evidence column
of a statement points into the repository and is private only.
grc validate checks statements, results (including document paths),
pre_audit links and catalogs.
The derive command computes statuses bottom-up:
- Finding → Control: a control with open findings is downgraded; a control
whose findings are all resolved is upgraded to
verified. If every one of those findings also carries evidence the control is additionally marked evidenced (a flag, not a separate status). - Control → Mapping: a mapping requirement whose controls are all verified
is marked
full; forresult-style frameworks it iscompliantonly when they are also evidenced. Partial coverage yieldspartial.
Terminology: "verified" is the only control status for "implemented and confirmed". The old
validatedstatus is a deprecated alias, read asverified; "evidence" is tracked separately.
When the policies directory exists, grc render generates the site's policies
section from its *.md files. Two independent front matter properties
apply:
---
visibility: public # public | private: decides who can read it
status: approved # draft | in_review | approved: lifecycle, display only
approved_date: 2026-09-10 # required when status is approved
---visibilityalone decides publication. Only an explicitpublicappears on the public site; a missing or unknown value is private (fail closed).statusnever affects visibility. Every policy, public or private, gets a status box under its heading.- The private site shows every policy; the public site only
visibility: public. grc validaterequires both properties, andapproved_datefor approved ones.- Other front matter (
sidebar_label,tags, ...) is passed through to Docusaurus. - Without a
policiesdirectory nothing is generated and hand-curated pages are left alone.
go install github.com/sirosfoundation/go-grc/cmd/grc@latestOr build from source:
git clone https://github.com/sirosfoundation/go-grc.git
cd go-grc
make build # → bin/grc
make install # → $GOBIN/grcdocker pull ghcr.io/sirosfoundation/go-grc:latest
# Or build locally
make dockerAll commands take a --root (-r) flag pointing at the compliance
repository:
# Show current compliance posture
grc -r /path/to/compliance status
# Sync findings ↔ GitHub issues
grc -r /path/to/compliance sync
# Derive statuses (preview first, then apply)
grc -r /path/to/compliance derive --dry-run
grc -r /path/to/compliance derive
# Generate the Docusaurus compliance site (public or private)
grc -r /path/to/compliance render --profile public
grc -r /path/to/compliance render --profile private
# Serve the rendered site over HTTP
grc -r /path/to/compliance serve --profile private --addr :8080
# Serve with GitHub webhook for automatic re-rendering
grc -r /path/to/compliance serve --webhook --webhook-secret "$GRC_WEBHOOK_SECRET"
# Export evidence package for auditors
grc -r /path/to/compliance export -o /tmp/audit-exportThe Docker image includes Node.js and pnpm so that grc serve can run
Docusaurus builds inside the container. On startup the server renders
markdown, runs pnpm install + docusaurus build, and then serves the
styled HTML site.
# Serve the compliance site (renders + builds on startup)
docker run -v /path/to/compliance:/data \
-p 8080:8080 \
ghcr.io/sirosfoundation/go-grc \
serve -r /data --profile private
# With GitHub webhook and daily scheduled rebuilds
docker run -v /path/to/compliance:/data \
-e GRC_WEBHOOK_SECRET=your-secret \
-p 8080:8080 \
ghcr.io/sirosfoundation/go-grc \
serve -r /data --webhook --rebuild-interval 24h
# Webhook rebuilds are debounced: a burst of pushes yields one rebuild, started
# after --webhook-debounce (default 30s) of quiet. Builds never overlap; a push
# during a build queues exactly one follow-up. 0 rebuilds immediately.
# One-shot render (markdown only, no Docusaurus build)
docker run --rm -v /path/to/compliance:/data \
ghcr.io/sirosfoundation/go-grc \
render -r /data --profile publicServe both public and private dashboards side by side. A named volume for
node_modules caches npm dependencies across container restarts so only the
Docusaurus build (~20 s) runs instead of a full pnpm install (~2 min):
# docker-compose.yml
services:
public:
image: ghcr.io/sirosfoundation/go-grc:latest
volumes:
- .:/data
- public-site-cache:/data/site/node_modules
command: ["serve", "-r", "/data", "--profile", "public", "--addr", ":8080"]
ports:
- "8080:8080"
restart: unless-stopped
private:
image: ghcr.io/sirosfoundation/go-grc:latest
volumes:
- .:/data
- private-site-cache:/data/site/node_modules
command: ["serve", "-r", "/data", "--profile", "private", "--addr", ":8080"]
ports:
- "8081:8080"
restart: unless-stopped
volumes:
public-site-cache:
private-site-cache:docker compose up -d
# Public dashboard: http://localhost:8080
# Private dashboard: http://localhost:8081To enable automatic re-rendering on GitHub push, add a webhook service:
webhook:
image: ghcr.io/sirosfoundation/go-grc:latest
volumes:
- .:/data
- webhook-site-cache:/data/site/node_modules
command: ["serve", "-r", "/data", "--profile", "private", "--addr", ":8080", "--webhook", "--rebuild-interval", "24h"]
ports:
- "8082:8080"
environment:
- GRC_WEBHOOK_SECRET=${GRC_WEBHOOK_SECRET}
restart: unless-stoppedIn a compliance repository with a Makefile wired to grc, the typical CI
pipeline is:
make pipeline # sync → derive → render
make build # render + docusaurus buildProject settings are stored in .grc.yaml at the repository root:
project:
name: "My Compliance Dashboard"
repo: "myorg/compliance"
url: "https://compliance.example.com"
catalog:
dir: catalog
subdirs: [technical, organizational]
mappings:
dir: mappings
audits:
dir: audits # real audit cycles: <cycle>/{audit,soa}.yaml
assessments:
dir: assessments # internal self-assessments, never public
policies:
dir: policies # policy documents; the site's policies section is generated from it
site:
dir: site/docs
frameworks:
- id: iso27001
name: "ISO 27001 Annex A"
catalog_file: iso27001-annexa.yaml
mapping_file: iso27001-annexa.yaml
key_field: annex_a
risk_register:
dir: risk-register
files: [platform.yaml]
year_cycle:
title: "Compliance Year Cycle"
source: "https://calendar.google.com/calendar/ical/.../basic.ics"
public: falsecmd/grc/
main.go CLI entrypoint (cobra)
sync/ GitHub ↔ findings synchronisation
derive/ Bottom-up status derivation
render/ Docusaurus page generation (controls, frameworks,
risk register, year-cycle, CSF overview, findings)
serve/ HTTP server with GitHub webhook support
export/ Evidence package export
status/ Read-only status overview
validate/ Config and data validation
initialize/ Scaffold a new compliance repository
oscal/ OSCAL SSP export
risk/ Risk register CLI operations
pkg/
catalog/ Control catalog + framework catalog loaders (YAML)
audit/ Finding & evidence types, audit file loader/saver
mapping/ Framework mapping types and loader
config/ Project configuration (.grc.yaml) and path resolution
github/ go-github wrapper for issue/PR operations
risk/ Risk register types and loader
yearcycle/ iCal parser + year-cycle projection
make setup # Install golangci-lint, goimports, gosec, staticcheck
make test # Run tests with -race
make lint # Run golangci-lint
make coverage # Generate coverage report
make fmt # gofmt + goimports
make vet # go vet
make docker # Build Docker imageBSD 2-Clause — see LICENSE.