#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // EOS Policy Enforcement v2 — Förklara VARFÖR och HUR man häver blockering // Erik-krav: "Ett bra enforcement-system säger inte bara NEJ. Det säger: vilken regel, varför, evidens, och exakt vad som krävs." // ═══════════════════════════════════════════════════════════════════════════ import { execSync } from 'child_process'; import { existsSync, appendFileSync } from 'fs'; const ENFORCEMENT_LOG = '/home/bernt/.openclaw/workspace/EOS/enforcement-log-v2.jsonl'; /** * Policyer med förklaringar och vägar tillbaka */ const POLICIES = [ { id: 'block-direct-prod', name: 'Blockera direkt skrivning till produktion', rule: 'Ingen agent får skriva direkt till produktion', why: 'Direkta ändringar i produktion kan inte spåras, testas eller återställas. De skapar "drift" som skiljer sig från kodbasen.', evidence: (action) => `Kör på server: ${existsSync('/etc/ec2-release') ? 'Ja' : 'Nej'}`, requiredToUnblock: [ 'Gör ändringen i en branch', 'Skapa PR för code review', 'Låt CI köra tester', 'Mergea till main', 'Låt pipeline deploya' ], check: (action) => { const isProd = process.env.NODE_ENV === 'production' || existsSync('/etc/ec2-release'); const isWrite = action.type === 'write' || action.type === 'deploy'; return { blocked: isProd && isWrite, reason: isProd && isWrite ? 'Direkt skrivning till produktion' : null }; } }, { id: 'block-no-pipeline-deploy', name: 'Blockera deployment utan pipeline', rule: 'Ingen deployment utan godkänd CI/CD pipeline', why: 'Manuell deployment är felbenägen och kan inte reproduceras. Pipeline säkerställer att tester körs och att deployment är spårbart.', evidence: (action) => `CI/CD hittad: ${existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') ? 'Ja' : 'Nej'}`, requiredToUnblock: [ 'Skapa .github/workflows/deploy.yml', 'Konfigurera deployment-steg', 'Verifiera att pipeline kan köras', 'Dokumentera pipeline i README' ], check: (action) => { if (action.type !== 'deploy') return { blocked: false }; const hasPipeline = existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') || existsSync('/home/bernt/repos/quixzoom.com/.gitlab-ci.yml'); return { blocked: !hasPipeline, reason: !hasPipeline ? 'Ingen CI/CD pipeline konfigurerad' : null }; } }, { id: 'block-server-edit', name: 'Blockera redigering på server', rule: 'Ingen redigering direkt på server', why: 'Ändringar på server är inte versionshanterade och kan förloras vid omstart eller deployment. All kod ska gå via Git.', evidence: (action) => `Kör på server: ${existsSync('/etc/ec2-release') ? 'Ja' : 'Nej'}`, requiredToUnblock: [ 'Gör ändringen lokalt eller i en dev-miljö', 'Commita ändringen', 'Pusha till remote', 'Låt CI/CD deploya till server' ], check: (action) => { const isServer = existsSync('/etc/ec2-release') || existsSync('/var/lib/cloud'); const isEdit = action.type === 'edit' || action.type === 'write'; return { blocked: isServer && isEdit, reason: isServer && isEdit ? 'Redigering på server' : null }; } }, { id: 'block-uncommitted', name: 'Blockera ändring utan commit', rule: 'Ingen ny ändring när ocommitade filer finns', why: 'Ocommitade ändringar riskerar att gå förlorade eller konflikta med nya ändringar. Varje ändring ska vara spårbart.', evidence: (action) => { try { const status = execSync('git status --short', { cwd: '/home/bernt/repos/quixzoom.com', encoding: 'utf8' }); const count = status.trim().split('\n').filter(l => l.trim()).length; return `${count} ocommitade filer`; } catch { return 'Kunde inte kontrollera git-status'; } }, requiredToUnblock: [ 'Kör "git status" för att se ocommitade filer', 'Bestäm: commita eller återställ varje fil', 'Kör "git add" för filer som ska commitas', 'Kör "git commit -m \"beskrivande meddelande\""', 'Kör "git push" för att synka med remote' ], check: (action) => { if (action.type === 'commit') return { blocked: false }; try { const status = execSync('git status --short', { cwd: '/home/bernt/repos/quixzoom.com', encoding: 'utf8' }); const hasUncommitted = status.trim().length > 0; return { blocked: hasUncommitted && (action.type === 'write' || action.type === 'edit'), reason: hasUncommitted ? 'Ocommitade ändringar finns' : null }; } catch { return { blocked: true, reason: 'Kunde inte verifiera git-status' }; } } }, { id: 'block-no-migration', name: 'Blockera databasändring utan migration', rule: 'Ingen databasändring utan versionshanterad migration', why: 'Databasändringar utan migration kan inte reproduceras eller återställas. De riskerar att bryta andra miljöer.', evidence: (action) => `Migrationer hittade: ${existsSync('/home/bernt/repos/quixzoom.com/migrations') ? 'Ja' : 'Nej'}`, requiredToUnblock: [ 'Skapa migrations-katalog om den saknas', 'Använd Prisma/TypeORM/Sequelize för att generera migration', 'Verifiera migration lokalt', 'Commita migration-filen', 'Dokumentera migrationsprocessen' ], check: (action) => { if (!action.target?.match(/database|db|sql/i)) return { blocked: false }; const hasMigration = existsSync('/home/bernt/repos/quixzoom.com/migrations') || existsSync('/home/bernt/repos/quixzoom.com/prisma'); return { blocked: !hasMigration, reason: !hasMigration ? 'Inga databasmigrationer konfigurerade' : null }; } }, { id: 'block-no-api-contract', name: 'Blockera API-ändring utan kontrakt', rule: 'Ingen API-ändring utan uppdaterat kontrakt', why: 'API-ändringar utan kontrakt bryter konsumenter. OpenAPI är den officiella sanningen om API:ets gränssnitt.', evidence: (action) => `OpenAPI hittad: ${existsSync('/home/bernt/repos/quixzoom.com/openapi.yaml') ? 'Ja' : 'Nej'}`, requiredToUnblock: [ 'Skapa openapi.yaml om den saknas', 'Uppdatera OpenAPI med nya endpoints/ändringar', 'Verifiera att kontraktet är validerbart', 'Commita ändringar i både kod och kontrakt', 'Uppdatera API-dokumentation' ], check: (action) => { if (!action.target?.match(/api|endpoint|route/i)) return { blocked: false }; const hasContract = existsSync('/home/bernt/repos/quixzoom.com/openapi.yaml') || existsSync('/home/bernt/repos/quixzoom.com/openapi.json'); return { blocked: !hasContract, reason: !hasContract ? 'Inget API-kontrakt (OpenAPI) hittat' : null }; } } ]; class PolicyEnforcementV2 { constructor() { this.policies = POLICIES; } validateAction(action) { console.log(`🔍 Validerar åtgärd: ${action.type} ${action.target || ''}\n`); const violations = []; for (const policy of this.policies) { const result = policy.check(action); if (result.blocked) { const violation = { policy: policy.id, name: policy.name, rule: policy.rule, why: policy.why, evidence: typeof policy.evidence === 'function' ? policy.evidence(action) : policy.evidence, requiredToUnblock: policy.requiredToUnblock, reason: result.reason }; violations.push(violation); this.logEnforcement({ action, policy: policy.id, blocked: true, reason: result.reason, timestamp: new Date().toISOString() }); } } const result = { action, allowed: violations.length === 0, violations, timestamp: new Date().toISOString() }; this.printResult(result); return result; } printResult(result) { if (result.allowed) { console.log('✅ ÅTGÄRD GODKÄND\n'); console.log(` ${result.action.type} ${result.action.target || ''} kan utföras`); } else { console.log('🔴 ÅTGÄRD BLOCKERAD\n'); console.log(` ${result.action.type} ${result.action.target || ''} kan INTE utföras\n`); for (const v of result.violations) { console.log(` 📋 REGEL: ${v.rule}`); console.log(` ❓ VARFÖR: ${v.why}`); console.log(` 📊 EVIDENS: ${v.evidence}`); console.log(); console.log(' 🔓 FÖR ATT HÄVA BLOCKERINGEN:\n'); for (const step of v.requiredToUnblock) { console.log(` 1. ${step}`); } console.log(); } } } logEnforcement(entry) { appendFileSync(ENFORCEMENT_LOG, JSON.stringify(entry) + '\n', 'utf8'); } } // ── Main ────────────────────────────────────────────────────────────────── const enforcement = new PolicyEnforcementV2(); const action = { type: process.argv[2] || 'deploy', target: process.argv[3] || 'production' }; enforcement.validateAction(action);