Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions apps/api/src/routes/agents/enrollment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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 () => {
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/routes/agents/enrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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) {
Expand All @@ -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);
}

Expand All @@ -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') {
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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',
Expand Down
Loading