"use strict";
/**
 * SSRF protection for the scanner endpoint.
 *
 * Validates URLs against an allowlist of public code hosting providers.
 * Rejects private IP ranges, localhost, and link-local addresses.
 * Prevents DNS rebinding by resolving hostnames before making requests.
 *
 * Canonical location: src/security/ssrf-guard.ts
 * Re-exported from: packages/governance-server/src/ssrf-guard.ts
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkUrl = checkUrl;
exports.addAllowedHost = addAllowedHost;
exports.getAllowedHosts = getAllowedHosts;
exports.registerLoopbackSession = registerLoopbackSession;
exports.revokeLoopbackSession = revokeLoopbackSession;
exports.checkUrlWithSession = checkUrlWithSession;
const dns = __importStar(require("node:dns/promises"));
const net = __importStar(require("node:net"));
// ---------------------------------------------------------------------------
// Allowlisted domains for scanner
// ---------------------------------------------------------------------------
const ALLOWED_HOSTS = new Set([
    'github.com',
    'gitlab.com',
    'bitbucket.org',
    'dev.azure.com',
    'codeberg.org',
    'sr.ht',
    'raw.githubusercontent.com',
    'api.github.com',
    'gitlab.com',
]);
// ---------------------------------------------------------------------------
// Private/reserved IP ranges
// ---------------------------------------------------------------------------
const PRIVATE_RANGES_V4 = [
    { prefix: '10.', mask: 8 },
    { prefix: '172.16.', mask: 12 },
    { prefix: '172.17.', mask: 12 },
    { prefix: '172.18.', mask: 12 },
    { prefix: '172.19.', mask: 12 },
    { prefix: '172.20.', mask: 12 },
    { prefix: '172.21.', mask: 12 },
    { prefix: '172.22.', mask: 12 },
    { prefix: '172.23.', mask: 12 },
    { prefix: '172.24.', mask: 12 },
    { prefix: '172.25.', mask: 12 },
    { prefix: '172.26.', mask: 12 },
    { prefix: '172.27.', mask: 12 },
    { prefix: '172.28.', mask: 12 },
    { prefix: '172.29.', mask: 12 },
    { prefix: '172.30.', mask: 12 },
    { prefix: '172.31.', mask: 12 },
    { prefix: '192.168.', mask: 16 },
    { prefix: '127.', mask: 8 },
    { prefix: '0.', mask: 8 },
    { prefix: '169.254.', mask: 16 }, // link-local
    { prefix: '100.64.', mask: 10 }, // CGNAT
];
function isPrivateIpV4(ip) {
    for (const range of PRIVATE_RANGES_V4) {
        if (ip.startsWith(range.prefix))
            return true;
    }
    return false;
}
function isPrivateIpV6(ip) {
    const lower = ip.toLowerCase();
    if (lower === '::1')
        return true; // loopback
    if (lower.startsWith('fe80:'))
        return true; // link-local
    if (lower.startsWith('fc') || lower.startsWith('fd'))
        return true; // unique local
    if (lower === '::' || lower === '0:0:0:0:0:0:0:0')
        return true; // unspecified
    // IPv4-mapped IPv6
    if (lower.startsWith('::ffff:')) {
        const v4 = lower.slice(7);
        return isPrivateIpV4(v4);
    }
    return false;
}
function isPrivateIp(ip) {
    if (net.isIPv4(ip))
        return isPrivateIpV4(ip);
    if (net.isIPv6(ip))
        return isPrivateIpV6(ip);
    return true; // unknown format, reject
}
// ---------------------------------------------------------------------------
// SSRF guard
// ---------------------------------------------------------------------------
async function checkUrl(url, allowCustomHosts = false) {
    // Parse URL
    let parsed;
    try {
        parsed = new URL(url);
    }
    catch {
        return { safe: false, reason: 'Invalid URL format' };
    }
    // HTTPS only
    if (parsed.protocol !== 'https:') {
        return { safe: false, reason: 'Only HTTPS URLs are allowed' };
    }
    // No auth in URL
    if (parsed.username || parsed.password) {
        return { safe: false, reason: 'URLs with embedded credentials are not allowed' };
    }
    // No non-standard ports
    if (parsed.port && parsed.port !== '443') {
        return { safe: false, reason: 'Non-standard ports are not allowed' };
    }
    // No raw IP addresses
    if (net.isIP(parsed.hostname)) {
        return { safe: false, reason: 'Raw IP addresses are not allowed. Use domain names.' };
    }
    // Domain allowlist check (unless custom hosts enabled for enterprise)
    if (!allowCustomHosts && !ALLOWED_HOSTS.has(parsed.hostname)) {
        return {
            safe: false,
            reason: `Host '${parsed.hostname}' is not in the allowed list. Allowed: ${Array.from(ALLOWED_HOSTS).join(', ')}`,
        };
    }
    // DNS resolution to prevent DNS rebinding
    try {
        const addresses = await dns.resolve4(parsed.hostname);
        if (addresses.length === 0) {
            return { safe: false, reason: 'DNS resolution returned no addresses' };
        }
        // Check ALL resolved IPs (not just the first)
        for (const ip of addresses) {
            if (isPrivateIp(ip)) {
                return {
                    safe: false,
                    reason: `DNS resolved to private IP ${ip}. This may indicate DNS rebinding.`,
                };
            }
        }
        return { safe: true, resolvedIp: addresses[0] };
    }
    catch (err) {
        // Also try IPv6
        try {
            const addresses6 = await dns.resolve6(parsed.hostname);
            if (addresses6.length === 0) {
                return { safe: false, reason: 'DNS resolution returned no addresses' };
            }
            for (const ip of addresses6) {
                if (isPrivateIp(ip)) {
                    return {
                        safe: false,
                        reason: `DNS resolved to private IPv6 ${ip}. This may indicate DNS rebinding.`,
                    };
                }
            }
            return { safe: true, resolvedIp: addresses6[0] };
        }
        catch {
            return { safe: false, reason: `DNS resolution failed for ${parsed.hostname}: ${err.message}` };
        }
    }
}
/**
 * Add a custom allowed host (for enterprise tier).
 */
function addAllowedHost(host) {
    ALLOWED_HOSTS.add(host);
}
/**
 * Get the current allowed hosts list.
 */
function getAllowedHosts() {
    return Array.from(ALLOWED_HOSTS);
}
// ---------------------------------------------------------------------------
// Session-scoped loopback allowlist (for OllamaAdapter)
//
// OllamaAdapter traffic goes to 127.0.0.1 (or ::1) by design. Rather than
// globally loosening the SSRF guard, adapters register a session token that
// marks exactly one loopback allowance. The token is invalidated on revoke.
//
// This is scoped: only the checkUrlWithSession path honours session tokens.
// The existing checkUrl() path is NOT modified.
// ---------------------------------------------------------------------------
const LOOPBACK_SESSION_TOKENS = new Set();
/**
 * Register a session-scoped loopback allowance.
 * Returns the token that must be passed to checkUrlWithSession.
 * Call revokeLoopbackSession(token) when the adapter is torn down.
 */
function registerLoopbackSession(token) {
    LOOPBACK_SESSION_TOKENS.add(token);
}
/**
 * Revoke a previously registered loopback session.
 */
function revokeLoopbackSession(token) {
    LOOPBACK_SESSION_TOKENS.delete(token);
}
/**
 * Session-aware URL check. If the request is to a loopback address
 * (127.0.0.1 or ::1) and sessionToken is a registered loopback session,
 * the check is allowed. All other requests fall through to checkUrl().
 *
 * This is the entry point adapters should use when they need loopback access.
 */
async function checkUrlWithSession(url, sessionToken) {
    if (sessionToken && LOOPBACK_SESSION_TOKENS.has(sessionToken)) {
        // Allow loopback URLs for registered sessions only
        let parsed;
        try {
            parsed = new URL(url);
        }
        catch {
            return { safe: false, reason: 'Invalid URL format' };
        }
        const host = parsed.hostname;
        if (host === '127.0.0.1' || host === '::1' || host === 'localhost') {
            return { safe: true, resolvedIp: host };
        }
    }
    return checkUrl(url);
}
//# sourceMappingURL=ssrf-guard.js.map