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 ( -