Skip to content

Commit 27aa636

Browse files
committed
refactor(hosted-web): split container hardening contracts
1 parent 5e688fd commit 27aa636

2 files changed

Lines changed: 90 additions & 66 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
export const RESULT_FORMAT = 'hosted-container-hardening-verifier-result/v2';
2+
export const COMPOSE_PATH = 'docker/docker-compose.yml';
3+
export const PROFILES = Object.freeze(['personal', 'keycloak']);
4+
export const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/iu;
5+
export const POSITIVE_DURATION_PATTERN = /^[1-9]\d*(?:ms|s|m|h)(?:\d+(?:ms|s|m|h))*$/u;
6+
7+
// prettier-ignore
8+
export const PROFILE_SERVICES = Object.freeze({ personal: Object.freeze(['agent-teams-personal', 'caddy-personal']), keycloak: Object.freeze(['agent-teams-keycloak', 'agent-teams-keycloak-secret-init', 'caddy', 'keycloak', 'keycloak-postgres', 'keycloak-volume-init']) });
9+
// prettier-ignore
10+
export const LONG_RUNNING_SERVICES = new Set(['agent-teams-personal', 'agent-teams-keycloak', 'caddy', 'caddy-personal', 'keycloak', 'keycloak-postgres']);
11+
// prettier-ignore
12+
export const EXPECTED_USERS = Object.freeze({ 'agent-teams-personal': '1000:1000', 'agent-teams-keycloak': '1000:1000', 'agent-teams-keycloak-secret-init': '1000:1000', caddy: '1000:1000', 'caddy-personal': '1000:1000', keycloak: '1000:0', 'keycloak-postgres': '70:70', 'keycloak-volume-init': '1000:1000' });
13+
// prettier-ignore
14+
export const EXPECTED_DEPENDENCIES = Object.freeze({ 'agent-teams-personal': Object.freeze({ 'caddy-personal': 'service_healthy' }), 'agent-teams-keycloak': Object.freeze({ 'agent-teams-keycloak-secret-init': 'service_completed_successfully', caddy: 'service_healthy', keycloak: 'service_healthy', 'keycloak-volume-init': 'service_completed_successfully' }), keycloak: Object.freeze({ caddy: 'service_healthy', 'keycloak-postgres': 'service_healthy', 'keycloak-volume-init': 'service_completed_successfully' }), 'keycloak-volume-init': Object.freeze({ caddy: 'service_healthy' }) });
15+
// prettier-ignore
16+
export const APP_HEALTHCHECK = Object.freeze(['CMD', 'node', '-e', "fetch('http://127.0.0.1:3456/api/auth/status').then(r=>{if(!r.ok)process.exit(1)})"]);
17+
// prettier-ignore
18+
export const CADDY_HEALTHCHECK = Object.freeze(['CMD-SHELL', 'test -s /data/caddy/pki/authorities/local/root.crt && wget -q --spider http://127.0.0.1:2019/config/']);
19+
export const POSTGRES_HEALTHCHECK = Object.freeze([
20+
'CMD-SHELL',
21+
'pg_isready -U keycloak -d keycloak',
22+
]);
23+
// prettier-ignore
24+
export const EXPECTED_TMPFS = Object.freeze({ 'agent-teams-personal': Object.freeze(['/run/agent-teams:mode=0700,uid=1000,gid=1000', '/tmp:mode=1777']), 'agent-teams-keycloak': Object.freeze(['/run/agent-teams:mode=0700,uid=1000,gid=1000', '/tmp:mode=1777']), caddy: Object.freeze(['/tmp:mode=1777']), 'caddy-personal': Object.freeze(['/tmp:mode=1777']), keycloak: Object.freeze(['/opt/keycloak/data/import:mode=0700,uid=1000,gid=0', '/opt/keycloak/data/tmp:mode=0700,uid=1000,gid=0', '/run/keycloak:mode=0700,uid=1000,gid=0', '/tmp:mode=1777']), 'keycloak-postgres': Object.freeze(['/tmp:mode=1777', '/var/run/postgresql:mode=0775,uid=70,gid=70']), 'keycloak-volume-init': Object.freeze([]), 'agent-teams-keycloak-secret-init': Object.freeze([]) });
25+
// prettier-ignore
26+
export const DEFAULT_RENDER_ENVIRONMENT = Object.freeze({ CLAUDE_DIR: '/tmp/agent-teams-hosted-config-claude', HOSTED_SECRETS_DIR: '/tmp/agent-teams-hosted-config-secrets', NODE_IMAGE_DIGEST: `sha256:${'a'.repeat(64)}`, KEYCLOAK_IMAGE_DIGEST: `sha256:${'b'.repeat(64)}`, POSTGRES_IMAGE_DIGEST: `sha256:${'c'.repeat(64)}`, CADDY_IMAGE_DIGEST: `sha256:${'d'.repeat(64)}` });
27+
28+
export function isObject(value) {
29+
return value !== null && typeof value === 'object' && !Array.isArray(value);
30+
}
31+
32+
export function compareText(left, right) {
33+
return String(left).localeCompare(String(right));
34+
}
35+
36+
export function sameValues(actual, expected) {
37+
if (!Array.isArray(actual)) return expected.length === 0;
38+
return (
39+
JSON.stringify([...actual].sort(compareText)) ===
40+
JSON.stringify([...expected].sort(compareText))
41+
);
42+
}
43+
44+
export function sameSequence(actual, expected) {
45+
return Array.isArray(actual) && JSON.stringify(actual) === JSON.stringify(expected);
46+
}
47+
48+
export function isPositive(value) {
49+
const number = typeof value === 'number' ? value : Number(value);
50+
return Number.isFinite(number) && number > 0;
51+
}
52+
53+
export function isPositiveDuration(value) {
54+
return typeof value === 'string' && POSITIVE_DURATION_PATTERN.test(value);
55+
}
56+
57+
export function resultFor(checkedServices, checkedProfiles, violations) {
58+
const uniqueViolations = [...new Set(violations)].sort(compareText);
59+
return {
60+
format: RESULT_FORMAT,
61+
status: uniqueViolations.length === 0 ? 'passed' : 'failed',
62+
summary: {
63+
checkedProfiles,
64+
checkedServices,
65+
violations: uniqueViolations.length,
66+
},
67+
violations: uniqueViolations,
68+
};
69+
}

scripts/ci/verify-hosted-container-hardening.mjs

Lines changed: 21 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,31 @@ import { existsSync, readFileSync } from 'node:fs';
55
import { dirname, join, resolve } from 'node:path';
66
import { fileURLToPath, pathToFileURL } from 'node:url';
77

8+
import {
9+
APP_HEALTHCHECK,
10+
CADDY_HEALTHCHECK,
11+
COMPOSE_PATH,
12+
DEFAULT_RENDER_ENVIRONMENT,
13+
DIGEST_PATTERN,
14+
EXPECTED_DEPENDENCIES,
15+
EXPECTED_TMPFS,
16+
EXPECTED_USERS,
17+
LONG_RUNNING_SERVICES,
18+
POSTGRES_HEALTHCHECK,
19+
PROFILES,
20+
PROFILE_SERVICES,
21+
compareText,
22+
isObject,
23+
isPositive,
24+
isPositiveDuration,
25+
resultFor,
26+
sameSequence,
27+
sameValues,
28+
} from './verify-hosted-container-hardening-contracts.mjs';
829
import { verifyHostedNoTerminalDockerfile } from './verify-hosted-no-terminal-artifact.mjs';
930

10-
const RESULT_FORMAT = 'hosted-container-hardening-verifier-result/v2';
1131
const SCRIPT_DIRECTORY = dirname(fileURLToPath(import.meta.url));
1232
const REPOSITORY_ROOT = resolve(SCRIPT_DIRECTORY, '../..');
13-
const COMPOSE_PATH = 'docker/docker-compose.yml';
14-
const PROFILES = Object.freeze(['personal', 'keycloak']);
15-
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/iu;
16-
const POSITIVE_DURATION_PATTERN = /^[1-9]\d*(?:ms|s|m|h)(?:\d+(?:ms|s|m|h))*$/u;
17-
18-
// prettier-ignore
19-
const PROFILE_SERVICES = Object.freeze({ personal: Object.freeze(['agent-teams-personal', 'caddy-personal']), keycloak: Object.freeze(['agent-teams-keycloak', 'agent-teams-keycloak-secret-init', 'caddy', 'keycloak', 'keycloak-postgres', 'keycloak-volume-init']) });
20-
// prettier-ignore
21-
const LONG_RUNNING_SERVICES = new Set(['agent-teams-personal', 'agent-teams-keycloak', 'caddy', 'caddy-personal', 'keycloak', 'keycloak-postgres']);
22-
// prettier-ignore
23-
const EXPECTED_USERS = Object.freeze({ 'agent-teams-personal': '1000:1000', 'agent-teams-keycloak': '1000:1000', 'agent-teams-keycloak-secret-init': '1000:1000', caddy: '1000:1000', 'caddy-personal': '1000:1000', keycloak: '1000:0', 'keycloak-postgres': '70:70', 'keycloak-volume-init': '1000:1000' });
24-
// prettier-ignore
25-
const EXPECTED_DEPENDENCIES = Object.freeze({ 'agent-teams-personal': Object.freeze({ 'caddy-personal': 'service_healthy' }), 'agent-teams-keycloak': Object.freeze({ 'agent-teams-keycloak-secret-init': 'service_completed_successfully', caddy: 'service_healthy', keycloak: 'service_healthy', 'keycloak-volume-init': 'service_completed_successfully' }), keycloak: Object.freeze({ caddy: 'service_healthy', 'keycloak-postgres': 'service_healthy', 'keycloak-volume-init': 'service_completed_successfully' }), 'keycloak-volume-init': Object.freeze({ caddy: 'service_healthy' }) });
26-
// prettier-ignore
27-
const APP_HEALTHCHECK = Object.freeze(['CMD', 'node', '-e', "fetch('http://127.0.0.1:3456/api/auth/status').then(r=>{if(!r.ok)process.exit(1)})"]);
28-
// prettier-ignore
29-
const CADDY_HEALTHCHECK = Object.freeze(['CMD-SHELL', 'test -s /data/caddy/pki/authorities/local/root.crt && wget -q --spider http://127.0.0.1:2019/config/']);
30-
const POSTGRES_HEALTHCHECK = Object.freeze(['CMD-SHELL', 'pg_isready -U keycloak -d keycloak']);
31-
// prettier-ignore
32-
const EXPECTED_TMPFS = Object.freeze({ 'agent-teams-personal': Object.freeze(['/run/agent-teams:mode=0700,uid=1000,gid=1000', '/tmp:mode=1777']), 'agent-teams-keycloak': Object.freeze(['/run/agent-teams:mode=0700,uid=1000,gid=1000', '/tmp:mode=1777']), caddy: Object.freeze(['/tmp:mode=1777']), 'caddy-personal': Object.freeze(['/tmp:mode=1777']), keycloak: Object.freeze(['/opt/keycloak/data/import:mode=0700,uid=1000,gid=0', '/opt/keycloak/data/tmp:mode=0700,uid=1000,gid=0', '/run/keycloak:mode=0700,uid=1000,gid=0', '/tmp:mode=1777']), 'keycloak-postgres': Object.freeze(['/tmp:mode=1777', '/var/run/postgresql:mode=0775,uid=70,gid=70']), 'keycloak-volume-init': Object.freeze([]), 'agent-teams-keycloak-secret-init': Object.freeze([]) });
33-
// prettier-ignore
34-
const DEFAULT_RENDER_ENVIRONMENT = Object.freeze({ CLAUDE_DIR: '/tmp/agent-teams-hosted-config-claude', HOSTED_SECRETS_DIR: '/tmp/agent-teams-hosted-config-secrets', NODE_IMAGE_DIGEST: `sha256:${'a'.repeat(64)}`, KEYCLOAK_IMAGE_DIGEST: `sha256:${'b'.repeat(64)}`, POSTGRES_IMAGE_DIGEST: `sha256:${'c'.repeat(64)}`, CADDY_IMAGE_DIGEST: `sha256:${'d'.repeat(64)}` });
3533

3634
/**
3735
* Verifies the effective Docker Compose model, not indentation or YAML spelling.
@@ -751,49 +749,6 @@ function verifyExactNames(actual, expected, prefix, violations) {
751749
if (!sameValues(actual, expected)) violations.push(`${prefix}_inventory_invalid`);
752750
}
753751

754-
function isObject(value) {
755-
return value !== null && typeof value === 'object' && !Array.isArray(value);
756-
}
757-
758-
function sameValues(actual, expected) {
759-
if (!Array.isArray(actual)) return expected.length === 0;
760-
return (
761-
JSON.stringify([...actual].sort(compareText)) ===
762-
JSON.stringify([...expected].sort(compareText))
763-
);
764-
}
765-
766-
function sameSequence(actual, expected) {
767-
return Array.isArray(actual) && JSON.stringify(actual) === JSON.stringify(expected);
768-
}
769-
770-
function isPositive(value) {
771-
const number = typeof value === 'number' ? value : Number(value);
772-
return Number.isFinite(number) && number > 0;
773-
}
774-
775-
function isPositiveDuration(value) {
776-
return typeof value === 'string' && POSITIVE_DURATION_PATTERN.test(value);
777-
}
778-
779-
function resultFor(checkedServices, checkedProfiles, violations) {
780-
const uniqueViolations = [...new Set(violations)].sort(compareText);
781-
return {
782-
format: RESULT_FORMAT,
783-
status: uniqueViolations.length === 0 ? 'passed' : 'failed',
784-
summary: {
785-
checkedProfiles,
786-
checkedServices,
787-
violations: uniqueViolations.length,
788-
},
789-
violations: uniqueViolations,
790-
};
791-
}
792-
793-
function compareText(left, right) {
794-
return String(left).localeCompare(String(right));
795-
}
796-
797752
function parseArguments(argv) {
798753
const options = {};
799754
for (let index = 0; index < argv.length; index += 1) {

0 commit comments

Comments
 (0)