Skip to content

Commit 656a458

Browse files
authored
feat: Add SCM-aware manifest file URL generation and fix report links (#119)
- Add get_manifest_file_url() method with GitHub/GitLab/Bitbucket support - Support environment variables for custom SCM servers (GitHub Enterprise, self-hosted GitLab, Bitbucket Server) - Fix manifest file links in security comments to use proper SCM URLs instead of Socket dashboard URLs - Fix 'View full report' links to use diff_url for PRs and report_url for non-PR scans - Add base_path parameter to create_full_scan() for improved path handling - Update socketdev dependency to >=3.0.5 for latest features - Add os module import for environment variable access - Update type hints for better code clarity
1 parent 8bd8b83 commit 656a458

File tree

6 files changed

+123
-21
lines changed

6 files changed

+123
-21
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.2.7"
9+
version = "2.2.8"
1010
requires-python = ">= 3.10"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [
@@ -16,7 +16,7 @@ dependencies = [
1616
'GitPython',
1717
'packaging',
1818
'python-dotenv',
19-
'socketdev>=3.0.0,<4.0.0'
19+
'socketdev>=3.0.5,<4.0.0'
2020
]
2121
readme = "README.md"
2222
description = "Socket Security CLI for CI/CD"

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.2.7'
2+
__version__ = '2.2.8'

socketsecurity/core/__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -451,21 +451,22 @@ def empty_head_scan_file() -> List[str]:
451451
log.debug(f"Created temporary empty file for baseline scan: {temp_path}")
452452
return [temp_path]
453453

454-
def create_full_scan(self, files: List[str], params: FullScanParams) -> FullScan:
454+
def create_full_scan(self, files: List[str], params: FullScanParams, base_path: str = None) -> FullScan:
455455
"""
456456
Creates a new full scan via the Socket API.
457457
458458
Args:
459459
files: List of file paths to scan
460460
params: Parameters for the full scan
461+
base_path: Base path for the scan (optional)
461462
462463
Returns:
463464
FullScan object with scan results
464465
"""
465466
log.info("Creating new full scan")
466467
create_full_start = time.time()
467468

468-
res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50)
469+
res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50, base_path=base_path)
469470
if not res.success:
470471
log.error(f"Error creating full scan: {res.message}, status: {res.status}")
471472
raise Exception(f"Error creating full scan: {res.message}, status: {res.status}")
@@ -523,7 +524,7 @@ def create_full_scan_with_report_url(
523524
try:
524525
# Create new scan
525526
new_scan_start = time.time()
526-
new_full_scan = self.create_full_scan(files, params)
527+
new_full_scan = self.create_full_scan(files, params, base_path=path)
527528
new_scan_end = time.time()
528529
log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
529530
except APIFailure as e:
@@ -899,7 +900,7 @@ def create_new_diff(
899900
# Create baseline scan with empty file
900901
empty_files = Core.empty_head_scan_file()
901902
try:
902-
head_full_scan = self.create_full_scan(empty_files, tmp_params)
903+
head_full_scan = self.create_full_scan(empty_files, tmp_params, base_path=path)
903904
head_full_scan_id = head_full_scan.id
904905
log.debug(f"Created empty baseline scan: {head_full_scan_id}")
905906

@@ -922,7 +923,7 @@ def create_new_diff(
922923
# Create new scan
923924
try:
924925
new_scan_start = time.time()
925-
new_full_scan = self.create_full_scan(files, params)
926+
new_full_scan = self.create_full_scan(files, params, base_path=path)
926927
new_scan_end = time.time()
927928
log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
928929
except APIFailure as e:

socketsecurity/core/messages.py

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import logging
3+
import os
34
import re
45
from pathlib import Path
56
from mdutils import MdUtils
@@ -29,6 +30,92 @@ def map_severity_to_sarif(severity: str) -> str:
2930
}
3031
return severity_mapping.get(severity.lower(), "note")
3132

33+
@staticmethod
34+
def get_manifest_file_url(diff: Diff, manifest_path: str, config=None) -> str:
35+
"""
36+
Generate proper URL for manifest file based on the repository type and diff URL.
37+
38+
:param diff: Diff object containing diff_url and report_url
39+
:param manifest_path: Path to the manifest file (can contain multiple files separated by ';')
40+
:param config: Configuration object to determine SCM type
41+
:return: Properly formatted URL for the manifest file
42+
"""
43+
if not manifest_path:
44+
return ""
45+
46+
# Handle multiple manifest files separated by ';' - use the first one
47+
first_manifest = manifest_path.split(';')[0] if ';' in manifest_path else manifest_path
48+
49+
# Clean up the manifest path - remove build agent paths and normalize
50+
clean_path = first_manifest
51+
52+
# Remove common build agent path prefixes
53+
prefixes_to_remove = [
54+
'opt/buildagent/work/',
55+
'/opt/buildagent/work/',
56+
'home/runner/work/',
57+
'/home/runner/work/',
58+
]
59+
60+
for prefix in prefixes_to_remove:
61+
if clean_path.startswith(prefix):
62+
# Find the part after the build ID (usually a hash)
63+
parts = clean_path[len(prefix):].split('/', 2)
64+
if len(parts) >= 3:
65+
clean_path = parts[2] # Take everything after build ID and repo name
66+
break
67+
68+
# Remove leading slashes
69+
clean_path = clean_path.lstrip('/')
70+
71+
# Determine SCM type from config or diff_url
72+
scm_type = "api" # Default to API
73+
if config and hasattr(config, 'scm'):
74+
scm_type = config.scm.lower()
75+
elif hasattr(diff, 'diff_url') and diff.diff_url:
76+
diff_url = diff.diff_url.lower()
77+
if 'github.com' in diff_url or 'github' in diff_url:
78+
scm_type = "github"
79+
elif 'gitlab' in diff_url:
80+
scm_type = "gitlab"
81+
elif 'bitbucket' in diff_url:
82+
scm_type = "bitbucket"
83+
84+
# Generate URL based on SCM type using config information
85+
# NEVER use diff.diff_url for SCM URLs - those are Socket URLs for "View report" links
86+
if scm_type == "github":
87+
if config and hasattr(config, 'repo') and config.repo:
88+
# Get branch from config, default to main
89+
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
90+
# Construct GitHub URL from repo info (could be github.com or GitHub Enterprise)
91+
github_server = os.getenv('GITHUB_SERVER_URL', 'https://github.com')
92+
return f"{github_server}/{config.repo}/blob/{branch}/{clean_path}"
93+
94+
elif scm_type == "gitlab":
95+
if config and hasattr(config, 'repo') and config.repo:
96+
# Get branch from config, default to main
97+
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
98+
# Construct GitLab URL from repo info (could be gitlab.com or self-hosted GitLab)
99+
gitlab_server = os.getenv('CI_SERVER_URL', 'https://gitlab.com')
100+
return f"{gitlab_server}/{config.repo}/-/blob/{branch}/{clean_path}"
101+
102+
elif scm_type == "bitbucket":
103+
if config and hasattr(config, 'repo') and config.repo:
104+
# Get branch from config, default to main
105+
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
106+
# Construct Bitbucket URL from repo info (could be bitbucket.org or Bitbucket Server)
107+
bitbucket_server = os.getenv('BITBUCKET_SERVER_URL', 'https://bitbucket.org')
108+
return f"{bitbucket_server}/{config.repo}/src/{branch}/{clean_path}"
109+
110+
# Fallback to Socket file view for API or unknown repository types
111+
if hasattr(diff, 'report_url') and diff.report_url:
112+
# Strip leading slash and URL encode for Socket dashboard
113+
socket_path = clean_path.lstrip('/')
114+
encoded_path = socket_path.replace('/', '%2F')
115+
return f"{diff.report_url}?tab=files&file={encoded_path}"
116+
117+
return ""
118+
32119
@staticmethod
33120
def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) -> tuple:
34121
"""
@@ -301,12 +388,13 @@ def create_security_comment_json(diff: Diff) -> dict:
301388
return output
302389

303390
@staticmethod
304-
def security_comment_template(diff: Diff) -> str:
391+
def security_comment_template(diff: Diff, config=None) -> str:
305392
"""
306393
Generates the security comment template in the new required format.
307394
Dynamically determines placement of the alerts table if markers like `<!-- start-socket-alerts-table -->` are used.
308395
309396
:param diff: Diff - Contains the detected vulnerabilities and warnings.
397+
:param config: Optional configuration object to determine SCM type.
310398
:return: str - The formatted Markdown/HTML string.
311399
"""
312400
# Group license policy violations by PURL (ecosystem/package@version)
@@ -348,6 +436,8 @@ def security_comment_template(diff: Diff) -> str:
348436
severity_icon = Messages.get_severity_icon(alert.severity)
349437
action = "Block" if alert.error else "Warn"
350438
details_open = ""
439+
# Generate proper manifest URL
440+
manifest_url = Messages.get_manifest_file_url(diff, alert.manifests, config)
351441
# Generate a table row for each alert
352442
comment += f"""
353443
<!-- start-socket-alert-{alert.pkg_name}@{alert.pkg_version} -->
@@ -360,7 +450,7 @@ def security_comment_template(diff: Diff) -> str:
360450
<details {details_open}>
361451
<summary>{alert.pkg_name}@{alert.pkg_version} - {alert.title}</summary>
362452
<p><strong>Note:</strong> {alert.description}</p>
363-
<p><strong>Source:</strong> <a href="{alert.manifests}">Manifest File</a></p>
453+
<p><strong>Source:</strong> <a href="{manifest_url}">Manifest File</a></p>
364454
<p>ℹ️ Read more on:
365455
<a href="{alert.purl}">This package</a> |
366456
<a href="{alert.url}">This alert</a> |
@@ -405,8 +495,12 @@ def security_comment_template(diff: Diff) -> str:
405495
for finding in license_findings:
406496
comment += f" <li>{finding}</li>\n"
407497

498+
499+
# Generate proper manifest URL for license violations
500+
license_manifest_url = Messages.get_manifest_file_url(diff, first_alert.manifests, config)
501+
408502
comment += f""" </ul>
409-
<p><strong>From:</strong> {first_alert.manifests}</p>
503+
<p><strong>From:</strong> <a href="{license_manifest_url}">Manifest File</a></p>
410504
<p>ℹ️ Read more on: <a href="{first_alert.purl}">This package</a> | <a href="https://socket.dev/alerts/license">What is a license policy violation?</a></p>
411505
<blockquote>
412506
<p><em>Next steps:</em> Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at <strong>support@socket.dev</strong>.</p>
@@ -420,12 +514,19 @@ def security_comment_template(diff: Diff) -> str:
420514
"""
421515

422516
# Close table
423-
comment += """
517+
# Use diff_url for PRs, report_url for non-PR scans
518+
view_report_url = ""
519+
if hasattr(diff, 'diff_url') and diff.diff_url:
520+
view_report_url = diff.diff_url
521+
elif hasattr(diff, 'report_url') and diff.report_url:
522+
view_report_url = diff.report_url
523+
524+
comment += f"""
424525
</tbody>
425526
</table>
426527
<!-- end-socket-alerts-table -->
427528
428-
[View full report](https://socket.dev/...&action=error%2Cwarn)
529+
[View full report]({view_report_url}?action=error%2Cwarn)
429530
"""
430531

431532
return comment
@@ -519,7 +620,7 @@ def create_acceptable_risk(md: MdUtils, ignore_commands: list) -> MdUtils:
519620
return md
520621

521622
@staticmethod
522-
def create_security_alert_table(diff: Diff, md: MdUtils) -> (MdUtils, list, dict):
623+
def create_security_alert_table(diff: Diff, md: MdUtils) -> tuple[MdUtils, list, dict]:
523624
"""
524625
Creates the detected issues table based on the Security Policy
525626
:param diff: Diff - Diff report with the detected issues
@@ -730,7 +831,7 @@ def create_console_security_alert_table(diff: Diff) -> PrettyTable:
730831
return alert_table
731832

732833
@staticmethod
733-
def create_sources(alert: Issue, style="md") -> [str, str]:
834+
def create_sources(alert: Issue, style="md") -> tuple[str, str]:
734835
sources = []
735836
manifests = []
736837

socketsecurity/socketcli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def main_code():
275275
overview_comment = Messages.dependency_overview_template(diff)
276276
log.debug("Creating Security Issues Comment")
277277

278-
security_comment = Messages.security_comment_template(diff)
278+
security_comment = Messages.security_comment_template(diff, config)
279279

280280
new_security_comment = True
281281
new_overview_comment = True

uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)