Skip to content

Commit f817195

Browse files
authored
Merge pull request #7 from stackabletech/feat/command_for_upload_sbom
feat: command to upload SBOMs for a release
2 parents 0ccc2bb + d5c4c69 commit f817195

2 files changed

Lines changed: 76 additions & 23 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Scan specific release
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
release:
6+
description: 'Release version to scan'
7+
required: true
8+
default: '0.0.0-dev'
9+
10+
jobs:
11+
scan_release:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
15+
- name: Init submodules
16+
run: git submodule update --init --recursive
17+
- name: Set up Cosign
18+
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
19+
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
20+
with:
21+
python-version: 3.11
22+
- name: Setup Rust
23+
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
24+
with:
25+
toolchain: stable
26+
- name: Run image
27+
uses: abatilo/actions-poetry@3765cf608f2d4a72178a9fc5b918668e542b89b1 # v4.0.0
28+
with:
29+
poetry-version: 1.7.1
30+
- name: Install CycloneDX CLI
31+
run: |
32+
curl -sSfL https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.30.0/cyclonedx-linux-x64 -o /usr/local/bin/cyclonedx
33+
chmod +x /usr/local/bin/cyclonedx
34+
- name: Install deps
35+
run: poetry install
36+
- name: Upload SBOMs for release
37+
id: scan
38+
run: poetry run python stack_scanner/main.py upload-sbom-release ${{ secrets.SECOBSERVE_API_TOKEN }} ${{ github.event.inputs.release }}

stack_scanner/main.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import urllib.parse
1111
import urllib.request
1212

13+
from typing import Optional
14+
15+
1316
excluded_products = [
1417
"hello-world",
1518
"java-base",
@@ -264,7 +267,7 @@ def _build_base_env(secobserve_api_token: str, product_name: str, branch_name: s
264267
_GRYPE_REPORT = "grype.json"
265268

266269

267-
def _combined_scan_script(env: dict, mode: str) -> str:
270+
def _combined_scan_script(env: dict, mode: str, upload_sbom: Optional[bool] = False) -> str:
268271
"""Return a shell script that scans with Trivy and Grype, then uploads results.
269272
270273
Trivy and Grype scan the same target independently, so they are launched
@@ -279,29 +282,33 @@ def _combined_scan_script(env: dict, mode: str) -> str:
279282
280283
The existing per-scanner entrypoints are reused (with ``SO_UPLOAD=false`` so
281284
they only scan) to avoid duplicating the scanner invocation flags here.
282-
283-
In ``sbom`` mode the target itself is a CycloneDX SBOM, so it is additionally
284-
uploaded to SecObserve. In ``image`` mode there is no SBOM file to upload.
285285
"""
286-
script = (
287-
"export TRIVY_NO_PROGRESS=true\n"
288-
f"SO_UPLOAD=false REPORT_NAME={_TRIVY_REPORT} /entrypoints/entrypoint_trivy_{mode}.sh &\n"
289-
"trivy_pid=$!\n"
290-
f"SO_UPLOAD=false REPORT_NAME={_GRYPE_REPORT} /entrypoints/entrypoint_grype_{mode}.sh &\n"
291-
"grype_pid=$!\n"
292-
'wait "$trivy_pid" || echo "WARNING: Trivy scan failed"\n'
293-
'wait "$grype_pid" || echo "WARNING: Grype scan failed"\n'
294-
f"if [ -f {_TRIVY_REPORT} ]; then SO_FILE_NAME={_TRIVY_REPORT} SO_PARSER_NAME=CycloneDX "
295-
'file_upload_observations.sh; else echo "WARNING: no Trivy report to upload"; fi\n'
296-
f"if [ -f {_GRYPE_REPORT} ]; then SO_FILE_NAME={_GRYPE_REPORT} SO_PARSER_NAME=CycloneDX "
297-
'file_upload_observations.sh; else echo "WARNING: no Grype report to upload"; fi\n'
298-
)
299-
if mode == "sbom":
300-
script += f"SO_FILE_NAME={env.get('TARGET')} file_upload_sbom.sh\n"
286+
287+
if upload_sbom:
288+
if mode == "sbom":
289+
script = f"SO_FILE_NAME={env.get('TARGET')} file_upload_sbom.sh\n"
290+
else:
291+
print(f"Can't upload SBOM because mode is {mode}")
292+
script = ""
293+
else:
294+
script = (
295+
"export TRIVY_NO_PROGRESS=true\n"
296+
f"SO_UPLOAD=false REPORT_NAME={_TRIVY_REPORT} /entrypoints/entrypoint_trivy_{mode}.sh &\n"
297+
"trivy_pid=$!\n"
298+
f"SO_UPLOAD=false REPORT_NAME={_GRYPE_REPORT} /entrypoints/entrypoint_grype_{mode}.sh &\n"
299+
"grype_pid=$!\n"
300+
'wait "$trivy_pid" || echo "WARNING: Trivy scan failed"\n'
301+
'wait "$grype_pid" || echo "WARNING: Grype scan failed"\n'
302+
f"if [ -f {_TRIVY_REPORT} ]; then SO_FILE_NAME={_TRIVY_REPORT} SO_PARSER_NAME=CycloneDX "
303+
'file_upload_observations.sh; else echo "WARNING: no Trivy report to upload"; fi\n'
304+
f"if [ -f {_GRYPE_REPORT} ]; then SO_FILE_NAME={_GRYPE_REPORT} SO_PARSER_NAME=CycloneDX "
305+
'file_upload_observations.sh; else echo "WARNING: no Grype report to upload"; fi\n'
306+
)
307+
301308
return script
302309

303310

304-
def _run_combined_scan(env: dict, mode: str) -> None:
311+
def _run_combined_scan(env: dict, mode: str, upload_sbom: Optional[bool] = False) -> None:
305312
"""Run Trivy and Grype in a single container for one target, then upload."""
306313
cmd = [
307314
"docker", "run",
@@ -312,7 +319,7 @@ def _run_combined_scan(env: dict, mode: str) -> None:
312319
for key, value in env.items():
313320
cmd.extend(["-e", f"{key}={value}"])
314321
cmd.append(SECOBSERVE_SCANNER_IMAGE)
315-
cmd.extend(["-c", _combined_scan_script(env, mode)])
322+
cmd.extend(["-c", _combined_scan_script(env, mode, upload_sbom)])
316323

317324
print(f"docker run (combined trivy+grype, {mode} mode) TARGET={env.get('TARGET')}")
318325
subprocess.run(cmd)
@@ -429,13 +436,17 @@ def main():
429436
for release in releases:
430437
scan_release(secobserve_api_token, release)
431438
sys.exit(0)
439+
elif sys.argv[1] == "upload-sbom-release":
440+
secobserve_api_token = sys.argv[2]
441+
release = sys.argv[3]
442+
scan_release(secobserve_api_token, release, upload_sbom=True)
432443
else:
433444
secobserve_api_token = sys.argv[2]
434445
release = sys.argv[3]
435446
scan_release(secobserve_api_token, release)
436447

437448

438-
def scan_release(secobserve_api_token: str, release: str) -> None:
449+
def scan_release(secobserve_api_token: str, release: str, upload_sbom: Optional[bool] = False) -> None:
439450
"""Scan all operator and product images of a single SDP release."""
440451
checkout = "main" if release == DEV_RELEASE else "tags/" + release
441452

@@ -500,6 +511,7 @@ def scan_release(secobserve_api_token: str, release: str) -> None:
500511
f"{REGISTRY_URL}/sdp/{product_name}:{release}-{arch}",
501512
product_name,
502513
f"{release}-{arch}",
514+
upload_sbom,
503515
)
504516

505517
if use_conf_py:
@@ -515,6 +527,7 @@ def scan_release(secobserve_api_token: str, release: str) -> None:
515527
f"{REGISTRY_URL}/sdp/{product_name}:{product_version}-{arch}",
516528
product_name,
517529
f"{product_version}-{arch}",
530+
upload_sbom,
518531
)
519532
else:
520533
for product_name, versions in product_versions.items():
@@ -527,6 +540,7 @@ def scan_release(secobserve_api_token: str, release: str) -> None:
527540
f"{REGISTRY_URL}/sdp/{product_name}:{product_version}-{arch}",
528541
product_name,
529542
f"{product_version}-{arch}",
543+
upload_sbom,
530544
)
531545

532546
# Scan additional infrastructure/third-party images using Harbor API tag discovery.
@@ -546,6 +560,7 @@ def scan_image(
546560
image: str,
547561
product_name: str,
548562
branch_name: str,
563+
upload_sbom: Optional[bool] = False,
549564
) -> None:
550565
mode = "sbom"
551566
extract_sbom_cmd = [
@@ -582,7 +597,7 @@ def scan_image(
582597

583598
env = _build_base_env(secobserve_api_token, product_name, branch_name)
584599
env["TARGET"] = image if mode == "image" else "/tmp/bom.json"
585-
_run_combined_scan(env, mode)
600+
_run_combined_scan(env, mode, upload_sbom)
586601

587602

588603
if __name__ == "__main__":

0 commit comments

Comments
 (0)