Skip to content

Commit a1c7a4f

Browse files
authored
CM-47505 - Add support for Unity Version Control (formerly Plastic SCM) (#298)
1 parent 2901f82 commit a1c7a4f

File tree

2 files changed

+95
-0
lines changed

2 files changed

+95
-0
lines changed

cycode/cli/commands/scan/code_scanner.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from cycode.cli.utils.progress_bar import ScanProgressBarSection
3333
from cycode.cli.utils.scan_batch import run_parallel_batched_scan
3434
from cycode.cli.utils.scan_utils import set_issue_detected
35+
from cycode.cli.utils.shell_executor import shell
3536
from cycode.cyclient import logger
3637
from cycode.cyclient.config import set_logging_level
3738
from cycode.cyclient.models import Detection, DetectionSchema, DetectionsPerFile, ZippedFileScanResult
@@ -666,6 +667,9 @@ def get_scan_parameters(context: click.Context, paths: Optional[Tuple[str]] = No
666667
return scan_parameters
667668

668669
remote_url = try_get_git_remote_url(paths[0])
670+
if not remote_url:
671+
remote_url = try_to_get_plastic_remote_url(paths[0])
672+
669673
if remote_url:
670674
# TODO(MarshalX): remove hardcode in context
671675
context.obj['remote_url'] = remote_url
@@ -684,6 +688,93 @@ def try_get_git_remote_url(path: str) -> Optional[str]:
684688
return None
685689

686690

691+
def _get_plastic_repository_name(path: str) -> Optional[str]:
692+
"""Gets the name of the Plastic repository from the current working directory.
693+
694+
The command to execute is:
695+
cm status --header --machinereadable --fieldseparator=":::"
696+
697+
Example of status header in machine-readable format:
698+
STATUS:::0:::Project/RepoName:::OrgName@ServerInfo
699+
"""
700+
701+
try:
702+
command = [
703+
'cm',
704+
'status',
705+
'--header',
706+
'--machinereadable',
707+
f'--fieldseparator={consts.PLASTIC_VCS_DATA_SEPARATOR}',
708+
]
709+
710+
status = shell(command=command, timeout=consts.PLASTIC_VSC_CLI_TIMEOUT, working_directory=path)
711+
if not status:
712+
logger.debug('Failed to get Plastic repository name (command failed)')
713+
return None
714+
715+
status_parts = status.split(consts.PLASTIC_VCS_DATA_SEPARATOR)
716+
if len(status_parts) < 2:
717+
logger.debug('Failed to parse Plastic repository name (command returned unexpected format)')
718+
return None
719+
720+
return status_parts[2].strip()
721+
except Exception as e:
722+
logger.debug('Failed to get Plastic repository name', exc_info=e)
723+
return None
724+
725+
726+
def _get_plastic_repository_list(working_dir: Optional[str] = None) -> Dict[str, str]:
727+
"""Gets the list of Plastic repositories and their GUIDs.
728+
729+
The command to execute is:
730+
cm repo list --format="{repname}:::{repguid}"
731+
732+
Example line with data:
733+
Project/RepoName:::tapo1zqt-wn99-4752-h61m-7d9k79d40r4v
734+
735+
Each line represents an individual repository.
736+
"""
737+
738+
repo_name_to_guid = {}
739+
740+
try:
741+
command = ['cm', 'repo', 'ls', f'--format={{repname}}{consts.PLASTIC_VCS_DATA_SEPARATOR}{{repguid}}']
742+
743+
status = shell(command=command, timeout=consts.PLASTIC_VSC_CLI_TIMEOUT, working_directory=working_dir)
744+
if not status:
745+
logger.debug('Failed to get Plastic repository list (command failed)')
746+
return repo_name_to_guid
747+
748+
status_lines = status.splitlines()
749+
for line in status_lines:
750+
data_parts = line.split(consts.PLASTIC_VCS_DATA_SEPARATOR)
751+
if len(data_parts) < 2:
752+
logger.debug('Failed to parse Plastic repository list line (unexpected format), %s', {'line': line})
753+
continue
754+
755+
repo_name, repo_guid = data_parts
756+
repo_name_to_guid[repo_name.strip()] = repo_guid.strip()
757+
758+
return repo_name_to_guid
759+
except Exception as e:
760+
logger.debug('Failed to get Plastic repository list', exc_info=e)
761+
return repo_name_to_guid
762+
763+
764+
def try_to_get_plastic_remote_url(path: str) -> Optional[str]:
765+
repository_name = _get_plastic_repository_name(path)
766+
if not repository_name:
767+
return None
768+
769+
repository_map = _get_plastic_repository_list(path)
770+
if repository_name not in repository_map:
771+
logger.debug('Failed to get Plastic repository GUID (repository not found in the list)')
772+
return None
773+
774+
repository_guid = repository_map[repository_name]
775+
return f'{consts.PLASTIC_VCS_REMOTE_URI_PREFIX}{repository_guid}'
776+
777+
687778
def exclude_irrelevant_detections(
688779
detections: List[Detection], scan_type: str, command_scan_type: str, severity_threshold: str
689780
) -> List[Detection]:

cycode/cli/consts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,7 @@
230230
SCA_SKIP_RESTORE_DEPENDENCIES_FLAG = 'no-restore'
231231

232232
SCA_GRADLE_ALL_SUB_PROJECTS_FLAG = 'gradle-all-sub-projects'
233+
234+
PLASTIC_VCS_DATA_SEPARATOR = ':::'
235+
PLASTIC_VSC_CLI_TIMEOUT = 10
236+
PLASTIC_VCS_REMOTE_URI_PREFIX = 'plastic::'

0 commit comments

Comments
 (0)