Skip to content

Commit ae3ec61

Browse files
committed
Validate identity realm candidates
1 parent cca1fcc commit ae3ec61

9 files changed

Lines changed: 434 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- Added a reproducible Datadog example refresh target and checked-in full example output.
2929
- Added fixture integration evidence refs and validation coverage for integration evidence.
3030
- Added Forgejo migration candidate validation for the GitHub Enterprise assessment path.
31+
- Added identity realm/client candidate validation for the Okta/Auth0 assessment path.
3132
- Updated CI and release workflows to Node.js 24-native GitHub Actions.
3233

3334
## 0.1.0 - 2026-05-24

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Included in the current implementation:
116116
- OpenTelemetry Collector sketch.
117117
- ArgoCD starter manifest.
118118
- Typed migration plan manifest and phase-gate Markdown plan.
119-
- Validation report with embedded JSON Schema checks, Grafana dashboard, Prometheus alert, OpenTelemetry collector, ArgoCD, and Forgejo migration candidate checks, YAML/JSON parsing, evidence ref checks, secret scan, and optional `promtool`/`kubeconform` checks.
119+
- Validation report with embedded JSON Schema checks, Grafana dashboard, Prometheus alert, OpenTelemetry collector, ArgoCD, Forgejo migration, and identity realm/client candidate checks, YAML/JSON parsing, evidence ref checks, secret scan, and optional `promtool`/`kubeconform` checks.
120120
- Evidence bundle export.
121121
- Evidence bundle path-safety checks that reject symlinks in exported project sections.
122122
- No-op assist provider and explicit opt-in LiteLLM assist.
@@ -160,7 +160,7 @@ The release checklist lives in `docs/release.md`. A release build should pass `m
160160

161161
GitHub Enterprise to Forgejo collects repository, team, branch protection, Actions workflow, secret metadata, runner, deploy key, and GitHub App installation metadata from live GitHub/GitHub Enterprise APIs or local fixtures. It generates Forgejo migration assessment, CI compatibility, branch protection mapping, runner migration, repository ownership reports, and a validated Forgejo migration candidate YAML.
162162

163-
Okta/Auth0 to Keycloak/Zitadel collects applications, SAML/OIDC client metadata, groups, policies, MFA settings, redirect URIs, owners, and break-glass account metadata from live Okta/Auth0 APIs or local fixtures. It generates identity migration risk, realm/client candidate config, break-glass, cutover, and rollback artifacts.
163+
Okta/Auth0 to Keycloak/Zitadel collects applications, SAML/OIDC client metadata, groups, policies, MFA settings, redirect URIs, owners, and break-glass account metadata from live Okta/Auth0 APIs or local fixtures. It generates identity migration risk, validated realm/client candidate config, break-glass, cutover, and rollback artifacts.
164164

165165
Cloudflare/Akamai to Varnish/HAProxy/Coraza collects DNS records, WAF rules, cache rules, redirects, origins, TLS settings, bot rules, and page rules from live Cloudflare/Akamai APIs or local fixtures. The Akamai collector uses read-only EdgeGrid-authenticated calls for Edge DNS, Property Manager, and optional AppSec metadata. It generates VCL, HAProxy, Coraza, cache parity, and WAF enforcement review artifacts.
166166

docs/cli.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ The CI test suite also runs this definition-of-done pipeline against the fixture
2727

2828
The checked-in Datadog example can be refreshed with `make example VERSION=0.1.0-dev`. CI also runs `make example-smoke` as part of `make verify` to ensure the example fixture still completes the full pipeline.
2929

30-
`openexit validate` performs typed consistency checks, embedded JSON Schema validation, Grafana dashboard candidate validation, Prometheus alert-rule candidate validation, OpenTelemetry collector candidate validation, ArgoCD candidate validation, Forgejo migration candidate validation, YAML/JSON parse checks, evidence reference checks, secret scanning, and optional external tool checks when `promtool` or `kubeconform` are installed.
30+
`openexit validate` performs typed consistency checks, embedded JSON Schema validation, Grafana dashboard candidate validation, Prometheus alert-rule candidate validation, OpenTelemetry collector candidate validation, ArgoCD candidate validation, Forgejo migration candidate validation, identity realm/client candidate validation, YAML/JSON parse checks, evidence reference checks, secret scanning, and optional external tool checks when `promtool` or `kubeconform` are installed.
3131

3232
`openexit export` refuses to package symlinks from exported project sections, even with `--force`, so evidence bundles cannot accidentally include files from outside the project tree.
3333

34-
Generate individual artifacts with `openexit generate --artifact <name>`. The primary Datadog path supports `mapping`, `assessment`, `risk-register`, `manual-review`, `cost-drivers`, `target-architecture`, `acceptance-criteria`, `rollback-plan`, `runbook`, `restore-drill-checklist`, `alert-shadowing-plan`, `migration-plan`, `grafana-dashboards`, `prometheus-rules`, `opentelemetry`, and `argocd`. The GitHub Enterprise path also supports `forgejo-migration-candidate`.
34+
Generate individual artifacts with `openexit generate --artifact <name>`. The primary Datadog path supports `mapping`, `assessment`, `risk-register`, `manual-review`, `cost-drivers`, `target-architecture`, `acceptance-criteria`, `rollback-plan`, `runbook`, `restore-drill-checklist`, `alert-shadowing-plan`, `migration-plan`, `grafana-dashboards`, `prometheus-rules`, `opentelemetry`, and `argocd`. The GitHub Enterprise path also supports `forgejo-migration-candidate`; the identity path also supports `realm-client-candidate`.
3535

3636
The GitHub Enterprise to Forgejo fixture path uses local JSON metadata:
3737

docs/okta-auth0-to-keycloak-zitadel.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,6 @@ Generated artifacts:
6161
- `identity-cutover-plan.md`
6262
- `identity-rollback-plan.md`
6363

64+
Validation checks the identity realm/client candidate for source/target metadata, non-production safety flags, absence of credentials, realm provider metadata, application/client coverage, group coverage, policy coverage, MFA settings, break-glass account metadata, and preserved evidence refs.
65+
6466
The assessment flags missing owners, missing group mappings, insecure redirect URIs, OIDC implicit grant usage, SAML signing metadata gaps, MFA policy gaps, weak MFA factors, and break-glass account issues.

docs/release.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Release-blocking v0.1 requirements:
1616
- Markdown handover artifacts.
1717
- Grafana, Prometheus, OpenTelemetry, and ArgoCD candidate generators.
1818
- Typed migration plan manifest with assessment, pilot, shadow, and cutover phase gates.
19-
- Validation engine with embedded JSON Schema checks, Grafana dashboard, Prometheus alert, OpenTelemetry collector, ArgoCD, and Forgejo migration candidate checks, YAML/JSON parsing, evidence refs, secret scan, optional promtool, and optional kubeconform.
19+
- Validation engine with embedded JSON Schema checks, Grafana dashboard, Prometheus alert, OpenTelemetry collector, ArgoCD, Forgejo migration, and identity realm/client candidate checks, YAML/JSON parsing, evidence refs, secret scan, optional promtool, and optional kubeconform.
2020
- Evidence bundle export with checksums and OpenExit version metadata.
2121
- No-op AI assist and optional external assist behind explicit opt-in.
2222
- Documentation, examples, CI, release draft workflow, and reproducible release artifacts.
@@ -52,6 +52,7 @@ The AI provider path is complete for local fixture assessment workflows and incl
5252
- [ ] `validation/validation-report.md` includes `opentelemetry-candidate: passed` for the Datadog definition-of-done pipeline.
5353
- [ ] `validation/validation-report.md` includes `argocd-candidate: passed` for the Datadog definition-of-done pipeline.
5454
- [ ] `validation/validation-report.md` includes `forgejo-migration-candidate: passed` for the GitHub Enterprise to Forgejo pipeline.
55+
- [ ] `validation/validation-report.md` includes `identity-realm-client-candidate: passed` for the Okta/Auth0 to Keycloak/Zitadel pipeline.
5556
- [ ] `CHANGELOG.md` has a `0.1.0` section.
5657
- [ ] Exported bundle README includes version, commit, build date, bundle timestamp, and candidate warning.
5758
- [ ] Export refuses symlinks in exported project sections, including when `--force` is used.

internal/app/e2e_test.go

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -516,20 +516,7 @@ func TestValidationRejectsBrokenForgejoRepositoryCandidate(t *testing.T) {
516516

517517
func TestIdentityFixturePipeline(t *testing.T) {
518518
projectDir := filepath.Join(t.TempDir(), "identity-demo")
519-
fixturePath := filepath.Join("..", "..", "testdata", "identity", "small.json")
520-
521-
commands := [][]string{
522-
{"init", projectDir, "--source", "identity", "--target", "keycloak-zitadel"},
523-
{"collect", "identity-fixture", "--project", projectDir, "--input", fixturePath},
524-
{"assess", "--project", projectDir, "--target", "keycloak-zitadel"},
525-
{"generate", "--project", projectDir, "--all"},
526-
{"validate", "--project", projectDir},
527-
}
528-
for _, args := range commands {
529-
if err := executeForTest(args...); err != nil {
530-
t.Fatalf("openexit %s failed: %v", strings.Join(args, " "), err)
531-
}
532-
}
519+
runIdentityFixturePipeline(t, projectDir)
533520
for _, rel := range []string{
534521
"inventory/openexit.inventory.yaml",
535522
"mapping/openexit.mapping.yaml",
@@ -566,6 +553,67 @@ func TestIdentityFixturePipeline(t *testing.T) {
566553
t.Fatalf("expected assessment finding %s", id)
567554
}
568555
}
556+
report, err := os.ReadFile(filepath.Join(projectDir, "validation", "validation-report.md"))
557+
if err != nil {
558+
t.Fatal(err)
559+
}
560+
if !strings.Contains(string(report), "identity-realm-client-candidate: passed") {
561+
t.Fatalf("expected identity candidate validation marker, got:\n%s", string(report))
562+
}
563+
}
564+
565+
func TestValidationRejectsProductionReadyIdentityCandidate(t *testing.T) {
566+
projectDir := filepath.Join(t.TempDir(), "identity-demo")
567+
runIdentityFixturePipeline(t, projectDir)
568+
candidatePath := filepath.Join(projectDir, "generated-config", "identity", "realm-client-candidate.yaml")
569+
data, err := os.ReadFile(candidatePath)
570+
if err != nil {
571+
t.Fatal(err)
572+
}
573+
corrupt := strings.Replace(string(data), "productionReady: false", "productionReady: true", 1)
574+
if corrupt == string(data) {
575+
t.Fatal("test fixture did not contain productionReady marker")
576+
}
577+
if err := os.WriteFile(candidatePath, []byte(corrupt), 0o644); err != nil {
578+
t.Fatal(err)
579+
}
580+
if err := executeForTest("validate", "--project", projectDir); err == nil {
581+
t.Fatal("expected validation to fail when identity candidate is marked production ready")
582+
}
583+
report, err := os.ReadFile(filepath.Join(projectDir, "validation", "validation-report.md"))
584+
if err != nil {
585+
t.Fatal(err)
586+
}
587+
if !strings.Contains(string(report), "identity-realm-client-candidate") || !strings.Contains(string(report), "productionReady") {
588+
t.Fatalf("expected identity candidate productionReady failure in validation report, got:\n%s", string(report))
589+
}
590+
}
591+
592+
func TestValidationRejectsBrokenIdentityClientCandidate(t *testing.T) {
593+
projectDir := filepath.Join(t.TempDir(), "identity-demo")
594+
runIdentityFixturePipeline(t, projectDir)
595+
candidatePath := filepath.Join(projectDir, "generated-config", "identity", "realm-client-candidate.yaml")
596+
data, err := os.ReadFile(candidatePath)
597+
if err != nil {
598+
t.Fatal(err)
599+
}
600+
corrupt := strings.Replace(string(data), "clientId: api-web", "clientId: api-web-drifted", 1)
601+
if corrupt == string(data) {
602+
t.Fatal("test fixture did not contain clientId metadata")
603+
}
604+
if err := os.WriteFile(candidatePath, []byte(corrupt), 0o644); err != nil {
605+
t.Fatal(err)
606+
}
607+
if err := executeForTest("validate", "--project", projectDir); err == nil {
608+
t.Fatal("expected validation to fail when identity client candidate drifts from inventory")
609+
}
610+
report, err := os.ReadFile(filepath.Join(projectDir, "validation", "validation-report.md"))
611+
if err != nil {
612+
t.Fatal(err)
613+
}
614+
if !strings.Contains(string(report), "identity-realm-client-candidate") || !strings.Contains(string(report), "clientId") {
615+
t.Fatalf("expected identity candidate clientId failure in validation report, got:\n%s", string(report))
616+
}
569617
}
570618

571619
func TestEdgeFixturePipeline(t *testing.T) {
@@ -708,6 +756,23 @@ func runGitHubEnterpriseFixturePipeline(t *testing.T, projectDir string) {
708756
}
709757
}
710758

759+
func runIdentityFixturePipeline(t *testing.T, projectDir string) {
760+
t.Helper()
761+
fixturePath := filepath.Join("..", "..", "testdata", "identity", "small.json")
762+
commands := [][]string{
763+
{"init", projectDir, "--source", "identity", "--target", "keycloak-zitadel"},
764+
{"collect", "identity-fixture", "--project", projectDir, "--input", fixturePath},
765+
{"assess", "--project", projectDir, "--target", "keycloak-zitadel"},
766+
{"generate", "--project", projectDir, "--all"},
767+
{"validate", "--project", projectDir},
768+
}
769+
for _, args := range commands {
770+
if err := executeForTest(args...); err != nil {
771+
t.Fatalf("openexit %s failed: %v", strings.Join(args, " "), err)
772+
}
773+
}
774+
}
775+
711776
func executeForTest(args ...string) error {
712777
cmd := NewRootCommand()
713778
cmd.SetArgs(args)

internal/generate/markdown.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -852,11 +852,15 @@ func writeIdentityRealmClientCandidate(ctx *Context) error {
852852
"name": inventory.Slug(ctx.Assessment.Metadata.Project),
853853
"provider": ctx.Inventory.Source.Site,
854854
},
855-
"clients": identityClientCandidates(ctx.Inventory.Assets.IdentityApps),
856-
"groups": identityGroupCandidates(ctx.Inventory.Assets.IdentityGroups),
857-
"policies": identityPolicyCandidates(ctx.Inventory.Assets.IdentityPolicies),
858-
"mfaSettings": identityMFACandidates(ctx.Inventory.Assets.MFASettings),
859-
"breakGlassAccounts": identityBreakGlassCandidates(ctx.Inventory.Assets.BreakGlassAccounts),
855+
"clients": identityClientCandidates(ctx.Inventory.Assets.IdentityApps),
856+
"groups": identityGroupCandidates(ctx.Inventory.Assets.IdentityGroups),
857+
"policies": identityPolicyCandidates(ctx.Inventory.Assets.IdentityPolicies),
858+
"mfaSettings": identityMFACandidates(ctx.Inventory.Assets.MFASettings),
859+
"breakGlassAccounts": identityBreakGlassCandidates(ctx.Inventory.Assets.BreakGlassAccounts),
860+
"humanReviewRequired": true,
861+
"credentialsIncluded": false,
862+
"productionReady": false,
863+
"candidateGeneratedFrom": "redacted-openexit-inventory",
860864
}
861865
data, err := yaml.Marshal(candidate)
862866
if err != nil {
@@ -865,7 +869,7 @@ func writeIdentityRealmClientCandidate(ctx *Context) error {
865869
if err := os.WriteFile(filepath.Join(dir, "realm-client-candidate.yaml"), data, 0o644); err != nil {
866870
return err
867871
}
868-
readme := "# Identity Candidate Config\n\nThis directory contains deterministic Keycloak/Zitadel candidate configuration derived from redacted identity inventory. Review every client, redirect URI, policy, and break-glass account before use.\n"
872+
readme := "# Identity Candidate Config\n\nThis directory contains deterministic Keycloak/Zitadel candidate configuration derived from redacted identity inventory. It contains no client secrets, user passwords, MFA secrets, or token values, and it is not production-ready. Review every client, redirect URI, policy, MFA setting, and break-glass account before use.\n"
869873
return os.WriteFile(filepath.Join(dir, "README.md"), []byte(readme), 0o644)
870874
}
871875

0 commit comments

Comments
 (0)