Skip to content

CM-47505 - Add support for Unity Version Control (formerly Plastic SCM) #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 23, 2025
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
91 changes: 91 additions & 0 deletions cycode/cli/commands/scan/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from cycode.cli.utils.progress_bar import ScanProgressBarSection
from cycode.cli.utils.scan_batch import run_parallel_batched_scan
from cycode.cli.utils.scan_utils import set_issue_detected
from cycode.cli.utils.shell_executor import shell
from cycode.cyclient import logger
from cycode.cyclient.config import set_logging_level
from cycode.cyclient.models import Detection, DetectionSchema, DetectionsPerFile, ZippedFileScanResult
Expand Down Expand Up @@ -666,6 +667,9 @@ def get_scan_parameters(context: click.Context, paths: Optional[Tuple[str]] = No
return scan_parameters

remote_url = try_get_git_remote_url(paths[0])
if not remote_url:
remote_url = try_to_get_plastic_remote_url(paths[0])

if remote_url:
# TODO(MarshalX): remove hardcode in context
context.obj['remote_url'] = remote_url
Expand All @@ -684,6 +688,93 @@ def try_get_git_remote_url(path: str) -> Optional[str]:
return None


def _get_plastic_repository_name(path: str) -> Optional[str]:
"""Gets the name of the Plastic repository from the current working directory.

The command to execute is:
cm status --header --machinereadable --fieldseparator=":::"

Example of status header in machine-readable format:
STATUS:::0:::Project/RepoName:::OrgName@ServerInfo
"""

try:
command = [
'cm',
'status',
'--header',
'--machinereadable',
f'--fieldseparator={consts.PLASTIC_VCS_DATA_SEPARATOR}',
]

status = shell(command=command, timeout=consts.PLASTIC_VSC_CLI_TIMEOUT, working_directory=path)
if not status:
logger.debug('Failed to get Plastic repository name (command failed)')
return None

status_parts = status.split(consts.PLASTIC_VCS_DATA_SEPARATOR)
if len(status_parts) < 2:
logger.debug('Failed to parse Plastic repository name (command returned unexpected format)')
return None

return status_parts[2].strip()
except Exception as e:
logger.debug('Failed to get Plastic repository name', exc_info=e)
return None


def _get_plastic_repository_list(working_dir: Optional[str] = None) -> Dict[str, str]:
"""Gets the list of Plastic repositories and their GUIDs.

The command to execute is:
cm repo list --format="{repname}:::{repguid}"

Example line with data:
Project/RepoName:::tapo1zqt-wn99-4752-h61m-7d9k79d40r4v

Each line represents an individual repository.
"""

repo_name_to_guid = {}

try:
command = ['cm', 'repo', 'ls', f'--format={{repname}}{consts.PLASTIC_VCS_DATA_SEPARATOR}{{repguid}}']

status = shell(command=command, timeout=consts.PLASTIC_VSC_CLI_TIMEOUT, working_directory=working_dir)
if not status:
logger.debug('Failed to get Plastic repository list (command failed)')
return repo_name_to_guid

status_lines = status.splitlines()
for line in status_lines:
data_parts = line.split(consts.PLASTIC_VCS_DATA_SEPARATOR)
if len(data_parts) < 2:
logger.debug('Failed to parse Plastic repository list line (unexpected format), %s', {'line': line})
continue

repo_name, repo_guid = data_parts
repo_name_to_guid[repo_name.strip()] = repo_guid.strip()

return repo_name_to_guid
except Exception as e:
logger.debug('Failed to get Plastic repository list', exc_info=e)
return repo_name_to_guid


def try_to_get_plastic_remote_url(path: str) -> Optional[str]:
repository_name = _get_plastic_repository_name(path)
if not repository_name:
return None

repository_map = _get_plastic_repository_list(path)
if repository_name not in repository_map:
logger.debug('Failed to get Plastic repository GUID (repository not found in the list)')
return None

repository_guid = repository_map[repository_name]
return f'{consts.PLASTIC_VCS_REMOTE_URI_PREFIX}{repository_guid}'


def exclude_irrelevant_detections(
detections: List[Detection], scan_type: str, command_scan_type: str, severity_threshold: str
) -> List[Detection]:
Expand Down
4 changes: 4 additions & 0 deletions cycode/cli/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,7 @@
SCA_SKIP_RESTORE_DEPENDENCIES_FLAG = 'no-restore'

SCA_GRADLE_ALL_SUB_PROJECTS_FLAG = 'gradle-all-sub-projects'

PLASTIC_VCS_DATA_SEPARATOR = ':::'
PLASTIC_VSC_CLI_TIMEOUT = 10
PLASTIC_VCS_REMOTE_URI_PREFIX = 'plastic::'
Loading