05ed037fe8
- DNS: pilot.landvex.com -> 16.170.83.169 - TLS: Let's Encrypt certificate (expires 2026-09-30) - Nginx: reverse proxy with SSL termination - API: https://pilot.landvex.com/api/v1/missions - UI: https://pilot.landvex.com/ - Upload: POST /api/v1/missions/import (multipart/form-data) Verified: ✅ https://pilot.landvex.com/health ✅ https://pilot.landvex.com/version ✅ https://pilot.landvex.com/api/v1/missions (list) ✅ https://pilot.landvex.com/api/v1/missions/:id (get) ✅ POST /api/v1/missions/import (video upload) ✅ UI loads with title 'LandveX Intelligence Lab' Next: Pilot 001 — Break the system!
295 lines
9.1 KiB
JavaScript
295 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// EOS Policy Enforcement — Förhindra åtgärder, inte bara logga
|
|
// Erik-krav: "Om en agent försöker deploya utan pipeline ska EOS förhindra åtgärden"
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
import { execSync } from 'child_process';
|
|
import { existsSync, appendFileSync } from 'fs';
|
|
|
|
const ENFORCEMENT_LOG = '/home/bernt/.openclaw/workspace/EOS/enforcement-log.jsonl';
|
|
|
|
/**
|
|
* Verkställbara policyer — dessa förhindrar åtgärder
|
|
*/
|
|
const POLICIES = [
|
|
{
|
|
id: 'block-direct-prod',
|
|
name: 'Blockera direkt skrivning till produktion',
|
|
check: (action) => {
|
|
// Kontrollera om vi är på produktionsserver
|
|
const isProd = process.env.NODE_ENV === 'production' ||
|
|
existsSync('/etc/ec2-release');
|
|
const isWrite = action.type === 'write' || action.type === 'deploy';
|
|
|
|
return {
|
|
allowed: !(isProd && isWrite),
|
|
reason: isProd && isWrite ? 'Direkt skrivning till produktion blockerad' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-no-pipeline-deploy',
|
|
name: 'Blockera deployment utan pipeline',
|
|
check: (action) => {
|
|
if (action.type !== 'deploy') return { allowed: true };
|
|
|
|
const hasPipeline = existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/.gitlab-ci.yml');
|
|
|
|
return {
|
|
allowed: hasPipeline,
|
|
reason: hasPipeline ? null : 'Deployment utan CI/CD pipeline blockerad'
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-server-edit',
|
|
name: 'Blockera redigering på server',
|
|
check: (action) => {
|
|
const isServer = existsSync('/etc/ec2-release') || existsSync('/var/lib/cloud');
|
|
const isEdit = action.type === 'edit' || action.type === 'write';
|
|
|
|
return {
|
|
allowed: !(isServer && isEdit),
|
|
reason: isServer && isEdit ? 'Redigering på server blockerad' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-uncommitted',
|
|
name: 'Blockera ändring utan commit',
|
|
check: (action) => {
|
|
if (action.type !== 'commit' && action.type !== 'write') return { allowed: true };
|
|
|
|
try {
|
|
const status = execSync('git status --short', {
|
|
cwd: '/home/bernt/repos/quixzoom.com',
|
|
encoding: 'utf8'
|
|
});
|
|
const hasUncommitted = status.trim().length > 0;
|
|
|
|
// Tillåt commit-action även med ocommitade filer (det är poängen)
|
|
if (action.type === 'commit') return { allowed: true };
|
|
|
|
return {
|
|
allowed: !hasUncommitted,
|
|
reason: hasUncommitted ? 'Ocommitade ändringar finns — commita först' : null
|
|
};
|
|
} catch {
|
|
return { allowed: false, reason: 'Kunde inte verifiera git-status' };
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'block-no-migration',
|
|
name: 'Blockera databasändring utan migration',
|
|
check: (action) => {
|
|
if (!action.target?.match(/database|db|sql/i)) return { allowed: true };
|
|
|
|
const hasMigration = existsSync('/home/bernt/repos/quixzoom.com/migrations') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/prisma');
|
|
|
|
return {
|
|
allowed: hasMigration,
|
|
reason: hasMigration ? null : 'Databasändring utan migration blockerad'
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-no-api-contract',
|
|
name: 'Blockera API-ändring utan kontrakt',
|
|
check: (action) => {
|
|
if (!action.target?.match(/api|endpoint|route/i)) return { allowed: true };
|
|
|
|
const hasContract = existsSync('/home/bernt/repos/quixzoom.com/openapi.yaml') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/openapi.json');
|
|
|
|
return {
|
|
allowed: hasContract,
|
|
reason: hasContract ? null : 'API-ändring utan kontrakt blockerad'
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'require-human-approval',
|
|
name: 'Kräv mänskligt godkännande för kritiska åtgärder',
|
|
check: (action) => {
|
|
const criticalActions = ['delete', 'drop', 'destroy', 'remove'];
|
|
const isCritical = criticalActions.some(a => action.type?.includes(a));
|
|
|
|
return {
|
|
allowed: !isCritical,
|
|
reason: isCritical ? 'KRITISK ÅTGÄRD — Kräver mänskligt godkännande' : null,
|
|
requiresApproval: isCritical
|
|
};
|
|
}
|
|
}
|
|
];
|
|
|
|
class PolicyEnforcement {
|
|
constructor() {
|
|
this.policies = POLICIES;
|
|
}
|
|
|
|
/**
|
|
* Validera en åtgärd mot alla policyer
|
|
*/
|
|
validateAction(action) {
|
|
console.log(`🔍 Validerar åtgärd: ${action.type} ${action.target || ''}\n`);
|
|
|
|
const violations = [];
|
|
let requiresApproval = false;
|
|
|
|
for (const policy of this.policies) {
|
|
const result = policy.check(action);
|
|
|
|
if (!result.allowed) {
|
|
violations.push({
|
|
policy: policy.id,
|
|
name: policy.name,
|
|
reason: result.reason
|
|
});
|
|
|
|
this.logEnforcement({
|
|
action,
|
|
policy: policy.id,
|
|
allowed: false,
|
|
reason: result.reason,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
if (result.requiresApproval) {
|
|
requiresApproval = true;
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
action,
|
|
allowed: violations.length === 0,
|
|
violations,
|
|
requiresApproval,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
this.printResult(result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Försök utföra en åtgärd (med policy-kontroll)
|
|
*/
|
|
executeAction(action) {
|
|
const validation = this.validateAction(action);
|
|
|
|
if (!validation.allowed) {
|
|
console.log('🔴 ÅTGÄRD BLOCKERAD\n');
|
|
for (const v of validation.violations) {
|
|
console.log(` ${v.name}`);
|
|
console.log(` ${v.reason}`);
|
|
}
|
|
console.log();
|
|
return { executed: false, validation };
|
|
}
|
|
|
|
if (validation.requiresApproval) {
|
|
console.log('🟡 ÅTGÄRD KRÄVER GODKÄNNANDE\n');
|
|
console.log(' Väntar på mänskligt godkännande...\n');
|
|
return { executed: false, pendingApproval: true, validation };
|
|
}
|
|
|
|
console.log('✅ ÅTGÄRD GODKÄND\n');
|
|
return { executed: true, validation };
|
|
}
|
|
|
|
logEnforcement(entry) {
|
|
appendFileSync(ENFORCEMENT_LOG, JSON.stringify(entry) + '\n', 'utf8');
|
|
}
|
|
|
|
printResult(result) {
|
|
if (result.allowed && !result.requiresApproval) {
|
|
console.log(`✅ ${result.action.type} — Godkänd`);
|
|
} else if (result.requiresApproval) {
|
|
console.log(`🟡 ${result.action.type} — Kräver godkännande`);
|
|
} else {
|
|
console.log(`🔴 ${result.action.type} — Blockerad`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Visa policyer
|
|
*/
|
|
listPolicies() {
|
|
console.log('📋 POLICYER\n');
|
|
for (const policy of this.policies) {
|
|
console.log(` ${policy.name}`);
|
|
console.log(` ID: ${policy.id}`);
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Visa enforcement-logg
|
|
*/
|
|
showLog() {
|
|
if (!existsSync(ENFORCEMENT_LOG)) {
|
|
console.log('Ingen enforcement-logg hittad\n');
|
|
return;
|
|
}
|
|
|
|
const entries = readFileSync(ENFORCEMENT_LOG, 'utf8')
|
|
.split('\n')
|
|
.filter(line => line.trim())
|
|
.map(line => {
|
|
try { return JSON.parse(line); } catch { return null; }
|
|
})
|
|
.filter(Boolean);
|
|
|
|
console.log(`📊 ENFORCEMENT-LOGG (${entries.length} entries)\n`);
|
|
|
|
const blocked = entries.filter(e => !e.allowed);
|
|
console.log(` Blockerade: ${blocked.length}`);
|
|
console.log(` Godkända: ${entries.length - blocked.length}`);
|
|
console.log();
|
|
|
|
for (const entry of entries.slice(-10)) {
|
|
const icon = entry.allowed ? '✅' : '🔴';
|
|
console.log(` ${icon} ${entry.action.type} ${entry.action.target || ''}`);
|
|
if (entry.reason) {
|
|
console.log(` ${entry.reason}`);
|
|
}
|
|
}
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
|
|
const enforcement = new PolicyEnforcement();
|
|
const command = process.argv[2] || '--list';
|
|
|
|
if (command === '--list') {
|
|
enforcement.listPolicies();
|
|
} else if (command === '--validate') {
|
|
const action = {
|
|
type: process.argv[3] || 'deploy',
|
|
target: process.argv[4] || 'production'
|
|
};
|
|
enforcement.validateAction(action);
|
|
} else if (command === '--execute') {
|
|
const action = {
|
|
type: process.argv[3] || 'deploy',
|
|
target: process.argv[4] || 'production'
|
|
};
|
|
enforcement.executeAction(action);
|
|
} else if (command === '--log') {
|
|
enforcement.showLog();
|
|
} else {
|
|
console.log('Användning:');
|
|
console.log(' node policy-enforcement.mjs --list # Lista policyer');
|
|
console.log(' node policy-enforcement.mjs --validate <type> [target] # Validera åtgärd');
|
|
console.log(' node policy-enforcement.mjs --execute <type> [target] # Utför med kontroll');
|
|
console.log(' node policy-enforcement.mjs --log # Visa logg');
|
|
}
|