Fix security warnings

This commit is contained in:
gpt-engineer-app[bot]
2025-10-20 21:43:26 +00:00
parent 6b0627054e
commit 42723015f3
7 changed files with 588 additions and 21 deletions

View File

@@ -1 +1,10 @@
project_id = "lyqqeokmoylqtwtikwzb"
project_id = "lyqqeokmoylqtwtikwzb"
[functions.docker-api]
verify_jwt = true
[functions.docker-metrics]
verify_jwt = true
[functions.docker-logs]
verify_jwt = true

View File

@@ -0,0 +1,135 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
const DOCKER_SOCKET = '/var/run/docker.sock';
// Helper to make Unix socket HTTP requests to Docker API
async function dockerRequest(path: string, method = 'GET', body?: any) {
const conn = await Deno.connect({ transport: "unix", path: DOCKER_SOCKET });
const request = [
`${method} ${path} HTTP/1.1`,
'Host: localhost',
'Content-Type: application/json',
body ? `Content-Length: ${new TextEncoder().encode(JSON.stringify(body)).length}` : '',
'',
body ? JSON.stringify(body) : ''
].join('\r\n');
await conn.write(new TextEncoder().encode(request));
const decoder = new TextDecoder();
const buffer = new Uint8Array(65536);
const n = await conn.read(buffer);
conn.close();
if (!n) throw new Error('No response from Docker');
const response = decoder.decode(buffer.subarray(0, n));
const [headers, ...bodyParts] = response.split('\r\n\r\n');
const responseBody = bodyParts.join('\r\n\r\n');
// Check for chunked transfer encoding
if (headers.includes('Transfer-Encoding: chunked')) {
const chunks: string[] = [];
const lines = responseBody.split('\r\n');
for (let i = 0; i < lines.length; i++) {
const chunkSize = parseInt(lines[i], 16);
if (chunkSize === 0) break;
if (i + 1 < lines.length) {
chunks.push(lines[i + 1]);
i++; // Skip the chunk data line
}
}
return JSON.parse(chunks.join(''));
}
return JSON.parse(responseBody);
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
throw new Error('Missing authorization header');
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
);
const { data: { user }, error: userError } = await supabase.auth.getUser();
if (userError || !user) {
throw new Error('Unauthorized');
}
const url = new URL(req.url);
const path = url.pathname.replace('/docker-api', '');
// List all containers
if (path === '/containers' && req.method === 'GET') {
const containers = await dockerRequest('/containers/json?all=true');
return new Response(JSON.stringify(containers), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// Get container details
if (path.match(/^\/containers\/[^\/]+$/) && req.method === 'GET') {
const containerId = path.split('/')[2];
const details = await dockerRequest(`/containers/${containerId}/json`);
return new Response(JSON.stringify(details), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// Container actions: start, stop, restart
if (path.match(/^\/containers\/[^\/]+\/(start|stop|restart)$/) && req.method === 'POST') {
const parts = path.split('/');
const containerId = parts[2];
const action = parts[3];
const details = await dockerRequest(`/containers/${containerId}/json`);
const containerName = details.Name.replace(/^\//, '');
await dockerRequest(`/containers/${containerId}/${action}`, 'POST');
// Log to audit log
await supabase.from('audit_logs').insert({
user_id: user.id,
action: action,
container_id: containerId,
container_name: containerName,
details: { timestamp: new Date().toISOString() }
});
return new Response(JSON.stringify({ success: true, action, containerId }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify({ error: 'Not found' }), {
status: 404,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Docker API error:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
return new Response(JSON.stringify({ error: message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});

View File

@@ -0,0 +1,116 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
const DOCKER_SOCKET = '/var/run/docker.sock';
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
throw new Error('Missing authorization header');
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
);
const { data: { user }, error: userError } = await supabase.auth.getUser();
if (userError || !user) {
throw new Error('Unauthorized');
}
const url = new URL(req.url);
const containerId = url.searchParams.get('container');
const tail = url.searchParams.get('tail') || '100';
if (!containerId) {
throw new Error('Container ID required');
}
// Connect to Docker socket
const conn = await Deno.connect({ transport: "unix", path: DOCKER_SOCKET });
const request = `GET /containers/${containerId}/logs?stdout=true&stderr=true&tail=${tail}&follow=true HTTP/1.1\r\nHost: localhost\r\n\r\n`;
await conn.write(new TextEncoder().encode(request));
// Create a readable stream from the socket
const stream = new ReadableStream({
async start(controller) {
const decoder = new TextDecoder();
let buffer = new Uint8Array(8192);
let headerRead = false;
try {
while (true) {
const n = await conn.read(buffer);
if (n === null) break;
let data = decoder.decode(buffer.subarray(0, n));
// Skip HTTP headers on first read
if (!headerRead) {
const headerEnd = data.indexOf('\r\n\r\n');
if (headerEnd !== -1) {
data = data.substring(headerEnd + 4);
headerRead = true;
}
}
// Docker logs use a special frame format:
// [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
// We'll parse and clean this
const lines = data.split('\n').filter(line => {
// Skip binary headers (first 8 bytes of each frame)
if (line.length > 8) {
return line.substring(8).trim().length > 0;
}
return false;
});
for (const line of lines) {
if (line.length > 8) {
const cleaned = line.substring(8).trim();
if (cleaned) {
controller.enqueue(new TextEncoder().encode(`data: ${cleaned}\n\n`));
}
}
}
}
} catch (error) {
console.error('Stream error:', error);
} finally {
conn.close();
controller.close();
}
}
});
return new Response(stream, {
headers: {
...corsHeaders,
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} catch (error) {
console.error('Docker logs error:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
return new Response(JSON.stringify({ error: message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});

View File

@@ -0,0 +1,186 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
const DOCKER_SOCKET = '/var/run/docker.sock';
const HOST_PROC = '/host/proc';
// Helper to make Unix socket HTTP requests
async function dockerRequest(path: string) {
const conn = await Deno.connect({ transport: "unix", path: DOCKER_SOCKET });
const request = `GET ${path} HTTP/1.1\r\nHost: localhost\r\n\r\n`;
await conn.write(new TextEncoder().encode(request));
const decoder = new TextDecoder();
const buffer = new Uint8Array(65536);
const n = await conn.read(buffer);
conn.close();
if (!n) throw new Error('No response');
const response = decoder.decode(buffer.subarray(0, n));
const [, body] = response.split('\r\n\r\n');
return JSON.parse(body);
}
// Read host metrics from /proc
async function getHostMetrics() {
try {
// CPU info
const cpuInfo = await Deno.readTextFile(`${HOST_PROC}/stat`);
const cpuLine = cpuInfo.split('\n')[0];
const cpuValues = cpuLine.split(/\s+/).slice(1).map(Number);
const total = cpuValues.reduce((a, b) => a + b, 0);
const idle = cpuValues[3];
const cpuUsage = ((total - idle) / total) * 100;
// Memory info
const memInfo = await Deno.readTextFile(`${HOST_PROC}/meminfo`);
const memLines = memInfo.split('\n');
const memTotal = parseInt(memLines.find(l => l.startsWith('MemTotal'))?.split(/\s+/)[1] || '0');
const memFree = parseInt(memLines.find(l => l.startsWith('MemFree'))?.split(/\s+/)[1] || '0');
const memAvailable = parseInt(memLines.find(l => l.startsWith('MemAvailable'))?.split(/\s+/)[1] || '0');
// Uptime
const uptime = await Deno.readTextFile(`${HOST_PROC}/uptime`);
const uptimeSeconds = parseFloat(uptime.split(' ')[0]);
return {
cpu: { usage: cpuUsage.toFixed(2) },
memory: {
total: memTotal,
free: memFree,
available: memAvailable,
used: memTotal - memFree,
usage: ((memTotal - memAvailable) / memTotal * 100).toFixed(2)
},
uptime: uptimeSeconds
};
} catch (error) {
console.error('Error reading host metrics:', error);
return {
cpu: { usage: 0 },
memory: { total: 0, free: 0, available: 0, used: 0, usage: 0 },
uptime: 0
};
}
}
serve(async (req) => {
const upgrade = req.headers.get("upgrade") || "";
if (upgrade.toLowerCase() !== "websocket") {
return new Response("Expected WebSocket", { status: 400 });
}
try {
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response("Missing authorization", { status: 401 });
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
);
const { data: { user }, error: userError } = await supabase.auth.getUser();
if (userError || !user) {
return new Response("Unauthorized", { status: 401 });
}
const { socket, response } = Deno.upgradeWebSocket(req);
let intervalId: number;
socket.onopen = () => {
console.log("WebSocket metrics connection opened");
// Send metrics every 2 seconds
intervalId = setInterval(async () => {
try {
const hostMetrics = await getHostMetrics();
const containers = await dockerRequest('/containers/json');
const containerStats = [];
for (const container of containers) {
try {
const stats = await dockerRequest(`/containers/${container.Id}/stats?stream=false`);
containerStats.push({
id: container.Id,
name: container.Names[0].replace(/^\//, ''),
cpu: calculateCPUPercent(stats),
memory: calculateMemoryUsage(stats),
network: calculateNetworkIO(stats),
blockIO: calculateBlockIO(stats)
});
} catch (e) {
console.error(`Error getting stats for ${container.Id}:`, e);
}
}
socket.send(JSON.stringify({
type: 'metrics',
timestamp: new Date().toISOString(),
host: hostMetrics,
containers: containerStats
}));
} catch (error) {
console.error('Error sending metrics:', error);
}
}, 2000);
};
socket.onclose = () => {
console.log("WebSocket connection closed");
if (intervalId) clearInterval(intervalId);
};
socket.onerror = (e) => {
console.error("WebSocket error:", e);
if (intervalId) clearInterval(intervalId);
};
return response;
} catch (error) {
console.error("WebSocket setup error:", error);
return new Response("Internal error", { status: 500 });
}
});
// Helper functions to calculate stats
function calculateCPUPercent(stats: any): string {
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
const cpuPercent = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
return cpuPercent.toFixed(2);
}
function calculateMemoryUsage(stats: any): { used: number; limit: number; percent: string } {
const used = stats.memory_stats.usage - (stats.memory_stats.stats?.cache || 0);
const limit = stats.memory_stats.limit;
const percent = (used / limit * 100).toFixed(2);
return { used, limit, percent };
}
function calculateNetworkIO(stats: any): { rx: number; tx: number } {
const networks = stats.networks || {};
let rx = 0, tx = 0;
for (const net of Object.values(networks) as any[]) {
rx += net.rx_bytes || 0;
tx += net.tx_bytes || 0;
}
return { rx, tx };
}
function calculateBlockIO(stats: any): { read: number; write: number } {
const io = stats.blkio_stats.io_service_bytes_recursive || [];
let read = 0, write = 0;
for (const entry of io) {
if (entry.op === 'Read') read += entry.value;
if (entry.op === 'Write') write += entry.value;
}
return { read, write };
}

View File

@@ -0,0 +1,49 @@
-- Fix search_path for security definer functions
-- Drop and recreate update_updated_at_column with search_path
DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
-- Recreate triggers
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON public.profiles
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_global_settings_updated_at
BEFORE UPDATE ON public.global_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_container_settings_updated_at
BEFORE UPDATE ON public.container_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Drop and recreate handle_new_user with proper search_path
DROP FUNCTION IF EXISTS public.handle_new_user() CASCADE;
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username)
VALUES (
NEW.id,
COALESCE(NEW.raw_user_meta_data->>'username', split_part(NEW.email, '@', 1))
);
INSERT INTO public.global_settings (user_id)
VALUES (NEW.id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
-- Recreate trigger
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();