mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(iac): use branch as region for IaC findings (#9295)
This commit is contained in:
@@ -27,6 +27,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Add verification for provider ID in MongoDB Atlas provider [(#9211)](https://github.com/prowler-cloud/prowler/pull/9211)
|
||||
- Add Prowler ThreatScore for the K8S provider [(#9235)](https://github.com/prowler-cloud/prowler/pull/9235)
|
||||
- Add `postgresql_flexible_server_entra_id_authentication_enabled` check for Azure provider [(#8764)](https://github.com/prowler-cloud/prowler/pull/8764)
|
||||
- Add branch name to IaC provider region [(#9296)](https://github.com/prowler-cloud/prowler/pull/9295)
|
||||
|
||||
### Changed
|
||||
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)
|
||||
|
||||
@@ -314,9 +314,7 @@ class Finding(BaseModel):
|
||||
)
|
||||
output_data["resource_uid"] = getattr(check_output, "resource_name", "")
|
||||
# For IaC, resource_line_range only exists on CheckReportIAC, not on Finding objects
|
||||
output_data["region"] = getattr(
|
||||
check_output, "resource_line_range", "file"
|
||||
)
|
||||
output_data["region"] = getattr(check_output, "region", "global")
|
||||
output_data["resource_line_range"] = getattr(
|
||||
check_output, "resource_line_range", ""
|
||||
)
|
||||
|
||||
@@ -241,7 +241,7 @@ class HTML(Output):
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Severity</th>
|
||||
<th scope="col">Service Name</th>
|
||||
<th scope="col">{"Line Range" if provider.type == "iac" else "Region"}</th>
|
||||
<th scope="col">Region</th>
|
||||
<th style="width:20%" scope="col">Check ID</th>
|
||||
<th style="width:20%" scope="col">Check Title</th>
|
||||
<th scope="col">Resource ID</th>
|
||||
|
||||
@@ -325,7 +325,7 @@ class Scan:
|
||||
resource_name=report.resource_name,
|
||||
resource_details=report.resource_details,
|
||||
resource_tags={}, # IaC doesn't have resource tags
|
||||
region="global", # IaC doesn't have regions
|
||||
region=report.region, # IaC region is the branch name
|
||||
compliance={}, # IaC doesn't have compliance mappings yet
|
||||
raw=report.resource, # The raw finding dict
|
||||
)
|
||||
|
||||
@@ -45,11 +45,12 @@ class IacProvider(Provider):
|
||||
self.scan_repository_url = scan_repository_url
|
||||
self.scanners = scanners
|
||||
self.exclude_path = exclude_path
|
||||
self.region = "global"
|
||||
self.region = "branch"
|
||||
self.audited_account = "local-iac"
|
||||
self._session = None
|
||||
self._identity = "prowler"
|
||||
self._auth_method = "No auth"
|
||||
self._temp_clone_dir = None # Track temporary directory for cleanup
|
||||
|
||||
if scan_repository_url:
|
||||
oauth_app_token = oauth_app_token or environ.get("GITHUB_OAUTH_APP_TOKEN")
|
||||
@@ -80,6 +81,20 @@ class IacProvider(Provider):
|
||||
"No GitHub authentication method provided; proceeding without authentication."
|
||||
)
|
||||
|
||||
# Clone repository and detect branch during initialization
|
||||
# This ensures the branch is detected for both CLI and API usage
|
||||
self._temp_clone_dir, branch_name = self._clone_repository(
|
||||
self.scan_repository_url,
|
||||
self.github_username,
|
||||
self.personal_access_token,
|
||||
self.oauth_app_token,
|
||||
)
|
||||
# Update scan_path to point to the cloned repository
|
||||
self.scan_path = self._temp_clone_dir
|
||||
# Update region with the detected branch name
|
||||
self.region = branch_name
|
||||
logger.info(f"Updated region to branch: {branch_name}")
|
||||
|
||||
# Audit Config
|
||||
if config_content:
|
||||
self._audit_config = config_content
|
||||
@@ -131,6 +146,20 @@ class IacProvider(Provider):
|
||||
def fixer_config(self):
|
||||
return self._fixer_config
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup temporary directory when provider is destroyed"""
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove temporary cloned repository if it exists"""
|
||||
if self._temp_clone_dir:
|
||||
try:
|
||||
logger.info(f"Removing temporary directory {self._temp_clone_dir}...")
|
||||
shutil.rmtree(self._temp_clone_dir)
|
||||
self._temp_clone_dir = None
|
||||
except Exception as error:
|
||||
logger.warning(f"Failed to remove temporary directory: {error}")
|
||||
|
||||
def setup_session(self):
|
||||
"""IAC provider doesn't need a session since it uses Trivy directly"""
|
||||
return None
|
||||
@@ -208,6 +237,8 @@ class IacProvider(Provider):
|
||||
)
|
||||
if finding_status == "MUTED":
|
||||
report.muted = True
|
||||
# Set the region from the provider
|
||||
report.region = self.region
|
||||
return report
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
@@ -215,15 +246,50 @@ class IacProvider(Provider):
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
def _detect_branch_name(self, repo_path: str) -> str:
|
||||
"""
|
||||
Detect the current branch name from a cloned repository.
|
||||
|
||||
Args:
|
||||
repo_path: Path to the cloned repository
|
||||
|
||||
Returns:
|
||||
str: The branch name, defaulting to "main" if detection fails
|
||||
"""
|
||||
try:
|
||||
import os
|
||||
|
||||
# Read .git/HEAD to detect the current branch
|
||||
head_file = os.path.join(repo_path, ".git", "HEAD")
|
||||
if os.path.exists(head_file):
|
||||
with open(head_file, "r") as f:
|
||||
content = f.read().strip()
|
||||
# Format: "ref: refs/heads/branch-name"
|
||||
if content.startswith("ref: refs/heads/"):
|
||||
branch_name = content[16:] # Remove "ref: refs/heads/"
|
||||
logger.info(f"Detected branch: {branch_name}")
|
||||
return branch_name
|
||||
|
||||
# Fallback: return "main" as default
|
||||
logger.warning("Could not detect branch name, defaulting to 'main'")
|
||||
return "main"
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f"Error detecting branch name: {error}")
|
||||
return "main" # Safe fallback
|
||||
|
||||
def _clone_repository(
|
||||
self,
|
||||
repository_url: str,
|
||||
github_username: str = None,
|
||||
personal_access_token: str = None,
|
||||
oauth_app_token: str = None,
|
||||
) -> str:
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Clone a git repository to a temporary directory, supporting GitHub authentication.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (temporary_directory, branch_name)
|
||||
"""
|
||||
try:
|
||||
original_url = repository_url
|
||||
@@ -276,33 +342,36 @@ class IacProvider(Provider):
|
||||
porcelain.clone(repository_url, temporary_directory, depth=1)
|
||||
logger.info("Repository cloned successfully!")
|
||||
|
||||
return temporary_directory
|
||||
# Detect the branch name from the cloned repository
|
||||
branch_name = self._detect_branch_name(temporary_directory)
|
||||
|
||||
return temporary_directory, branch_name
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
|
||||
)
|
||||
|
||||
def run(self) -> List[CheckReportIAC]:
|
||||
temp_dir = None
|
||||
if self.scan_repository_url:
|
||||
scan_dir = temp_dir = self._clone_repository(
|
||||
self.scan_repository_url,
|
||||
getattr(self, "github_username", None),
|
||||
getattr(self, "personal_access_token", None),
|
||||
getattr(self, "oauth_app_token", None),
|
||||
)
|
||||
else:
|
||||
scan_dir = self.scan_path
|
||||
"""
|
||||
Execute the IaC scan.
|
||||
|
||||
Note: Repository cloning and branch detection now happen in __init__(),
|
||||
so this method just runs the scan and returns results.
|
||||
For CLI compatibility, cleanup is still performed at the end.
|
||||
"""
|
||||
try:
|
||||
# Collect all batches from the generator
|
||||
# scan_path now points to either the local directory or the cloned repo
|
||||
reports = []
|
||||
for batch in self.run_scan(scan_dir, self.scanners, self.exclude_path):
|
||||
for batch in self.run_scan(
|
||||
self.scan_path, self.scanners, self.exclude_path
|
||||
):
|
||||
reports.extend(batch)
|
||||
finally:
|
||||
if temp_dir:
|
||||
logger.info(f"Removing temporary directory {temp_dir}...")
|
||||
shutil.rmtree(temp_dir)
|
||||
# Clean up temporary directory if this was a repository scan
|
||||
# This ensures CLI usage cleans up immediately after run()
|
||||
if self._temp_clone_dir:
|
||||
self.cleanup()
|
||||
|
||||
return reports
|
||||
|
||||
|
||||
@@ -662,6 +662,7 @@ class TestFinding:
|
||||
check_output.resource_name = "aws_s3_bucket.example"
|
||||
check_output.resource_path = "/path/to/iac/file.tf"
|
||||
check_output.resource_line_range = "1:5"
|
||||
check_output.region = "main" # Branch name for remote IaC scans
|
||||
check_output.resource = {
|
||||
"resource": "aws_s3_bucket.example",
|
||||
"value": {},
|
||||
@@ -685,7 +686,7 @@ class TestFinding:
|
||||
assert finding_output.auth_method == "No auth"
|
||||
assert finding_output.resource_name == "aws_s3_bucket.example"
|
||||
assert finding_output.resource_uid == "aws_s3_bucket.example"
|
||||
assert finding_output.region == "1:5"
|
||||
assert finding_output.region == "main" # Branch name, not line range
|
||||
assert finding_output.status == Status.PASS
|
||||
assert finding_output.status_extended == "mock_status_extended"
|
||||
assert finding_output.muted is False
|
||||
|
||||
@@ -398,7 +398,7 @@ def get_aws_html_header(args: list) -> str:
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<a href="https://github.com/prowler-cloud/prowler/"><img class="float-left card-img-left mt-4 mr-4 ml-4"
|
||||
src=https://prowler.com/wp-content/uploads/logo-html.png
|
||||
src=https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png
|
||||
alt="prowler-logo"
|
||||
style="width: 15rem; height:auto;"/></a>
|
||||
<div class="card">
|
||||
|
||||
@@ -148,19 +148,31 @@ class TestIacProvider:
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_provider_run_remote_scan(self):
|
||||
scan_repository_url = "https://github.com/user/repo"
|
||||
provider = IacProvider(scan_repository_url=scan_repository_url)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.iac.iac_provider.IacProvider._clone_repository",
|
||||
return_value=temp_dir,
|
||||
return_value=(temp_dir, "main"),
|
||||
) as mock_clone,
|
||||
mock.patch(
|
||||
"prowler.providers.iac.iac_provider.IacProvider.run_scan"
|
||||
) as mock_run_scan,
|
||||
):
|
||||
# Repository cloning now happens during __init__
|
||||
provider = IacProvider(scan_repository_url=scan_repository_url)
|
||||
|
||||
# Verify clone was called during initialization
|
||||
mock_clone.assert_called_once_with(
|
||||
scan_repository_url, None, None, None
|
||||
)
|
||||
|
||||
# Verify region was updated with branch name
|
||||
assert provider.region == "main"
|
||||
|
||||
# Run the scan
|
||||
provider.run()
|
||||
mock_clone.assert_called_with(scan_repository_url, None, None, None)
|
||||
|
||||
# Verify scan was called with the cloned directory
|
||||
mock_run_scan.assert_called_with(
|
||||
temp_dir, ["vuln", "misconfig", "secret"], []
|
||||
)
|
||||
@@ -183,17 +195,22 @@ class TestIacProvider:
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_print_credentials_remote(self):
|
||||
repo_url = "https://github.com/user/repo"
|
||||
provider = IacProvider(scan_repository_url=repo_url)
|
||||
with mock.patch("builtins.print") as mock_print:
|
||||
provider.print_credentials()
|
||||
assert any(
|
||||
f"Repository: \x1b[33m{repo_url}\x1b[0m" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
assert any(
|
||||
"Scanning remote IaC repository:" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with mock.patch(
|
||||
"prowler.providers.iac.iac_provider.IacProvider._clone_repository",
|
||||
return_value=(temp_dir, "main"),
|
||||
):
|
||||
provider = IacProvider(scan_repository_url=repo_url)
|
||||
with mock.patch("builtins.print") as mock_print:
|
||||
provider.print_credentials()
|
||||
assert any(
|
||||
f"Repository: \x1b[33m{repo_url}\x1b[0m" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
assert any(
|
||||
"Scanning remote IaC repository:" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_iac_provider_process_check_medium_severity(self, mock_subprocess):
|
||||
@@ -664,25 +681,79 @@ class TestIacProvider:
|
||||
def test_clone_repository_no_auth(self, _mock_mkdtemp, mock_clone):
|
||||
provider = IacProvider()
|
||||
url = "https://github.com/user/repo.git"
|
||||
provider._clone_repository(url)
|
||||
with mock.patch.object(provider, "_detect_branch_name", return_value="main"):
|
||||
temp_dir, branch_name = provider._clone_repository(url)
|
||||
mock_clone.assert_called_with(url, "/tmp/fake-dir", depth=1)
|
||||
assert temp_dir == "/tmp/fake-dir"
|
||||
assert branch_name == "main"
|
||||
|
||||
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")
|
||||
@mock.patch("tempfile.mkdtemp", return_value="/tmp/fake-dir")
|
||||
def test_clone_repository_with_pat(self, _mock_mkdtemp, mock_clone):
|
||||
provider = IacProvider()
|
||||
url = "https://github.com/user/repo.git"
|
||||
provider._clone_repository(
|
||||
url, github_username="user", personal_access_token="token123"
|
||||
)
|
||||
with mock.patch.object(provider, "_detect_branch_name", return_value="develop"):
|
||||
temp_dir, branch_name = provider._clone_repository(
|
||||
url, github_username="user", personal_access_token="token123"
|
||||
)
|
||||
expected_url = "https://user:token123@github.com/user/repo.git"
|
||||
mock_clone.assert_called_with(expected_url, "/tmp/fake-dir", depth=1)
|
||||
assert temp_dir == "/tmp/fake-dir"
|
||||
assert branch_name == "develop"
|
||||
|
||||
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")
|
||||
@mock.patch("tempfile.mkdtemp", return_value="/tmp/fake-dir")
|
||||
def test_clone_repository_with_oauth(self, _mock_mkdtemp, mock_clone):
|
||||
provider = IacProvider()
|
||||
url = "https://github.com/user/repo.git"
|
||||
provider._clone_repository(url, oauth_app_token="oauth456")
|
||||
with mock.patch.object(provider, "_detect_branch_name", return_value="master"):
|
||||
temp_dir, branch_name = provider._clone_repository(
|
||||
url, oauth_app_token="oauth456"
|
||||
)
|
||||
expected_url = "https://oauth2:oauth456@github.com/user/repo.git"
|
||||
mock_clone.assert_called_with(expected_url, "/tmp/fake-dir", depth=1)
|
||||
assert temp_dir == "/tmp/fake-dir"
|
||||
assert branch_name == "master"
|
||||
|
||||
def test_detect_branch_name_main(self):
|
||||
"""Test detecting 'main' branch from .git/HEAD"""
|
||||
provider = IacProvider()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a mock .git/HEAD file with main branch
|
||||
git_dir = os.path.join(temp_dir, ".git")
|
||||
os.makedirs(git_dir)
|
||||
head_file = os.path.join(git_dir, "HEAD")
|
||||
with open(head_file, "w") as f:
|
||||
f.write("ref: refs/heads/main\n")
|
||||
|
||||
branch_name = provider._detect_branch_name(temp_dir)
|
||||
assert branch_name == "main"
|
||||
|
||||
def test_detect_branch_name_custom_branch(self):
|
||||
"""Test detecting custom branch like 'develop' from .git/HEAD"""
|
||||
provider = IacProvider()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a mock .git/HEAD file with develop branch
|
||||
git_dir = os.path.join(temp_dir, ".git")
|
||||
os.makedirs(git_dir)
|
||||
head_file = os.path.join(git_dir, "HEAD")
|
||||
with open(head_file, "w") as f:
|
||||
f.write("ref: refs/heads/develop\n")
|
||||
|
||||
branch_name = provider._detect_branch_name(temp_dir)
|
||||
assert branch_name == "develop"
|
||||
|
||||
def test_detect_branch_name_fallback(self):
|
||||
"""Test fallback to 'main' when .git/HEAD doesn't exist"""
|
||||
provider = IacProvider()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Don't create .git/HEAD file
|
||||
branch_name = provider._detect_branch_name(temp_dir)
|
||||
assert branch_name == "main"
|
||||
|
||||
def test_detect_branch_name_error_handling(self):
|
||||
"""Test error handling returns 'main' as fallback"""
|
||||
provider = IacProvider()
|
||||
# Pass a non-existent directory
|
||||
branch_name = provider._detect_branch_name("/non/existent/path")
|
||||
assert branch_name == "main"
|
||||
|
||||
@@ -14,6 +14,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- PDF reporting for NIS2 compliance framework [(#9170)](https://github.com/prowler-cloud/prowler/pull/9170)
|
||||
- External resource link to IaC findings for direct navigation to source code in Git repositories [(#9151)](https://github.com/prowler-cloud/prowler/pull/9151)
|
||||
- New Overview page and new app styles [(#9234)](https://github.com/prowler-cloud/prowler/pull/9234)
|
||||
- Use branch name as region for IaC findings [(#9296)](https://github.com/prowler-cloud/prowler/pull/9296)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export const FindingDetail = ({
|
||||
providerDetails.uid,
|
||||
resource.name,
|
||||
extractLineRangeFromUid(attributes.uid) || "",
|
||||
resource.region,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
||||
@@ -167,7 +167,12 @@ export const ResourceDetail = ({
|
||||
// Build Git URL for IaC resources
|
||||
const gitUrl =
|
||||
providerData.provider === "iac"
|
||||
? buildGitFileUrl(providerData.uid, attributes.name, "")
|
||||
? buildGitFileUrl(
|
||||
providerData.uid,
|
||||
attributes.name,
|
||||
"",
|
||||
attributes.region,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (selectedFindingId) {
|
||||
|
||||
@@ -31,12 +31,14 @@ export function extractLineRangeFromUid(findingUid: string): string | null {
|
||||
* @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;
|
||||
@@ -70,19 +72,38 @@ export function buildGitFileUrl(
|
||||
|
||||
// Build URL based on Git provider
|
||||
if (hostname.includes("github")) {
|
||||
return buildGitHubUrl(normalizedUrl, cleanFilePath, startLine, endLine);
|
||||
return buildGitHubUrl(
|
||||
normalizedUrl,
|
||||
cleanFilePath,
|
||||
startLine,
|
||||
endLine,
|
||||
branch,
|
||||
);
|
||||
} else if (hostname.includes("gitlab")) {
|
||||
return buildGitLabUrl(normalizedUrl, cleanFilePath, startLine, endLine);
|
||||
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);
|
||||
return buildGitHubUrl(
|
||||
normalizedUrl,
|
||||
cleanFilePath,
|
||||
startLine,
|
||||
endLine,
|
||||
branch,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error building Git file URL:", error);
|
||||
@@ -116,17 +137,18 @@ function parseLineRange(lineRange: string): {
|
||||
|
||||
/**
|
||||
* Builds GitHub-style URL
|
||||
* Format: https://github.com/user/repo/blob/main/path/file.tf#L10-L15
|
||||
* 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 {
|
||||
// Assume main/master branch for simplicity
|
||||
const branch = "main";
|
||||
let url = `${baseUrl}/blob/${branch}/${filePath}`;
|
||||
// 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) {
|
||||
@@ -141,16 +163,17 @@ function buildGitHubUrl(
|
||||
|
||||
/**
|
||||
* Builds GitLab-style URL
|
||||
* Format: https://gitlab.com/user/repo/-/blob/main/path/file.tf#L10-15
|
||||
* 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 branch = "main";
|
||||
let url = `${baseUrl}/-/blob/${branch}/${filePath}`;
|
||||
const branchName = branch || "main";
|
||||
let url = `${baseUrl}/-/blob/${branchName}/${filePath}`;
|
||||
|
||||
if (startLine !== null) {
|
||||
if (endLine !== null && endLine !== startLine) {
|
||||
@@ -165,16 +188,17 @@ function buildGitLabUrl(
|
||||
|
||||
/**
|
||||
* Builds Bitbucket-style URL
|
||||
* Format: https://bitbucket.org/user/repo/src/main/path/file.tf#lines-10:15
|
||||
* 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 branch = "main";
|
||||
let url = `${baseUrl}/src/${branch}/${filePath}`;
|
||||
const branchName = branch || "main";
|
||||
let url = `${baseUrl}/src/${branchName}/${filePath}`;
|
||||
|
||||
if (startLine !== null) {
|
||||
if (endLine !== null && endLine !== startLine) {
|
||||
|
||||
Reference in New Issue
Block a user