diff --git a/apps/web/src/components/analytics/InsightsPanel.tsx b/apps/web/src/components/analytics/InsightsPanel.tsx index 196ad21..03f990c 100644 --- a/apps/web/src/components/analytics/InsightsPanel.tsx +++ b/apps/web/src/components/analytics/InsightsPanel.tsx @@ -324,6 +324,12 @@ export function InsightsPanel() { ) : generateMutation.data ? (
+ + + + + AI Generated +

{generateMutation.data.narrative}

@@ -333,6 +339,12 @@ export function InsightsPanel() {
) : cachedNarrativeQuery.data?.narrative ? (
+ + + + + AI Generated +

{cachedNarrativeQuery.data.narrative}

diff --git a/apps/web/src/components/assistant/ChatMessage.tsx b/apps/web/src/components/assistant/ChatMessage.tsx index a0ff608..e1e699e 100644 --- a/apps/web/src/components/assistant/ChatMessage.tsx +++ b/apps/web/src/components/assistant/ChatMessage.tsx @@ -120,7 +120,15 @@ export function ChatMessage({ role, content }: ChatMessageProps) { {isUser ? ( {content} ) : ( -
{rendered}
+ <> + + + + + AI Generated + +
{rendered}
+ )}
diff --git a/apps/web/src/components/assistant/ChatPanel.tsx b/apps/web/src/components/assistant/ChatPanel.tsx index 3116ee5..33ce83f 100644 --- a/apps/web/src/components/assistant/ChatPanel.tsx +++ b/apps/web/src/components/assistant/ChatPanel.tsx @@ -213,6 +213,11 @@ export function ChatPanel({ onClose }: { onClose: () => void }) { + {/* AI Disclaimer (EGAI 4.3.1.4) */} +
+ AI responses may be inaccurate. Always verify critical information before acting on it. +
+ {/* Messages */}
{messages.length === 0 && !isLoading && ( diff --git a/apps/web/src/components/projects/CoverArtSection.tsx b/apps/web/src/components/projects/CoverArtSection.tsx index 4917bb9..9547963 100644 --- a/apps/web/src/components/projects/CoverArtSection.tsx +++ b/apps/web/src/components/projects/CoverArtSection.tsx @@ -22,6 +22,7 @@ export function CoverArtSection({ projectId, coverImageUrl, coverFocusY = 50, pr const [customPrompt, setCustomPrompt] = useState(""); const [showPromptInput, setShowPromptInput] = useState(false); const [showFocusSlider, setShowFocusSlider] = useState(false); + const [isAiGenerated, setIsAiGenerated] = useState(false); const fileInputRef = useRef(null); const utils = trpc.useUtils(); @@ -40,6 +41,7 @@ export function CoverArtSection({ projectId, coverImageUrl, coverFocusY = 50, pr ...(customPrompt.trim() ? { prompt: customPrompt.trim() } : {}), }); setImageUrl(result.coverImageUrl); + setIsAiGenerated(true); setShowPromptInput(false); setCustomPrompt(""); void utils.project.getById.invalidate({ id: projectId }); @@ -165,6 +167,15 @@ export function CoverArtSection({ projectId, coverImageUrl, coverFocusY = 50, pr /> {/* Gradient overlay at bottom for readability */}
+ {/* AI Generated badge (EGAI 4.3.1.3) */} + {isAiGenerated && ( + + + + + AI Generated + + )}
) : (
+ + + + + AI Generated +

{localSummary}

{localUpdatedAt && (

diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c17b0ec..9207698 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -8,10 +8,17 @@ services: POSTGRES_DB: capakraken POSTGRES_USER: capakraken POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} + command: > + postgres + -c log_connections=on + -c log_disconnections=on + -c log_statement=ddl + -c log_line_prefix='%t [%p] %u@%d ' + -c log_min_duration_statement=1000 volumes: - capakraken_prod_pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U capakraken"] + test: ["CMD-SHELL", "pg_isready -U capakraken -d capakraken"] interval: 10s timeout: 5s retries: 5 diff --git a/docker-compose.yml b/docker-compose.yml index 3af8d92..a4af6bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,10 +7,17 @@ services: POSTGRES_DB: capakraken POSTGRES_USER: capakraken POSTGRES_PASSWORD: capakraken_dev + command: > + postgres + -c log_connections=on + -c log_disconnections=on + -c log_statement=ddl + -c log_line_prefix='%t [%p] %u@%d ' + -c log_min_duration_statement=1000 volumes: - capakraken_pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U capakraken"] + test: ["CMD-SHELL", "pg_isready -U capakraken -d capakraken"] interval: 5s timeout: 3s retries: 5 diff --git a/docs/security-architecture.md b/docs/security-architecture.md index 4e8bfc9..c632b95 100644 --- a/docs/security-architecture.md +++ b/docs/security-architecture.md @@ -172,14 +172,23 @@ Browser -> Next.js (port 3100) -> tRPC -> Prisma -> PostgreSQL (port 5433) - All user inputs validated by Zod schemas before reaching the data layer - JSONB fields (blueprints, skill matrices, permission overrides) are type-checked at the application boundary -### Recommendations for Production Hardening +### Active Hardening Measures + +- **PostgreSQL audit logging** enabled via `docker-compose.yml` command flags: + - `log_connections=on` / `log_disconnections=on` — all connection lifecycle events + - `log_statement=ddl` — all DDL statements (CREATE, ALTER, DROP) + - `log_min_duration_statement=1000` — slow queries (>1s) logged for performance review + - `log_line_prefix='%t [%p] %u@%d '` — timestamp, PID, user, and database in every log line +- **SUPERUSER removed** from the application database user (`capakraken`); hardening script at `scripts/harden-postgres.sh` +- **Minimal privilege grants**: application user has only SELECT, INSERT, UPDATE, DELETE on tables and USAGE/SELECT on sequences — no CREATE, DROP, or SUPERUSER capabilities + +### Recommendations for Further Production Hardening 1. **Enable PostgreSQL SSL/TLS**: Set `ssl: true` in the Prisma connection string and configure `postgresql.conf` with `ssl = on`, `ssl_cert_file`, `ssl_key_file` -2. **Enable query audit logging**: Set `log_statement = 'all'` (or `'ddl'` minimum) in `postgresql.conf` to capture all executed statements for forensic review -3. **Restrict connections by IP**: Configure `pg_hba.conf` to accept connections only from the application container's subnet (e.g., `172.18.0.0/16`) -4. **Use separate database roles**: Create a read-only role for reporting queries and a migration-only role for schema changes, limiting the default application role to DML operations -5. **Enable connection pooling**: Use PgBouncer in production to limit maximum connections and prevent resource exhaustion attacks -6. **Backup encryption**: Ensure `pg_dump` backups are encrypted at rest (GPG or filesystem-level encryption) +2. **Restrict connections by IP**: Configure `pg_hba.conf` to accept connections only from the application container's subnet (e.g., `172.18.0.0/16`) +3. **Use separate database roles**: Create a read-only role for reporting queries and a migration-only role for schema changes, limiting the default application role to DML operations +4. **Enable connection pooling**: Use PgBouncer in production to limit maximum connections and prevent resource exhaustion attacks +5. **Backup encryption**: Ensure `pg_dump` backups are encrypted at rest (GPG or filesystem-level encryption) ### Redis Security diff --git a/packages/api/src/router/assistant-tools.ts b/packages/api/src/router/assistant-tools.ts index 38b0c12..def2c7d 100644 --- a/packages/api/src/router/assistant-tools.ts +++ b/packages/api/src/router/assistant-tools.ts @@ -19,6 +19,24 @@ import { emitTaskStatusChanged, emitBroadcastSent, } from "../sse/event-bus.js"; +import { logger } from "../lib/logger.js"; + +// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ────── + +const MUTATION_TOOLS = new Set([ + "create_allocation", "cancel_allocation", "update_allocation_status", + "update_resource", "deactivate_resource", "create_resource", + "update_project", "create_project", "delete_project", + "create_vacation", "approve_vacation", "reject_vacation", "cancel_vacation", + "set_entitlement", "create_demand", "fill_demand", + "generate_project_cover", "remove_project_cover", + "create_role", "update_role", "delete_role", + "create_client", "update_client", + "create_org_unit", "update_org_unit", + "send_broadcast", "create_task_for_user", "create_reminder", + "update_task_status", "execute_task_action", + "create_comment", "resolve_comment", +]); // ─── Types ────────────────────────────────────────────────────────────────── @@ -5613,6 +5631,15 @@ export async function executeTool( try { const params = JSON.parse(args); + + // Audit-log all mutation tool executions (EGAI 4.1.3.1 / IAAI 3.6.26) + if (MUTATION_TOOLS.has(name)) { + logger.info( + { tool: name, params, userId: ctx.userId, userRole: ctx.userRole }, + "AI assistant mutation tool executed", + ); + } + const result = await executor(params, ctx); // Detect action payloads (e.g. navigation, invalidation) diff --git a/packages/api/src/router/assistant.ts b/packages/api/src/router/assistant.ts index 83ff821..f0a3df5 100644 --- a/packages/api/src/router/assistant.ts +++ b/packages/api/src/router/assistant.ts @@ -32,7 +32,7 @@ Deine Fähigkeiten: Wichtige Regeln: - Antworte in der Sprache des Users (Deutsch oder Englisch) - Geldbeträge: intern in Cent, konvertiere zu EUR für den User -- Vor Datenänderungen: kurze Zusammenfassung + Bestätigung einholen +- KRITISCH — Human-in-the-Loop (EGAI 4.1.3.1 / IAAI 3.6.26): Bevor du eine Aktion ausführst, die Daten erstellt, ändert oder löscht (create, update, delete, approve, reject, cancel, deactivate, fill, set, generate, remove, send), MUSST du dem User IMMER zuerst eine Zusammenfassung zeigen, was du tun wirst, und EXPLIZIT auf seine Bestätigung warten. Führe NIEMALS eine schreibende Aktion aus ohne vorherige Bestätigung des Users. Wenn der User "ja", "ok", "mach das", "bestätigt" o.ä. antwortet, dann erst ausführen. - Sei KURZ und DIREKT. Keine langen Erklärungen wenn nicht nötig. Antworte knapp und präzise. - Rufe Tools PARALLEL auf wenn möglich (z.B. search_resources + list_allocations gleichzeitig) - Fasse Ergebnisse kompakt zusammen — keine unnötigen Wiederholungen der Tool-Ergebnisse diff --git a/scripts/harden-postgres.sh b/scripts/harden-postgres.sh new file mode 100755 index 0000000..eb9d440 --- /dev/null +++ b/scripts/harden-postgres.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Remove SUPERUSER from the application database user +# Run after initial setup: bash scripts/harden-postgres.sh + +CONTAINER="planarchy-postgres-1" # Note: container name may still use old naming +DB_USER="capakraken" +DB_NAME="capakraken" + +echo "Hardening PostgreSQL for $DB_USER..." + +# Remove SUPERUSER privilege +docker exec $CONTAINER psql -U postgres -c "ALTER USER $DB_USER NOSUPERUSER;" + +# Grant only needed permissions +docker exec $CONTAINER psql -U postgres -d $DB_NAME -c " + GRANT CONNECT ON DATABASE $DB_NAME TO $DB_USER; + GRANT USAGE ON SCHEMA public TO $DB_USER; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $DB_USER; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $DB_USER; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $DB_USER; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO $DB_USER; +" + +echo "Done. $DB_USER no longer has SUPERUSER."