@@ -108,6 +108,118 @@ def get_harbor_recent_tags(project: str, repository: str) -> list[str] | None:
108108 return tags
109109
110110
111+ def get_latest_github_release (owner : str , repo : str ) -> str | None :
112+ """Fetch the tag name of the latest GitHub release for a repository."""
113+ url = f"https://api.github.com/repos/{ owner } /{ repo } /releases/latest"
114+ request = urllib .request .Request (url )
115+ request .add_header ("Accept" , "application/vnd.github+json" )
116+ request .add_header ("User-Agent" , "stack-scanner" )
117+
118+ try :
119+ with urllib .request .urlopen (request ) as response :
120+ data = json .loads (response .read ().decode ())
121+ return data ["tag_name" ]
122+ except (urllib .error .URLError , json .JSONDecodeError , KeyError ) as error :
123+ print (f"Failed to fetch latest { owner } /{ repo } release: { error } " )
124+ return None
125+
126+
127+ def scan_stackablectl (secobserve_api_token : str ) -> None :
128+ """Download and scan the latest stackablectl binary from GitHub releases.
129+
130+ Uses rootfs mode for both Trivy and Grype, which supports scanning standalone
131+ binaries for embedded dependency information. Once the project publishes a
132+ CycloneDX SBOM, this should be replaced with SBOM-based scanning.
133+ """
134+ version = get_latest_github_release ("stackabletech" , "stackable-cockpit" )
135+ if version is None :
136+ print ("WARNING: Could not determine latest stackablectl version, skipping." )
137+ return
138+
139+ print (f"Scanning stackablectl { version } " )
140+ binary_name = "stackablectl-x86_64-unknown-linux-gnu"
141+ download_url = (
142+ f"https://github.com/stackabletech/stackable-cockpit/releases/download"
143+ f"/{ version } /{ binary_name } "
144+ )
145+ binary_path = f"/tmp/stackable/{ binary_name } "
146+
147+ request = urllib .request .Request (download_url )
148+ request .add_header ("User-Agent" , "stack-scanner" )
149+ try :
150+ with urllib .request .urlopen (request ) as response :
151+ with open (binary_path , "wb" ) as f :
152+ f .write (response .read ())
153+ print (f"Downloaded stackablectl binary to { binary_path } " )
154+ except urllib .error .URLError as error :
155+ print (f"Failed to download stackablectl binary: { error } " )
156+ return
157+
158+ scan_binary (secobserve_api_token , binary_name , "stackablectl" , version )
159+
160+
161+ def scan_binary (
162+ secobserve_api_token : str ,
163+ file_name : str ,
164+ product_name : str ,
165+ branch_name : str ,
166+ ) -> None :
167+ """Scan a local binary file using Trivy and Grype in rootfs mode.
168+
169+ The file must reside under /tmp/stackable/ so it is accessible inside the
170+ scanner container (which mounts that directory to /tmp).
171+ """
172+ # Run Trivy
173+ env = {}
174+ env ["TARGET" ] = f"/tmp/{ file_name } "
175+ env ["SO_UPLOAD" ] = "true"
176+ env ["SO_PRODUCT_NAME" ] = product_name
177+ env ["SO_API_BASE_URL" ] = "https://secobserve-backend.stackable.tech"
178+ env ["SO_API_TOKEN" ] = secobserve_api_token
179+ env ["SO_BRANCH_NAME" ] = branch_name
180+ env ["TMPDIR" ] = "/tmp/trivy_tmp"
181+ env ["TRIVY_CACHE_DIR" ] = "/tmp/trivy_cache"
182+ env ["REPORT_NAME" ] = "trivy.json"
183+
184+ cmd = [
185+ "docker" ,
186+ "run" ,
187+ "--entrypoint" ,
188+ "/entrypoints/entrypoint_trivy_rootfs.sh" ,
189+ "-v" ,
190+ "/tmp/stackable:/tmp" ,
191+ "-v" ,
192+ "/var/run/docker.sock:/var/run/docker.sock" ,
193+ ]
194+ for key , value in env .items ():
195+ cmd .extend (["-e" , f"{ key } ={ value } " ])
196+ cmd .append ("oci.stackable.tech/sandbox/secobserve-scanners:latest" )
197+
198+ print (" " .join (cmd ))
199+ subprocess .run (cmd )
200+
201+ # Run Grype
202+ env ["FURTHER_PARAMETERS" ] = "--by-cve"
203+ env ["GRYPE_DB_CACHE_DIR" ] = "/tmp/grype_db_cache"
204+ env ["REPORT_NAME" ] = "grype.json"
205+
206+ cmd = [
207+ "docker" ,
208+ "run" ,
209+ "--entrypoint" ,
210+ "/entrypoints/entrypoint_grype_rootfs.sh" ,
211+ "-v" ,
212+ "/tmp/stackable:/tmp" ,
213+ "-v" ,
214+ "/var/run/docker.sock:/var/run/docker.sock" ,
215+ ]
216+ for key , value in env .items ():
217+ cmd .extend (["-e" , f"{ key } ={ value } " ])
218+ cmd .append ("oci.stackable.tech/sandbox/secobserve-scanners:latest" )
219+
220+ subprocess .run (cmd )
221+
222+
111223def scan_additional_images (secobserve_api_token : str ) -> None :
112224 """Scan additional images that are not part of the regular versioned Stackable release.
113225
@@ -260,6 +372,9 @@ def main():
260372 # already or are arch-agnostic manifests.
261373 scan_additional_images (secobserve_api_token )
262374
375+ # Scan the latest stackablectl binary from GitHub releases.
376+ scan_stackablectl (secobserve_api_token )
377+
263378
264379def scan_image (
265380 secobserve_api_token : str ,
0 commit comments