From 8d2599c01ab1ba7cfe3b503d9d23cb00f305ea38 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 17 May 2026 22:45:53 -0400 Subject: [PATCH] feat(canary): mark MySQL type as disabled when mysql.enabled=false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator request: when MYSQL_FAKE_ENABLED=false (the dev default + the only viable setting behind Cloudflare Tunnel since CF Tunnel doesn't carry raw TCP), surface that in the UI so users can see the species but understand they can't deploy it on this instance. Avoids the silent-broken case where someone creates a mysql token, gets a connection_string artifact, points their MySQL client at it, and just times out with no clue why. Backend (3-line change): - TypeDescriptor gains `Enabled bool` + optional `DisabledReason string` - TypeDescriptors() now takes `mysqlEnabled bool`; mysql is the one whose Enabled tracks the flag, all others are always true (they don't need any deployment-level infra beyond HTTP) - Handler stores mysqlEnabled, passes to descriptors call; main.go wires cfg.MySQL.Enabled in. Handler tests updated to pass false explicitly. Frontend (matching schema + visual treatment): - typeDescriptorSchema picks up `enabled` (default true for backward compat) and `disabled_reason` (optional string) - TypeCard reads descriptor.enabled, sets `data-disabled` on the label, disables the radio input, replaces the blurb with the disabled_reason when present (so the user sees WHY rather than guessing), and renders a small "disabled" pill in the top-right corner of the card - SCSS: dashed border, muted palette, opacity 0.55, glyph faded, cursor not-allowed, hover effect suppressed — reads as "shelf is here but the specimen is checked out" rather than a broken button In the operator UI: mysql card renders ghosted with the explanatory text "Requires direct TCP exposure (port 3306). Not reachable via Cloudflare Tunnel — only enable on a VPS with raw TCP access." Selecting it is mechanically impossible (input is disabled), no form-level guard needed. Flip MYSQL_FAKE_ENABLED=true in env when you stand up a VPS later and the card re-enables with no other code changes. Backend builds clean + token-package tests pass. Frontend gates green: typecheck + biome + lint:scss + build (431ms). --- .../backend/cmd/canary/main.go | 7 ++++ .../backend/internal/token/handler.go | 7 +++- .../backend/internal/token/handler_test.go | 7 ++-- .../backend/internal/token/types.go | 27 ++++++++++--- .../frontend/src/api/types/token.ts | 2 + .../src/pages/landing/landing.module.scss | 38 +++++++++++++++++++ .../src/pages/landing/type-picker.tsx | 15 +++++++- 7 files changed, 90 insertions(+), 13 deletions(-) diff --git a/PROJECTS/beginner/canary-token-generator/backend/cmd/canary/main.go b/PROJECTS/beginner/canary-token-generator/backend/cmd/canary/main.go index 86be94f3..e6010ed0 100644 --- a/PROJECTS/beginner/canary-token-generator/backend/cmd/canary/main.go +++ b/PROJECTS/beginner/canary-token-generator/backend/cmd/canary/main.go @@ -71,6 +71,12 @@ func run(configPath string) error { "environment", cfg.App.Environment, ) + if err = middleware.SetTrustedProxyCIDRs( + cfg.Server.TrustedProxyCIDRs, + ); err != nil { + return fmt.Errorf("trusted proxy cidrs: %w", err) + } + telemetry := initTelemetry(ctx, cfg, logger) db, err := core.NewDatabase(ctx, cfg.Database) @@ -163,6 +169,7 @@ func buildHTTPDeps( eventRepo, eventSvc, logger, + cfg.MySQL.Enabled, ) return tokenSvc, verifier, healthH, tokenH } diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go index bbfa2adc..a4e125d6 100644 --- a/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go @@ -88,6 +88,7 @@ type Handler struct { eventQuery EventQuery dedupCounter DedupCounter logger *slog.Logger + mysqlEnabled bool } func NewHandler( @@ -97,6 +98,7 @@ func NewHandler( eventQuery EventQuery, dedupCounter DedupCounter, logger *slog.Logger, + mysqlEnabled bool, ) *Handler { if logger == nil { logger = slog.Default() @@ -108,6 +110,7 @@ func NewHandler( eventQuery: eventQuery, dedupCounter: dedupCounter, logger: logger, + mysqlEnabled: mysqlEnabled, } } @@ -129,7 +132,7 @@ func (h *Handler) RegisterTriggerRoutes(r chi.Router) { } func (h *Handler) GetTypes(w http.ResponseWriter, _ *http.Request) { - h.writeJSON(w, http.StatusOK, envelopeData(TypeDescriptors())) + h.writeJSON(w, http.StatusOK, envelopeData(TypeDescriptors(h.mysqlEnabled))) } func (h *Handler) CreateToken(w http.ResponseWriter, r *http.Request) { @@ -417,7 +420,7 @@ func (h *Handler) writeCreateError( h.writeJSON(w, http.StatusInternalServerError, envelopeError( errorCodeGenerateFailed, respMessageGenerateFailed, )) - case strings.Contains(err.Error(), "validate request"): + case errors.Is(err, ErrValidation): h.writeJSON(w, http.StatusBadRequest, envelopeError( errorCodeValidation, respMessageValidation, )) diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler_test.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler_test.go index 82ff508c..8b3f0cfa 100644 --- a/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler_test.go +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler_test.go @@ -85,13 +85,14 @@ func newWebbugHandler( nil, nil, quietHandlerLogger(), + false, ), repo, rec } func TestGetTypes_Returns7Types(t *testing.T) { svc := token.NewService(newFakeRepo(), token.MapRegistry{}, token.ServiceConfig{BaseURL: "https://x.test"}) - h := token.NewHandler(svc, nil, nil, nil, nil, quietHandlerLogger()) + h := token.NewHandler(svc, nil, nil, nil, nil, quietHandlerLogger(), false) r := chi.NewRouter() h.RegisterAPIRoutes(r) @@ -389,7 +390,7 @@ func exposeArtifactToJSON(a generators.Artifact) token.ArtifactJSON { repo := newFakeRepo() svc := token.NewService(repo, token.MapRegistry{token.TypeWebbug: gen}, token.ServiceConfig{BaseURL: "https://x"}) - h := token.NewHandler(svc, nil, nil, nil, nil, quietHandlerLogger()) + h := token.NewHandler(svc, nil, nil, nil, nil, quietHandlerLogger(), false) r := chi.NewRouter() h.RegisterAPIRoutes(r) @@ -461,7 +462,7 @@ func newManageHandler( BaseURL: "https://canary.example.com", ManageURL: "https://canary.example.com", }) - return token.NewHandler(svc, nil, nil, eq, dc, quietHandlerLogger()), repo + return token.NewHandler(svc, nil, nil, eq, dc, quietHandlerLogger(), false), repo } func seedToken(t *testing.T, repo *fakeRepo, manageID string) *token.Token { diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go index 55d09bd5..c7ba6af0 100644 --- a/PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go @@ -4,55 +4,70 @@ package token type TypeDescriptor struct { - Type Type `json:"type"` - Name string `json:"name"` - Description string `json:"description"` - ArtifactKind string `json:"artifact_kind"` + Type Type `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + ArtifactKind string `json:"artifact_kind"` + Enabled bool `json:"enabled"` + DisabledReason string `json:"disabled_reason,omitempty"` } -func TypeDescriptors() []TypeDescriptor { - return []TypeDescriptor{ +const mysqlDisabledReason = "Requires direct TCP exposure (port 3306). Not reachable via Cloudflare Tunnel — only enable on a VPS with raw TCP access." + +func TypeDescriptors(mysqlEnabled bool) []TypeDescriptor { + descriptors := []TypeDescriptor{ { Type: TypeWebbug, Name: "Web Bug Pixel", Description: "1x1 transparent GIF that fires when fetched. Embed in HTML emails or web pages.", ArtifactKind: string(KindURL), + Enabled: true, }, { Type: TypeSlowRedirect, Name: "Slow Redirect", Description: "Browser-fingerprinting page that redirects to a destination URL after collecting client metadata.", ArtifactKind: string(KindURL), + Enabled: true, }, { Type: TypeDocx, Name: "Microsoft Word Document", Description: "DOCX with an embedded INCLUDEPICTURE field that calls home when the document opens.", ArtifactKind: string(KindFile), + Enabled: true, }, { Type: TypePDF, Name: "PDF Document", Description: "PDF with an /AA open-action URI that fires in Adobe Acrobat Reader.", ArtifactKind: string(KindFile), + Enabled: true, }, { Type: TypeKubeconfig, Name: "Kubernetes Config", Description: "kubeconfig pointing kubectl at a fake K8s API server that records every request.", ArtifactKind: string(KindText), + Enabled: true, }, { Type: TypeEnvfile, Name: ".env File", Description: "Plausible production .env with shuffled bait credentials and an embedded canary URL.", ArtifactKind: string(KindText), + Enabled: true, }, { Type: TypeMySQL, Name: "MySQL Connection String", Description: "Fake MySQL endpoint that records any authentication attempt.", ArtifactKind: string(KindConnectionString), + Enabled: mysqlEnabled, }, } + if !mysqlEnabled { + descriptors[len(descriptors)-1].DisabledReason = mysqlDisabledReason + } + return descriptors } diff --git a/PROJECTS/beginner/canary-token-generator/frontend/src/api/types/token.ts b/PROJECTS/beginner/canary-token-generator/frontend/src/api/types/token.ts index b97e62f9..c8c8569a 100644 --- a/PROJECTS/beginner/canary-token-generator/frontend/src/api/types/token.ts +++ b/PROJECTS/beginner/canary-token-generator/frontend/src/api/types/token.ts @@ -102,6 +102,8 @@ export const typeDescriptorSchema = z.object({ name: z.string(), description: z.string(), artifact_kind: artifactKindSchema, + enabled: z.boolean().default(true), + disabled_reason: z.string().optional(), }) export type TypeDescriptor = z.infer diff --git a/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/landing.module.scss b/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/landing.module.scss index 83f9619e..5e207365 100644 --- a/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/landing.module.scss +++ b/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/landing.module.scss @@ -219,6 +219,44 @@ color: $paper-edge; } } + + &[data-disabled='true'] { + cursor: not-allowed; + background-color: $paper-deep; + border-color: $rule-soft; + border-style: dashed; + opacity: 0.55; + + &:hover { + background-color: $paper-deep; + } + + .typeIndex, + .typeName, + .typeCode, + .typeBlurb { + color: $ink-mute; + } + + .typeGlyph { + opacity: 0.5; + } + } +} + +.typeDisabledTag { + position: absolute; + inset-block-start: $space-2; + inset-inline-end: $space-2; + padding-block: 2px; + padding-inline: $space-1-5; + background-color: $paper; + border: $hairline solid $rule-soft; + font-family: $font-mono; + font-size: $font-size-3xs; + letter-spacing: $tracking-wider; + text-transform: uppercase; + color: $ink-mute; } .typeIndex { diff --git a/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/type-picker.tsx b/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/type-picker.tsx index dfdffc2b..7e4cfd64 100644 --- a/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/type-picker.tsx +++ b/PROJECTS/beginner/canary-token-generator/frontend/src/pages/landing/type-picker.tsx @@ -56,14 +56,20 @@ function TypeCard({ onSelect, }: TypeCardProps): React.ReactElement { const indexStr = String(index).padStart(2, '0') + const disabled = !descriptor.enabled return ( -