Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"

[project]
name = "socketsecurity"
version = "2.2.7"
version = "2.2.8"
requires-python = ">= 3.10"
license = {"file" = "LICENSE"}
dependencies = [
Expand All @@ -16,7 +16,7 @@ dependencies = [
'GitPython',
'packaging',
'python-dotenv',
'socketdev>=3.0.0,<4.0.0'
'socketdev>=3.0.5,<4.0.0'
]
readme = "README.md"
description = "Socket Security CLI for CI/CD"
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__author__ = 'socket.dev'
__version__ = '2.2.7'
__version__ = '2.2.8'
11 changes: 6 additions & 5 deletions socketsecurity/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,21 +451,22 @@ def empty_head_scan_file() -> List[str]:
log.debug(f"Created temporary empty file for baseline scan: {temp_path}")
return [temp_path]

def create_full_scan(self, files: List[str], params: FullScanParams) -> FullScan:
def create_full_scan(self, files: List[str], params: FullScanParams, base_path: str = None) -> FullScan:
"""
Creates a new full scan via the Socket API.

Args:
files: List of file paths to scan
params: Parameters for the full scan
base_path: Base path for the scan (optional)

Returns:
FullScan object with scan results
"""
log.info("Creating new full scan")
create_full_start = time.time()

res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50)
res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50, base_path=base_path)
if not res.success:
log.error(f"Error creating full scan: {res.message}, status: {res.status}")
raise Exception(f"Error creating full scan: {res.message}, status: {res.status}")
Expand Down Expand Up @@ -523,7 +524,7 @@ def create_full_scan_with_report_url(
try:
# Create new scan
new_scan_start = time.time()
new_full_scan = self.create_full_scan(files, params)
new_full_scan = self.create_full_scan(files, params, base_path=path)
new_scan_end = time.time()
log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
except APIFailure as e:
Expand Down Expand Up @@ -899,7 +900,7 @@ def create_new_diff(
# Create baseline scan with empty file
empty_files = Core.empty_head_scan_file()
try:
head_full_scan = self.create_full_scan(empty_files, tmp_params)
head_full_scan = self.create_full_scan(empty_files, tmp_params, base_path=path)
head_full_scan_id = head_full_scan.id
log.debug(f"Created empty baseline scan: {head_full_scan_id}")

Expand All @@ -922,7 +923,7 @@ def create_new_diff(
# Create new scan
try:
new_scan_start = time.time()
new_full_scan = self.create_full_scan(files, params)
new_full_scan = self.create_full_scan(files, params, base_path=path)
new_scan_end = time.time()
log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
except APIFailure as e:
Expand Down
115 changes: 108 additions & 7 deletions socketsecurity/core/messages.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import os
import re
from pathlib import Path
from mdutils import MdUtils
Expand Down Expand Up @@ -29,6 +30,92 @@ def map_severity_to_sarif(severity: str) -> str:
}
return severity_mapping.get(severity.lower(), "note")

@staticmethod
def get_manifest_file_url(diff: Diff, manifest_path: str, config=None) -> str:
"""
Generate proper URL for manifest file based on the repository type and diff URL.

:param diff: Diff object containing diff_url and report_url
:param manifest_path: Path to the manifest file (can contain multiple files separated by ';')
:param config: Configuration object to determine SCM type
:return: Properly formatted URL for the manifest file
"""
if not manifest_path:
return ""

# Handle multiple manifest files separated by ';' - use the first one
first_manifest = manifest_path.split(';')[0] if ';' in manifest_path else manifest_path

# Clean up the manifest path - remove build agent paths and normalize
clean_path = first_manifest

# Remove common build agent path prefixes
prefixes_to_remove = [
'opt/buildagent/work/',
'/opt/buildagent/work/',
'home/runner/work/',
'/home/runner/work/',
]

for prefix in prefixes_to_remove:
if clean_path.startswith(prefix):
# Find the part after the build ID (usually a hash)
parts = clean_path[len(prefix):].split('/', 2)
if len(parts) >= 3:
clean_path = parts[2] # Take everything after build ID and repo name
break

# Remove leading slashes
clean_path = clean_path.lstrip('/')

# Determine SCM type from config or diff_url
scm_type = "api" # Default to API
if config and hasattr(config, 'scm'):
scm_type = config.scm.lower()
elif hasattr(diff, 'diff_url') and diff.diff_url:
diff_url = diff.diff_url.lower()
if 'github.com' in diff_url or 'github' in diff_url:
scm_type = "github"
elif 'gitlab' in diff_url:
scm_type = "gitlab"
elif 'bitbucket' in diff_url:
scm_type = "bitbucket"

# Generate URL based on SCM type using config information
# NEVER use diff.diff_url for SCM URLs - those are Socket URLs for "View report" links
if scm_type == "github":
if config and hasattr(config, 'repo') and config.repo:
# Get branch from config, default to main
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
# Construct GitHub URL from repo info (could be github.com or GitHub Enterprise)
github_server = os.getenv('GITHUB_SERVER_URL', 'https://github.com')
return f"{github_server}/{config.repo}/blob/{branch}/{clean_path}"

elif scm_type == "gitlab":
if config and hasattr(config, 'repo') and config.repo:
# Get branch from config, default to main
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
# Construct GitLab URL from repo info (could be gitlab.com or self-hosted GitLab)
gitlab_server = os.getenv('CI_SERVER_URL', 'https://gitlab.com')
return f"{gitlab_server}/{config.repo}/-/blob/{branch}/{clean_path}"

elif scm_type == "bitbucket":
if config and hasattr(config, 'repo') and config.repo:
# Get branch from config, default to main
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
# Construct Bitbucket URL from repo info (could be bitbucket.org or Bitbucket Server)
bitbucket_server = os.getenv('BITBUCKET_SERVER_URL', 'https://bitbucket.org')
return f"{bitbucket_server}/{config.repo}/src/{branch}/{clean_path}"

# Fallback to Socket file view for API or unknown repository types
if hasattr(diff, 'report_url') and diff.report_url:
# Strip leading slash and URL encode for Socket dashboard
socket_path = clean_path.lstrip('/')
encoded_path = socket_path.replace('/', '%2F')
return f"{diff.report_url}?tab=files&file={encoded_path}"

return ""

@staticmethod
def find_line_in_file(packagename: str, packageversion: str, manifest_file: str) -> tuple:
"""
Expand Down Expand Up @@ -301,12 +388,13 @@ def create_security_comment_json(diff: Diff) -> dict:
return output

@staticmethod
def security_comment_template(diff: Diff) -> str:
def security_comment_template(diff: Diff, config=None) -> str:
"""
Generates the security comment template in the new required format.
Dynamically determines placement of the alerts table if markers like `<!-- start-socket-alerts-table -->` are used.

:param diff: Diff - Contains the detected vulnerabilities and warnings.
:param config: Optional configuration object to determine SCM type.
:return: str - The formatted Markdown/HTML string.
"""
# Group license policy violations by PURL (ecosystem/package@version)
Expand Down Expand Up @@ -348,6 +436,8 @@ def security_comment_template(diff: Diff) -> str:
severity_icon = Messages.get_severity_icon(alert.severity)
action = "Block" if alert.error else "Warn"
details_open = ""
# Generate proper manifest URL
manifest_url = Messages.get_manifest_file_url(diff, alert.manifests, config)
# Generate a table row for each alert
comment += f"""
<!-- start-socket-alert-{alert.pkg_name}@{alert.pkg_version} -->
Expand All @@ -360,7 +450,7 @@ def security_comment_template(diff: Diff) -> str:
<details {details_open}>
<summary>{alert.pkg_name}@{alert.pkg_version} - {alert.title}</summary>
<p><strong>Note:</strong> {alert.description}</p>
<p><strong>Source:</strong> <a href="{alert.manifests}">Manifest File</a></p>
<p><strong>Source:</strong> <a href="{manifest_url}">Manifest File</a></p>
<p>ℹ️ Read more on:
<a href="{alert.purl}">This package</a> |
<a href="{alert.url}">This alert</a> |
Expand Down Expand Up @@ -405,8 +495,12 @@ def security_comment_template(diff: Diff) -> str:
for finding in license_findings:
comment += f" <li>{finding}</li>\n"


# Generate proper manifest URL for license violations
license_manifest_url = Messages.get_manifest_file_url(diff, first_alert.manifests, config)

comment += f""" </ul>
<p><strong>From:</strong> {first_alert.manifests}</p>
<p><strong>From:</strong> <a href="{license_manifest_url}">Manifest File</a></p>
<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>
<blockquote>
<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>[email protected]</strong>.</p>
Expand All @@ -420,12 +514,19 @@ def security_comment_template(diff: Diff) -> str:
"""

# Close table
comment += """
# Use diff_url for PRs, report_url for non-PR scans
view_report_url = ""
if hasattr(diff, 'diff_url') and diff.diff_url:
view_report_url = diff.diff_url
elif hasattr(diff, 'report_url') and diff.report_url:
view_report_url = diff.report_url

comment += f"""
</tbody>
</table>
<!-- end-socket-alerts-table -->

[View full report](https://socket.dev/...&action=error%2Cwarn)
[View full report]({view_report_url}?action=error%2Cwarn)
"""

return comment
Expand Down Expand Up @@ -519,7 +620,7 @@ def create_acceptable_risk(md: MdUtils, ignore_commands: list) -> MdUtils:
return md

@staticmethod
def create_security_alert_table(diff: Diff, md: MdUtils) -> (MdUtils, list, dict):
def create_security_alert_table(diff: Diff, md: MdUtils) -> tuple[MdUtils, list, dict]:
"""
Creates the detected issues table based on the Security Policy
:param diff: Diff - Diff report with the detected issues
Expand Down Expand Up @@ -730,7 +831,7 @@ def create_console_security_alert_table(diff: Diff) -> PrettyTable:
return alert_table

@staticmethod
def create_sources(alert: Issue, style="md") -> [str, str]:
def create_sources(alert: Issue, style="md") -> tuple[str, str]:
sources = []
manifests = []

Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/socketcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def main_code():
overview_comment = Messages.dependency_overview_template(diff)
log.debug("Creating Security Issues Comment")

security_comment = Messages.security_comment_template(diff)
security_comment = Messages.security_comment_template(diff, config)

new_security_comment = True
new_overview_comment = True
Expand Down
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading