Skip to content

Commit 764df31

Browse files
committed
Validate edge config candidates
1 parent ae3ec61 commit 764df31

10 files changed

Lines changed: 366 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- Added fixture integration evidence refs and validation coverage for integration evidence.
3030
- Added Forgejo migration candidate validation for the GitHub Enterprise assessment path.
3131
- Added identity realm/client candidate validation for the Okta/Auth0 assessment path.
32+
- Added edge VCL, HAProxy, and Coraza candidate validation for the Cloudflare/Akamai assessment path.
3233
- Updated CI and release workflows to Node.js 24-native GitHub Actions.
3334

3435
## 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, Forgejo migration, and identity realm/client 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, identity realm/client, and edge VCL/HAProxy/Coraza 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.
@@ -162,7 +162,7 @@ GitHub Enterprise to Forgejo collects repository, team, branch protection, Actio
162162

163163
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

165-
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.
165+
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 validated VCL, HAProxy, Coraza, cache parity, and WAF enforcement review artifacts.
166166

167167
OpenAI/Anthropic to vLLM/LiteLLM collects model usage classes, token volumes, latency expectations, sensitive prompt categories, tool usage, and fallback behavior from local fixtures. It can also collect model-grouped aggregate OpenAI completions usage, aggregate Anthropic Messages API usage, available model metadata where exposed, server web-search tool metadata, and hourly peak estimates from live provider APIs without storing prompts or credentials. The path generates self-hosted LLM readiness, LiteLLM routing, vLLM sizing, evaluation, and data sensitivity artifacts.
168168

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ GitHub Enterprise to Forgejo keeps the same local-first collect, normalize, anal
1212

1313
Okta/Auth0 to Keycloak/Zitadel reuses the same normalized inventory and evidence model, but generates identity-specific planning artifacts and a candidate realm/client YAML instead of Datadog target configs. It supports fixture import and read-only live Okta/Auth0 collectors.
1414

15-
Cloudflare/Akamai to Varnish/HAProxy/Coraza generates edge-specific VCL, HAProxy, and Coraza candidate files plus cache and WAF review reports. It supports fixture import plus read-only live Cloudflare and Akamai collectors.
15+
Cloudflare/Akamai to Varnish/HAProxy/Coraza generates and validates edge-specific VCL, HAProxy, and Coraza candidate files plus cache and WAF review reports. It supports fixture import plus read-only live Cloudflare and Akamai collectors.
1616

1717
OpenAI/Anthropic to vLLM/LiteLLM generates AI provider readiness, LiteLLM routing, vLLM sizing, evaluation, and data sensitivity artifacts. It supports fixture import plus read-only live OpenAI/Anthropic aggregate usage collectors.
1818

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, 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.
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, edge VCL/HAProxy/Coraza 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`; the identity path also supports `realm-client-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`; the edge path also supports `vcl-candidates`, `haproxy-candidates`, and `coraza-rule-candidates`.
3535

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

docs/cloudflare-akamai-to-varnish-haproxy-coraza.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ Generated artifacts:
5050
- `cache-parity-report.md`
5151
- `waf-enforcement-risk-report.md`
5252

53+
Validation checks edge candidates for OpenExit review markers, absence of secret-like content, Varnish backend/cache/page-rule coverage, HAProxy redirect/origin/TLS/health-check coverage, Coraza WAF/bot-rule coverage, and preserved evidence refs.
54+
5355
The assessment flags wildcard and high-TTL DNS records, observe-only or managed WAF rules, challenge actions, cache-key parity issues, redirect pattern translation, origin TLS gaps, low TLS versions, disabled HSTS, and coupled page-rule behavior.

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, Forgejo migration, and identity realm/client 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, identity realm/client, and edge VCL/HAProxy/Coraza 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.
@@ -53,6 +53,7 @@ The AI provider path is complete for local fixture assessment workflows and incl
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.
5555
- [ ] `validation/validation-report.md` includes `identity-realm-client-candidate: passed` for the Okta/Auth0 to Keycloak/Zitadel pipeline.
56+
- [ ] `validation/validation-report.md` includes `edge-candidates: passed` for the Cloudflare/Akamai to Varnish/HAProxy/Coraza pipeline.
5657
- [ ] `CHANGELOG.md` has a `0.1.0` section.
5758
- [ ] Exported bundle README includes version, commit, build date, bundle timestamp, and candidate warning.
5859
- [ ] 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
@@ -618,20 +618,7 @@ func TestValidationRejectsBrokenIdentityClientCandidate(t *testing.T) {
618618

619619
func TestEdgeFixturePipeline(t *testing.T) {
620620
projectDir := filepath.Join(t.TempDir(), "edge-demo")
621-
fixturePath := filepath.Join("..", "..", "testdata", "edge", "small.json")
622-
623-
commands := [][]string{
624-
{"init", projectDir, "--source", "edge", "--target", "varnish-haproxy-coraza"},
625-
{"collect", "edge-fixture", "--project", projectDir, "--input", fixturePath},
626-
{"assess", "--project", projectDir, "--target", "varnish-haproxy-coraza"},
627-
{"generate", "--project", projectDir, "--all"},
628-
{"validate", "--project", projectDir},
629-
}
630-
for _, args := range commands {
631-
if err := executeForTest(args...); err != nil {
632-
t.Fatalf("openexit %s failed: %v", strings.Join(args, " "), err)
633-
}
634-
}
621+
runEdgeFixturePipeline(t, projectDir)
635622
for _, rel := range []string{
636623
"inventory/openexit.inventory.yaml",
637624
"mapping/openexit.mapping.yaml",
@@ -668,6 +655,67 @@ func TestEdgeFixturePipeline(t *testing.T) {
668655
t.Fatalf("expected assessment finding %s", id)
669656
}
670657
}
658+
report, err := os.ReadFile(filepath.Join(projectDir, "validation", "validation-report.md"))
659+
if err != nil {
660+
t.Fatal(err)
661+
}
662+
if !strings.Contains(string(report), "edge-candidates: passed") {
663+
t.Fatalf("expected edge candidate validation marker, got:\n%s", string(report))
664+
}
665+
}
666+
667+
func TestValidationRejectsBrokenEdgeVCLCandidate(t *testing.T) {
668+
projectDir := filepath.Join(t.TempDir(), "edge-demo")
669+
runEdgeFixturePipeline(t, projectDir)
670+
candidatePath := filepath.Join(projectDir, "generated-config", "edge", "varnish", "default.candidate.vcl")
671+
data, err := os.ReadFile(candidatePath)
672+
if err != nil {
673+
t.Fatal(err)
674+
}
675+
corrupt := strings.Replace(string(data), "set beresp.ttl = 86400s", "set beresp.ttl = 60s", 1)
676+
if corrupt == string(data) {
677+
t.Fatal("test fixture did not contain VCL edge TTL")
678+
}
679+
if err := os.WriteFile(candidatePath, []byte(corrupt), 0o644); err != nil {
680+
t.Fatal(err)
681+
}
682+
if err := executeForTest("validate", "--project", projectDir); err == nil {
683+
t.Fatal("expected validation to fail when VCL TTL drifts from inventory")
684+
}
685+
report, err := os.ReadFile(filepath.Join(projectDir, "validation", "validation-report.md"))
686+
if err != nil {
687+
t.Fatal(err)
688+
}
689+
if !strings.Contains(string(report), "edge-candidates") || !strings.Contains(string(report), "edge TTL") {
690+
t.Fatalf("expected edge candidate TTL failure in validation report, got:\n%s", string(report))
691+
}
692+
}
693+
694+
func TestValidationRejectsBrokenEdgeHAProxyCandidate(t *testing.T) {
695+
projectDir := filepath.Join(t.TempDir(), "edge-demo")
696+
runEdgeFixturePipeline(t, projectDir)
697+
candidatePath := filepath.Join(projectDir, "generated-config", "edge", "haproxy", "haproxy.candidate.cfg")
698+
data, err := os.ReadFile(candidatePath)
699+
if err != nil {
700+
t.Fatal(err)
701+
}
702+
corrupt := strings.Replace(string(data), "ssl verify none sni str(api-origin.example.com)", "ssl verify required sni str(api-origin.example.com)", 1)
703+
if corrupt == string(data) {
704+
t.Fatal("test fixture did not contain HAProxy TLS verify marker")
705+
}
706+
if err := os.WriteFile(candidatePath, []byte(corrupt), 0o644); err != nil {
707+
t.Fatal(err)
708+
}
709+
if err := executeForTest("validate", "--project", projectDir); err == nil {
710+
t.Fatal("expected validation to fail when HAProxy TLS verification drifts from inventory")
711+
}
712+
report, err := os.ReadFile(filepath.Join(projectDir, "validation", "validation-report.md"))
713+
if err != nil {
714+
t.Fatal(err)
715+
}
716+
if !strings.Contains(string(report), "edge-candidates") || !strings.Contains(string(report), "TLS verify none") {
717+
t.Fatalf("expected edge candidate TLS verification failure in validation report, got:\n%s", string(report))
718+
}
671719
}
672720

673721
func TestAIProviderFixturePipeline(t *testing.T) {
@@ -773,6 +821,23 @@ func runIdentityFixturePipeline(t *testing.T, projectDir string) {
773821
}
774822
}
775823

824+
func runEdgeFixturePipeline(t *testing.T, projectDir string) {
825+
t.Helper()
826+
fixturePath := filepath.Join("..", "..", "testdata", "edge", "small.json")
827+
commands := [][]string{
828+
{"init", projectDir, "--source", "edge", "--target", "varnish-haproxy-coraza"},
829+
{"collect", "edge-fixture", "--project", projectDir, "--input", fixturePath},
830+
{"assess", "--project", projectDir, "--target", "varnish-haproxy-coraza"},
831+
{"generate", "--project", projectDir, "--all"},
832+
{"validate", "--project", projectDir},
833+
}
834+
for _, args := range commands {
835+
if err := executeForTest(args...); err != nil {
836+
t.Fatalf("openexit %s failed: %v", strings.Join(args, " "), err)
837+
}
838+
}
839+
}
840+
776841
func executeForTest(args ...string) error {
777842
cmd := NewRootCommand()
778843
cmd.SetArgs(args)

internal/generate/markdown.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,7 @@ func writeEdgeVCLCandidates(ctx *Context) error {
12541254
fmt.Fprintln(&b, "sub vcl_recv {")
12551255
for _, rule := range ctx.Inventory.Assets.CacheRules {
12561256
pattern := vclRegex(rule.Pattern)
1257+
fmt.Fprintf(&b, " # cache-rule %s evidence=%s\n", sanitizeConfigValue(rule.ID), rule.EvidenceRef)
12571258
if strings.EqualFold(rule.Action, "bypass") {
12581259
fmt.Fprintf(&b, " if (req.url ~ %q) { return (pass); }\n", pattern)
12591260
continue
@@ -1264,6 +1265,7 @@ func writeEdgeVCLCandidates(ctx *Context) error {
12641265
}
12651266
for _, rule := range ctx.Inventory.Assets.PageRules {
12661267
if rule.Enabled && containsString(rule.Actions, "cache_everything") {
1268+
fmt.Fprintf(&b, " # page-rule %s evidence=%s\n", sanitizeConfigValue(rule.ID), rule.EvidenceRef)
12671269
fmt.Fprintf(&b, " if (req.http.host + req.url ~ %q) { unset req.http.Cookie; }\n", vclRegex(rule.Target))
12681270
}
12691271
}
@@ -1272,6 +1274,7 @@ func writeEdgeVCLCandidates(ctx *Context) error {
12721274
fmt.Fprintln(&b, "sub vcl_backend_response {")
12731275
for _, rule := range ctx.Inventory.Assets.CacheRules {
12741276
if strings.EqualFold(rule.Action, "cache") && rule.EdgeTTL > 0 {
1277+
fmt.Fprintf(&b, " # cache-rule %s evidence=%s\n", sanitizeConfigValue(rule.ID), rule.EvidenceRef)
12751278
fmt.Fprintf(&b, " if (bereq.url ~ %q) { set beresp.ttl = %ds; }\n", vclRegex(rule.Pattern), rule.EdgeTTL)
12761279
}
12771280
}
@@ -1296,6 +1299,7 @@ func writeVCLBackends(b *strings.Builder, origins []inventory.OriginConfig) {
12961299
if i == 0 {
12971300
name = "default"
12981301
}
1302+
fmt.Fprintf(b, "# origin %s evidence=%s\n", sanitizeConfigValue(origin.ID), origin.EvidenceRef)
12991303
fmt.Fprintf(b, "backend %s {\n", name)
13001304
fmt.Fprintf(b, " .host = %q;\n", origin.Hostname)
13011305
fmt.Fprintf(b, " .port = %q;\n", fmt.Sprintf("%d", origin.Port))
@@ -1326,6 +1330,7 @@ func writeEdgeHAProxyCandidates(ctx *Context) error {
13261330
}
13271331
for _, origin := range ctx.Inventory.Assets.Origins {
13281332
name := configIdentifier(origin.ID)
1333+
fmt.Fprintf(&b, " # origin %s evidence=%s\n", sanitizeConfigValue(origin.ID), origin.EvidenceRef)
13291334
fmt.Fprintf(&b, " acl host_%s hdr(host) -i %s\n", name, sanitizeConfigValue(origin.HostHeader))
13301335
fmt.Fprintf(&b, " use_backend be_%s if host_%s\n", name, name)
13311336
}
@@ -1346,6 +1351,7 @@ func writeEdgeHAProxyCandidates(ctx *Context) error {
13461351
if origin.HealthCheck {
13471352
check = " check"
13481353
}
1354+
fmt.Fprintf(&b, "# origin %s evidence=%s\n", sanitizeConfigValue(origin.ID), origin.EvidenceRef)
13491355
fmt.Fprintf(&b, "backend be_%s\n", name)
13501356
fmt.Fprintf(&b, " server %s %s:%d%s%s\n", name, sanitizeConfigValue(origin.Hostname), origin.Port, ssl, check)
13511357
}
@@ -1371,6 +1377,7 @@ func writeEdgeCorazaCandidates(ctx *Context) error {
13711377
if !rule.Enabled {
13721378
action = "pass,nolog"
13731379
}
1380+
fmt.Fprintf(&b, "# waf-rule %s evidence=%s\n", sanitizeConfigValue(rule.ID), rule.EvidenceRef)
13741381
fmt.Fprintf(&b, "SecRule REQUEST_URI \"@contains %s\" \"id:%d,phase:2,%s,msg:'%s'\"\n", corazaMatchValue(rule.Expression), 100000+i, action, sanitizeConfigValue(rule.Name))
13751382
}
13761383
for i, rule := range ctx.Inventory.Assets.BotRules {
@@ -1381,6 +1388,7 @@ func writeEdgeCorazaCandidates(ctx *Context) error {
13811388
if !rule.Enabled {
13821389
action = "pass,nolog"
13831390
}
1391+
fmt.Fprintf(&b, "# bot-rule %s evidence=%s\n", sanitizeConfigValue(rule.ID), rule.EvidenceRef)
13841392
fmt.Fprintf(&b, "SecRule REQUEST_HEADERS:User-Agent \"@rx .+\" \"id:%d,phase:1,%s,msg:'%s'\"\n", 110000+i, action, sanitizeConfigValue(rule.Name))
13851393
}
13861394
if err := os.WriteFile(filepath.Join(dir, "coraza-rules.candidate.conf"), []byte(b.String()), 0o644); err != nil {

0 commit comments

Comments
 (0)