Skip to content

Commit bd2108f

Browse files
author
Yoan Moscatelli
committed
✨ add cyclonedx merge
1 parent 1fce4e5 commit bd2108f

File tree

19 files changed

+655
-278
lines changed

19 files changed

+655
-278
lines changed

.devcontainer/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ patool==4.0.0
55
pre-commit==4.1.0
66
click==8.1.8
77
python-magic==0.4.27
8-
pydantic==2.11.2
8+
pydantic==2.11.2
9+
pyyaml==6.0.2

.github/scripts/update_scanners.py

Lines changed: 0 additions & 80 deletions
This file was deleted.

.github/scripts/update_tools.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
""""
3+
Update scanner versions in the versions.yaml file.
4+
This script fetches the latest release versions of the specified scanners
5+
from their GitHub repositories and updates the version strings in the
6+
versions.yaml file while preserving the original file format.
7+
"""
8+
9+
from pathlib import Path
10+
import os
11+
import sys
12+
import yaml
13+
import requests
14+
15+
# Add the root directory to the Python path
16+
root_dir = Path(__file__).resolve().parents[2]
17+
18+
def load_packages():
19+
"""Load package configurations from versions.yaml file."""
20+
yaml_path = os.path.join(root_dir, "versions.yaml")
21+
try:
22+
with open(yaml_path, 'r', encoding='utf-8') as file:
23+
data = yaml.safe_load(file)
24+
# Expect data["packages"] to be a dict
25+
packages = data.get("packages", {})
26+
data["packages"] = packages
27+
return data
28+
except yaml.YAMLError as error:
29+
print(f"⚠ Error loading versions.yaml: {str(error)}")
30+
raise
31+
32+
def get_latest_release(package_name, package_info):
33+
"""Fetch the latest release version from a GitHub repository."""
34+
editor = package_info["editor"]
35+
url = f"https://api.github.com/repos/{editor}/{package_name}/releases/latest"
36+
try:
37+
response = requests.get(url, timeout=10)
38+
response.raise_for_status()
39+
return response.json()["tag_name"].lstrip("v")
40+
except requests.RequestException as error:
41+
print(f"⚠ Error fetching latest version for {package_name}: {str(error)}")
42+
return None
43+
44+
def update_versions(file_path):
45+
"""Update the tools versions in the specified file while preserving format."""
46+
data = load_packages()
47+
updates_made = False
48+
packages = data.get("packages", {})
49+
50+
# Iterate over package dictionary
51+
for package_name, package in packages.items():
52+
current_version = package["default_version"]
53+
latest_version = get_latest_release(package_name, package)
54+
if not latest_version:
55+
continue
56+
if current_version != latest_version:
57+
print(f"⬆ Updating {package_name} from {current_version} to {latest_version}")
58+
package["default_version"] = latest_version
59+
updates_made = True
60+
else:
61+
print(f"✓ {package_name} is already at latest version {latest_version}")
62+
63+
if not updates_made:
64+
print("✓ All tools are already at their latest versions. No updates needed.")
65+
return
66+
67+
# Write the updated data back to the file
68+
with open(file_path, "w", encoding="utf8") as file:
69+
yaml.dump(data, file, default_flow_style=False, sort_keys=False)
70+
print("✓ Updates applied to", file_path)
71+
72+
if __name__ == "__main__":
73+
update_versions(os.path.join(root_dir, "versions.yaml"))

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ repos:
3232
- 'gitpython~=3.1.44'
3333
- 'python-magic~=0.4.27'
3434
- 'pydantic~=2.11.2'
35+
- 'pyyaml~=6.0.2'

README.md

Lines changed: 106 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ using [Syft](https://github.com/anchore/syft), with optional vulnerability scann
99
## Basic Usage
1010

1111
```yaml
12-
- uses: scality/sbom@v2.0.0
12+
- uses: scality/sbom@v2
1313
with:
1414
target: /usr/local/bin
1515
```
@@ -26,17 +26,19 @@ The main [SBOM action](action.yaml) is responsible for generating SBOMs.
2626
| -------------------- | ------------------------------------------------------------------------------------------- | ------------ |
2727
| `grype-version` | Grype version to use | `0.91.0` |
2828
| `syft-version` | Syft version to use | `1.22.0` |
29-
| `target` | The target to scan (file, directory, image, ISO, or repo) | `./` |
30-
| `target-type` | Type of target to scan (file, directory, image, iso, repo) | `file` |
31-
| `output-format` | Format of the generated SBOM | `cyclonedx-json` |
29+
| `target` | The target to scan (path or image) | `./` |
30+
| `target-type` | Type of target to scan (file, directory, image, iso) | `file` |
31+
| `output-format` | Format of the generated SBOM (cyclonedx-json cyclonedx-xml github-json spdx-json spdx-tag-value syft-json syft-table syft-text template) | `cyclonedx-json` |
3232
| `output-file` | A specific file location to store the SBOM | |
3333
| `output-dir` | Directory to store generated SBOM files | `/tmp/sbom` |
3434
| `exclude-mediatypes` | Media types to exclude for images (comma-separated) | |
3535
| `distro` | Linux distribution of the target (if not auto-detected) | |
3636
| `name` | Override the detected name of the target | |
3737
| `version` | Override the detected version of the target | |
38+
| `merge` | Merge multiple SBOMs into a single file | `false` |
39+
| `merge_hierarchical` | Merge multiple SBOMs into a single hierarchical file | `false` |
3840
| `vuln` | Enable vulnerability scanning | `false` |
39-
| `vuln-output-format` | Format for the vulnerability report (HTML or JSON) when `vuln` is enabled | `json` |
41+
| `vuln-output-format` | Format for the vulnerability report when `vuln` is enabled (supports `json`, `html`, `csv`, `table`, or comma-separated values like `html,json`) | `cyclonedx-json` |
4042
| `vuln-output-file` | A specific file location to store the vulnerability report | |
4143

4244
## Example Usage
@@ -46,18 +48,32 @@ The main [SBOM action](action.yaml) is responsible for generating SBOMs.
4648
Use the `output-format` and `vuln-output-format` parameters to choose the SBOM and vulnerability report formats:
4749

4850
```yaml
49-
- uses: scality/sbom@v2.0.0
51+
- uses: scality/sbom@v2
5052
with:
5153
target: ./artifacts
5254
output-format: cyclonedx-json # SBOM format
53-
vuln: true # Enable vulnerability scanning
54-
vuln-output-format: html # Vulnerability report format
55+
vuln: true # Enable vulnerability scanning
56+
vuln-output-format: html # Generate HTML vulnerability report
57+
```
58+
59+
The HTML format provides an interactive report with a dynamic table for better visualization of vulnerabilities, allowing for easier filtering and sorting.
60+
61+
### Multiple vulnerability report formats
62+
63+
You can generate multiple formats simultaneously by using comma-separated values:
64+
65+
```yaml
66+
- uses: scality/sbom@v2
67+
with:
68+
target: ./artifacts
69+
vuln: true
70+
vuln-output-format: html,json # Generate both HTML and JSON reports
5571
```
5672

5773
### Specify target type explicitly
5874

5975
```yaml
60-
- uses: scality/sbom@v2.0.0
76+
- uses: scality/sbom@v2
6177
with:
6278
target: myimage.tar
6379
target-type: image
@@ -68,7 +84,7 @@ Use the `output-format` and `vuln-output-format` parameters to choose the SBOM a
6884
For images (like those built using Oras) that use custom mediatypes not supported by Skopeo:
6985

7086
```yaml
71-
- uses: scality/sbom@v2.0.0
87+
- uses: scality/sbom@v2
7288
with:
7389
target: ./images
7490
target-type: image
@@ -78,7 +94,7 @@ For images (like those built using Oras) that use custom mediatypes not supporte
7894
### Enable vulnerability scanning
7995

8096
```yaml
81-
- uses: scality/sbom@v2.0.0
97+
- uses: scality/sbom@v2
8298
with:
8399
target: ./
84100
vuln: true
@@ -113,7 +129,7 @@ jobs:
113129
path: ${{ env.BASE_PATH }}/repo/myrepo
114130
115131
- name: Generate SBOM for repository
116-
uses: scality/sbom@v2.0.0
132+
uses: scality/sbom@v2
117133
with:
118134
target: ${{ env.BASE_PATH }}/repo/myrepo
119135
target-type: file
@@ -130,13 +146,16 @@ jobs:
130146
curl -sSfL -o ${{ env.BASE_PATH }}/iso/my.iso -u $ARTIFACTS_USER:$ARTIFACTS_PASSWORD $ARTIFACTS_URL/my.iso
131147
132148
- name: Generate SBOM for ISO
133-
uses: scality/sbom@v2.0.0
149+
uses: scality/sbom@v2
134150
with:
135151
target: ${{ env.BASE_PATH }}/iso/my.iso
136152
target-type: iso
137153
version: "1.0.0"
138154
output-dir: ${{ env.SBOM_PATH }}
139155
vuln: true
156+
vuln-output-format: html
157+
merge: true
158+
merge_hierarchical: true
140159
141160
- name: Upload artifacts
142161
uses: actions/upload-artifact@v4
@@ -211,10 +230,6 @@ In the generated SBOM files, you will find CycloneDX metadata. Examples include:
211230
}
212231
```
213232

214-
## References
215-
216-
HTML template for **Grype** vulnerability reports was modified from [Grype Contrib](https://github.com/opt-nc/grype-contribs).
217-
218233
## Core Workflow
219234

220235
```mermaid
@@ -343,3 +358,77 @@ flowchart TD
343358

344359
1. If `vuln` is enabled, the provider’s `vuln()` method uses Grype to scan the SBOM.
345360
2. Grype generates a vulnerability report saved as: `{target_type}_{name}_{version}_vuln.json`.
361+
362+
## Merge Explanation
363+
364+
The merge is per default not hierarchical for the `components` field of a `component`. This means that components that were contained in the `components` of an already present component will just be added as new components under the SBOMs’ `components` sections. The `--hierarchical` flag allows for hierarchical merges. This affects only the top level components of the merged SBOM. The structured of nested components is preserved in both cases (except the removal of already present components), as shown for *component 4* in the image below.
365+
366+
```mermaid
367+
flowchart TD
368+
subgraph "SBOM 1"
369+
0["Component 0 <br> metadata component"]
370+
01["Component 1"]
371+
02["Component 2"]
372+
373+
0 -->|contains| 01
374+
0 -->|contains| 02
375+
end
376+
```
377+
378+
```mermaid
379+
flowchart TD
380+
subgraph "SBOM 2"
381+
1["Component 1 <br> metadata component"]
382+
13["Component 3"]
383+
14["Component 4"]
384+
15["Component 5"]
385+
386+
1 -->|contains| 13
387+
1 -->|contains| 14
388+
14 -->|contains| 15
389+
end
390+
```
391+
392+
### Default merge:
393+
394+
```mermaid
395+
flowchart TD
396+
subgraph "Merged SBOM"
397+
0["Component 0 <br> metadata component"]
398+
1["Component 1"]
399+
2["Component 2"]
400+
3["Component 3"]
401+
4["Component 4"]
402+
5["Component 5"]
403+
404+
0 -->|contains| 1
405+
0 -->|contains| 2
406+
0 -->|contains| 3
407+
0 -->|contains| 4
408+
4 -->|contains| 5
409+
end
410+
```
411+
412+
### Hierarchical merge:
413+
414+
```mermaid
415+
flowchart TD
416+
subgraph "Merged SBOM"
417+
0["Component 0 <br> metadata component"]
418+
1["Component 1"]
419+
2["Component 2"]
420+
3["Component 3"]
421+
4["Component 4"]
422+
5["Component 5"]
423+
424+
0 -->|contains| 1
425+
0 -->|contains| 2
426+
1 -->|contains| 3
427+
1 -->|contains| 4
428+
4 -->|contains| 5
429+
end
430+
```
431+
432+
### References
433+
- [CycloneDX Specification](https://cyclonedx.org/docs/1.6/json/)
434+
- [CycloneDX Merge](https://festo-se.github.io/cyclonedx-editor-validator/usage/merge.html)

0 commit comments

Comments
 (0)