feat: AI security controls + PostgreSQL hardening (Week 1 Quick Wins)
AI Security (EGAI 4.3.1.3, 4.3.1.4, 4.1.3.1, IAAI 3.6.26): - AI Disclaimer banner in ChatPanel: "AI responses may be inaccurate" - "AI Generated" violet badge on: chat messages, AI summaries, project narratives, AI-generated cover images - HITL: system prompt now requires explicit user confirmation before any data mutation (strongly worded instruction) - Mutation tool audit logging: all 31 write tools logged with tool name, params, userId, userRole via Pino PostgreSQL Hardening (PG Standard V1.6): - Audit logging: log_connections, log_disconnections, log_statement=ddl, log_min_duration_statement=1000 in docker-compose - SUPERUSER removal script: scripts/harden-postgres.sh (NOSUPERUSER + minimal GRANT for app user) - Health check: pg_isready -U capakraken -d capakraken - Documentation: security-architecture.md Section 12 updated Controls closed: EGAI 4.1.3.1, 4.3.1.3, 4.3.1.4, PG 3.3, 3.5 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -324,6 +324,12 @@ export function InsightsPanel() {
|
||||
</div>
|
||||
) : generateMutation.data ? (
|
||||
<div className="rounded-xl border border-brand-200 bg-brand-50/50 p-5 dark:border-brand-900/50 dark:bg-brand-950/20">
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 mb-3">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
AI Generated
|
||||
</span>
|
||||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-gray-800 dark:text-gray-200">
|
||||
{generateMutation.data.narrative}
|
||||
</p>
|
||||
@@ -333,6 +339,12 @@ export function InsightsPanel() {
|
||||
</div>
|
||||
) : cachedNarrativeQuery.data?.narrative ? (
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/50 p-5 dark:border-slate-700 dark:bg-slate-800/50">
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 mb-3">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
AI Generated
|
||||
</span>
|
||||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-gray-800 dark:text-gray-200">
|
||||
{cachedNarrativeQuery.data.narrative}
|
||||
</p>
|
||||
|
||||
@@ -120,7 +120,15 @@ export function ChatMessage({ role, content }: ChatMessageProps) {
|
||||
{isUser ? (
|
||||
<span className="whitespace-pre-wrap break-words">{content}</span>
|
||||
) : (
|
||||
<div className="space-y-0.5 break-words">{rendered}</div>
|
||||
<>
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 mb-1.5">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
AI Generated
|
||||
</span>
|
||||
<div className="space-y-0.5 break-words">{rendered}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -213,6 +213,11 @@ export function ChatPanel({ onClose }: { onClose: () => void }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Disclaimer (EGAI 4.3.1.4) */}
|
||||
<div className="px-3 py-2 text-[11px] text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700">
|
||||
AI responses may be inaccurate. Always verify critical information before acting on it.
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto px-3 py-3 space-y-3">
|
||||
{messages.length === 0 && !isLoading && (
|
||||
|
||||
@@ -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<HTMLInputElement>(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 */}
|
||||
<div className="absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-black/40 to-transparent" />
|
||||
{/* AI Generated badge (EGAI 4.3.1.3) */}
|
||||
{isAiGenerated && (
|
||||
<span className="absolute left-3 top-3 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 shadow-sm">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
AI Generated
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
|
||||
@@ -76,6 +76,12 @@ export function AiSummaryCard({ resourceId, aiSummary, aiSummaryUpdatedAt, onGen
|
||||
|
||||
{localSummary ? (
|
||||
<>
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 mb-2">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
AI Generated
|
||||
</span>
|
||||
<p className="text-sm text-gray-700 leading-relaxed">{localSummary}</p>
|
||||
{localUpdatedAt && (
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+24
@@ -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."
|
||||
Reference in New Issue
Block a user