|
1 | 1 | import { Command } from 'commander'; |
2 | | -import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers } from './cli'; |
| 2 | +import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, validateAgentBaseImage } from './cli'; |
3 | 3 | import { redactSecrets } from './redact-secrets'; |
4 | 4 | import * as fs from 'fs'; |
5 | 5 | import * as path from 'path'; |
@@ -774,4 +774,149 @@ describe('cli', () => { |
774 | 774 | expect(DEFAULT_DNS_SERVERS).toEqual(['8.8.8.8', '8.8.4.4']); |
775 | 775 | }); |
776 | 776 | }); |
| 777 | + |
| 778 | + describe('validateAgentBaseImage', () => { |
| 779 | + describe('valid images', () => { |
| 780 | + it('should accept official Ubuntu images', () => { |
| 781 | + expect(validateAgentBaseImage('ubuntu:22.04')).toEqual({ valid: true }); |
| 782 | + expect(validateAgentBaseImage('ubuntu:24.04')).toEqual({ valid: true }); |
| 783 | + expect(validateAgentBaseImage('ubuntu:20.04')).toEqual({ valid: true }); |
| 784 | + }); |
| 785 | + |
| 786 | + it('should accept catthehacker runner images', () => { |
| 787 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-22.04')).toEqual({ valid: true }); |
| 788 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-24.04')).toEqual({ valid: true }); |
| 789 | + }); |
| 790 | + |
| 791 | + it('should accept catthehacker full images', () => { |
| 792 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-22.04')).toEqual({ valid: true }); |
| 793 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-24.04')).toEqual({ valid: true }); |
| 794 | + }); |
| 795 | + |
| 796 | + it('should accept catthehacker act images', () => { |
| 797 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-22.04')).toEqual({ valid: true }); |
| 798 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-24.04')).toEqual({ valid: true }); |
| 799 | + }); |
| 800 | + |
| 801 | + it('should accept images with SHA256 digest pinning', () => { |
| 802 | + expect(validateAgentBaseImage('ubuntu:22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); |
| 803 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); |
| 804 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); |
| 805 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); |
| 806 | + }); |
| 807 | + }); |
| 808 | + |
| 809 | + describe('invalid images', () => { |
| 810 | + it('should reject arbitrary images', () => { |
| 811 | + const result = validateAgentBaseImage('malicious-registry.com/evil:latest'); |
| 812 | + expect(result.valid).toBe(false); |
| 813 | + expect(result.error).toContain('Invalid base image'); |
| 814 | + }); |
| 815 | + |
| 816 | + it('should reject images with typos', () => { |
| 817 | + const result = validateAgentBaseImage('ubunto:22.04'); |
| 818 | + expect(result.valid).toBe(false); |
| 819 | + expect(result.error).toContain('Invalid base image'); |
| 820 | + }); |
| 821 | + |
| 822 | + it('should reject non-ubuntu official images', () => { |
| 823 | + const result = validateAgentBaseImage('alpine:latest'); |
| 824 | + expect(result.valid).toBe(false); |
| 825 | + expect(result.error).toContain('Invalid base image'); |
| 826 | + }); |
| 827 | + |
| 828 | + it('should reject unknown registries', () => { |
| 829 | + const result = validateAgentBaseImage('docker.io/library/ubuntu:22.04'); |
| 830 | + expect(result.valid).toBe(false); |
| 831 | + expect(result.error).toContain('Invalid base image'); |
| 832 | + }); |
| 833 | + |
| 834 | + it('should reject images from other catthehacker registries', () => { |
| 835 | + const result = validateAgentBaseImage('ghcr.io/catthehacker/debian:latest'); |
| 836 | + expect(result.valid).toBe(false); |
| 837 | + expect(result.error).toContain('Invalid base image'); |
| 838 | + }); |
| 839 | + |
| 840 | + it('should reject ubuntu with non-standard tags', () => { |
| 841 | + const result = validateAgentBaseImage('ubuntu:latest'); |
| 842 | + expect(result.valid).toBe(false); |
| 843 | + expect(result.error).toContain('Invalid base image'); |
| 844 | + }); |
| 845 | + |
| 846 | + it('should reject empty image string', () => { |
| 847 | + const result = validateAgentBaseImage(''); |
| 848 | + expect(result.valid).toBe(false); |
| 849 | + expect(result.error).toContain('Invalid base image'); |
| 850 | + }); |
| 851 | + |
| 852 | + it('should reject ubuntu with only major version', () => { |
| 853 | + const result = validateAgentBaseImage('ubuntu:22'); |
| 854 | + expect(result.valid).toBe(false); |
| 855 | + expect(result.error).toContain('Invalid base image'); |
| 856 | + }); |
| 857 | + |
| 858 | + it('should reject catthehacker with wrong prefix', () => { |
| 859 | + const result = validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:minimal-22.04'); |
| 860 | + expect(result.valid).toBe(false); |
| 861 | + expect(result.error).toContain('Invalid base image'); |
| 862 | + }); |
| 863 | + |
| 864 | + it('should reject malformed SHA256 digest (too short)', () => { |
| 865 | + const result = validateAgentBaseImage('ubuntu:22.04@sha256:abc123'); |
| 866 | + expect(result.valid).toBe(false); |
| 867 | + expect(result.error).toContain('Invalid base image'); |
| 868 | + }); |
| 869 | + |
| 870 | + it('should reject image with path traversal attempt', () => { |
| 871 | + const result = validateAgentBaseImage('../ubuntu:22.04'); |
| 872 | + expect(result.valid).toBe(false); |
| 873 | + expect(result.error).toContain('Invalid base image'); |
| 874 | + }); |
| 875 | + |
| 876 | + it('should provide helpful error message with allowed options', () => { |
| 877 | + const result = validateAgentBaseImage('invalid:image'); |
| 878 | + expect(result.valid).toBe(false); |
| 879 | + expect(result.error).toContain('ubuntu:XX.XX'); |
| 880 | + expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:runner-XX.XX'); |
| 881 | + expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:full-XX.XX'); |
| 882 | + expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:act-XX.XX'); |
| 883 | + expect(result.error).toContain('@sha256:'); |
| 884 | + }); |
| 885 | + }); |
| 886 | + |
| 887 | + describe('regex pattern coverage', () => { |
| 888 | + // Ensure each regex pattern in SAFE_BASE_IMAGE_PATTERNS is individually tested |
| 889 | + it('should match pattern 1: plain ubuntu version', () => { |
| 890 | + expect(validateAgentBaseImage('ubuntu:18.04')).toEqual({ valid: true }); |
| 891 | + expect(validateAgentBaseImage('ubuntu:26.10')).toEqual({ valid: true }); |
| 892 | + }); |
| 893 | + |
| 894 | + it('should match pattern 2: catthehacker runner/full/act without digest', () => { |
| 895 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-18.04')).toEqual({ valid: true }); |
| 896 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-26.10')).toEqual({ valid: true }); |
| 897 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-22.04')).toEqual({ valid: true }); |
| 898 | + }); |
| 899 | + |
| 900 | + it('should match pattern 3: catthehacker with SHA256 digest', () => { |
| 901 | + const digest = 'sha256:' + '1234567890abcdef'.repeat(4); |
| 902 | + expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:runner-22.04@${digest}`)).toEqual({ valid: true }); |
| 903 | + expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:full-24.04@${digest}`)).toEqual({ valid: true }); |
| 904 | + expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:act-22.04@${digest}`)).toEqual({ valid: true }); |
| 905 | + }); |
| 906 | + |
| 907 | + it('should match pattern 4: plain ubuntu with SHA256 digest', () => { |
| 908 | + const digest = 'sha256:' + 'abcdef1234567890'.repeat(4); |
| 909 | + expect(validateAgentBaseImage(`ubuntu:22.04@${digest}`)).toEqual({ valid: true }); |
| 910 | + expect(validateAgentBaseImage(`ubuntu:24.04@${digest}`)).toEqual({ valid: true }); |
| 911 | + }); |
| 912 | + |
| 913 | + it('should reject images that almost match but do not exactly', () => { |
| 914 | + // Nearly matching but invalid |
| 915 | + expect(validateAgentBaseImage('ubuntu:22.04 ').valid).toBe(false); // trailing space |
| 916 | + expect(validateAgentBaseImage(' ubuntu:22.04').valid).toBe(false); // leading space |
| 917 | + expect(validateAgentBaseImage('Ubuntu:22.04').valid).toBe(false); // capital U |
| 918 | + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:Runner-22.04').valid).toBe(false); // capital R |
| 919 | + }); |
| 920 | + }); |
| 921 | + }); |
777 | 922 | }); |
0 commit comments