/**
 * Credential Scope Audit Scanner
 *
 * Inspects credentials and tokens for over-scoping and rotation needs.
 * Detects common credential patterns and analyzes their permissions:
 * - GitHub PATs with excessive scopes
 * - AWS credentials with AdministratorAccess
 * - Unrotated tokens older than 90 days
 * - Hardcoded credentials in config
 *
 * @module scanners/agent/credential-scope-audit
 */
import { createHash } from "crypto";
import { readFile } from "fs/promises";
import { join } from "path";
const CREDENTIAL_PATTERNS = [
    // GitHub Personal Access Token (classic)
    {
        type: "github-pat",
        pattern: /ghp_[A-Za-z0-9]{36}/g,
        recommended: ["read:repo", "read:user"],
    },
    // GitHub Personal Access Token (fine-grained)
    {
        type: "github-pat",
        pattern: /github_pat_[A-Za-z0-9_]{22,}/g,
    },
    // GitHub App installation token
    {
        type: "github-app",
        pattern: /ghs_[A-Za-z0-9]{36}/g,
    },
    // AWS Access Key ID
    {
        type: "aws-access-key",
        pattern: /AKIA[A-Z0-9]{16}/g,
        recommended: ["Specific IAM policy, not AdministratorAccess"],
        isOverscoped: () => true, // Can't determine scopes from key
    },
    // GCP Service Account
    {
        type: "gcp-service-account",
        pattern: /"type"\s*:\s*"service_account"/g,
    },
    // Azure Service Principal
    {
        type: "azure-service-principal",
        pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
    },
    // Generic API Key patterns
    {
        type: "api-key",
        pattern: /(?:api[_-]?key|apikey|api_secret)\s*[=:]\s*["']?([A-Za-z0-9_\-]{20,})["']?/gi,
        recommended: ["Rotate and scope to specific endpoints"],
    },
    // OpenAI API Key
    {
        type: "api-key",
        pattern: /sk-[A-Za-z0-9]{20,}/g,
        recommended: ["Set usage limits and monitor spending"],
    },
    // JWT Token
    {
        type: "jwt",
        pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
    },
    // Bearer Token in configs
    {
        type: "oauth-token",
        pattern: /bearer\s+[A-Za-z0-9_\-\.]{20,}/gi,
    },
];
// ============================================================================
// Known Overscoped Patterns
// ============================================================================
/**
 * GitHub PAT scopes that are often over-provisioned
 */
const GITHUB_OVERSCOPED_PATTERNS = [
    { scope: "repo", severity: "high", reason: "Full repository access - consider read:* only" },
    { scope: "admin:org", severity: "critical", reason: "Organization admin - rarely needed" },
    { scope: "admin:repo_hook", severity: "high", reason: "Webhook admin - security risk" },
    { scope: "admin:enterprise", severity: "critical", reason: "Enterprise admin - extreme risk" },
    { scope: "delete_repo", severity: "critical", reason: "Can delete repositories" },
    { scope: "write:packages", severity: "medium", reason: "Can publish packages" },
];
/**
 * AWS IAM policies that indicate over-provisioning
 */
const AWS_OVERSCOPED_POLICIES = [
    { policy: "AdministratorAccess", severity: "critical", reason: "Full AWS admin access" },
    { policy: "PowerUserAccess", severity: "high", reason: "Near-full access except IAM" },
    { policy: "IAMFullAccess", severity: "critical", reason: "Can create/modify IAM policies" },
    { policy: "*:*", severity: "critical", reason: "Wildcard all actions/resources" },
    { policy: "s3:*", severity: "high", reason: "Full S3 access - scope to specific buckets" },
    { policy: "ec2:*", severity: "high", reason: "Full EC2 access - scope to specific instances" },
];
// ============================================================================
// Config File Scanning
// ============================================================================
/**
 * Scan a config file for credentials
 */
async function scanConfigFile(filePath, content) {
    const findings = [];
    const lines = content.split("\n");
    for (const credPattern of CREDENTIAL_PATTERNS) {
        credPattern.pattern.lastIndex = 0;
        let match;
        while ((match = credPattern.pattern.exec(content)) !== null) {
            // Calculate line number
            const beforeMatch = content.substring(0, match.index);
            const lineNumber = beforeMatch.split("\n").length;
            // Get context for scope extraction
            const lineContent = lines[lineNumber - 1] || "";
            // Redact the credential
            const redactedId = redactCredential(match[0]);
            findings.push({
                credentialType: credPattern.type,
                identifier: redactedId,
                currentScopes: credPattern.extractScopes
                    ? credPattern.extractScopes(match[0], content)
                    : ["Unknown - manual review required"],
                recommendedScopes: credPattern.recommended || ["Scope to minimum required permissions"],
                unusedScopes: [],
                severity: credPattern.isOverscoped?.(credPattern.recommended || []) ? "high" : "medium",
                rotationRecommended: true,
                ageInDays: undefined, // Can't determine from static analysis
            });
        }
    }
    return findings;
}
/**
 * Redact a credential for safe logging
 */
function redactCredential(credential) {
    if (credential.length <= 8) {
        return "****";
    }
    return credential.slice(0, 4) + "****" + credential.slice(-4);
}
/**
 * Scan directory for config files containing credentials
 */
async function scanForCredentials(dirPath) {
    const results = [];
    // Files to scan
    const configPatterns = [
        ".env",
        ".env.local",
        ".env.development",
        ".env.production",
        "config.json",
        "secrets.json",
        ".vaspera/secrets.json",
        "server.json",
        "claude_desktop_config.json",
    ];
    for (const pattern of configPatterns) {
        const filePath = join(dirPath, pattern);
        try {
            const content = await readFile(filePath, "utf-8");
            const findings = await scanConfigFile(filePath, content);
            if (findings.length > 0) {
                results.push({ file: filePath, findings });
            }
        }
        catch {
            // File doesn't exist or can't be read
        }
    }
    return results;
}
// ============================================================================
// Environment Variable Analysis
// ============================================================================
/**
 * Analyze environment variables for credentials
 */
function analyzeEnvironmentVariables() {
    const findings = [];
    // Check common credential environment variables
    const credentialEnvVars = [
        { name: "GITHUB_TOKEN", type: "github-pat" },
        { name: "GITHUB_PAT", type: "github-pat" },
        { name: "GH_TOKEN", type: "github-pat" },
        { name: "AWS_ACCESS_KEY_ID", type: "aws-access-key" },
        { name: "AWS_SECRET_ACCESS_KEY", type: "aws-access-key" },
        { name: "OPENAI_API_KEY", type: "api-key" },
        { name: "ANTHROPIC_API_KEY", type: "api-key" },
        { name: "AZURE_CLIENT_SECRET", type: "azure-service-principal" },
        { name: "GOOGLE_APPLICATION_CREDENTIALS", type: "gcp-service-account" },
    ];
    for (const { name, type } of credentialEnvVars) {
        const value = process.env[name];
        if (value) {
            findings.push({
                credentialType: type,
                identifier: `${name}=${redactCredential(value)}`,
                currentScopes: ["Environment variable - scopes unknown"],
                recommendedScopes: ["Review and minimize scopes"],
                unusedScopes: [],
                severity: "medium",
                rotationRecommended: true,
            });
        }
    }
    return findings;
}
// ============================================================================
// Scope Analysis
// ============================================================================
/**
 * Check for GitHub PAT scope over-provisioning
 */
function analyzeGitHubScopes(scopes) {
    const issues = [];
    let maxSeverity = "low";
    for (const scope of scopes) {
        for (const pattern of GITHUB_OVERSCOPED_PATTERNS) {
            if (scope.toLowerCase().includes(pattern.scope.toLowerCase())) {
                issues.push(`${pattern.scope}: ${pattern.reason}`);
                if (["critical", "high"].includes(pattern.severity) &&
                    (maxSeverity === "low" || maxSeverity === "medium")) {
                    maxSeverity = pattern.severity;
                }
            }
        }
    }
    return {
        overscoped: issues.length > 0,
        issues,
        severity: maxSeverity,
    };
}
// ============================================================================
// Finding Conversion
// ============================================================================
/**
 * Convert credential findings to deterministic findings
 */
function credentialsToDeterministicFindings(findings, fileResults) {
    const deterministicFindings = [];
    // File-based findings
    for (const { file, findings: fileFindings } of fileResults) {
        for (const finding of fileFindings) {
            deterministicFindings.push({
                scanner: "semgrep",
                ruleId: `credential-scope:${finding.credentialType}`,
                file,
                line: 0,
                message: `${finding.credentialType}: Credential found in config file - review scope and rotation`,
                severity: finding.severity,
                confidence: 100,
                evidence: [
                    `Identifier: ${finding.identifier}`,
                    `Current scopes: ${finding.currentScopes.join(", ")}`,
                    `Recommended: ${finding.recommendedScopes.join(", ")}`,
                    finding.rotationRecommended ? "Rotation recommended" : "",
                ]
                    .filter(Boolean)
                    .join("\n"),
                metadata: {
                    agentScanner: "credential-scope-audit",
                    credentialType: finding.credentialType,
                    rotationRecommended: finding.rotationRecommended,
                },
            });
        }
    }
    // Environment variable findings
    for (const finding of findings) {
        deterministicFindings.push({
            scanner: "semgrep",
            ruleId: `credential-scope:env:${finding.credentialType}`,
            file: "environment",
            line: 0,
            message: `${finding.credentialType}: Credential in environment - verify minimal scopes`,
            severity: finding.severity,
            confidence: 100,
            evidence: [
                `Variable: ${finding.identifier.split("=")[0]}`,
                `Type: ${finding.credentialType}`,
                `Recommendation: ${finding.recommendedScopes.join(", ")}`,
            ].join("\n"),
            metadata: {
                agentScanner: "credential-scope-audit",
                credentialType: finding.credentialType,
                source: "environment",
            },
        });
    }
    // Summary
    const totalFindings = deterministicFindings.length;
    if (totalFindings > 0) {
        const byType = {};
        for (const f of [...findings, ...fileResults.flatMap((r) => r.findings)]) {
            byType[f.credentialType] = (byType[f.credentialType] || 0) + 1;
        }
        const criticalCount = deterministicFindings.filter((f) => f.severity === "critical").length;
        const highCount = deterministicFindings.filter((f) => f.severity === "high").length;
        deterministicFindings.push({
            scanner: "semgrep",
            ruleId: "credential-scope:summary",
            file: "mcp-manifest",
            line: 0,
            message: `Credential scope audit found ${totalFindings} credential(s) requiring review`,
            severity: criticalCount > 0 ? "critical" : highCount > 0 ? "high" : "medium",
            confidence: 100,
            evidence: [
                `Total credentials found: ${totalFindings}`,
                `By type:`,
                ...Object.entries(byType).map(([type, count]) => `  - ${type}: ${count}`),
                "",
                "Recommendations:",
                "  - Apply principle of least privilege",
                "  - Rotate credentials regularly (every 90 days)",
                "  - Use short-lived tokens where possible",
                "  - Avoid storing credentials in config files",
            ].join("\n"),
            metadata: {
                agentScanner: "credential-scope-audit",
                totalCredentials: totalFindings,
                byType,
            },
        });
    }
    return deterministicFindings;
}
// ============================================================================
// Main Scanner Function
// ============================================================================
/**
 * Run credential scope audit scanner
 */
export async function runCredentialScopeAudit(manifest, options) {
    const startTime = Date.now();
    const scanPath = options?.scanPath || process.cwd();
    // Scan config files
    const fileResults = await scanForCredentials(scanPath);
    // Scan additional files if specified
    if (options?.additionalFiles) {
        for (const filePath of options.additionalFiles) {
            try {
                const content = await readFile(filePath, "utf-8");
                const findings = await scanConfigFile(filePath, content);
                if (findings.length > 0) {
                    fileResults.push({ file: filePath, findings });
                }
            }
            catch {
                // Skip unreadable files
            }
        }
    }
    // Scan environment if requested
    const envFindings = [];
    if (options?.scanEnvironment !== false) {
        envFindings.push(...analyzeEnvironmentVariables());
    }
    // Convert to deterministic findings
    const findings = credentialsToDeterministicFindings(envFindings, fileResults);
    // Calculate manifest hash
    const manifestHash = createHash("sha256")
        .update(JSON.stringify(manifest))
        .digest("hex");
    return {
        scanner: "credential-scope-audit",
        findings,
        duration: Date.now() - startTime,
        success: true,
        mcpServerName: manifest.name,
        mcpServerVersion: manifest.version,
        manifestHash,
        version: "1.0.0",
        rulesUsed: ["config-scan", "env-scan"],
    };
}
/**
 * Check if credential scope audit is available
 */
export async function checkCredentialScopeAuditAvailable() {
    return {
        scanner: "credential-scope-audit",
        available: true,
        version: "1.0.0",
    };
}
/**
 * Get credential audit summary
 */
export function getCredentialAuditSummary(result) {
    const summaryFinding = result.findings.find((f) => f.ruleId === "credential-scope:summary");
    if (!summaryFinding?.metadata) {
        return null;
    }
    const meta = summaryFinding.metadata;
    return {
        totalCredentials: meta.totalCredentials || 0,
        byType: meta.byType || {},
    };
}
//# sourceMappingURL=credential-scope-audit.js.map