Skip to content

Commit 0a4d161

Browse files
authored
Merge pull request #28 from lool/syft
workflows: debos: Generate SBOM of rootfs with syft
2 parents ad5dc22 + e85aa02 commit 0a4d161

File tree

3 files changed

+172
-12
lines changed

3 files changed

+172
-12
lines changed

.github/workflows/debos.yml

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ concurrency:
1717
group: ${{ github.workflow }}-${{ github.ref }}
1818
cancel-in-progress: true
1919

20+
env:
21+
# image build id; used for SBOM generation; TODO: should be used in image metadata too
22+
BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
23+
2024
jobs:
2125
build-debos:
26+
name: Build and upload debos recipes
2227
outputs:
2328
url: ${{ steps.upload_artifacts.outputs.url }}
2429
runs-on: [self-hosted, qcom-u2404, arm64]
@@ -86,23 +91,23 @@ jobs:
8691
debos -t u_boot_rb1:rb1-boot.img \
8792
debos-recipes/qualcomm-linux-debian-flash.yaml
8893
89-
- name: Stage build artifacts for publishing
94+
- name: Stage debos artifacts for publishing
9095
run: |
9196
set -ux
9297
# create a directory for the current run
93-
BUILD_DIR="./uploads"
94-
mkdir -vp "${BUILD_DIR}"
98+
dir="debos-artifacts"
99+
mkdir -v "${dir}"
95100
# copy output files
96-
cp -av rootfs.tar.gz "${BUILD_DIR}"
97-
cp -av dtbs.tar.gz "${BUILD_DIR}"
98-
cp -av disk-ufs.img.gz "${BUILD_DIR}"
99-
cp -av disk-sdcard.img.gz "${BUILD_DIR}"
101+
cp -av rootfs.tar.gz "${dir}"
102+
cp -av dtbs.tar.gz "${dir}"
103+
cp -av disk-ufs.img.gz "${dir}"
104+
cp -av disk-sdcard.img.gz "${dir}"
100105
# TODO: separate flash_* directories between UFS and eMMC
101-
tar -cvf "${BUILD_DIR}"/flash-ufs.tar.gz \
106+
tar -cvf "${dir}"/flash-ufs.tar.gz \
102107
disk-ufs.img1 \
103108
disk-ufs.img2 \
104109
flash_rb3*
105-
tar -cvf "${BUILD_DIR}"/flash-emmc.tar.gz \
110+
tar -cvf "${dir}"/flash-emmc.tar.gz \
106111
disk-sdcard.img1 \
107112
disk-sdcard.img2 \
108113
flash_rb1*
@@ -111,4 +116,54 @@ jobs:
111116
uses: qualcomm-linux/upload-private-artifact-action@v1
112117
id: upload_artifacts
113118
with:
114-
path: ./uploads
119+
path: debos-artifacts
120+
121+
- name: Unpack rootfs to generate SBOM
122+
run: mkdir -v rootfs && tar -C rootfs -xf rootfs.tar.gz
123+
124+
# Syft is not packaged in Debian; it's available as a binary tarball or
125+
# as container image from upstream; it's available on arm64 and x86
126+
- name: Install Syft
127+
run: |
128+
set -ux
129+
apt -y install curl
130+
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
131+
132+
- name: Generate SBOMs with Syft
133+
run: |
134+
set -ux
135+
bin/syft --version
136+
SYFT_FORMAT_PRETTY=true bin/syft \
137+
-o cyclonedx-json=rootfs-sbom.cyclonedx.json \
138+
-o spdx-json=rootfs-sbom.spdx.json \
139+
-o syft-json=rootfs-sbom.syft.json \
140+
-o syft-text=rootfs-sbom.syft.txt \
141+
-o syft-table \
142+
--parallelism `nproc` \
143+
--select-catalogers debian \
144+
--source-name qualcomm-linux-debian-rootfs \
145+
--source-version "${BUILD_ID}" \
146+
-v \
147+
scan rootfs
148+
149+
- name: Generate license summary from Syft report
150+
run: |
151+
set -ux
152+
scripts/syft-license-summary.py \
153+
--rootfs rootfs rootfs-sbom.syft.json |
154+
tee rootfs-sbom.syft-license-summary.csv.txt
155+
156+
- name: Stage SBOMs for publishing
157+
run: |
158+
set -ux
159+
gzip rootfs-sbom.*
160+
dir="sboms"
161+
mkdir -v sboms
162+
cp -av rootfs-sbom.*.gz sboms
163+
164+
- name: Upload SBOMs as private artifacts
165+
uses: qualcomm-linux/upload-private-artifact-action@v1
166+
id: upload_sbom_artifacts
167+
with:
168+
path: sboms
169+

.github/workflows/shellcheck.yml renamed to .github/workflows/static-checks.yml

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Static analysis of shell scripts with ShellCheck
1+
name: Static analysis of scripts
22

33
on:
44
# run on pull requests to the main branch
@@ -19,6 +19,34 @@ concurrency:
1919
cancel-in-progress: true
2020

2121
jobs:
22+
flake8:
23+
name: Install and run Flake8 on Python scripts
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Install flake8
27+
run: sudo apt update && sudo apt -y install flake8
28+
29+
- uses: actions/checkout@v4
30+
with:
31+
fetch-depth: 0
32+
33+
- name: Run Flake8
34+
run: flake8 scripts/*.py
35+
36+
pylint:
37+
name: Install and run Pylint on Python scripts
38+
runs-on: ubuntu-latest
39+
steps:
40+
- name: Install Pylint
41+
run: sudo apt update && sudo apt -y install pylint
42+
43+
- uses: actions/checkout@v4
44+
with:
45+
fetch-depth: 0
46+
47+
- name: Run Pylint (error mode)
48+
run: pylint --errors-only scripts/*.py
49+
2250
shellcheck:
2351
name: Install and run ShellCheck on shell scripts
2452
runs-on: ubuntu-latest
@@ -31,5 +59,5 @@ jobs:
3159
fetch-depth: 0
3260

3361
- name: Run ShellCheck
34-
run: shellcheck scripts/*
62+
run: shellcheck scripts/*.sh
3563

scripts/syft-license-summary.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
5+
# input is a Syft JSON file as the first argument; output is a
6+
# human-readable summary of source packages and their licenses in CSV
7+
# format
8+
9+
import json
10+
import hashlib
11+
import argparse
12+
import os
13+
from collections import defaultdict
14+
15+
16+
def load_syft_json(file_path):
17+
with open(file_path, 'r') as f:
18+
return json.load(f)
19+
20+
21+
def sha256_of_file(path):
22+
try:
23+
with open(path, 'rb') as f:
24+
return hashlib.sha256(f.read()).hexdigest()
25+
except Exception:
26+
return "unreadable"
27+
28+
29+
def group_by_source_package(data):
30+
grouped = defaultdict(lambda: {
31+
"binaries": set(),
32+
"licenses": set(),
33+
"copyrights": {},
34+
"source_version": None
35+
})
36+
for artifact in data.get("artifacts", []):
37+
metadata = artifact.get("metadata", {})
38+
binary = metadata.get("package", "unknown")
39+
source = metadata.get("source") or binary
40+
version = metadata.get("version", "")
41+
source_version = metadata.get("sourceVersion") or version
42+
grouped[source]["binaries"].add(binary)
43+
grouped[source]["source_version"] = source_version
44+
for lic in artifact.get("licenses", []):
45+
grouped[source]["licenses"].add(lic.get("value", "unknown"))
46+
for loc in artifact.get("locations", []):
47+
path = loc.get("path", "")
48+
if "copyright" in path:
49+
grouped[source]["copyrights"][binary] = path
50+
return grouped
51+
52+
53+
def print_table(grouped, rootfs_path):
54+
print("source,version,binaries,licenses,copyright_sha256")
55+
for source, data in grouped.items():
56+
binaries = " ".join(sorted(data["binaries"]))
57+
licenses = " ".join(sorted(data["licenses"]))
58+
version = data["source_version"] or "unknown"
59+
hashes = set()
60+
for path in data["copyrights"].values():
61+
full_path = os.path.join(rootfs_path, path.lstrip('/'))
62+
hashes.add(sha256_of_file(full_path))
63+
hash_summary = " ".join(sorted(hashes))
64+
print(f"{source},{version},{binaries},{licenses},{hash_summary}")
65+
66+
67+
if __name__ == "__main__":
68+
parser = argparse.ArgumentParser(
69+
description="Summarize Syft license data.")
70+
parser.add_argument("syft_json", help="Path to the Syft JSON file")
71+
parser.add_argument("--rootfs", required=True,
72+
help="Base path to the root filesystem")
73+
args = parser.parse_args()
74+
75+
syft_data = load_syft_json(args.syft_json)
76+
syft_grouped = group_by_source_package(syft_data)
77+
print_table(syft_grouped, args.rootfs)

0 commit comments

Comments
 (0)