@@ -133,25 +133,40 @@ def create_sbom_output(self, diff: Diff) -> dict:
133133    @staticmethod  
134134    def  expand_brace_pattern (pattern : str ) ->  List [str ]:
135135        """ 
136-         Expands brace expressions (e.g., {a,b,c}) into separate patterns. 
137-         """ 
138-         brace_regex  =  re .compile (r"\{([^{}]+)\}" )
139- 
140-         # Expand all brace groups 
141-         expanded_patterns  =  [pattern ]
142-         while  any ("{"  in  p  for  p  in  expanded_patterns ):
143-             new_patterns  =  []
144-             for  pat  in  expanded_patterns :
145-                 match  =  brace_regex .search (pat )
146-                 if  match :
147-                     options  =  match .group (1 ).split ("," )  # Extract values inside {} 
148-                     prefix , suffix  =  pat [:match .start ()], pat [match .end ():]
149-                     new_patterns .extend ([prefix  +  opt  +  suffix  for  opt  in  options ])
150-                 else :
151-                     new_patterns .append (pat )
152-             expanded_patterns  =  new_patterns 
153- 
154-         return  expanded_patterns 
136+         Recursively expands brace expressions (e.g., {a,b,c}) into separate patterns, supporting nested braces. 
137+         """ 
138+         def  recursive_expand (pat : str ) ->  List [str ]:
139+             stack  =  []
140+             for  i , c  in  enumerate (pat ):
141+                 if  c  ==  '{' :
142+                     stack .append (i )
143+                 elif  c  ==  '}'  and  stack :
144+                     start  =  stack .pop ()
145+                     if  not  stack :
146+                         # Found the outermost pair 
147+                         before  =  pat [:start ]
148+                         after  =  pat [i + 1 :]
149+                         inner  =  pat [start + 1 :i ]
150+                         # Split on commas not inside nested braces 
151+                         options  =  []
152+                         depth  =  0 
153+                         last  =  0 
154+                         for  j , ch  in  enumerate (inner ):
155+                             if  ch  ==  '{' :
156+                                 depth  +=  1 
157+                             elif  ch  ==  '}' :
158+                                 depth  -=  1 
159+                             elif  ch  ==  ','  and  depth  ==  0 :
160+                                 options .append (inner [last :j ])
161+                                 last  =  j + 1 
162+                         options .append (inner [last :])
163+                         results  =  []
164+                         for  opt  in  options :
165+                             expanded  =  before  +  opt  +  after 
166+                             results .extend (recursive_expand (expanded ))
167+                         return  results 
168+             return  [pat ]
169+         return  recursive_expand (pattern )
155170
156171    @staticmethod  
157172    def  is_excluded (file_path : str , excluded_dirs : Set [str ]) ->  bool :
@@ -176,13 +191,7 @@ def find_files(self, path: str) -> List[str]:
176191        files : Set [str ] =  set ()
177192
178193        # Get supported patterns from the API 
179-         try :
180-             patterns  =  self .get_supported_patterns ()
181-         except  Exception  as  e :
182-             log .error (f"Error getting supported patterns from API: { e }  )
183-             log .warning ("Falling back to local patterns" )
184-             from  .utils  import  socket_globs  as  fallback_patterns 
185-             patterns  =  fallback_patterns 
194+         patterns  =  self .get_supported_patterns ()
186195
187196        for  ecosystem  in  patterns :
188197            if  ecosystem  in  self .config .excluded_ecosystems :
@@ -425,7 +434,7 @@ def create_packages_dict(self, sbom_artifacts: list[SocketArtifact]) -> dict[str
425434        packages  =  {}
426435        top_level_count  =  {}
427436        for  artifact  in  sbom_artifacts :
428-             package  =  Package .from_diff_artifact ( artifact . __dict__ )
437+             package  =  Package .from_socket_artifact ( asdict ( artifact ) )
429438            if  package .id  in  packages :
430439                print ("Duplicate package?" )
431440            else :
@@ -534,44 +543,22 @@ def update_package_values(pkg: Package) -> Package:
534543        pkg .url  +=  f"/{ pkg .name } { pkg .version }  
535544        return  pkg 
536545
537-     def  get_added_and_removed_packages (
538-             self ,
539-             head_full_scan_id : str ,
540-             new_full_scan_id : str ,
541-             merge : bool  =  False ,
542-             external_href : str  =  None ,
543-     ) ->  Tuple [Dict [str , Package ], Dict [str , Package ], str , str ]:
546+     def  get_added_and_removed_packages (self , head_full_scan_id : str , new_full_scan_id : str ) ->  Tuple [Dict [str , Package ], Dict [str , Package ]]:
544547        """ 
545548        Get packages that were added and removed between scans. 
546549
547550        Args: 
548-             head_full_scan_id: Previous scan 
549-             new_full_scan_id: New scan just created 
550-             merge: Whether the scan is merged into the default branch 
551-             external_href: External reference 
551+             head_full_scan: Previous scan (may be None if first scan) 
552+             head_full_scan_id: New scan just created 
553+ 
552554        Returns: 
553555            Tuple of (added_packages, removed_packages) dictionaries 
554556        """ 
555557
556558        log .info (f"Comparing scans - Head scan ID: { head_full_scan_id } { new_full_scan_id }  )
557559        diff_start  =  time .time ()
558560        try :
559-             params  =  {
560-                 "before" : head_full_scan_id ,
561-                 "after" : new_full_scan_id ,
562-                 "description" : f"Diff scan between head { head_full_scan_id } { new_full_scan_id }  ,
563-                 "merge" : merge ,
564-             }
565-             if  external_href :
566-                 params ["external_href" ] =  external_href 
567-             new_diff_scan  =  self .sdk .diffscans .create_from_ids (self .config .org_slug , params )
568-             data  =  new_diff_scan .get ("diff_scan" , {})
569-             diff_scan_id  =  data .get ("id" )
570-             if  not  diff_scan_id :
571-                 log .error (f"Failed to get diff scan ID for { new_full_scan_id }  )
572-                 log .error (new_diff_scan )
573-                 sys .exit (1 )
574-             diff_report  =  self .sdk .diffscans .get (self .config .org_slug , diff_scan_id )
561+             diff_report  =  self .sdk .fullscans .stream_diff (self .config .org_slug , head_full_scan_id , new_full_scan_id , use_types = True ).data 
575562        except  APIFailure  as  e :
576563            log .error (f"API Error: { e }  )
577564            sys .exit (1 )
@@ -581,63 +568,44 @@ def get_added_and_removed_packages(
581568            log .error (f"Stack trace:\n { traceback .format_exc ()}  )
582569            raise 
583570
584-         diff_data  =  diff_report .get ("diff_scan" , {})
585571        diff_end  =  time .time ()
586-         diff_url  =  diff_data .get ("html_url" )
587-         after_data  =  diff_data .get ("after_full_scan" )
588-         if  after_data :
589-             new_full_scan_url  =  after_data .get ("html_url" )
590-         else :
591-             new_full_scan_url  =  "" 
592-         artifacts  =  diff_data .get ("artifacts" , {})
593-         added  =  artifacts .get ("added" , [])
594-         removed  =  artifacts .get ("removed" , [])
595-         unchanged  =  artifacts .get ("unchanged" , [])
596-         replaced  =  artifacts .get ("replaced" , [])
597-         updated  =  artifacts .get ("updated" , [])
598572        log .info (f"Diff Report Gathered in { diff_end  -  diff_start :.2f}  )
599573        log .info ("Diff report artifact counts:" )
600-         log .info (f"Added: { len (added )}  )
601-         log .info (f"Removed: { len (removed )}  )
602-         log .info (f"Unchanged: { len (unchanged )}  )
603-         log .info (f"Replaced: { len (replaced )}  )
604-         log .info (f"Updated: { len (updated )}  )
574+         log .info (f"Added: { len (diff_report . artifacts . added )}  )
575+         log .info (f"Removed: { len (diff_report . artifacts . removed )}  )
576+         log .info (f"Unchanged: { len (diff_report . artifacts . unchanged )}  )
577+         log .info (f"Replaced: { len (diff_report . artifacts . replaced )}  )
578+         log .info (f"Updated: { len (diff_report . artifacts . updated )}  )
605579
606-         added_artifacts  =  added  +  updated 
607-         removed_artifacts  =  removed 
580+         added_artifacts  =  diff_report . artifacts . added  +  diff_report . artifacts . updated 
581+         removed_artifacts  =  diff_report . artifacts . removed   +   diff_report . artifacts . replaced 
608582
609583        added_packages : Dict [str , Package ] =  {}
610584        removed_packages : Dict [str , Package ] =  {}
611585
612586        for  artifact  in  added_artifacts :
613-             artifact_id  =  artifact .get ("id" )
614-             artifact_name  =  artifact .get ("name" )
615-             artifact_version  =  artifact .get ("version" )
616587            try :
617-                 pkg  =  Package .from_diff_artifact (artifact )
588+                 pkg  =  Package .from_diff_artifact (asdict ( artifact ) )
618589                pkg  =  Core .update_package_values (pkg )
619-                 added_packages [pkg .id ] =  pkg 
590+                 added_packages [artifact .id ] =  pkg 
620591            except  KeyError :
621-                 log .error (f"KeyError: Could not create package from added artifact { artifact_id }  )
622-                 log .error (f"Artifact details - name: { artifact_name } { artifact_version }  )
592+                 log .error (f"KeyError: Could not create package from added artifact { artifact . id }  )
593+                 log .error (f"Artifact details - name: { artifact . name } { artifact . version }  )
623594                log .error ("No matching packages found in new_full_scan" )
624595
625596        for  artifact  in  removed_artifacts :
626-             artifact_id  =  artifact .get ("id" )
627-             artifact_name  =  artifact .get ("name" )
628-             artifact_version  =  artifact .get ("version" )
629597            try :
630-                 pkg  =  Package .from_diff_artifact (artifact )
598+                 pkg  =  Package .from_diff_artifact (asdict ( artifact ) )
631599                pkg  =  Core .update_package_values (pkg )
632600                if  pkg .namespace :
633601                    pkg .purl  +=  f"{ pkg .namespace } { pkg .purl }  
634-                 removed_packages [pkg .id ] =  pkg 
602+                 removed_packages [artifact .id ] =  pkg 
635603            except  KeyError :
636-                 log .error (f"KeyError: Could not create package from removed artifact { artifact_id }  )
637-                 log .error (f"Artifact details - name: { artifact_name } { artifact_version }  )
604+                 log .error (f"KeyError: Could not create package from removed artifact { artifact . id }  )
605+                 log .error (f"Artifact details - name: { artifact . name } { artifact . version }  )
638606                log .error ("No matching packages found in head_full_scan" )
639607
640-         return  added_packages , removed_packages ,  diff_url ,  new_full_scan_url 
608+         return  added_packages , removed_packages 
641609
642610    def  create_new_diff (
643611            self ,
@@ -683,6 +651,7 @@ def create_new_diff(
683651        try :
684652            new_scan_start  =  time .time ()
685653            new_full_scan  =  self .create_full_scan (files_for_sending , params )
654+             new_full_scan .sbom_artifacts  =  self .get_sbom_data (new_full_scan .id )
686655            new_scan_end  =  time .time ()
687656            log .info (f"Total time to create new full scan: { new_scan_end  -  new_scan_start :.2f}  )
688657        except  APIFailure  as  e :
@@ -694,15 +663,26 @@ def create_new_diff(
694663            log .error (f"Stack trace:\n { traceback .format_exc ()}  )
695664            raise 
696665
697-         added_packages ,  removed_packages ,  diff_url ,  report_url   =  self .get_added_and_removed_packages ( 
698-              head_full_scan_id , 
699-             new_full_scan . id 
700-         )
666+         scans_ready   =  self .check_full_scans_status ( head_full_scan_id ,  new_full_scan . id ) 
667+         if   scans_ready   is   False : 
668+             log . error ( f"Full scans did not complete within  { self . config . timeout }  seconds" ) 
669+         added_packages ,  removed_packages   =   self . get_added_and_removed_packages ( head_full_scan_id ,  new_full_scan . id )
701670
702671        diff  =  self .create_diff_report (added_packages , removed_packages )
672+ 
673+         base_socket  =  "https://socket.dev/dashboard/org" 
703674        diff .id  =  new_full_scan .id 
675+ 
676+         report_url  =  f"{ base_socket } { self .config .org_slug } { diff .id }  
677+         if  not  params .include_license_details :
678+             report_url  +=  "?include_license_details=false" 
704679        diff .report_url  =  report_url 
705-         diff .diff_url  =  diff_url 
680+ 
681+         if  head_full_scan_id  is  not None :
682+             diff .diff_url  =  f"{ base_socket } { self .config .org_slug } { head_full_scan_id } { diff .id }  
683+         else :
684+             diff .diff_url  =  diff .report_url 
685+ 
706686        return  diff 
707687
708688    def  create_diff_report (
0 commit comments