diff --git a/src/assets/__tests__/__snapshots__/dockerfile-render.test.ts.snap b/src/assets/__tests__/__snapshots__/dockerfile-render.test.ts.snap index 4bc6e9294..90bb1457d 100644 --- a/src/assets/__tests__/__snapshots__/dockerfile-render.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/dockerfile-render.test.ts.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Dockerfile enableOtel rendering > renders opentelemetry-instrument CMD when enableOtel is true > Dockerfile-enableOtel-true 1`] = ` -"FROM public.ecr.aws/docker/library/python:3.12-slim-bookworm +"FROM public.ecr.aws/docker/library/python:3.12-slim-trixie RUN pip install --no-cache-dir uv @@ -41,7 +41,7 @@ CMD ["opentelemetry-instrument", "python", "-m", "main"] `; exports[`Dockerfile enableOtel rendering > renders plain python CMD when enableOtel is false > Dockerfile-enableOtel-false 1`] = ` -"FROM public.ecr.aws/docker/library/python:3.12-slim-bookworm +"FROM public.ecr.aws/docker/library/python:3.12-slim-trixie RUN pip install --no-cache-dir uv diff --git a/src/assets/container/python/Dockerfile b/src/assets/container/python/Dockerfile index 60a0e87fc..cb3569eff 100644 --- a/src/assets/container/python/Dockerfile +++ b/src/assets/container/python/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/python:3.12-slim-bookworm +FROM public.ecr.aws/docker/library/python:3.12-slim-trixie RUN pip install --no-cache-dir uv diff --git a/src/cli/commands/import/actions.ts b/src/cli/commands/import/actions.ts index 990fe006f..f8fac62fb 100644 --- a/src/cli/commands/import/actions.ts +++ b/src/cli/commands/import/actions.ts @@ -9,6 +9,7 @@ import type { } from '../../../schema'; import { validateAwsCredentials } from '../../aws/account'; import { arnPrefix } from '../../aws/partition'; +import { PYTHON_BASE_IMAGE } from '../../constants'; import { ExecLogger } from '../../logging'; import { setupPythonProject } from '../../operations/python/setup'; import { executeCdkImportPipeline } from './import-pipeline'; @@ -383,7 +384,7 @@ export async function handleImport(options: ImportOptions): Promise ({ existsSync: vi.fn(), + readFileSync: vi.fn(), })); vi.mock('../../../../lib', () => ({ @@ -26,6 +27,7 @@ vi.mock('../../../../lib', () => ({ })); const mockedExistsSync = vi.mocked(existsSync); +const mockedReadFileSync = vi.mocked(readFileSync); const CONFIG_ROOT = '/project/agentcore'; @@ -44,6 +46,11 @@ describe('validateContainerAgents', () => { vi.clearAllMocks(); }); + // Default readFileSync to return a safe Dockerfile so the warning check doesn't fail on unrelated tests + function mockValidDockerfile(): void { + mockedReadFileSync.mockReturnValue('FROM public.ecr.aws/docker/library/python:3.12-slim-trixie\n'); + } + it('does nothing when there are no Container agents', () => { const spec = makeSpec([{ name: 'zip-agent', build: 'CodeZip', codeLocation: dir('agents/zip-agent') }]); @@ -53,6 +60,7 @@ describe('validateContainerAgents', () => { it('does nothing when Container agent has a valid Dockerfile', () => { mockedExistsSync.mockReturnValue(true); + mockValidDockerfile(); const spec = makeSpec([ { name: 'container-agent', build: 'Container', codeLocation: dir('agents/container-agent') }, @@ -72,6 +80,7 @@ describe('validateContainerAgents', () => { it('only validates Container agents and skips CodeZip agents', () => { mockedExistsSync.mockReturnValue(true); + mockValidDockerfile(); const spec = makeSpec([ { name: 'zip-agent', build: 'CodeZip', codeLocation: dir('agents/zip-agent') }, @@ -104,6 +113,7 @@ describe('validateContainerAgents', () => { it('checks for custom dockerfile name when specified', () => { mockedExistsSync.mockReturnValue(true); + mockValidDockerfile(); const spec = makeSpec([ { name: 'gpu-agent', build: 'Container', codeLocation: dir('agents/gpu'), dockerfile: 'Dockerfile.gpu' }, @@ -124,4 +134,50 @@ describe('validateContainerAgents', () => { expect(() => validateContainerAgents(spec, CONFIG_ROOT)).toThrow(/Dockerfile\.gpu not found/); }); + + it('warns when Dockerfile uses deprecated bookworm base image', () => { + mockedExistsSync.mockReturnValue(true); + mockedReadFileSync.mockReturnValue( + 'FROM public.ecr.aws/docker/library/python:3.12-slim-bookworm\nRUN pip install uv' + ); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + const spec = makeSpec([{ name: 'my-agent', build: 'Container', codeLocation: dir('agents/my-agent') }]); + + expect(() => validateContainerAgents(spec, CONFIG_ROOT)).not.toThrow(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('CVE-2026-42010')); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('my-agent')); + + warnSpy.mockRestore(); + }); + + it('does not warn when Dockerfile uses trixie base image', () => { + mockedExistsSync.mockReturnValue(true); + mockedReadFileSync.mockReturnValue( + 'FROM public.ecr.aws/docker/library/python:3.12-slim-trixie\nRUN pip install uv' + ); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + const spec = makeSpec([{ name: 'my-agent', build: 'Container', codeLocation: dir('agents/my-agent') }]); + + expect(() => validateContainerAgents(spec, CONFIG_ROOT)).not.toThrow(); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('does not warn when bookworm appears in a non-FROM line', () => { + mockedExistsSync.mockReturnValue(true); + mockedReadFileSync.mockReturnValue( + 'FROM public.ecr.aws/docker/library/python:3.12-slim-trixie\n# migrated from slim-bookworm\nRUN pip install uv' + ); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + const spec = makeSpec([{ name: 'my-agent', build: 'Container', codeLocation: dir('agents/my-agent') }]); + + expect(() => validateContainerAgents(spec, CONFIG_ROOT)).not.toThrow(); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); }); diff --git a/src/cli/operations/deploy/preflight.ts b/src/cli/operations/deploy/preflight.ts index 6577eb529..0c004002d 100644 --- a/src/cli/operations/deploy/preflight.ts +++ b/src/cli/operations/deploy/preflight.ts @@ -6,7 +6,7 @@ import { CdkToolkitWrapper, createCdkToolkitWrapper, silentIoHost } from '../../ import { checkBootstrapStatus, checkStacksStatus, formatCdkEnvironment } from '../../cloudformation'; import { cleanupStaleLockFiles } from '../../tui/utils'; import type { IIoHost } from '@aws-cdk/toolkit-lib'; -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import * as path from 'node:path'; export interface PreflightContext { @@ -198,6 +198,8 @@ export function validateContainerAgents(projectSpec: AgentCoreProjectSpec, confi errors.push( `Agent "${agent.name}": ${agent.dockerfile ?? DOCKERFILE_NAME} not found at ${dockerfilePath}. Container agents require a Dockerfile.` ); + } else { + warnDeprecatedBaseImage(dockerfilePath, agent.name); } } } @@ -206,6 +208,27 @@ export function validateContainerAgents(projectSpec: AgentCoreProjectSpec, confi } } +const DEPRECATED_BASE_IMAGES = ['slim-bookworm']; + +function warnDeprecatedBaseImage(dockerfilePath: string, agentName: string): void { + try { + const content = readFileSync(dockerfilePath, 'utf-8'); + for (const line of content.split('\n')) { + if (!/^\s*FROM\s+/i.test(line)) continue; + for (const image of DEPRECATED_BASE_IMAGES) { + if (line.includes(image)) { + console.warn( + `Warning: Agent "${agentName}" Dockerfile uses a base image containing "${image}" which is affected by ` + + `CVE-2026-42010 (GnuTLS authentication bypass). Update the FROM line to use a Trixie-based variant.` + ); + } + } + } + } catch { + // Non-fatal — if we can't read the file, the existing validation will handle it + } +} + /** * Builds the CDK project. */