fix(canary-phase1): clear all post-phase-1 audit observations + header normalization

No 'logged for later' — every non-blocking finding from the Phase 1 audits
is fixed now, in this commit, before declaring the phase truly closed.

Code cleanups (5 items):
1. Database.SQLDB() *sql.DB accessor on core.Database; main.go uses
   db.SQLDB() instead of the awkward db.DB.DB triple-chain.
2. Hoisted list-default literals (50, 20) to package-level
   defaultListLimit consts in token + event repositories. MEMORY.md
   'no magic numbers' rule honored.
3. Moved ptr[T any] helper to internal/testutil/ptr.go as testutil.Ptr;
   removed local copies from token + event repository_test.go.
4. Renamed CHECK constraints in 0001_create_tokens.sql to match spec
   text exactly: chk_token_type → chk_type, chk_alert_channel → chk_channel.
   Spec was the original contract; impl now aligns.
5. Resolved APP_ENVIRONMENT vs ENVIRONMENT divergence per spec §12.1:
   - compose.yml: ENVIRONMENT=production → APP_ENVIRONMENT=production
   - dev.compose.yml: ENVIRONMENT=development → APP_ENVIRONMENT=development
   - config.go envKeyMap: ENVIRONMENT → APP_ENVIRONMENT mapping

Concurrency bug fix (real, not just polish):
- core/migrations.go: added sync.Mutex around goose calls. goose's
  package-level state (SetBaseFS, SetDialect) raced under t.Parallel()
  testcontainers tests. Race detector caught it under -race after
  parallel test counts climbed. Mutex serializes goose invocation
  globally; testcontainer-parallelism otherwise unaffected.

Header normalization (50 files):
- Bulk-normalized every project file's header to canonical
  '©AngelaMos | 2026' (no space, with © glyph, year 2026) per MEMORY.md
  style rule. Eliminated 4 distinct non-canonical variants: '// AngelaMos',
  '// ©AngelaMos | 2025', '// © AngelaMos | 202X', '# AngelaMos'.
- Verified clean: grep returns zero non-canonical headers across the
  whole project tree (excluding gitignored docs/ and node_modules/).

Backlog discipline:
- Created docs/plans/BACKLOG.md with strict format: open items only,
  HIGH/MEDIUM/LOW severity, must-clear-before-ship contract.
- BACKLOG currently has zero open items — every observation was fixed
  here, not deferred. Closed-items section logs what was cleared.

Verification:
- go build ./...                                     clean
- go vet ./...                                       clean
- go test -tags=integration -race ./internal/token/...  11/11 PASS
- go test -tags=integration -race ./internal/event/...   9/9 PASS
- docker compose -f compose.yml config              parses
- docker compose -f dev.compose.yml config          parses
- grep for non-canonical headers                     zero matches
This commit is contained in:
CarterPerez-dev 2026-05-10 06:15:26 -04:00
parent 7a98ef8d72
commit 697f4909d7
60 changed files with 85 additions and 69 deletions

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026 # ©AngelaMos | 2026
# .air.toml - Hot reload configuration # .air.toml - Hot reload configuration
root = "." root = "."

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026 # ©AngelaMos | 2026
# .golangci.yml # .golangci.yml
version: "2" version: "2"

View File

@ -67,7 +67,7 @@ func run(configPath string) error {
} }
logger.Info("database connected") logger.Info("database connected")
if err := core.RunMigrations(db.DB.DB); err != nil { if err := core.RunMigrations(db.SQLDB()); err != nil {
return fmt.Errorf("run migrations: %w", err) return fmt.Errorf("run migrations: %w", err)
} }
logger.Info("migrations applied") logger.Info("migrations applied")

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026 # ©AngelaMos | 2026
# config.yaml - Default configuration # config.yaml - Default configuration
app: app:

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// config.go // config.go
package config package config
@ -199,7 +199,7 @@ func loadDefaults(k *koanf.Koanf) error {
var envKeyMap = map[string]string{ var envKeyMap = map[string]string{
"DATABASE_URL": "database.url", "DATABASE_URL": "database.url",
"REDIS_URL": "redis.url", "REDIS_URL": "redis.url",
"ENVIRONMENT": "app.environment", "APP_ENVIRONMENT": "app.environment",
"HOST": "server.host", "HOST": "server.host",
"PORT": "server.port", "PORT": "server.port",
"LOG_LEVEL": "log.level", "LOG_LEVEL": "log.level",

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// database.go // database.go
package core package core
@ -20,6 +20,10 @@ type Database struct {
DB *sqlx.DB DB *sqlx.DB
} }
func (d *Database) SQLDB() *sql.DB {
return d.DB.DB
}
func NewDatabase( func NewDatabase(
ctx context.Context, ctx context.Context,
cfg config.DatabaseConfig, cfg config.DatabaseConfig,

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// errors.go // errors.go
package core package core

View File

@ -7,6 +7,7 @@ import (
"database/sql" "database/sql"
"embed" "embed"
"fmt" "fmt"
"sync"
"github.com/pressly/goose/v3" "github.com/pressly/goose/v3"
) )
@ -14,7 +15,12 @@ import (
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrationsFS embed.FS var migrationsFS embed.FS
var gooseMu sync.Mutex
func RunMigrations(db *sql.DB) error { func RunMigrations(db *sql.DB) error {
gooseMu.Lock()
defer gooseMu.Unlock()
goose.SetBaseFS(migrationsFS) goose.SetBaseFS(migrationsFS)
if err := goose.SetDialect("postgres"); err != nil { if err := goose.SetDialect("postgres"); err != nil {

View File

@ -25,9 +25,9 @@ CREATE TABLE tokens (
metadata JSONB NOT NULL DEFAULT '{}'::jsonb, metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
CONSTRAINT chk_token_type CHECK (type IN CONSTRAINT chk_type CHECK (type IN
('webbug', 'slowredirect', 'docx', 'pdf', 'kubeconfig', 'envfile', 'mysql')), ('webbug', 'slowredirect', 'docx', 'pdf', 'kubeconfig', 'envfile', 'mysql')),
CONSTRAINT chk_alert_channel CHECK (alert_channel IN ('telegram', 'webhook')), CONSTRAINT chk_channel CHECK (alert_channel IN ('telegram', 'webhook')),
CONSTRAINT chk_telegram_complete CHECK ( CONSTRAINT chk_telegram_complete CHECK (
alert_channel <> 'telegram' OR alert_channel <> 'telegram' OR
(telegram_bot IS NOT NULL AND telegram_chat IS NOT NULL) (telegram_bot IS NOT NULL AND telegram_chat IS NOT NULL)

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// redis.go // redis.go
package core package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// response.go // response.go
package core package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// security.go // security.go
package core package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// telemetry.go // telemetry.go
package core package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// validation.go // validation.go
package core package core

View File

@ -16,6 +16,8 @@ import (
var ErrNotFound = errors.New("event not found") var ErrNotFound = errors.New("event not found")
const defaultListLimit = 20
type Repository struct { type Repository struct {
db *sqlx.DB db *sqlx.DB
} }
@ -77,7 +79,7 @@ func (r *Repository) ListByToken(
ctx context.Context, tokenID string, opts ListOptions, ctx context.Context, tokenID string, opts ListOptions,
) (ListResult, error) { ) (ListResult, error) {
if opts.Limit <= 0 { if opts.Limit <= 0 {
opts.Limit = 20 opts.Limit = defaultListLimit
} }
q := `SELECT ` + selectColumns + ` q := `SELECT ` + selectColumns + `

View File

@ -20,8 +20,6 @@ import (
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token" "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
) )
func ptr[T any](v T) *T { return &v }
func newRepos(t *testing.T) (*sqlx.DB, *token.Repository, *event.Repository) { func newRepos(t *testing.T) (*sqlx.DB, *token.Repository, *event.Repository) {
t.Helper() t.Helper()
db := sqlx.NewDb(testutil.NewTestDB(t), "pgx") db := sqlx.NewDb(testutil.NewTestDB(t), "pgx")
@ -36,7 +34,7 @@ func seedToken(t *testing.T, repo *token.Repository, id string) *token.Token {
Type: token.TypeWebbug, Type: token.TypeWebbug,
Memo: "event-test", Memo: "event-test",
AlertChannel: token.ChannelWebhook, AlertChannel: token.ChannelWebhook,
WebhookURL: ptr("https://example.com/hook"), WebhookURL: testutil.Ptr("https://example.com/hook"),
CreatedIP: "203.0.113.1", CreatedIP: "203.0.113.1",
CreatedFP: "abcdef0123456789", CreatedFP: "abcdef0123456789",
Metadata: json.RawMessage(`{}`), Metadata: json.RawMessage(`{}`),
@ -80,7 +78,7 @@ func TestRepository_ListByToken_CursorPagination(t *testing.T) {
e := &event.Event{ e := &event.Event{
TokenID: tok.ID, TokenID: tok.ID,
SourceIP: "203.0.113.45", SourceIP: "203.0.113.45",
UserAgent: ptr(string(rune('A' + i))), UserAgent: testutil.Ptr(string(rune('A' + i))),
} }
require.NoError(t, evtRepo.Insert(ctx, e)) require.NoError(t, evtRepo.Insert(ctx, e))
} }
@ -223,7 +221,7 @@ func TestRepository_PruneToLimit_KeepsNewest(t *testing.T) {
var ids []int64 var ids []int64
for i := range 5 { for i := range 5 {
e := &event.Event{TokenID: tok.ID, SourceIP: "203.0.113.45", UserAgent: ptr(string(rune('A' + i)))} e := &event.Event{TokenID: tok.ID, SourceIP: "203.0.113.45", UserAgent: testutil.Ptr(string(rune('A' + i)))}
require.NoError(t, evtRepo.Insert(ctx, e)) require.NoError(t, evtRepo.Insert(ctx, e))
ids = append(ids, e.ID) ids = append(ids, e.ID)
time.Sleep(2 * time.Millisecond) time.Sleep(2 * time.Millisecond)

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// handler.go // handler.go
package health package health

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// headers.go // headers.go
package middleware package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// logging.go // logging.go
package middleware package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// ratelimit.go // ratelimit.go
package middleware package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// request_id.go // request_id.go
package middleware package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026 // ©AngelaMos | 2026
// server.go // server.go
package server package server

View File

@ -0,0 +1,6 @@
// ©AngelaMos | 2026
// ptr.go
package testutil
func Ptr[T any](v T) *T { return &v }

View File

@ -14,6 +14,8 @@ import (
var ErrNotFound = errors.New("token not found") var ErrNotFound = errors.New("token not found")
const defaultListLimit = 50
type Repository struct { type Repository struct {
db *sqlx.DB db *sqlx.DB
} }
@ -130,7 +132,7 @@ type ListOptions struct {
func (r *Repository) ListAll(ctx context.Context, opts ListOptions) ([]Token, error) { func (r *Repository) ListAll(ctx context.Context, opts ListOptions) ([]Token, error) {
if opts.Limit <= 0 { if opts.Limit <= 0 {
opts.Limit = 50 opts.Limit = defaultListLimit
} }
q := `SELECT ` + selectColumns + ` FROM tokens q := `SELECT ` + selectColumns + ` FROM tokens
ORDER BY created_at DESC ORDER BY created_at DESC

View File

@ -25,8 +25,6 @@ func newRepo(t *testing.T) *token.Repository {
return token.NewRepository(db) return token.NewRepository(db)
} }
func ptr[T any](v T) *T { return &v }
func sampleWebhookToken(id string) *token.Token { func sampleWebhookToken(id string) *token.Token {
return &token.Token{ return &token.Token{
ID: id, ID: id,
@ -34,7 +32,7 @@ func sampleWebhookToken(id string) *token.Token {
Type: token.TypeWebbug, Type: token.TypeWebbug,
Memo: "test webbug", Memo: "test webbug",
AlertChannel: token.ChannelWebhook, AlertChannel: token.ChannelWebhook,
WebhookURL: ptr("https://example.com/hook"), WebhookURL: testutil.Ptr("https://example.com/hook"),
CreatedIP: "203.0.113.10", CreatedIP: "203.0.113.10",
CreatedFP: "0123456789abcdef", CreatedFP: "0123456789abcdef",
Metadata: json.RawMessage(`{}`), Metadata: json.RawMessage(`{}`),
@ -48,10 +46,10 @@ func sampleTelegramToken(id string) *token.Token {
ManageID: uuid.New().String(), ManageID: uuid.New().String(),
Type: token.TypeDocx, Type: token.TypeDocx,
Memo: "Q4 bonuses", Memo: "Q4 bonuses",
Filename: ptr("Q4_Bonuses_2024.docx"), Filename: testutil.Ptr("Q4_Bonuses_2024.docx"),
AlertChannel: token.ChannelTelegram, AlertChannel: token.ChannelTelegram,
TelegramBot: ptr("123456:ABCDEF"), TelegramBot: testutil.Ptr("123456:ABCDEF"),
TelegramChat: ptr("-1001234567890"), TelegramChat: testutil.Ptr("-1001234567890"),
CreatedIP: "198.51.100.5", CreatedIP: "198.51.100.5",
CreatedFP: "fedcba9876543210", CreatedFP: "fedcba9876543210",
Metadata: json.RawMessage(`{"include_keys":["aws"]}`), Metadata: json.RawMessage(`{"include_keys":["aws"]}`),

View File

@ -42,7 +42,7 @@ services:
dockerfile: ../infra/docker/canary.prod dockerfile: ../infra/docker/canary.prod
container_name: ${APP_NAME:-canary-token-generator}-canary container_name: ${APP_NAME:-canary-token-generator}-canary
environment: environment:
- ENVIRONMENT=production - APP_ENVIRONMENT=production
- DATABASE_URL=postgres://canary:${POSTGRES_PASSWORD}@postgres:5432/canary?sslmode=disable - DATABASE_URL=postgres://canary:${POSTGRES_PASSWORD}@postgres:5432/canary?sslmode=disable
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL} - PUBLIC_BASE_URL=${PUBLIC_BASE_URL}

View File

@ -54,7 +54,7 @@ services:
- canary_gocache:/root/.cache/go-build - canary_gocache:/root/.cache/go-build
- canary_gomodcache:/go/pkg/mod - canary_gomodcache:/go/pkg/mod
environment: environment:
- ENVIRONMENT=development - APP_ENVIRONMENT=development
- DATABASE_URL=postgres://canary:canary@postgres:5432/canary?sslmode=disable - DATABASE_URL=postgres://canary:canary@postgres:5432/canary?sslmode=disable
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://localhost:58495} - PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://localhost:58495}

View File

@ -1,4 +1,4 @@
# ©AngelaMos | 2025 # ©AngelaMos | 2026
# .stylelintignore # .stylelintignore
# Dependencies # Dependencies

View File

@ -1,4 +1,4 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// index.ts // index.ts
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2025 // ©AngelaMos | 2026
// index.ts // index.ts
// =================== // ===================

View File

@ -1,4 +1,4 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// index.ts // index.ts
// =================== // ===================

View File

@ -1,4 +1,4 @@
/** /**
* ©AngelaMos | 2025 * ©AngelaMos | 2026
* index.tsx * index.tsx
*/ */

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// config.ts // config.ts
// =================== // ===================
const API_VERSION = 'v1' const API_VERSION = 'v1'

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// api.config.ts // api.config.ts
// =================== // ===================

View File

@ -1,5 +1,5 @@
/** /**
* ©AngelaMos | 2025 * ©AngelaMos | 2026
* errors.ts * errors.ts
*/ */

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2025 // ©AngelaMos | 2026
// index.ts // index.ts
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2025 // ©AngelaMos | 2026
// query.config.ts // query.config.ts
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// routers.tsx // routers.tsx
// =================== // ===================

View File

@ -1,4 +1,4 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// shell.module.scss // shell.module.scss
// =================== // ===================

View File

@ -1,4 +1,4 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// toast.module.scss // toast.module.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// index.ts // index.ts
// =================== // ===================

View File

@ -1,5 +1,5 @@
/** /**
* ©AngelaMos | 2025 * ©AngelaMos | 2026
* ui.store.ts * ui.store.ts
*/ */

View File

@ -1,5 +1,5 @@
// =========================== // ===========================
// ©AngelaMos | 2025 // ©AngelaMos | 2026
// main.tsx // main.tsx
// =========================== // ===========================

View File

@ -1,4 +1,4 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// dashboard.module.scss // dashboard.module.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// index.tsx // index.tsx
// =================== // ===================

View File

@ -1,4 +1,4 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// landing.module.scss // landing.module.scss
// =================== // ===================

View File

@ -1,4 +1,4 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// settings.module.scss // settings.module.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// styles.scss // styles.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2025 // ©AngelaMos | 2026
// _fonts.scss // _fonts.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2025 // ©AngelaMos | 2026
// _index.scss // _index.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2025 // ©AngelaMos | 2026
// _mixins.scss // _mixins.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2026 // ©AngelaMos | 2026
// _reset.scss // _reset.scss
// =================== // ===================

View File

@ -1,5 +1,5 @@
// =================== // ===================
// © AngelaMos | 2025 // ©AngelaMos | 2026
// _tokens.scss // _tokens.scss
// =================== // ===================

View File

@ -1,4 +1,4 @@
// ©AngelaMos | 2025 // ©AngelaMos | 2026
// stylelint.config.js // stylelint.config.js
/** @type {import('stylelint').Config} */ /** @type {import('stylelint').Config} */

View File

@ -1,5 +1,5 @@
/** /**
* ©AngelaMos | 2025 * ©AngelaMos | 2026
* vite.config.ts * vite.config.ts
*/ */

View File

@ -1,5 +1,5 @@
# ============================================================================= # =============================================================================
# AngelaMos | 2026 # ©AngelaMos | 2026
# dev.nginx # dev.nginx
# ============================================================================= # =============================================================================
# Development server block: proxies to Vite dev server with HMR # Development server block: proxies to Vite dev server with HMR

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026 # ©AngelaMos | 2026
# nginx.conf # nginx.conf
# Development nginx configuration # Development nginx configuration

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026 # ©AngelaMos | 2026
# nginx.prod.conf # nginx.prod.conf
# Production nginx configuration # Production nginx configuration

View File

@ -1,5 +1,5 @@
# ============================================================================= # =============================================================================
# AngelaMos | 2026 # ©AngelaMos | 2026
# prod.nginx # prod.nginx
# ============================================================================= # =============================================================================
# Production server block: serves built static files # Production server block: serves built static files

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ============================================================================= # =============================================================================
# AngelaMos | 2026 # ©AngelaMos | 2026
# randomize-ports.sh # randomize-ports.sh
# ============================================================================= # =============================================================================
# Picks 4 unique random ports (10000-65000) and updates: # Picks 4 unique random ports (10000-65000) and updates: