Skip to content

Commit 1ad4995

Browse files
authored
Added support for polling full scans endpoint (#84)
1 parent e250d14 commit 1ad4995

File tree

6 files changed

+83
-23710
lines changed

6 files changed

+83
-23710
lines changed

pyproject.toml

Lines changed: 2 additions & 6 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.0.56"
9+
version = "2.1.0"
1010
requires-python = ">= 3.10"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [
@@ -16,7 +16,7 @@ dependencies = [
1616
'GitPython',
1717
'packaging',
1818
'python-dotenv',
19-
'socket-sdk-python>=2.0.21'
19+
'socket-sdk-python>=2.1.2,<3'
2020
]
2121
readme = "README.md"
2222
description = "Socket Security CLI for CI/CD"
@@ -63,10 +63,6 @@ include = [
6363
"socketsecurity/**/*.py",
6464
"socketsecurity/**/__init__.py"
6565
]
66-
omit = [
67-
"socketsecurity/core/issues.py", # Large data file
68-
"socketsecurity/core/licenses.py" # Large data file
69-
]
7066

7167
[tool.coverage.report]
7268
exclude_lines = [

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.0.56'
2+
__version__ = '2.1.0'

socketsecurity/core/__init__.py

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
import os
33
import sys
44
import time
5+
import io
56
from dataclasses import asdict
67
from glob import glob
8+
from io import BytesIO
79
from pathlib import PurePath
8-
from typing import BinaryIO, Dict, List, Tuple, Set
10+
from typing import BinaryIO, Dict, List, Tuple, Set, Union
911
import re
1012
from socketdev import socketdev
1113
from socketdev.exceptions import APIFailure
@@ -24,7 +26,6 @@
2426
Purl
2527
)
2628
from socketsecurity.core.exceptions import APIResourceNotFound
27-
from socketsecurity.core.licenses import Licenses
2829
from .socket_config import SocketConfig
2930
from .utils import socket_globs
3031
import importlib
@@ -278,6 +279,14 @@ def to_case_insensitive_regex(input_string: str) -> str:
278279
"""
279280
return ''.join(f'[{char.lower()}{char.upper()}]' if char.isalpha() else char for char in input_string)
280281

282+
@staticmethod
283+
def empty_head_scan_file() -> list[tuple[str, tuple[str, Union[BinaryIO, BytesIO]]]]:
284+
# Create an empty file for when no head full scan so that the diff endpoint can always be used
285+
empty_file_obj = io.BytesIO(b"")
286+
empty_filename = "initial_head_scan"
287+
empty_full_scan_file = [(empty_filename, (empty_filename, empty_file_obj))]
288+
return empty_full_scan_file
289+
281290
@staticmethod
282291
def load_files_for_sending(files: List[str], workspace: str) -> List[Tuple[str, Tuple[str, BinaryIO]]]:
283292
"""
@@ -311,7 +320,7 @@ def load_files_for_sending(files: List[str], workspace: str) -> List[Tuple[str,
311320

312321
return send_files
313322

314-
def create_full_scan(self, files: List[str], params: FullScanParams, has_head_scan: bool = False) -> FullScan:
323+
def create_full_scan(self, files: list[tuple[str, tuple[str, BytesIO]]], params: FullScanParams) -> FullScan:
315324
"""
316325
Creates a new full scan via the Socket API.
317326
@@ -331,16 +340,60 @@ def create_full_scan(self, files: List[str], params: FullScanParams, has_head_sc
331340
raise Exception(f"Error creating full scan: {res.message}, status: {res.status}")
332341

333342
full_scan = FullScan(**asdict(res.data))
334-
if not has_head_scan:
335-
full_scan.sbom_artifacts = self.get_sbom_data(full_scan.id)
336-
full_scan.packages = self.create_packages_dict(full_scan.sbom_artifacts)
337-
338343
create_full_end = time.time()
339344
total_time = create_full_end - create_full_start
340345
log.debug(f"New Full Scan created in {total_time:.2f} seconds")
341346

342347
return full_scan
343348

349+
def check_full_scans_status(self, head_full_scan_id: str, new_full_scan_id: str) -> bool:
350+
is_ready = False
351+
current_timeout = self.config.timeout
352+
self.sdk.set_timeout(0.5)
353+
try:
354+
self.sdk.fullscans.stream(self.config.org_slug, head_full_scan_id)
355+
except Exception:
356+
log.debug(f"Queued up full scan for processing ({head_full_scan_id})")
357+
358+
try:
359+
self.sdk.fullscans.stream(self.config.org_slug, new_full_scan_id)
360+
except Exception:
361+
log.debug(f"Queued up full scan for processing ({new_full_scan_id})")
362+
self.sdk.set_timeout(current_timeout)
363+
start_check = time.time()
364+
head_is_ready = False
365+
new_is_ready = False
366+
while not is_ready:
367+
head_full_scan_metadata = self.sdk.fullscans.metadata(self.config.org_slug, head_full_scan_id)
368+
if head_full_scan_metadata:
369+
head_state = head_full_scan_metadata.get("scan_state")
370+
else:
371+
head_state = None
372+
new_full_scan_metadata = self.sdk.fullscans.metadata(self.config.org_slug, new_full_scan_id)
373+
if new_full_scan_metadata:
374+
new_state = new_full_scan_metadata.get("scan_state")
375+
else:
376+
new_state = None
377+
if head_state and head_state == "resolve":
378+
head_is_ready = True
379+
if new_state and new_state == "resolve":
380+
new_is_ready = True
381+
if head_is_ready and new_is_ready:
382+
is_ready = True
383+
current_time = time.time()
384+
if current_time - start_check >= self.config.timeout:
385+
log.debug(
386+
f"Timeout reached while waiting for full scans to be ready "
387+
f"({head_full_scan_id}, {new_full_scan_id})"
388+
)
389+
break
390+
total_time = time.time() - start_check
391+
if is_ready:
392+
log.info(f"Full scans are ready in {total_time:.2f} seconds")
393+
else:
394+
log.warning(f"Full scans are not ready yet ({head_full_scan_id}, {new_full_scan_id})")
395+
return is_ready
396+
344397
def get_full_scan(self, full_scan_id: str) -> FullScan:
345398
"""
346399
Get a FullScan object for an existing full scan including sbom_artifacts and packages.
@@ -403,14 +456,9 @@ def get_package_license_text(self, package: Package) -> str:
403456
return ""
404457

405458
license_raw = package.license
406-
all_licenses = Licenses()
407-
license_str = Licenses.make_python_safe(license_raw)
408-
409-
if license_str is not None and hasattr(all_licenses, license_str):
410-
license_obj = getattr(all_licenses, license_str)
411-
return license_obj.licenseText
412-
413-
return ""
459+
data = self.sdk.licensemetadata.post([license_raw], {'includetext': 'true'})
460+
license_str = data.data[0].license if data and len(data) == 1 else ""
461+
return license_str
414462

415463
def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-branch") -> RepositoryInfo:
416464
"""
@@ -485,7 +533,7 @@ def update_package_values(pkg: Package) -> Package:
485533
pkg.url += f"/{pkg.name}/overview/{pkg.version}"
486534
return pkg
487535

488-
def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan: FullScan) -> Tuple[Dict[str, Package], Dict[str, Package]]:
536+
def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan_id: str) -> Tuple[Dict[str, Package], Dict[str, Package]]:
489537
"""
490538
Get packages that were added and removed between scans.
491539
@@ -496,14 +544,11 @@ def get_added_and_removed_packages(self, head_full_scan_id: str, new_full_scan:
496544
Returns:
497545
Tuple of (added_packages, removed_packages) dictionaries
498546
"""
499-
if head_full_scan_id is None:
500-
log.info(f"No head scan found. New scan ID: {new_full_scan.id}")
501-
return new_full_scan.packages, {}
502547

503-
log.info(f"Comparing scans - Head scan ID: {head_full_scan_id}, New scan ID: {new_full_scan.id}")
548+
log.info(f"Comparing scans - Head scan ID: {head_full_scan_id}, New scan ID: {new_full_scan_id}")
504549
diff_start = time.time()
505550
try:
506-
diff_report = self.sdk.fullscans.stream_diff(self.config.org_slug, head_full_scan_id, new_full_scan.id, use_types=True).data
551+
diff_report = self.sdk.fullscans.stream_diff(self.config.org_slug, head_full_scan_id, new_full_scan_id, use_types=True).data
507552
except APIFailure as e:
508553
log.error(f"API Error: {e}")
509554
sys.exit(1)
@@ -572,22 +617,27 @@ def create_new_diff(
572617
# Find manifest files
573618
files = self.find_files(path)
574619
files_for_sending = self.load_files_for_sending(files, path)
575-
has_head_scan = False
576620
if not files:
577621
return Diff(id="no_diff_id")
578622

579623
try:
580624
# Get head scan ID
581625
head_full_scan_id = self.get_head_scan_for_repo(params.repo)
582-
if head_full_scan_id is not None:
583-
has_head_scan = True
584626
except APIResourceNotFound:
585627
head_full_scan_id = None
586628

629+
if head_full_scan_id is None:
630+
tmp_params = params
631+
tmp_params.tmp = True
632+
tmp_params.set_as_pending_head = False
633+
tmp_params.make_default_branch = False
634+
head_full_scan = self.create_full_scan(Core.empty_head_scan_file(), params)
635+
head_full_scan_id = head_full_scan.id
636+
587637
# Create new scan
588638
try:
589639
new_scan_start = time.time()
590-
new_full_scan = self.create_full_scan(files_for_sending, params, has_head_scan)
640+
new_full_scan = self.create_full_scan(files_for_sending, params)
591641
new_full_scan.sbom_artifacts = self.get_sbom_data(new_full_scan.id)
592642
new_scan_end = time.time()
593643
log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
@@ -600,7 +650,10 @@ def create_new_diff(
600650
log.error(f"Stack trace:\n{traceback.format_exc()}")
601651
raise
602652

603-
added_packages, removed_packages = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan)
653+
scans_ready = self.check_full_scans_status(head_full_scan_id, new_full_scan.id)
654+
if scans_ready is False:
655+
log.error(f"Full scans did not complete within {self.config.timeout} seconds")
656+
added_packages, removed_packages = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan.id)
604657

605658
diff = self.create_diff_report(added_packages, removed_packages)
606659

0 commit comments

Comments
 (0)