A comprehensive guide for developers and DevSecOps engineers using OpenFactstore — a supply chain compliance fact store for financial services and regulated industries.
- Introduction
- Setup & Installation
- First Fact — Hello World Tutorial
- Key Features
- Flow Tags
- Flow Template YAML Specs
- Organisation Multi-tenancy
- Release Approval Workflow
- Deployment Gate & Policy Engine
- Environment Drift Detection
- Allow-listing Third-party Artifacts
- Snapshotting Scopes
- Pull Request Attestation
- Security Scan Integration
- OPA Policy Integration
- Regulatory Compliance Framework
- Dry-run Safe Mode
- CI/CD Integration
- Prometheus Metrics & Grafana Dashboards
- OIDC Provenance Attestation
- Attestation Type Processors
- Terraform Infrastructure as Code
- Security & Data Privacy
- CI/CD Integration Reference
- Monitoring
- Lifecycle Management
OpenFactstore is an open-source Supply Chain Compliance Fact Store — a system of record for proving that your software artifacts (container images, binaries) met all required security and quality gates before reaching production.
It is purpose-built for teams operating in regulated environments — financial services, healthcare, government — where evidence of compliance must be collected, timestamped, and queryable on demand for audit, incident response, and regulatory reporting.
At its core, OpenFactstore answers one question at deployment time:
"Has this artifact digest satisfied every required compliance check for the flow it belongs to?"
| Concept | Description |
|---|---|
| Flow | A named compliance policy defining which attestation types (e.g. junit, snyk, trivy) are required before an artifact may be deployed. Flows belong to an Organisation. |
| Trail | A record of one software build, capturing Git metadata (commit SHA, branch, PR number, author). Trails belong to a Flow. |
| Artifact | A container image or binary, identified by its SHA-256 digest, associated with a Trail. |
| Attestation | Evidence that a requirement was met (test run, scan report, approval decision). Has a status: PASSED, FAILED, or PENDING. |
| EvidenceFile | The actual evidence payload (JSON, XML, log file) attached to an Attestation, stored with a cryptographic hash. |
| Environment | A named deployment target (e.g. production, staging). Environments hold snapshots of what artifacts are currently deployed. |
| Organisation | A top-level tenant. Flows, users, and integrations are scoped to an Organisation. |
OpenFactstore follows Hexagonal Architecture (Ports and Adapters). Business logic in the core domain never depends on infrastructure — only on port interfaces. This means you can swap the database, add new API surfaces, or test business logic in isolation without Spring context.
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Vue 3 SPA) │
│ Browser ─► Vite Dev Server :5173 │
└────────────────────────────┬────────────────────────────────┘
│ HTTP / REST (Axios)
┌────────────────────────────▼────────────────────────────────┐
│ Backend (Spring Boot :8080) │
│ │
│ REST Controllers ─► Inbound Ports ─► Application │
│ Services │
│ │ │
│ Outbound Ports │
│ │ │
│ JPA Repository │
│ Adapters │
└────────────────────────────┬────────────────────────────────┘
│ JDBC
┌──────────────▼──────────────┐
│ PostgreSQL 16 │
└─────────────────────────────┘
Tech stack:
- Backend: Kotlin 2.0 · Spring Boot 3.4 · Spring Data JPA · PostgreSQL 16 · Flyway · Java 21
- Frontend: Vue 3 (Composition API) · TypeScript 5.4 · Vite 5 · Tailwind CSS 3.4 · Pinia · Axios
- Ops: Docker Compose · Prometheus · Grafana · HashiCorp Vault (optional)
- Java 21 (Eclipse Temurin recommended):
java -version - Node.js 20 + npm:
node -v && npm -v - Docker & Docker Compose:
docker compose version - Terraform ≥ 1.6 (optional — only needed for
infra/bootstrap):terraform -version - Go ≥ 1.25 (optional — needed to build the Terraform provider from source):
go version
The fastest way to run the full stack:
git clone https://github.com/MaximumTrainer/OpenFactstore.git
cd OpenFactstore
# Start everything: PostgreSQL, Vault, backend, Prometheus, Grafana
docker compose up --buildServices will be available at:
| Service | URL |
|---|---|
| Backend API | http://localhost:8080 |
| Swagger UI | http://localhost:8080/swagger-ui.html |
| Frontend | http://localhost:5173 (dev only) |
| Prometheus | http://localhost:9090 |
| Grafana | http://localhost:3000 (admin / changeme) |
| HashiCorp Vault | http://localhost:8200 |
💡 Pro-tip: To start only the database (for local backend development), run
docker compose up -d postgres vault. Then run./gradlew bootRunin thebackend/directory.
Backend:
# 1. Start dependent services
docker compose up -d postgres vault
# 2. Start the Spring Boot dev server
cd backend
./gradlew bootRun
# Listening on http://localhost:8080Frontend:
cd frontend
npm ci
npm run dev
# Listening on http://localhost:5173The backend reads these environment variables at startup. All have defaults suitable for local development with Docker Compose.
| Variable | Default | Description |
|---|---|---|
DB_HOST |
localhost |
PostgreSQL host |
DB_PORT |
5432 |
PostgreSQL port |
DB_NAME |
factstore |
Database name |
DB_USERNAME |
(required) | Database user |
DB_PASSWORD |
(required) | Database password |
VAULT_ENABLED |
false |
Enable HashiCorp Vault for evidence storage |
VAULT_ADDR |
http://localhost:8200 |
Vault address |
VAULT_TOKEN |
(required if enabled) | Vault root/app token |
FACTSTORE_SCM_ENCRYPTION_KEY |
default-dev-key-32chars!!!!!! |
AES-256-GCM key for SCM token encryption. Set to a strong random 32-char value in production. |
GF_SECURITY_ADMIN_PASSWORD |
changeme |
Grafana admin password |
💡 Pro-tip: For production, set
DB_PASSWORDandVAULT_TOKENvia a secrets manager (AWS Secrets Manager, GCP Secret Manager, Vault itself). Never hardcode credentials.
This tutorial walks you through the complete compliance lifecycle for a single build. You will create a Flow, record a Trail, attach an Attestation, and assert compliance — all with curl.
Ensure the backend is running (http://localhost:8080).
A Flow defines the attestation types your artifact must satisfy.
curl -s -X POST http://localhost:8080/api/v1/flows \
-H "Content-Type: application/json" \
-d '{
"name": "my-service-compliance",
"description": "Compliance flow for my-service",
"requiredAttestations": ["junit", "snyk"]
}' | jq .Response:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "my-service-compliance",
"description": "Compliance flow for my-service",
"requiredAttestations": ["junit", "snyk"],
"createdAt": "2025-01-01T10:00:00Z"
}Save the id as FLOW_ID:
export FLOW_ID="3fa85f64-5717-4562-b3fc-2c963f66afa6"A Trail represents a single build. Attach it to your Flow and record Git metadata.
curl -s -X POST http://localhost:8080/api/v1/trails \
-H "Content-Type: application/json" \
-d "{
\"flowId\": \"$FLOW_ID\",
\"gitCommitSha\": \"abc123def456\",
\"gitBranch\": \"main\",
\"gitAuthor\": \"alice\",
\"gitAuthorEmail\": \"alice@example.com\"
}" | jq .Response:
{
"id": "7b9d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
"flowId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"gitCommitSha": "abc123def456",
"gitBranch": "main",
"gitAuthor": "alice",
"status": "IN_PROGRESS"
}export TRAIL_ID="7b9d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f"Register the container image produced by this build.
curl -s -X POST "http://localhost:8080/api/v1/trails/$TRAIL_ID/artifacts" \
-H "Content-Type: application/json" \
-d '{
"name": "my-service",
"sha256Digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"tag": "v1.2.3",
"registry": "ghcr.io/my-org"
}' | jq .Record that unit tests passed:
curl -s -X POST "http://localhost:8080/api/v1/trails/$TRAIL_ID/attestations" \
-H "Content-Type: application/json" \
-d '{
"type": "junit",
"status": "PASSED",
"description": "All 247 unit tests passed",
"metadata": {
"total": 247,
"passed": 247,
"failed": 0
}
}' | jq .Record that a security scan passed:
curl -s -X POST "http://localhost:8080/api/v1/trails/$TRAIL_ID/attestations" \
-H "Content-Type: application/json" \
-d '{
"type": "snyk",
"status": "PASSED",
"description": "No critical vulnerabilities found",
"metadata": {
"critical": 0,
"high": 0,
"medium": 2
}
}' | jq .Check whether the artifact satisfies all requirements of the Flow:
curl -s -X POST http://localhost:8080/api/v1/assert \
-H "Content-Type: application/json" \
-d "{
\"flowId\": \"$FLOW_ID\",
\"sha256Digest\": \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"
}" | jq .COMPLIANT response:
{
"compliant": true,
"flowId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"sha256Digest": "sha256:e3b0c44...",
"checkedAt": "2025-01-01T10:05:00Z",
"attestations": [
{ "type": "junit", "status": "PASSED" },
{ "type": "snyk", "status": "PASSED" }
]
}If any required attestation is missing or failed, compliant will be false with a missingAttestations list.
💡 Pro-tip: Wire this
POST /api/v1/assertcall into your deployment pipeline as a deployment gate. Block the deploy ifcompliantisfalse.
Flows support arbitrary key-value tags for filtering, reporting, and multi-team organisation.
# Create a flow with tags
curl -s -X POST http://localhost:8080/api/v1/flows \
-H "Content-Type: application/json" \
-d '{
"name": "payments-service",
"requiredAttestations": ["junit", "trivy"],
"tags": {
"team": "payments",
"criticality": "high",
"pci-scope": "true"
}
}' | jq .Use tags to group flows for dashboards, notifications, and policy rules. Tags do not affect compliance evaluation.
Define flows as YAML templates for version-controlled, reusable compliance specs. Retrieve a flow's YAML template:
curl -s http://localhost:8080/api/v1/flows/$FLOW_ID/templateExample template output:
name: payments-service
description: Compliance flow for the payments microservice
requiredAttestations:
- junit
- snyk
- trivy
tags:
team: payments
criticality: highStore templates in your repository and use them to provision flows programmatically during bootstrap.
Attestations in a flow template can include an if: expression to make them conditional. The rule is only enforced when the expression evaluates to true at assertion time.
version: 1
trail:
attestations:
- name: jira-ticket
type: jira
artifacts:
- name: backend
attestations:
- name: unit-tests
type: junit
- name: security-scan
type: snyk
if: 'flow.tags["pci-scope"] == "true"'
- name: sonar-gate
type: sonar
if: 'matches(artifact.name, "^backend.*")'Supported expression syntax (evaluated by PolicyExpressionEvaluator):
| Expression | Example |
|---|---|
| Equality | flow.name == "payments-service" |
| Inequality | flow.name != "test-flow" |
| Regex match | matches(artifact.name, "^backend.*") |
| Existence check | exists(flow) |
| List membership | flow.name in ["svc-a", "svc-b"] |
| Logical AND | exists(flow) and flow.name == "x" |
| Logical OR | flow.name == "a" or flow.name == "b" |
| Negation | not exists(flow) |
Malformed expressions default to false (never throw an exception).
OpenFactstore supports multiple tenants via Organisations. Each organisation has its own flows, users, and integrations, fully isolated from other organisations.
# Create an organisation
curl -s -X POST http://localhost:8080/api/v1/organisations \
-H "Content-Type: application/json" \
-d '{
"name": "ACME Corp",
"slug": "acme-corp"
}' | jq .
# List flows scoped to an organisation
curl -s http://localhost:8080/api/v1/organisations/acme-corp/flows | jq .
# Invite a user to an organisation
curl -s -X POST http://localhost:8080/api/v1/organisations/acme-corp/members \
-H "Content-Type: application/json" \
-d '{
"userId": "user-uuid",
"role": "MEMBER"
}' | jq .Member roles: OWNER, ADMIN, MEMBER
For high-risk deployments, require explicit human approval before a trail can be considered compliant.
# Request approval for a trail
curl -s -X POST "http://localhost:8080/api/v1/trails/$TRAIL_ID/approvals" \
-H "Content-Type: application/json" \
-d '{
"requestedBy": "ci-bot",
"reason": "Release v1.2.3 to production"
}' | jq .
# Approve (by an authorised reviewer)
export APPROVAL_ID="approval-uuid-here"
curl -s -X POST "http://localhost:8080/api/v1/approvals/$APPROVAL_ID/approve" \
-H "Content-Type: application/json" \
-d '{
"approvedBy": "alice",
"comment": "LGTM — all checks pass"
}' | jq .
# Reject
curl -s -X POST "http://localhost:8080/api/v1/approvals/$APPROVAL_ID/reject" \
-H "Content-Type: application/json" \
-d '{
"rejectedBy": "bob",
"reason": "Pending security review"
}' | jq .💡 Pro-tip: Configure Slack notifications (see 4.14) so reviewers receive an interactive approval request directly in their Slack workspace.
The deployment gate evaluates whether an artifact is cleared to deploy to a specific environment, applying both compliance rules and deployment policies.
# Create a deployment policy
curl -s -X POST http://localhost:8080/api/v1/deployment-policies \
-H "Content-Type: application/json" \
-d '{
"name": "production-gate",
"flowId": "'$FLOW_ID'",
"requireApproval": true,
"blockOnFailedScan": true
}' | jq .
# Evaluate the deployment gate
curl -s -X POST http://localhost:8080/api/v1/gate/evaluate \
-H "Content-Type: application/json" \
-d '{
"artifactDigest": "sha256:e3b0c44...",
"targetEnvironment": "production",
"flowId": "'$FLOW_ID'"
}' | jq .Gate response:
{
"allowed": true,
"policyId": "production-gate-uuid",
"reasons": []
}Attach a YAML policy to a flow for fine-grained control over what evidence is required. Policies support the same conditional if: expression syntax as flow templates:
_schema: "https://factstore.io/policy/v1"
artifacts:
provenance:
required: true
trail-compliance:
required: true
attestations:
- name: junit
type: junit
- name: security-scan
type: snyk
if: 'flow.name == "payments-service"'
- name: sonar-gate
type: sonar
if: 'matches(artifact.name, "^backend.*")'# Create and attach a custom policy to a flow
curl -s -X POST http://localhost:8080/api/v1/policies \
-H "Content-Type: application/json" \
-d '{"name": "pci-baseline", "yaml": "<policy-yaml>"}' | jq .
export POLICY_ID="policy-uuid"
curl -s -X POST http://localhost:8080/api/v1/policy-attachments \
-H "Content-Type: application/json" \
-d '{"flowId": "'$FLOW_ID'", "policyId": "'$POLICY_ID'"}' | jq .Track what is deployed in each environment, compare snapshots, and detect when environments diverge from their baseline.
# Register an environment
curl -s -X POST http://localhost:8080/api/v1/environments \
-H "Content-Type: application/json" \
-d '{
"name": "production",
"description": "Production Kubernetes cluster"
}' | jq .
export ENV_ID="env-uuid"
# Record a snapshot (what is currently deployed)
curl -s -X POST "http://localhost:8080/api/v1/environments/$ENV_ID/snapshots" \
-H "Content-Type: application/json" \
-d '{
"artifacts": [
{
"name": "my-service",
"sha256Digest": "sha256:e3b0c44...",
"tag": "v1.2.3"
}
]
}' | jq .
# Set a baseline (the known-good state)
curl -s -X POST "http://localhost:8080/api/v1/environments/$ENV_ID/baselines" \
-H "Content-Type: application/json" \
-d '{ "description": "Post-release v1.2.3 baseline" }' | jq .
# Check for drift (after a new snapshot)
curl -s "http://localhost:8080/api/v1/environments/$ENV_ID/drift" | jq .Not all deployed artifacts are built by your own pipelines (e.g. sidecar proxies, operator images). Add them to the environment allow-list to prevent false drift alerts.
curl -s -X POST "http://localhost:8080/api/v1/environments/$ENV_ID/allowlist" \
-H "Content-Type: application/json" \
-d '{
"artifactName": "envoy-proxy",
"sha256Digest": "sha256:abc123...",
"reason": "Managed by platform team — not built by app pipeline"
}' | jq .Record scoped snapshots to track compliance state across different scopes (team, service group, namespace):
curl -s -X POST "http://localhost:8080/api/v1/environments/$ENV_ID/snapshots" \
-H "Content-Type: application/json" \
-d '{
"scope": "payments-team",
"artifacts": [
{ "name": "payments-api", "sha256Digest": "sha256:aaa...", "tag": "v2.1.0" },
{ "name": "payments-worker","sha256Digest": "sha256:bbb...", "tag": "v2.1.0" }
]
}' | jq .Diff two snapshots by their index:
curl -s "http://localhost:8080/api/v1/environments/$ENV_ID/diff?from=1&to=2" | jq .Record an attestation sourced directly from your SCM (GitHub, GitLab, Bitbucket) to prove a PR exists, was reviewed, and was merged by an authorised author.
Step 1 — Register an SCM integration for your organisation:
curl -s -X POST http://localhost:8080/api/v1/organisations/acme-corp/scm-integrations \
-H "Content-Type: application/json" \
-d '{
"provider": "github",
"baseUrl": "https://api.github.com",
"token": "ghp_your_token_here"
}' | jq .💡 Pro-tip: The token is encrypted at rest using AES-256-GCM. Set
FACTSTORE_SCM_ENCRYPTION_KEYto a strong, random 32-character key in production. If Vault is enabled (VAULT_ENABLED=true), tokens are additionally stored in HashiCorp Vault.
Step 2 — Record a PR attestation:
curl -s -X POST "http://localhost:8080/api/v1/trails/$TRAIL_ID/attestations/pull-request" \
-H "Content-Type: application/json" \
-d '{
"organisationSlug": "acme-corp",
"provider": "github",
"repositoryOwner": "my-org",
"repositoryName": "my-service",
"prNumber": 42
}' | jq .OpenFactstore fetches the PR details (title, author, merge status, reviewers) from the SCM and stores the evidence automatically.
Record the output of any security scanner (Snyk, Trivy, Grype, Semgrep) as a structured attestation.
curl -s -X POST "http://localhost:8080/api/v1/trails/$TRAIL_ID/security-scans" \
-H "Content-Type: application/json" \
-d '{
"tool": "trivy",
"version": "0.50.0",
"target": "my-service:v1.2.3",
"summary": {
"critical": 0,
"high": 1,
"medium": 3,
"low": 12
},
"passed": true
}' | jq .
# Set thresholds that a flow must satisfy
curl -s -X POST "http://localhost:8080/api/v1/flows/$FLOW_ID/security-thresholds" \
-H "Content-Type: application/json" \
-d '{
"maxCritical": 0,
"maxHigh": 0,
"maxMedium": 10
}' | jq .Upload Rego policy bundles and evaluate any artifact's compliance facts against your custom policies using Open Policy Agent.
# Upload a Rego policy bundle (ZIP file)
curl -s -X POST http://localhost:8080/api/v1/opa/bundles \
-F "file=@my-policy-bundle.zip" \
-F "name=security-baseline-v1" | jq .
export BUNDLE_ID="bundle-uuid"
# Activate the bundle
curl -s -X PUT "http://localhost:8080/api/v1/opa/bundles/$BUNDLE_ID/activate" | jq .
# Evaluate an artifact against the active policy
curl -s -X POST http://localhost:8080/api/v1/opa/evaluate \
-H "Content-Type: application/json" \
-d '{
"trailId": "'$TRAIL_ID'",
"artifactDigest": "sha256:e3b0c44..."
}' | jq .All policy decisions are logged and queryable via GET /api/v1/opa/decisions.
Map your compliance flows to regulatory controls (SOX, PCI-DSS, GDPR, ISO 27001) and generate audit reports automatically.
# Create a regulatory framework
curl -s -X POST http://localhost:8080/api/v1/frameworks \
-H "Content-Type: application/json" \
-d '{
"name": "PCI-DSS v4.0",
"description": "Payment Card Industry Data Security Standard"
}' | jq .
export FRAMEWORK_ID="framework-uuid"
# Add controls to the framework
curl -s -X POST "http://localhost:8080/api/v1/frameworks/$FRAMEWORK_ID/controls" \
-H "Content-Type: application/json" \
-d '{
"controlId": "6.3.3",
"description": "All software components are protected from known vulnerabilities",
"category": "Secure Software"
}' | jq .
# Map a compliance flow to a control
curl -s -X POST http://localhost:8080/api/v1/compliance/mappings \
-H "Content-Type: application/json" \
-d '{
"frameworkId": "'$FRAMEWORK_ID'",
"controlId": "6.3.3",
"flowId": "'$FLOW_ID'",
"attestationType": "trivy"
}' | jq .
# Run a compliance assessment
curl -s -X POST http://localhost:8080/api/v1/compliance/assess \
-H "Content-Type: application/json" \
-d '{
"trailId": "'$TRAIL_ID'",
"frameworkId": "'$FRAMEWORK_ID'"
}' | jq .
# Generate a regulatory report
curl -s "http://localhost:8080/api/v1/reports/regulatory/$FRAMEWORK_ID" | jq .Use the X-Dry-Run: true header on any mutating request to preview what would happen without persisting any data.
# Simulate an assertion without recording the result
curl -s -X POST http://localhost:8080/api/v1/assert \
-H "Content-Type: application/json" \
-H "X-Dry-Run: true" \
-d '{
"flowId": "'$FLOW_ID'",
"sha256Digest": "sha256:e3b0c44..."
}' | jq .The response includes a dryRun: true flag and a human-readable dryRunMessage. No side-effects are committed.
💡 Pro-tip: Use dry-run in pull request CI checks to validate that the artifact would be compliant without actually recording a compliance result. This gives early feedback without polluting the audit trail.
See the full guide at docs/ci-integration.md.
Quick example — GitHub Actions:
- name: Create Factstore trail
run: |
curl -s -X POST "${{ vars.FACTSTORE_BASE_URL }}/api/v1/trails" \
-H "Content-Type: application/json" \
-H "X-Factstore-CI-Context: github-actions" \
-d '{
"flowId": "${{ vars.FACTSTORE_FLOW_ID }}",
"gitAuthor": "${{ github.actor }}",
"gitAuthorEmail": "${{ github.actor }}@users.noreply.github.com"
}'
- name: Assert compliance before deploy
run: |
RESULT=$(curl -s -X POST "${{ vars.FACTSTORE_BASE_URL }}/api/v1/assert" \
-H "Content-Type: application/json" \
-d '{"flowId":"${{ vars.FACTSTORE_FLOW_ID }}","sha256Digest":"'"$DIGEST"'"}')
COMPLIANT=$(echo $RESULT | jq -r '.compliant')
if [ "$COMPLIANT" != "true" ]; then
echo "Deployment blocked: artifact is not compliant"
exit 1
fiThe X-Factstore-CI-Context header instructs the server to auto-populate gitCommitSha, gitBranch, and buildUrl from the CI environment's standard variables. See docs/ci-integration.md for all supported CI systems (GitHub Actions, GitLab CI, Jenkins, CircleCI, Azure DevOps).
💡 Terraform bootstrap in CI: Use the
infra/Terraform configuration to provision your Factstore instance's resources (environments, policies, flows) as code. The.github/workflows/terraform-verify.ymlworkflow demonstrates a full end-to-end verification: it starts the backend, applies the Terraform config, and asserts all resources exist before teardown. See §4.18 Terraform Infrastructure as Code.
OpenFactstore exposes Prometheus metrics at /actuator/prometheus. See Section 7: Monitoring for the full guide.
Record a CI/CD identity token (OIDC JWT) issued by GitHub Actions or GitLab as a tamper-evident provenance attestation. This proves which pipeline ran — not just that a build happened — by capturing the signed identity claims from the CI provider.
Allowed issuers:
https://token.actions.githubusercontent.com(GitHub Actions)https://gitlab.com(GitLab CI/CD)
The server decodes the JWT payload, validates the issuer, and enforces jti replay-protection (each token can only be submitted once).
GitHub Actions — obtain and submit OIDC token:
# .github/workflows/build.yml
- name: Record OIDC provenance attestation
permissions:
id-token: write # required to request the OIDC JWT
run: |
TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=factstore" | jq -r '.value')
curl -s -X POST "${{ vars.FACTSTORE_BASE_URL }}/api/v2/attestations/oidc" \
-H "Content-Type: application/json" \
-d "{
\"trailId\": \"${{ env.TRAIL_ID }}\",
\"token\": \"$TOKEN\",
\"orgSlug\": \"${{ vars.FACTSTORE_ORG_SLUG }}\"
}" | jq .Response:
{
"id": "attest-uuid",
"trailId": "trail-uuid",
"type": "oidc-provenance",
"status": "PASSED",
"details": {
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:my-org/my-service:ref:refs/heads/main",
"repository": "my-org/my-service",
"workflow": ".github/workflows/build.yml",
"ref": "refs/heads/main",
"sha": "abc123def456",
"actor": "alice"
}
}💡 Pro-tip: OIDC provenance is particularly valuable under PCI-DSS and SOX requirements for proving that production deployments originated from a known, audited pipeline — not a developer's laptop.
When you upload an evidence file for certain attestation types, OpenFactstore automatically parses the content and sets PASSED or FAILED without you having to specify the status manually. Upload evidence via the evidence vault endpoint and attach it to an attestation of the matching type.
Upload JUnit XML test results — OpenFactstore counts failures and errors across all <testsuite> elements.
# Record a junit attestation and upload XML evidence
curl -s -X POST "http://localhost:8080/api/v1/trails/$TRAIL_ID/attestations" \
-H "Content-Type: application/json" \
-d '{"type": "junit", "description": "Unit test results"}' | jq .
export ATTEST_ID="attest-uuid"
curl -s -X POST "http://localhost:8080/api/v1/evidence/$ATTEST_ID" \
-F "file=@target/surefire-reports/TEST-results.xml" | jq .
# → status auto-set to PASSED (0 failures) or FAILED (failures/errors > 0)
# → details: {"tests":247,"failures":0,"errors":0,"skipped":3}Upload a Snyk JSON output or any SARIF-format file.
curl -s -X POST "http://localhost:8080/api/v1/evidence/$ATTEST_ID" \
-F "file=@snyk-results.json" | jq .
# → PASSED if vulnerabilities == 0, FAILED otherwise
# → details: {"vulnerabilities":0}Upload the SonarQube quality gate API response (/api/qualitygates/project_status).
# Fetch quality gate status from SonarQube and submit as evidence
SONAR_STATUS=$(curl -s "$SONAR_HOST/api/qualitygates/project_status?projectKey=my-service")
curl -s -X POST "http://localhost:8080/api/v1/evidence/$ATTEST_ID" \
-H "Content-Type: application/json" \
--data-binary "$SONAR_STATUS" | jq .
# → PASSED if projectStatus.status == "OK", FAILED otherwise
# → details: {"qualityGateStatus":"OK"}Submit a Jira issue key (e.g. PROJ-123) or a JSON body with issueRef.
# Plain issue key
echo -n "PROJ-123" | curl -s -X POST "http://localhost:8080/api/v1/evidence/$ATTEST_ID" \
-H "Content-Type: text/plain" --data-binary @- | jq .
# → PASSED (matches [A-Z]+-\d+ pattern), FAILED otherwise
# → details: {"issueRef":"PROJ-123"}
# JSON body
curl -s -X POST "http://localhost:8080/api/v1/evidence/$ATTEST_ID" \
-H "Content-Type: application/json" \
-d '{"issueRef": "PROJ-123", "status": "Done"}' | jq .OpenFactstore ships a custom Terraform provider (terraform/) and a ready-to-use infrastructure configuration (infra/) that together let you manage your entire Factstore deployment — organisations, environments, policies, and flows — as version-controlled code.
| Resource | Terraform type | Description |
|---|---|---|
| Organisation | factstore_organisation |
Root multi-tenant organisation |
| Logical Environment | factstore_logical_environment |
Grouping of related environments |
| Environment | factstore_environment |
Deployment target (K8S, S3, Lambda, Generic) |
| Policy | factstore_policy |
Deployment policy with attestation requirements |
| Policy Attachment | factstore_policy_attachment |
Binds a policy to an environment |
| Flow | factstore_flow |
CI/CD pipeline definition |
Data sources are also available for factstore_environment and factstore_flow to read existing resources into other Terraform configurations.
The infra/ directory contains a turnkey configuration that provisions a standard Factstore instance:
infra/
├── versions.tf # Provider version lock (MaximumTrainer/factstore ~> 1.0)
├── variables.tf # factstore_url, factstore_token, org_slug, org_name
├── main.tf # All resources: org, envs, policies, attachments, flows
└── outputs.tf # Exports all resource IDs
Resources provisioned by infra/main.tf:
- Organisation
openfactstore - Logical environment
production-group - Environments
staging(K8S) andproduction(K8S) - Policy
baseline-requirements— provenance + junit/snyk, attached to staging - Policy
production-requirements— full compliance + pull-request, attached to production - Flows
backend-ci(junit + snyk) andfrontend-ci(build + snyk)
1. Build the Terraform provider from source:
cd terraform
go build -o terraform-provider-factstore .
PROVIDER_DIR=~/.terraform.d/plugins/registry.terraform.io/MaximumTrainer/factstore/1.0.0/linux_amd64
mkdir -p "$PROVIDER_DIR"
cp terraform-provider-factstore "$PROVIDER_DIR/"2. Configure the dev override (~/.terraformrc):
provider_installation {
dev_overrides {
"MaximumTrainer/factstore" = "~/.terraform.d/plugins/registry.terraform.io/MaximumTrainer/factstore/1.0.0/linux_amd64"
}
direct {}
}3. Start Factstore (see §2 Setup & Installation), then apply:
cd infra
terraform init
terraform apply \
-var="factstore_url=http://localhost:8080" \
-var="factstore_token=" # omit token when SECURITY_ENFORCE_AUTH is not setAfter apply, inspect outputs:
terraform output
# organisation_id = "3f8a1b2c-..."
# staging_environment_id = "7e2c9d4a-..."
# production_environment_id = "1a4b8f3c-..."
# backend_flow_id = "5d6e7f8a-..."
# ...| Variable | Default | Description |
|---|---|---|
factstore_url |
http://localhost:8080 |
Factstore API base URL |
factstore_token |
"" |
API token (required when SECURITY_ENFORCE_AUTH=true) |
org_slug |
openfactstore |
URL-safe organisation identifier |
org_name |
OpenFactstore |
Organisation display name |
Environment variable equivalents (set instead of -var flags):
export FACTSTORE_BASE_URL=http://localhost:8080
export FACTSTORE_API_TOKEN=your-token
terraform applyThe .github/workflows/terraform-verify.yml workflow runs on every push and pull request. It performs an end-to-end verification:
- Starts a PostgreSQL 16 service container
- Builds the Spring Boot JAR and starts the backend
- Waits for
/actuator/healthto return200 OK - Builds the Terraform provider binary from source
- Installs it as a dev override
- Runs
terraform applyagainst the live backend - Verifies each resource type exists via the Factstore REST API
- Runs
terraform destroyto clean up (runs even on failure)
This ensures every code change is verified against a real Factstore deployment before merging.
After bootstrapping, use terraform output to feed resource IDs into your CI pipelines:
# .github/workflows/build.yml
- name: Get Factstore flow ID
id: tf
working-directory: infra
run: echo "flow_id=$(terraform output -raw backend_flow_id)" >> $GITHUB_OUTPUT
- name: Create trail
run: |
curl -s -X POST "${{ vars.FACTSTORE_BASE_URL }}/api/v1/trails" \
-H "Content-Type: application/json" \
-d '{"flowId": "${{ steps.tf.outputs.flow_id }}"}'📖 See also: DEPLOY.md — Terraform Infrastructure Bootstrap for the full deployment reference including
terraform destroyinstructions and state management guidance.
⚠️ Current state: The server currently trusts all requests without authentication. API key enforcement is implemented (seePOST /api/v1/api-keys), but is not required by default in the current version. A future release will make authentication mandatory.
For production deployments:
- Create service accounts and API keys for each CI system:
# Create a service account for your CI pipeline
curl -s -X POST http://localhost:8080/api/v1/service-accounts \
-H "Content-Type: application/json" \
-d '{ "name": "github-actions-ci", "description": "GitHub Actions pipeline" }' | jq .
# Generate an API key — returned once, store securely
curl -s -X POST "http://localhost:8080/api/v1/service-accounts/$SA_ID/api-keys" \
-H "Content-Type: application/json" \
-d '{ "name": "prod-pipeline-key" }' | jq .- Pass the key in the
X-Api-Keyheader on all requests. - Rotate keys periodically using the revoke + regenerate flow.
| Storage | Dev default | Production recommendation |
|---|---|---|
| Relational data | H2 in-memory (unit tests) / PostgreSQL (dev/prod) | PostgreSQL 16 with encrypted volumes (e.g. AWS RDS with encryption-at-rest enabled) |
| Evidence files | Local DB (Base64) | HashiCorp Vault (VAULT_ENABLED=true) |
| SCM tokens | AES-256-GCM encrypted in PostgreSQL | Same — set FACTSTORE_SCM_ENCRYPTION_KEY to a strong random 32-char key + HashiCorp Vault for additional secret management |
💡 Pro-tip: Enable
VAULT_ENABLED=truein production to store all evidence files and SCM tokens in HashiCorp Vault. Thevaultservice is included in the Docker Compose file for local testing.
When you register an SCM integration (GitHub, GitLab, Bitbucket), the token is encrypted at rest using AES-256-GCM before being stored in PostgreSQL. The encryption key is derived from the FACTSTORE_SCM_ENCRYPTION_KEY environment variable (padded or truncated to 32 bytes). For production:
- Set
FACTSTORE_SCM_ENCRYPTION_KEYto a strong, randomly generated 32-character key. - Optionally set
VAULT_ENABLED=trueto back token storage with HashiCorp Vault. - Consider a KMS-backed Vault auto-unseal for fully automated operations.
- Rotate API keys every 90 days.
- Use separate service accounts per CI pipeline/environment.
- Restrict network access to the Factstore API to internal networks or VPN.
- Enable TLS on all service-to-service communication.
- Review the Grafana dashboards regularly to detect anomalous compliance patterns.
- Store
DB_PASSWORD,VAULT_TOKEN, and SCM tokens in a secrets manager, not in environment files committed to source control.
See docs/ci-integration.md for the complete guide, including examples for:
- GitHub Actions (with
X-Factstore-CI-Context: github-actions) - GitLab CI/CD (with
X-Factstore-CI-Context: gitlab-ci) - Jenkins (with
X-Factstore-CI-Context: jenkins) - CircleCI (with
X-Factstore-CI-Context: circleci) - Azure DevOps (with
X-Factstore-CI-Context: azure-devops)
The X-Factstore-CI-Context header auto-populates gitCommitSha, gitBranch, and buildUrl from well-known CI environment variables. Explicitly provided fields in the request body always take precedence.
GET /actuator/prometheus
OpenFactstore's Spring Boot application exposes metrics via the Micrometer → Prometheus bridge.
Custom metrics:
| Metric | Type | Description |
|---|---|---|
factstore_flows_total |
Counter | Total number of flows created |
factstore_trails_total |
Counter | Total number of trails created |
factstore_attestations_total |
Counter | Attestations recorded, labelled by type and status |
factstore_assert_requests_total |
Counter | Compliance assertions performed |
factstore_assert_compliant_total |
Counter | Assertions that returned compliant |
factstore_assert_noncompliant_total |
Counter | Assertions that returned non-compliant |
factstore_security_scans_total |
Counter | Security scans recorded, labelled by tool |
factstore_gate_evaluations_total |
Counter | Deployment gate evaluations |
Standard Spring Boot metrics (HTTP, JVM, HikariCP, etc.) are also exposed.
The monitoring/prometheus.yml file is pre-configured to scrape the backend at backend:8080. When running with Docker Compose, Prometheus is automatically configured.
The monitoring/dashboards/ directory contains four pre-built dashboards:
| Dashboard | File | Description |
|---|---|---|
| Compliance Overview | compliance-overview.json |
Flow compliance rates, attestation trends |
| Security Scans | security-scans.json |
Vulnerability trends by tool and severity |
| Deployment Gates | deployment-gates.json |
Gate pass/block rates by environment |
| Audit & Forensics | audit-forensics.json |
Audit event volume and anomaly detection |
Dashboards are auto-provisioned via monitoring/provisioning/. Open http://localhost:3000 and log in as admin / changeme (or the value of GF_SECURITY_ADMIN_PASSWORD).
Each build creates a new Trail. Trails are immutable once created — you can only add attestations, not modify existing ones. This creates an append-only, tamper-evident compliance history.
To see the history of compliance for an artifact, search by its SHA-256 digest:
curl -s "http://localhost:8080/api/v1/artifacts?sha256=sha256:e3b0c44..." | jq .To get the full chain of custody for an artifact:
curl -s "http://localhost:8080/api/v1/compliance/artifact/sha256:e3b0c44..." | jq .Trails move through the following states:
IN_PROGRESS → COMPLIANT
→ NON_COMPLIANT
| Status | Meaning |
|---|---|
IN_PROGRESS |
Trail has been created but not all required attestations have been recorded. |
COMPLIANT |
All required attestations are present and passed. The associated artifact is safe to deploy. |
NON_COMPLIANT |
One or more required attestations are missing or failed. |
Every mutating action is recorded in the immutable audit log:
# Query audit events
curl -s "http://localhost:8080/api/v1/audit?trailId=$TRAIL_ID&sortDesc=true" | jq .
# Export full audit trail for a trail
curl -s "http://localhost:8080/api/v1/reports/audit-trail/$TRAIL_ID" | jq .The current version does not implement automatic data pruning. For production:
- PostgreSQL's native partitioning by
createdAtmonth enables efficient pruning of old trails. - Archive trails older than your regulatory retention window (typically 7 years for SOX/PCI) to cold storage before deleting.
- Evidence files in Vault can be soft-deleted via
DELETE /api/v1/evidence/{entityType}/{entityId}and later purged.
For API reference, see docs/API_REFERENCE.md. For deployment, see DEPLOY.md. For security vulnerability reporting, see SECURITY.md.