Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# SBOM & Vulnerability Scanning Automation

This repository uses GitHub Actions to automatically generate a Software Bill of Materials (SBOM), scan for vulnerabilities, and produce package inventory reports.

All reports are named with the repository name for easy identification.

## Features

SBOM Generation: Uses Syft to generate an SPDX JSON SBOM.
SBOM Merging: Merges SBOMs for multiple tools if needed.
SBOM to CSV: Converts SBOM JSON to a CSV report.
Vulnerability Scanning: Uses Grype to scan the SBOM for vulnerabilities and outputs a CSV report.
Package Inventory: Extracts a simple package list (name, type, version) as a CSV.
Artifacts: All reports are uploaded as workflow artifacts with the repository name in the filename.

## Workflow Overview

The main workflow is defined in .github/workflows/sbom.yml

## Scripts

scripts/create-sbom.sh
Generates an SBOM for the repo and for specified tools, merging them as needed.
scripts/update-sbom.py
Merges additional SBOMs into the main SBOM.
.github/scripts/sbom_json_to_csv.py
Converts the SBOM JSON to a detailed CSV report.
.github/scripts/grype_json_to_csv.py
Converts Grype’s vulnerability scan JSON output to a CSV report.
Output columns: REPO, NAME, INSTALLED, FIXED-IN, TYPE, VULNERABILITY, SEVERITY
.github/scripts/sbom_packages_to_csv.py
Extracts a simple package inventory from the SBOM.
Output columns: name, type, version

## Example Reports

Vulnerability Report
grype-report-[RepoName].csv
REPO,NAME,INSTALLED,FIXED-IN,TYPE,VULNERABILITY,SEVERITY
my-repo,Flask,2.1.2,,library,CVE-2022-12345,High
...

Package Inventory
sbom-packages-[RepoName].csv
name,type,version
Flask,library,2.1.2
Jinja2,library,3.1.2
...

## Usage

Push to main branch or run the workflow manually.
Download artifacts from the workflow run summary.

## Customization

Add more tools to scripts/create-sbom.sh as needed.
Modify scripts to adjust report formats or add more metadata.
28 changes: 28 additions & 0 deletions .github/scripts/grype_json_to_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import json
import csv
import sys

input_file = sys.argv[1] if len(sys.argv) > 1 else "grype-report.json"
output_file = sys.argv[2] if len(sys.argv) > 2 else "grype-report.csv"

with open(input_file, "r", encoding="utf-8") as f:
data = json.load(f)

columns = ["NAME", "INSTALLED", "FIXED-IN", "TYPE", "VULNERABILITY", "SEVERITY"]

with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=columns)
writer.writeheader()
for match in data.get("matches", []):
pkg = match.get("artifact", {})
vuln = match.get("vulnerability", {})
row = {
"NAME": pkg.get("name", ""),
"INSTALLED": pkg.get("version", ""),
"FIXED-IN": vuln.get("fix", {}).get("versions", [""])[0] if vuln.get("fix", {}).get("versions") else "",
"TYPE": pkg.get("type", ""),
"VULNERABILITY": vuln.get("id", ""),
"SEVERITY": vuln.get("severity", ""),
}
writer.writerow(row)
print(f"CSV export complete: {output_file}")
74 changes: 74 additions & 0 deletions .github/scripts/sbom_json_to_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import json
import csv
import sys
from pathlib import Path
from tabulate import tabulate

input_file = sys.argv[1] if len(sys.argv) > 1 else "sbom.json"
output_file = sys.argv[2] if len(sys.argv) > 2 else "sbom.csv"

with open(input_file, "r", encoding="utf-8") as f:
sbom = json.load(f)

packages = sbom.get("packages", [])

columns = [
"name",
"versionInfo",
"type",
"supplier",
"downloadLocation",
"licenseConcluded",
"licenseDeclared",
"externalRefs"
]

def get_type(pkg):
spdxid = pkg.get("SPDXID", "")
if "-" in spdxid:
parts = spdxid.split("-")
if len(parts) > 2:
return parts[2]
refs = pkg.get("externalRefs", [])
for ref in refs:
if ref.get("referenceType") == "purl":
return ref.get("referenceLocator", "").split("/")[0]
return ""

def get_external_refs(pkg):
refs = pkg.get("externalRefs", [])
return ";".join([ref.get("referenceLocator", "") for ref in refs])

with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=columns)
writer.writeheader()
for pkg in packages:
row = {
"name": pkg.get("name", ""),
"versionInfo": pkg.get("versionInfo", ""),
"type": get_type(pkg),
"supplier": pkg.get("supplier", ""),
"downloadLocation": pkg.get("downloadLocation", ""),
"licenseConcluded": pkg.get("licenseConcluded", ""),
"licenseDeclared": pkg.get("licenseDeclared", ""),
"externalRefs": get_external_refs(pkg)
}
writer.writerow(row)

print(f"CSV export complete: {output_file}")

with open("sbom_table.txt", "w", encoding="utf-8") as f:
table = []
for pkg in packages:
row = [
pkg.get("name", ""),
pkg.get("versionInfo", ""),
get_type(pkg),
pkg.get("supplier", ""),
pkg.get("downloadLocation", ""),
pkg.get("licenseConcluded", ""),
pkg.get("licenseDeclared", ""),
get_external_refs(pkg)
]
table.append(row)
f.write(tabulate(table, columns, tablefmt="grid"))
28 changes: 28 additions & 0 deletions .github/scripts/sbom_packages_to_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import json
import csv
import sys
import os

input_file = sys.argv[1] if len(sys.argv) > 1 else "sbom.json"
repo_name = sys.argv[2] if len(sys.argv) > 2 else os.getenv("GITHUB_REPOSITORY", "unknown-repo").split("/")[-1]
output_file = f"sbom-packages-{repo_name}.csv"

with open(input_file, "r", encoding="utf-8") as f:
sbom = json.load(f)

packages = sbom.get("packages", [])

columns = ["name", "type", "version"]

with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=columns)
writer.writeheader()
for pkg in packages:
row = {
"name": pkg.get("name", ""),
"type": pkg.get("type", ""),
"version": pkg.get("versionInfo", "")
}
writer.writerow(row)

print(f"Package list CSV generated: {output_file}")
52 changes: 47 additions & 5 deletions .github/workflows/sbom.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: SBOM Check
name: SBOM Vulnerability Scanning

on:
workflow_dispatch:
Expand Down Expand Up @@ -56,13 +56,55 @@ jobs:
chmod +x syft

# Add to PATH for subsequent steps
echo "$(pwd)" >> $GITHUB_PATH
echo "$(pwd)" >> $GITHUB_PATH

- name: Create SBOM
run: bash scripts/create-sbom.sh terraform python tflint

- name: Upload SBOM as artifact
- name: Convert SBOM JSON to CSV
run: |
pip install --upgrade pip
pip install tabulate
REPO_NAME=$(basename $GITHUB_REPOSITORY)
python .github/scripts/sbom_json_to_csv.py sbom.json SBOM_${REPO_NAME}.csv

- name: Upload SBOM CSV as artifact
uses: actions/upload-artifact@v4
with:
name: sbom-csv
path: SBOM_${{ github.event.repository.name }}.csv

- name: Install Grype
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

- name: Scan SBOM for Vulnerabilities (JSON)
run: |
grype sbom:sbom.json -o json > grype-report.json



- name: Convert Grype JSON to CSV
run: |
pip install --upgrade pip
REPO_NAME=$(basename $GITHUB_REPOSITORY)
python .github/scripts/grype_json_to_csv.py grype-report.json grype-report-${REPO_NAME}.csv


- name: Upload Vulnerability Report
uses: actions/upload-artifact@v4
with:
name: grype-report
path: grype-report-${{ github.event.repository.name }}.csv

- name: Generate Package Inventory CSV
run: |
pip install --upgrade pip
REPO_NAME=$(basename $GITHUB_REPOSITORY)
python .github/scripts/sbom_packages_to_csv.py sbom.json $REPO_NAME

- name: Upload Package Inventory CSV
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
name: sbom-packages
path: sbom-packages-${{ github.event.repository.name }}.csv