/** * Extracts line range from a Finding UID * Finding UID format: {CheckID}-{resource_name}-{line_range} * Example: "AVD-AWS-0001-main.tf-10:15" -> "10:15" * * @param findingUid - The finding UID * @returns Line range string or null if not found */ export function extractLineRangeFromUid(findingUid: string): string | null { if (!findingUid) { return null; } // Split by dash and get the last part (line range) const parts = findingUid.split("-"); const lastPart = parts[parts.length - 1]; // Check if the last part is a line range in format "number:number" // This ensures we don't confuse numeric filenames with line ranges if (/^\d+:\d+$/.test(lastPart)) { return lastPart; } return null; } /** * Builds a Git repository URL with file path and line numbers * Supports GitHub, GitLab, Bitbucket, and generic Git URLs * * @param repoUrl - Repository URL (can be HTTPS or git@ format) * @param filePath - Path to the file in the repository * @param lineRange - Line range in format "10-15" or "10:15" or "10" * @param branch - Git branch name (defaults to "main" if not provided) * @returns Complete URL to the file with line numbers, or null if URL cannot be built */ export function buildGitFileUrl( repoUrl: string, filePath: string, lineRange: string, branch?: string, ): string | null { if (!repoUrl || !filePath) { return null; } try { // Normalize the repository URL let normalizedUrl = repoUrl.trim(); // Convert git@ format to HTTPS (best effort) if (normalizedUrl.startsWith("git@")) { // git@github.com:user/repo.git -> https://github.com/user/repo normalizedUrl = normalizedUrl .replace(/^git@/, "https://") .replace(/\.git$/, "") .replace(/:([^:]+)$/, "/$1"); // Replace last : with / } // Remove .git suffix if present normalizedUrl = normalizedUrl.replace(/\.git$/, ""); // Parse URL to determine provider const url = new URL(normalizedUrl); const hostname = url.hostname.toLowerCase(); // Clean up file path (remove leading slashes) const cleanFilePath = filePath.replace(/^\/+/, ""); // Parse line range const { startLine, endLine } = parseLineRange(lineRange); // Build URL based on Git provider if (hostname.includes("github")) { return buildGitHubUrl( normalizedUrl, cleanFilePath, startLine, endLine, branch, ); } else if (hostname.includes("gitlab")) { return buildGitLabUrl( normalizedUrl, cleanFilePath, startLine, endLine, branch, ); } else if (hostname.includes("bitbucket")) { return buildBitbucketUrl( normalizedUrl, cleanFilePath, startLine, endLine, branch, ); } else { // Generic Git provider - try GitHub format as fallback return buildGitHubUrl( normalizedUrl, cleanFilePath, startLine, endLine, branch, ); } } catch (error) { console.error("Error building Git file URL:", error); return null; } } /** * Parses line range string into start and end line numbers */ function parseLineRange(lineRange: string): { startLine: number | null; endLine: number | null; } { if (!lineRange || lineRange === "file") { return { startLine: null, endLine: null }; } // Handle formats: "10-15", "10:15", "10" // Safe regex: anchored pattern for line numbers only (no ReDoS risk) // eslint-disable-next-line security/detect-unsafe-regex const match = lineRange.match(/^(\d+)[-:]?(\d+)?$/); if (match) { const startLine = parseInt(match[1], 10); const endLine = match[2] ? parseInt(match[2], 10) : startLine; return { startLine, endLine }; } return { startLine: null, endLine: null }; } /** * Builds GitHub-style URL * Format: https://github.com/user/repo/blob/{branch}/path/file.tf#L10-L15 */ function buildGitHubUrl( baseUrl: string, filePath: string, startLine: number | null, endLine: number | null, branch?: string, ): string { // Use provided branch, default to "main" if not provided const branchName = branch || "main"; let url = `${baseUrl}/blob/${branchName}/${filePath}`; if (startLine !== null) { if (endLine !== null && endLine !== startLine) { url += `#L${startLine}-L${endLine}`; } else { url += `#L${startLine}`; } } return url; } /** * Builds GitLab-style URL * Format: https://gitlab.com/user/repo/-/blob/{branch}/path/file.tf#L10-15 */ function buildGitLabUrl( baseUrl: string, filePath: string, startLine: number | null, endLine: number | null, branch?: string, ): string { const branchName = branch || "main"; let url = `${baseUrl}/-/blob/${branchName}/${filePath}`; if (startLine !== null) { if (endLine !== null && endLine !== startLine) { url += `#L${startLine}-${endLine}`; } else { url += `#L${startLine}`; } } return url; } /** * Builds Bitbucket-style URL * Format: https://bitbucket.org/user/repo/src/{branch}/path/file.tf#lines-10:15 */ function buildBitbucketUrl( baseUrl: string, filePath: string, startLine: number | null, endLine: number | null, branch?: string, ): string { const branchName = branch || "main"; let url = `${baseUrl}/src/${branchName}/${filePath}`; if (startLine !== null) { if (endLine !== null && endLine !== startLine) { url += `#lines-${startLine}:${endLine}`; } else { url += `#lines-${startLine}`; } } return url; }