diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58354ada7..9c4202052 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ concurrency: env: NODE_VERSION: '22' PNPM_VERSION: '10.33.4' - GO_VERSION: '1.25.10' + GO_VERSION: '1.25.11' # SR-007: least-privilege default. CI runs on every PR (including forks) and # only needs to read the repo. Any job needing more must opt in per-job. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ecf7bdb89..fe904d14c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,7 +32,7 @@ jobs: if: matrix.language == 'go' uses: actions/setup-go@v6 with: - go-version: '1.25.10' + go-version: '1.25.11' cache-dependency-path: agent/go.sum - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbf24220f..f68e8bcfa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ on: env: NODE_VERSION: '22' PNPM_VERSION: '10.33.4' - GO_VERSION: '1.25.10' + GO_VERSION: '1.25.11' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8d9b0487e..d4aa31482 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -15,7 +15,7 @@ concurrency: env: NODE_VERSION: '22' PNPM_VERSION: '10.33.4' - GO_VERSION: '1.25.10' + GO_VERSION: '1.25.11' permissions: actions: read diff --git a/apps/api/src/routes/agents/enrollment.test.ts b/apps/api/src/routes/agents/enrollment.test.ts index 2af6a8b63..57b6fc49c 100644 --- a/apps/api/src/routes/agents/enrollment.test.ts +++ b/apps/api/src/routes/agents/enrollment.test.ts @@ -28,6 +28,10 @@ vi.mock('../../services/sentry', () => ({ captureException: vi.fn(), })); +vi.mock('../../services/anomalyMetrics', () => ({ + recordAgentEnrollment: vi.fn(), +})); + vi.mock('../../db/schema', () => ({ enrollmentKeys: { id: 'id', @@ -98,6 +102,7 @@ vi.mock('../../services/tenantStatus', () => ({ import { db } from '../../db'; import { writeAuditEvent } from '../../services/auditEvents'; +import { recordAgentEnrollment } from '../../services/anomalyMetrics'; import { getActiveOrgTenant } from '../../services/tenantStatus'; import * as manifestSigning from '../../services/manifestSigning'; import { enrollmentRoutes } from './enrollment'; @@ -848,6 +853,8 @@ describe('POST /agents/enroll — ENROLLMENT_SECRET_ENFORCEMENT_MODE', () => { result: 'denied', }) ); + // #984: the rejection must be visible to the EnrollmentSpike anomaly metric. + expect(recordAgentEnrollment).toHaveBeenCalledWith('error'); }); it('blocks production enrollment with no secret when mode is explicitly enforce', async () => { diff --git a/apps/api/src/routes/agents/enrollment.ts b/apps/api/src/routes/agents/enrollment.ts index 30b5acd9e..5c1b5341c 100644 --- a/apps/api/src/routes/agents/enrollment.ts +++ b/apps/api/src/routes/agents/enrollment.ts @@ -196,6 +196,7 @@ enrollmentRoutes.post('/enroll', zValidator('json', enrollSchema), async (c) => result: 'denied', errorMessage: 'Enrollment secret required', }); + recordAgentEnrollment('error'); return c.json({ error: 'Enrollment secret required' }, 403); } @@ -211,6 +212,7 @@ enrollmentRoutes.post('/enroll', zValidator('json', enrollSchema), async (c) => result: 'denied', errorMessage: 'Invalid enrollment secret', }); + recordAgentEnrollment('error'); return c.json({ error: 'Invalid enrollment secret' }, 403); } } else if (configuredSecret) { @@ -225,6 +227,7 @@ enrollmentRoutes.post('/enroll', zValidator('json', enrollSchema), async (c) => result: 'denied', errorMessage: 'Enrollment secret required', }); + recordAgentEnrollment('error'); return c.json({ error: 'Enrollment secret required' }, 403); } @@ -239,6 +242,7 @@ enrollmentRoutes.post('/enroll', zValidator('json', enrollSchema), async (c) => result: 'denied', errorMessage: 'Invalid enrollment secret', }); + recordAgentEnrollment('error'); return c.json({ error: 'Invalid enrollment secret' }, 403); } } else if (process.env.NODE_ENV === 'production') { @@ -281,6 +285,7 @@ enrollmentRoutes.post('/enroll', zValidator('json', enrollSchema), async (c) => result: 'denied', errorMessage: 'Enrollment secret required in production', }); + recordAgentEnrollment('error'); return c.json({ error: 'Enrollment secret required' }, 403); } } @@ -560,6 +565,7 @@ enrollmentRoutes.post('/enroll', zValidator('json', enrollSchema), async (c) => }).catch((err) => { console.error('[Enrollment] Failed to dispatch device-limit hook:', err instanceof Error ? err.message : err); }); + recordAgentEnrollment('error', deviceLimitPartnerId); throw new HTTPException(403, { message: JSON.stringify({ error: 'Device limit reached',