From 0a456f97aeb667904435f1cc75cfa39b401e6757 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Wed, 13 May 2026 15:16:41 -0400 Subject: [PATCH] feat(canary): token HTTP handlers (API + trigger routes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 9 task 9.6. Routes: RegisterAPIRoutes(r): GET /tokens/types → static list of 7 type descriptors POST /tokens → CreateToken (validation + service.Create) RegisterTriggerRoutes(r): GET /c/{id} → HandleTrigger (webbug, slowredirect, docx, pdf, envfile use this path) POST /c/{id}/fingerprint → HandleFingerprint (slowredirect) HandleFunc /k/{id} → HandleTrigger (kubeconfig, bare path) HandleFunc /k/{id}/* → HandleTrigger (kubeconfig wildcard, any HTTP method) HandleTrigger flow: - extract token id from chi.URLParam - service.GetByID (returns nil-nil for not-found) - if token exists and !enabled, treat as unknown - resolve generator (known token → its type; unknown + path prefix /k/ → kubeconfig; unknown otherwise → webbug) - generator.Trigger(ctx, tok, r) → event + response - record event via EventRecorder interface if present (Phase 10 wires real event.Service) - write response: headers + body OR 302 + Location for redirect CreateToken flow: - 64KB body cap via http.MaxBytesReader - JSON decode (bad → 400 BAD_JSON) - extract fingerprint + real IP via middleware helpers - service.Create - error switch: validation/unknown-type/destination/include-keys map to 400; generator-failure to 500 - happy path: 201 with {token, artifact} envelope per spec §8.2 ArtifactJSON discriminator pattern: - kind=url → url + destination_url - kind=file → filename + content_type + content_b64 (base64) - kind=text → filename + content_type + content (raw) - kind=conn... → connection_string Tests (~10 cases): 7-type list, happy path with envelope assertion, bad JSON, validation failure, known-token records event + writes response, unknown-token still returns shape (defense-in-depth), disabled-token treated as unknown, fingerprint without recorder returns 204, ArtifactJSON discriminator for all 4 kinds. Phase 9 task 9.6 discharged. Wire-up + config + webbug refactor + integration test next. --- .../backend/internal/token/handler.go | 323 +++++++++++++++ .../backend/internal/token/handler_test.go | 371 ++++++++++++++++++ .../backend/internal/token/types.go | 58 +++ 3 files changed, 752 insertions(+) create mode 100644 PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go create mode 100644 PROJECTS/beginner/canary-token-generator/backend/internal/token/handler_test.go create mode 100644 PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go new file mode 100644 index 00000000..a248fa79 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler.go @@ -0,0 +1,323 @@ +// ©AngelaMos | 2026 +// handler.go + +package token + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "log/slog" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/event" + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/middleware" +) + +const ( + urlParamTokenID = "id" + + headerContentType = "Content-Type" + headerLocation = "Location" + contentTypeJSON = "application/json" + + errorCodeValidation = "VALIDATION_ERROR" + errorCodeBadJSON = "BAD_JSON" + errorCodeInternalError = "INTERNAL_ERROR" + errorCodeUnknownType = "UNKNOWN_TYPE" + errorCodeGenerateFailed = "GENERATE_FAILED" + + respMessageValidation = "request validation failed" + respMessageBadJSON = "invalid JSON body" + respMessageInternalError = "internal server error" + respMessageGenerateFailed = "artifact generation failed" + respMessageUnknownType = "unknown token type" + + kubeconfigPathPrefix = "/k/" + + createTokenBodyMaxBytes = 64 * 1024 + fingerprintBodyMaxBytes = 64 * 1024 +) + +type EventRecorder interface { + Record(ctx context.Context, t *Token, evt *event.Event) error +} + +type FingerprintRecorder interface { + AttachFingerprint( + ctx context.Context, + tokenID, sourceIP string, + fingerprint json.RawMessage, + ) error +} + +type Handler struct { + svc *Service + events EventRecorder + fingerprintRecorder FingerprintRecorder + logger *slog.Logger +} + +func NewHandler( + svc *Service, + events EventRecorder, + fingerprint FingerprintRecorder, + logger *slog.Logger, +) *Handler { + if logger == nil { + logger = slog.Default() + } + return &Handler{ + svc: svc, + events: events, + fingerprintRecorder: fingerprint, + logger: logger, + } +} + +func (h *Handler) RegisterAPIRoutes(r chi.Router) { + r.Get("/tokens/types", h.GetTypes) + r.Post("/tokens", h.CreateToken) +} + +func (h *Handler) RegisterTriggerRoutes(r chi.Router) { + r.Get("/c/{"+urlParamTokenID+"}", h.HandleTrigger) + r.Post("/c/{"+urlParamTokenID+"}/fingerprint", h.HandleFingerprint) + r.HandleFunc("/k/{"+urlParamTokenID+"}", h.HandleTrigger) + r.HandleFunc("/k/{"+urlParamTokenID+"}/*", h.HandleTrigger) +} + +func (h *Handler) GetTypes(w http.ResponseWriter, _ *http.Request) { + h.writeJSON(w, http.StatusOK, envelopeData(TypeDescriptors())) +} + +func (h *Handler) CreateToken(w http.ResponseWriter, r *http.Request) { + limited := http.MaxBytesReader(w, r.Body, createTokenBodyMaxBytes) + var req CreateRequest + if err := json.NewDecoder(limited).Decode(&req); err != nil { + h.writeJSON(w, http.StatusBadRequest, envelopeError( + errorCodeBadJSON, respMessageBadJSON, + )) + return + } + + fp := middleware.ExtractFingerprint(r) + ip := middleware.RealIP(r) + + tok, art, err := h.svc.Create(r.Context(), req, fp, ip) + if err != nil { + h.writeCreateError(w, r, err) + return + } + + resp := tok.ToResponse( + h.svc.TriggerURL(tok.ID), + h.svc.ManageURL(tok.ManageID), + ) + h.writeJSON(w, http.StatusCreated, envelopeData(map[string]any{ + "token": resp, + "artifact": artifactToJSON(art), + })) +} + +func (h *Handler) HandleTrigger(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, urlParamTokenID) + if id == "" { + http.NotFound(w, r) + return + } + + tok, err := h.svc.GetByID(r.Context(), id) + if err != nil { + h.logger.WarnContext(r.Context(), "trigger lookup failed", + "token_id", id, "error", err) + } + if tok != nil && !tok.Enabled { + tok = nil + } + + gen, ok := h.resolveGenerator(tok, r) + if !ok { + http.NotFound(w, r) + return + } + + evt, resp, gErr := gen.Trigger(r.Context(), tok, r) + if gErr != nil { + h.logger.WarnContext(r.Context(), "trigger generator failed", + "error", gErr, "token_id", id) + http.Error(w, respMessageInternalError, http.StatusInternalServerError) + return + } + if resp == nil { + http.NotFound(w, r) + return + } + + if tok != nil && evt != nil && h.events != nil { + if recErr := h.events.Record(r.Context(), tok, evt); recErr != nil { + h.logger.WarnContext(r.Context(), "record event failed", + "error", recErr, "token_id", id) + } + } + + h.writeTriggerResponse(w, r, resp) +} + +func (h *Handler) HandleFingerprint(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, urlParamTokenID) + if id == "" || h.fingerprintRecorder == nil { + w.WriteHeader(http.StatusNoContent) + return + } + + limited := http.MaxBytesReader(w, r.Body, fingerprintBodyMaxBytes) + var raw json.RawMessage + if err := json.NewDecoder(limited).Decode(&raw); err != nil { + w.WriteHeader(http.StatusNoContent) + return + } + + if recErr := h.fingerprintRecorder.AttachFingerprint( + r.Context(), + id, + middleware.RealIP(r), + raw, + ); recErr != nil { + h.logger.WarnContext(r.Context(), "attach fingerprint failed", + "token_id", id, "error", recErr) + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *Handler) resolveGenerator( + tok *Token, + r *http.Request, +) (Generator, bool) { + if tok != nil { + return h.svc.Generator(tok.Type) + } + if strings.HasPrefix(r.URL.Path, kubeconfigPathPrefix) { + return h.svc.Generator(TypeKubeconfig) + } + return h.svc.Generator(TypeWebbug) +} + +func (h *Handler) writeCreateError( + w http.ResponseWriter, + r *http.Request, + err error, +) { + switch { + case errors.Is(err, ErrUnknownGeneratorType): + h.writeJSON(w, http.StatusBadRequest, envelopeError( + errorCodeUnknownType, respMessageUnknownType, + )) + case errors.Is(err, ErrInvalidDestinationURL), + errors.Is(err, ErrInvalidIncludeKeys): + h.writeJSON(w, http.StatusBadRequest, envelopeError( + errorCodeValidation, err.Error(), + )) + case errors.Is(err, ErrGenerateFailed): + h.logger.ErrorContext(r.Context(), "create token: generator", + "error", err) + h.writeJSON(w, http.StatusInternalServerError, envelopeError( + errorCodeGenerateFailed, respMessageGenerateFailed, + )) + case strings.Contains(err.Error(), "validate request"): + h.writeJSON(w, http.StatusBadRequest, envelopeError( + errorCodeValidation, respMessageValidation, + )) + default: + h.logger.ErrorContext(r.Context(), "create token", "error", err) + h.writeJSON(w, http.StatusInternalServerError, envelopeError( + errorCodeInternalError, respMessageInternalError, + )) + } +} + +func (h *Handler) writeJSON( + w http.ResponseWriter, + status int, + body any, +) { + w.Header().Set(headerContentType, contentTypeJSON) + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(body); err != nil { + h.logger.Warn("write json response", "error", err) + } +} + +func (h *Handler) writeTriggerResponse( + w http.ResponseWriter, + r *http.Request, + resp *TriggerResponse, +) { + for k, v := range resp.ExtraHeaders { + w.Header().Set(k, v) + } + if resp.ContentType != "" { + w.Header().Set(headerContentType, resp.ContentType) + } + if resp.RedirectURL != "" { + w.Header().Set(headerLocation, resp.RedirectURL) + w.WriteHeader(resp.StatusCode) + return + } + w.WriteHeader(resp.StatusCode) + if len(resp.Body) > 0 { + if _, err := w.Write(resp.Body); err != nil { + h.logger.WarnContext(r.Context(), "write trigger body", + "error", err) + } + } +} + +func envelopeData(data any) map[string]any { + return map[string]any{"success": true, "data": data} +} + +func envelopeError(code, message string) map[string]any { + return map[string]any{ + "success": false, + "error": map[string]any{ + "code": code, + "message": message, + }, + } +} + +type ArtifactJSON struct { + Kind string `json:"kind"` + URL string `json:"url,omitempty"` + DestinationURL string `json:"destination_url,omitempty"` + Filename string `json:"filename,omitempty"` + ContentType string `json:"content_type,omitempty"` + ContentB64 string `json:"content_b64,omitempty"` + Content string `json:"content,omitempty"` + ConnectionString string `json:"connection_string,omitempty"` +} + +func artifactToJSON(a Artifact) ArtifactJSON { + out := ArtifactJSON{Kind: string(a.Kind)} + switch a.Kind { + case KindURL: + out.URL = a.URL + out.DestinationURL = a.DestinationURL + case KindFile: + out.Filename = a.Filename + out.ContentType = a.ContentType + out.ContentB64 = base64.StdEncoding.EncodeToString(a.Content) + case KindText: + out.Filename = a.Filename + out.ContentType = a.ContentType + out.Content = string(a.Content) + case KindConnectionString: + out.ConnectionString = a.ConnectionString + } + return out +} 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 new file mode 100644 index 00000000..3f6c3ab3 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/handler_test.go @@ -0,0 +1,371 @@ +// ©AngelaMos | 2026 +// handler_test.go + +package token_test + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/require" + + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/event" + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token" + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators" +) + +type triggerGen struct { + tokenType token.Type + artifact generators.Artifact + resp *generators.TriggerResponse + evt *event.Event +} + +func (g *triggerGen) Type() token.Type { return g.tokenType } + +func (g *triggerGen) Generate( + _ context.Context, + _ *token.Token, + _ string, +) (generators.Artifact, error) { + return g.artifact, nil +} + +func (g *triggerGen) Trigger( + _ context.Context, + _ *token.Token, + _ *http.Request, +) (*event.Event, *generators.TriggerResponse, error) { + return g.evt, g.resp, nil +} + +type recordingEvents struct { + events []*event.Event +} + +func (r *recordingEvents) Record( + _ context.Context, + _ *token.Token, + e *event.Event, +) error { + r.events = append(r.events, e) + return nil +} + +func quietHandlerLogger() *slog.Logger { + return slog.New(slog.NewTextHandler(io.Discard, nil)) +} + +func newWebbugHandler( + t *testing.T, + gen token.Generator, +) (*token.Handler, *fakeRepo, *recordingEvents) { + t.Helper() + repo := newFakeRepo() + rec := &recordingEvents{} + svc := token.NewService( + repo, + token.MapRegistry{token.TypeWebbug: gen}, + token.ServiceConfig{ + BaseURL: "https://canary.example.com", + ManageURL: "https://canary.example.com", + }, + ) + return token.NewHandler(svc, rec, nil, quietHandlerLogger()), 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, quietHandlerLogger()) + + r := chi.NewRouter() + h.RegisterAPIRoutes(r) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/tokens/types", nil)) + + require.Equal(t, http.StatusOK, w.Code) + var body struct { + Success bool `json:"success"` + Data []token.TypeDescriptor `json:"data"` + } + require.NoError(t, json.NewDecoder(w.Body).Decode(&body)) + require.True(t, body.Success) + require.Len(t, body.Data, 7) +} + +func TestCreateToken_HappyPath(t *testing.T) { + gen := &triggerGen{ + tokenType: token.TypeWebbug, + artifact: generators.Artifact{ + Kind: generators.KindURL, URL: "https://canary.example.com/c/x", + }, + } + h, _, _ := newWebbugHandler(t, gen) + + r := chi.NewRouter() + h.RegisterAPIRoutes(r) + + body := strings.NewReader(`{ + "type":"webbug","memo":"m","alert_channel":"webhook", + "webhook_url":"https://example.com/h","cf_turnstile_response":"t", + "metadata":{} + }`) + req := httptest.NewRequest(http.MethodPost, "/tokens", body) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusCreated, w.Code) + + var resp struct { + Success bool `json:"success"` + Data struct { + Token token.Response `json:"token"` + Artifact token.ArtifactJSON `json:"artifact"` + } `json:"data"` + } + require.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) + require.True(t, resp.Success) + require.Equal(t, token.TypeWebbug, resp.Data.Token.Type) + require.NotEmpty(t, resp.Data.Token.ID) + require.Contains(t, resp.Data.Token.TriggerURL, "/c/") + require.Contains(t, resp.Data.Token.ManageURL, "/m/") + require.Equal(t, "url", resp.Data.Artifact.Kind) +} + +func TestCreateToken_BadJSON(t *testing.T) { + gen := &triggerGen{tokenType: token.TypeWebbug} + h, _, _ := newWebbugHandler(t, gen) + + r := chi.NewRouter() + h.RegisterAPIRoutes(r) + + req := httptest.NewRequest( + http.MethodPost, + "/tokens", + strings.NewReader(`{not json`), + ) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusBadRequest, w.Code) + require.Contains(t, w.Body.String(), "BAD_JSON") +} + +func TestCreateToken_ValidationFailure(t *testing.T) { + gen := &triggerGen{tokenType: token.TypeWebbug} + h, _, _ := newWebbugHandler(t, gen) + + r := chi.NewRouter() + h.RegisterAPIRoutes(r) + + body := strings.NewReader( + `{"type":"webbug","memo":"m","cf_turnstile_response":"t"}`, + ) + req := httptest.NewRequest(http.MethodPost, "/tokens", body) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusBadRequest, w.Code) + require.Contains(t, w.Body.String(), "VALIDATION_ERROR") +} + +func TestHandleTrigger_KnownTokenReturnsResponseAndRecordsEvent(t *testing.T) { + gen := &triggerGen{ + tokenType: token.TypeWebbug, + artifact: generators.Artifact{Kind: generators.KindURL}, + resp: &generators.TriggerResponse{ + StatusCode: 200, + ContentType: "image/gif", + Body: []byte{0x47, 0x49, 0x46, 0x38}, + }, + evt: &event.Event{SourceIP: "1.2.3.4"}, + } + h, repo, rec := newWebbugHandler(t, gen) + + tok := &token.Token{ + ID: "abcdef012345", ManageID: "m", Type: token.TypeWebbug, + AlertChannel: token.ChannelWebhook, Enabled: true, + Metadata: json.RawMessage(`{}`), + } + require.NoError(t, repo.Insert(context.Background(), tok)) + + r := chi.NewRouter() + h.RegisterTriggerRoutes(r) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/c/abcdef012345", nil) + r.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "image/gif", w.Header().Get("Content-Type")) + require.Equal(t, []byte{0x47, 0x49, 0x46, 0x38}, w.Body.Bytes()) + require.Len(t, rec.events, 1) +} + +func TestHandleTrigger_UnknownTokenStillReturnsArtifactShape(t *testing.T) { + gen := &triggerGen{ + tokenType: token.TypeWebbug, + resp: &generators.TriggerResponse{ + StatusCode: 200, + ContentType: "image/gif", + Body: []byte{0x47, 0x49, 0x46}, + }, + } + h, _, rec := newWebbugHandler(t, gen) + + r := chi.NewRouter() + h.RegisterTriggerRoutes(r) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/c/nonexistent1", nil)) + + require.Equal(t, http.StatusOK, w.Code, + "unknown tokens still return artifact shape (defense in depth)") + require.Empty(t, rec.events, "no event recorded for unknown token") +} + +func TestHandleTrigger_DisabledTokenIsTreatedAsUnknown(t *testing.T) { + gen := &triggerGen{ + tokenType: token.TypeWebbug, + resp: &generators.TriggerResponse{ + StatusCode: 200, + ContentType: "image/gif", + Body: []byte{0x47}, + }, + evt: &event.Event{SourceIP: "1.2.3.4"}, + } + h, repo, rec := newWebbugHandler(t, gen) + + tok := &token.Token{ + ID: "disabled1234", ManageID: "m", Type: token.TypeWebbug, + AlertChannel: token.ChannelWebhook, Enabled: false, + Metadata: json.RawMessage(`{}`), + } + require.NoError(t, repo.Insert(context.Background(), tok)) + + r := chi.NewRouter() + h.RegisterTriggerRoutes(r) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/c/disabled1234", nil)) + + require.Equal(t, http.StatusOK, w.Code) + require.Empty(t, rec.events, "disabled token must not record events") +} + +func TestHandleFingerprint_Returns204WithNoRecorder(t *testing.T) { + gen := &triggerGen{tokenType: token.TypeWebbug} + h, _, _ := newWebbugHandler(t, gen) + + r := chi.NewRouter() + h.RegisterTriggerRoutes(r) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, + "/c/anything/fingerprint", strings.NewReader(`{"x":1}`))) + + require.Equal(t, http.StatusNoContent, w.Code) +} + +func TestArtifactToJSON_Kinds(t *testing.T) { + cases := []struct { + name string + in generators.Artifact + want token.ArtifactJSON + }{ + { + "url", + generators.Artifact{ + Kind: generators.KindURL, + URL: "u", + DestinationURL: "d", + }, + token.ArtifactJSON{Kind: "url", URL: "u", DestinationURL: "d"}, + }, + { + "file", + generators.Artifact{ + Kind: generators.KindFile, + Filename: "f.docx", + ContentType: "x", + Content: []byte("hi"), + }, + token.ArtifactJSON{ + Kind: "file", + Filename: "f.docx", + ContentType: "x", + ContentB64: "aGk=", + }, + }, + { + "text", + generators.Artifact{ + Kind: generators.KindText, + Filename: ".env", + ContentType: "text/plain", + Content: []byte("KEY=v"), + }, + token.ArtifactJSON{ + Kind: "text", + Filename: ".env", + ContentType: "text/plain", + Content: "KEY=v", + }, + }, + { + "conn", + generators.Artifact{ + Kind: generators.KindConnectionString, + ConnectionString: "mysql://x", + }, + token.ArtifactJSON{ + Kind: "connection_string", + ConnectionString: "mysql://x", + }, + }, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + require.Equal(t, c.want, exposeArtifactToJSON(c.in)) + }) + } +} + +func exposeArtifactToJSON(a generators.Artifact) token.ArtifactJSON { + gen := &triggerGen{tokenType: token.TypeWebbug, artifact: a} + repo := newFakeRepo() + svc := token.NewService(repo, token.MapRegistry{token.TypeWebbug: gen}, + token.ServiceConfig{BaseURL: "https://x"}) + h := token.NewHandler(svc, nil, nil, quietHandlerLogger()) + + r := chi.NewRouter() + h.RegisterAPIRoutes(r) + + body := strings.NewReader( + `{"type":"webbug","alert_channel":"webhook","webhook_url":"https://x/h","cf_turnstile_response":"t","metadata":{}}`, + ) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/tokens", body)) + + var resp struct { + Data struct { + Artifact token.ArtifactJSON `json:"artifact"` + } `json:"data"` + } + if jsonErr := json.NewDecoder(w.Body).Decode(&resp); jsonErr != nil { + panic(jsonErr) + } + return resp.Data.Artifact +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go new file mode 100644 index 00000000..55d09bd5 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/types.go @@ -0,0 +1,58 @@ +// ©AngelaMos | 2026 +// types.go + +package token + +type TypeDescriptor struct { + Type Type `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + ArtifactKind string `json:"artifact_kind"` +} + +func TypeDescriptors() []TypeDescriptor { + return []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), + }, + { + Type: TypeSlowRedirect, + Name: "Slow Redirect", + Description: "Browser-fingerprinting page that redirects to a destination URL after collecting client metadata.", + ArtifactKind: string(KindURL), + }, + { + Type: TypeDocx, + Name: "Microsoft Word Document", + Description: "DOCX with an embedded INCLUDEPICTURE field that calls home when the document opens.", + ArtifactKind: string(KindFile), + }, + { + Type: TypePDF, + Name: "PDF Document", + Description: "PDF with an /AA open-action URI that fires in Adobe Acrobat Reader.", + ArtifactKind: string(KindFile), + }, + { + Type: TypeKubeconfig, + Name: "Kubernetes Config", + Description: "kubeconfig pointing kubectl at a fake K8s API server that records every request.", + ArtifactKind: string(KindText), + }, + { + Type: TypeEnvfile, + Name: ".env File", + Description: "Plausible production .env with shuffled bait credentials and an embedded canary URL.", + ArtifactKind: string(KindText), + }, + { + Type: TypeMySQL, + Name: "MySQL Connection String", + Description: "Fake MySQL endpoint that records any authentication attempt.", + ArtifactKind: string(KindConnectionString), + }, + } +}