feat(demo): Tier 1 zero-deps demo + Tier 2 docker-compose scaffold

Tier 1 (cre demo): in-memory SQLite + tempfile .env rotator. Walks
through inventory -> before -> 4-step rotation -> after -> audit chain
verify. Narrates every step event live with green/red glyphs. Runs in
under 1 second on commodity hardware, no external deps.

Tier 2 (docker/docker-compose.yml): postgres:16, localstack
(secretsmanager), hashicorp/vault dev mode, plus a fake-GitHub Flask
app that mocks /user, POST/DELETE /user/personal-access-tokens.
config/demo-full.cr.example documents env vars for the full stack.

Also fixed env_file rotator newline preservation (lines(chomp: true)
+ explicit \n join) so the rotated file is properly delimited.
This commit is contained in:
CarterPerez-dev 2026-04-29 01:16:08 -04:00
parent 7ce0cb48bd
commit a19e8669d2
6 changed files with 235 additions and 8 deletions

View File

@ -0,0 +1,23 @@
# ©AngelaMos | 2026
# demo-full.cr.example
# Tier 2 demo config: docker-compose stack on localhost.
# Copy to demo-full.cr and load via `cre run --config=...` (full config loader
# wired in a future iteration).
# Database:
# DATABASE_URL=postgres://cre:cre@localhost:5432/cre
# AWS Secrets Manager (LocalStack):
# AWS_ACCESS_KEY_ID=test
# AWS_SECRET_ACCESS_KEY=test
# AWS_REGION=us-east-1
# AWS_ENDPOINT=http://localhost:4566
# Vault:
# VAULT_ADDR=http://localhost:8200
# VAULT_TOKEN=dev-root-token
# GitHub (fake API):
# GITHUB_API_BASE=http://localhost:7000
# GITHUB_TOKEN=ghp_admin_fake

View File

@ -0,0 +1,39 @@
# ©AngelaMos | 2026
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: cre
POSTGRES_PASSWORD: cre
POSTGRES_DB: cre
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cre"]
interval: 3s
timeout: 3s
retries: 10
localstack:
image: localstack/localstack:latest
environment:
SERVICES: secretsmanager
DEBUG: 0
ports: ["4566:4566"]
vault:
image: hashicorp/vault:latest
cap_add: [IPC_LOCK]
environment:
VAULT_DEV_ROOT_TOKEN_ID: dev-root-token
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
ports: ["8200:8200"]
command: vault server -dev
fake-github:
image: python:3.13-alpine
working_dir: /app
volumes: ["./fake-github:/app:ro"]
command: ["sh", "-c", "pip install --quiet flask && python /app/app.py"]
ports: ["7000:7000"]

View File

@ -0,0 +1,42 @@
"""
©AngelaMos | 2026
app.py
"""
from flask import Flask, jsonify, request
app = Flask(__name__)
TOKENS: dict[int, dict] = {}
NEXT_ID = 100000
@app.route("/user", methods=["GET"])
def user():
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return jsonify({"message": "Bad credentials"}), 401
return jsonify({"login": "fake-bot", "id": 1})
@app.route("/user/personal-access-tokens", methods=["POST"])
def create_pat():
global NEXT_ID
body = request.get_json() or {}
pat_id = NEXT_ID
NEXT_ID += 1
TOKENS[pat_id] = body
return jsonify({
"id": pat_id,
"token": f"ghp_fake_{pat_id}",
"expires_at": "2026-12-31T00:00:00Z",
})
@app.route("/user/personal-access-tokens/<int:pat_id>", methods=["DELETE"])
def delete_pat(pat_id):
TOKENS.pop(pat_id, None)
return "", 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7000)

View File

@ -0,0 +1,22 @@
# ===================
# ©AngelaMos | 2026
# tier_1_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/demo/tier_1"
describe CRE::Demo::Tier1 do
it "runs end-to-end and reports rotation success" do
io = IO::Memory.new
code = CRE::Demo::Tier1.run(io)
code.should eq 0
out = CRE::Tui::Ansi.strip(io.to_s)
out.should contain "Tier 1 demo"
out.should contain "BEFORE"
out.should contain "AFTER"
out.should contain "rotation completed"
out.should contain "audit events, hash chain valid"
end
end

View File

@ -3,14 +3,115 @@
# tier_1.cr
# ===================
require "../engine/event_bus"
require "../engine/rotation_orchestrator"
require "../persistence/sqlite/sqlite_persistence"
require "../rotators/env_file"
require "../audit/audit_log"
require "../tui/ansi"
module CRE::Demo
# Tier 1 demo is implemented in Phase 15. This stub keeps `cre demo`
# wired and compilable until then.
module Tier1
def self.run(io : IO) : Int32
io.puts "Tier 1 demo not yet implemented (Phase 15 of build)."
io.puts "Try: cre run --db=sqlite:cre.db"
0
tmp_env = File.tempname("cre-demo-", ".env")
File.write(tmp_env, "API_KEY=oldvalue-aaa\nOTHER=keep\n")
io.puts CRE::Tui::Ansi.cyan("Credential Rotation Enforcer - Tier 1 demo")
io.puts " (in-memory SQLite + ephemeral .env file rotator, zero external deps)"
io.puts ""
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
log = CRE::Audit::AuditLog.new(persist, Bytes.new(32, 0_u8), 1, 1024)
cred_id = UUID.random
cred = CRE::Domain::Credential.new(
id: cred_id,
external_id: "demo-#{tmp_env}",
kind: CRE::Domain::CredentialKind::EnvFile,
name: "API_KEY",
tags: {
"path" => tmp_env,
"key" => "API_KEY",
"bytes" => "16",
} of String => String,
updated_at: Time.utc - 60.days,
)
persist.credentials.insert(cred)
io.puts CRE::Tui::Ansi.bold("Step 1 - Inventory:")
io.puts " - #{cred.kind} '#{cred.name}' (id=#{short(cred.id)})"
io.puts " last updated #{cred.updated_at.to_rfc3339} (60 days ago - overdue)"
io.puts ""
io.puts CRE::Tui::Ansi.bold("Step 2 - File contents BEFORE:")
File.read(tmp_env).each_line { |line| io.puts " #{line}" }
io.puts ""
io.puts CRE::Tui::Ansi.bold("Step 3 - Rotating (4-step contract):")
bus = CRE::Engine::EventBus.new
ch = bus.subscribe(buffer: 256)
bus.run
rotator = CRE::Rotators::EnvFileRotator.new
orchestrator = CRE::Engine::RotationOrchestrator.new(bus, persist)
state = orchestrator.run(cred, rotator)
sleep 0.05.seconds
drain_steps(ch, io)
bus.stop
io.puts ""
io.puts CRE::Tui::Ansi.bold("Step 4 - File contents AFTER:")
File.read(tmp_env).each_line { |line| io.puts " #{line}" }
io.puts ""
io.puts CRE::Tui::Ansi.bold("Step 5 - Audit chain verification:")
ok = log.verify_chain
latest_seq = persist.audit.latest_seq
if ok
io.puts " #{CRE::Tui::Ansi.green("")} #{latest_seq} audit events, hash chain valid"
else
io.puts " #{CRE::Tui::Ansi.red("")} audit chain BROKEN"
end
persist.close
File.delete(tmp_env) if File.exists?(tmp_env)
io.puts ""
io.puts CRE::Tui::Ansi.dim("Demo complete. State #{state}. Try 'cre run --db=sqlite:cre.db' for the daemon.")
state == CRE::Persistence::RotationState::Completed ? 0 : 1
end
private def self.drain_steps(ch : ::Channel(CRE::Events::Event), io : IO) : Nil
loop do
select
when ev = ch.receive
narrate(ev, io)
else
break
end
end
end
private def self.narrate(ev : CRE::Events::Event, io : IO) : Nil
case ev
when CRE::Events::RotationStepStarted
io.puts " - step started: #{ev.step}"
when CRE::Events::RotationStepCompleted
io.puts " #{CRE::Tui::Ansi.green("")} step completed: #{ev.step}"
when CRE::Events::RotationStepFailed
io.puts " #{CRE::Tui::Ansi.red("")} step failed: #{ev.step} (#{ev.error})"
when CRE::Events::RotationCompleted
io.puts " #{CRE::Tui::Ansi.green("")} rotation completed"
when CRE::Events::RotationFailed
io.puts " #{CRE::Tui::Ansi.red("")} rotation failed: #{ev.reason}"
end
end
private def self.short(uuid : UUID) : String
uuid.to_s[0, 8]
end
end
end

View File

@ -44,11 +44,11 @@ module CRE::Rotators
pending_path = "#{path}.pending"
existing = File.exists?(path) ? File.read(path) : ""
lines = existing.lines.reject { |line| line.strip.starts_with?("#{key}=") }
lines = existing.lines(chomp: true).reject { |line| line.strip.starts_with?("#{key}=") }
new_value = String.new(s.ciphertext)
lines << "#{key}=#{new_value}\n"
lines << "#{key}=#{new_value}"
File.write(pending_path, lines.join, perm: 0o600)
File.write(pending_path, lines.join('\n') + "\n", perm: 0o600)
end
def verify(c : Domain::Credential, s : Domain::NewSecret) : Bool