4040
4141log = logging .getLogger (__name__ )
4242
43+ COSIGN_TAG_SUFFIXES = (".sig" , ".att" , ".sbom" )
44+
4345
4446class ContainerFirstStage (Stage ):
4547 """
@@ -60,6 +62,8 @@ def __init__(self, remote, signed_only):
6062 self .manifest_list_dcs = []
6163 self .manifest_dcs = []
6264 self .signature_dcs = []
65+ self ._synced_digests = set ()
66+ self ._full_tag_list = []
6367
6468 async def _download_manifest_data (self , manifest_url ):
6569 downloader = self .remote .get_downloader (url = manifest_url )
@@ -92,24 +96,57 @@ async def run(self):
9296 """
9397 ContainerFirstStage.
9498 """
95-
96- to_download = []
97- BATCH_SIZE = 500
98-
99- # it can be whether a separate sigstore location or registry with extended signatures API
10099 signature_source = await self .get_signature_source ()
101100
102101 async with ProgressReport (
103102 message = "Downloading tag list" , code = "sync.downloading.tag_list" , total = 1
104103 ) as pb :
105104 repo_name = self .remote .namespaced_upstream_name
106105 tag_list_url = "/v2/{name}/tags/list" .format (name = repo_name )
107- tag_list = await self .get_paginated_tag_list (tag_list_url , repo_name )
106+ self . _full_tag_list = await self .get_paginated_tag_list (tag_list_url , repo_name )
108107 tag_list = filter_resources (
109- tag_list , self .remote .include_tags , self .remote .exclude_tags
108+ self . _full_tag_list , self .remote .include_tags , self .remote .exclude_tags
110109 )
111110 await pb .aincrement ()
112111
112+ await self ._process_tags (tag_list , signature_source )
113+
114+ if self .remote .include_tags or self .remote .exclude_tags :
115+ companion_tags = self ._find_cosign_companion_tags (tag_list )
116+ if companion_tags :
117+ log .info (
118+ "Syncing %d cosign companion tag(s) for filtered images" ,
119+ len (companion_tags ),
120+ )
121+ await self ._process_tags (
122+ companion_tags , signature_source , msg = "Processing Cosign Companion Tags"
123+ )
124+
125+ await self .resolve_flush ()
126+
127+ def _find_cosign_companion_tags (self , filtered_tag_list ):
128+ """Find cosign companion tags for synced digests that were excluded by tag filtering."""
129+ rest_of_tags = filter_resources (self ._full_tag_list , [], filtered_tag_list )
130+ rest_of_tags = filter_resources (rest_of_tags , [], self .remote .exclude_tags )
131+ companion_tags = []
132+ for tag in rest_of_tags :
133+ if not tag .startswith ("sha256-" ):
134+ continue
135+ if not any (tag .endswith (suffix ) for suffix in COSIGN_TAG_SUFFIXES ):
136+ continue
137+ # Derive the image digest from the cosign tag name:
138+ # sha256-<hex>.<suffix> -> sha256:<hex>
139+ tag_without_suffix = tag .rsplit ("." , 1 )[0 ]
140+ digest = tag_without_suffix .replace ("-" , ":" , 1 )
141+ if digest in self ._synced_digests :
142+ companion_tags .append (tag )
143+ return companion_tags
144+
145+ async def _process_tags (self , tag_list , signature_source , msg = "Processing Tags" ):
146+ """Download and process a batch of tags, creating declarative content objects."""
147+ BATCH_SIZE = 500
148+ to_download = []
149+
113150 for tag_name in tag_list :
114151 relative_url = "/v2/{name}/manifests/{tag}" .format (
115152 name = self .remote .namespaced_upstream_name , tag = tag_name
@@ -121,7 +158,7 @@ async def run(self):
121158 )
122159
123160 async with ProgressReport (
124- message = "Processing Tags" ,
161+ message = msg ,
125162 code = "sync.processing.tag" ,
126163 total = len (tag_list ),
127164 ) as pb_parsed_tags :
@@ -134,22 +171,23 @@ async def run(self):
134171 content_data , raw_text_data , response = await artifact
135172
136173 digest = calculate_digest (raw_text_data )
174+ self ._synced_digests .add (digest )
137175 tag_name = response .url .split ("/" )[- 1 ]
138176
139- # Look for cosign signatures
140- # cosign signature has a tag convention 'sha256-1234.sig'
141177 if self .signed_only and not signature_source :
178+ is_cosign_companion = tag_name .startswith ("sha256-" ) and any (
179+ tag_name .endswith (s ) for s in COSIGN_TAG_SUFFIXES
180+ )
142181 if (
143- not (tag_name .endswith (".sig" ) and tag_name .startswith ("sha256-" ))
144- and f"sha256-{ digest .removeprefix ('sha256:' )} .sig" not in tag_list
182+ not is_cosign_companion
183+ and f"sha256-{ digest .removeprefix ('sha256:' )} .sig"
184+ not in self ._full_tag_list
145185 ):
146- # skip this tag, there is no corresponding signature
147186 log .info (
148187 "The unsigned image {digest} can't be synced "
149188 "due to a requirement to sync signed content "
150189 "only." .format (digest = digest )
151190 )
152- # Count the skipped tagks as parsed too.
153191 await pb_parsed_tags .aincrement ()
154192 continue
155193
@@ -170,6 +208,7 @@ async def run(self):
170208 ):
171209 listed_manifest = await listed_manifest_task
172210 man_dc = listed_manifest ["manifest_dc" ]
211+ self ._synced_digests .add (man_dc .content .digest )
173212 if signature_source is not None :
174213 man_sig_dcs = await self .create_signatures (man_dc , signature_source )
175214 if self .signed_only and not man_sig_dcs :
@@ -183,8 +222,6 @@ async def run(self):
183222 tag = tag_name ,
184223 )
185224 )
186- # do not pass down the pipeline a manifest list with unsigned
187- # manifests.
188225 break
189226 self .signature_dcs .extend (man_sig_dcs )
190227 list_dc .extra_data ["listed_manifests" ].append (listed_manifest )
@@ -196,8 +233,6 @@ async def run(self):
196233 list_sig_dcs = await self .create_signatures (list_dc , signature_source )
197234 if list_sig_dcs :
198235 self .signature_dcs .extend (list_sig_dcs )
199- # only pass the manifest list and tag down the pipeline if there were no
200- # issues with signatures (no `break` in the `for` loop)
201236 tag_dc .extra_data ["tagged_manifest_dc" ] = list_dc
202237 for listed_manifest in list_dc .extra_data ["listed_manifests" ]:
203238 await self .handle_blobs (
@@ -215,7 +250,6 @@ async def run(self):
215250 if signature_source is not None :
216251 man_sig_dcs = await self .create_signatures (man_dc , signature_source )
217252 if self .signed_only and not man_sig_dcs :
218- # do not pass down the pipeline unsigned manifests
219253 continue
220254 self .signature_dcs .extend (man_sig_dcs )
221255 tag_dc .extra_data ["tagged_manifest_dc" ] = man_dc
@@ -237,8 +271,6 @@ async def run(self):
237271 ):
238272 await self .resolve_flush ()
239273
240- await self .resolve_flush ()
241-
242274 async def get_signature_source (self ):
243275 """
244276 Find out where signatures come from: sigstore, extension API or not available at all.
0 commit comments