End-to-End SBOM & Vulnerability Scanning with Syft and Grype
Introduction
End-to-End SBOM & Vulnerability Scanning with Syft and Grype

Introduction
Modern applications are built using hundreds (sometimes thousands) of open‑source libraries and dependencies. While this accelerates development, it also introduces security, license, and compliance risks.
To address these risks, security teams rely on:
- SBOM (Software Bill of Materials) — a complete inventory of software components
- Vulnerability scanning — detection of known CVEs in those components
In this guide, we will cover an end‑to‑end DevSecOps workflow using Syft and Grype from Anchore, and finally generate a human‑friendly HTML security report using a custom Python script.
What Is an SBOM?
SBOM (Software Bill of Materials) is a formal, machine‑readable list of all components included in a software application.
SBOM Typically Contains
- Libraries and dependencies
- Software component names
- Author and supplier information
- License details
- Version information
- Open‑source vs proprietary components
- Dependency relationships
- Compliance and audit requirements
SBOM Standards Explained
1. SPDX (Software Package Data Exchange)
SPDX is a widely adopted SBOM standard maintained by the Linux Foundation.
Key features:
- License and copyright tracking
- Security references
- Standardized machine‑readable format
- ISO/IEC 5962:2021 certified
SPDX is commonly used for legal and compliance audits.
2. CycloneDX
CycloneDX is a lightweight, security‑focused SBOM standard designed for modern DevSecOps pipelines.
Key advantages:
- Optimized for vulnerability analysis
- Cloud‑native and CI/CD friendly
- Supports JSON, XML, and Protobuf formats
- Strong ecosystem support (Syft, Grype, OWASP tools)
In this tutorial, we use CycloneDX JSON format.
Tool Overview

Syft — SBOM Generator
Syft scans applications, container images, and file systems to generate SBOMs.
What Syft detects:
- OS packages
- Language dependencies (Python, Node, Java, Go, etc.)
- Application binaries
- Container image layers
Grype — Vulnerability Scanner
Grype is an open‑source vulnerability scanner that consumes SBOMs and detects known CVEs.
Why Grype is critical:
- Scans container images, directories, and SBOMs
- Uses multiple vulnerability databases
- CI/CD and DevSecOps friendly
- Developed and maintained by Anchore
Step 1: Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
Add Syft to PATH
sudo mv syft /usr/local/bin/
Verify Installation
syft version
Step 2: Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh
Add Grype to PATH
sudo mv grype /usr/local/bin/
Verify Installation
grype version
Step 3: Generate SBOM Using Syft
Navigate to your project directory and run:
syft . -o cyclonedx-json > sbom.json
This command:
- Scans the current directory
- Generates a CycloneDX SBOM
- Saves output as sbom.json
Step 4: Validate the SBOM
(Optional but recommended)
cyclonedx-cli validate --input-file sbom.json
This ensures the SBOM complies with the CycloneDX specification.
Step 5: Scan SBOM with Grype
Now scan the SBOM for vulnerabilities:
grype sbom:sbom.json -o json > vuln-report.json
This produces a structured vulnerability report containing:
- CVE IDs
- Severity levels
- Affected packages
- Installed vs fixed versions
- References and advisories
Step 6: Convert JSON Reports to HTML
JSON reports are machine‑friendly but not ideal for human review. To improve readability, we use a custom Python script to convert:
- sbom.json
- vuln-report.json
into a professional HTML vulnerability report.
Steps
- Place the Python script in the same directory as:
- sbom.json
- vuln-report.json
- Create the script file:
touch generate_report.py
- Paste your custom Python code into generate_report.py
- Run the script:
import json
from pathlib import Path
from datetime import datetime
# Load data
with open("sbom.json") as f: sbom = json.load(f)
with open("vuln-report.json") as f: vuln_report = json.load(f)
# Create component lookup
components = {f"{c.get('name')}:{c.get('version')}": c
for c in sbom.get("components", []) if c.get("name") and c.get("version")}
# Process vulnerabilities
vuln_mapping = []
for match in vuln_report.get("matches", []):
artifact = match.get("artifact", {})
vuln = match.get("vulnerability", {})
name, version = artifact.get("name"), artifact.get("version")
if f"{name}:{version}" in components:
vuln_mapping.append({
"name": name,
"version": version,
"vulnerability": vuln.get("id"),
"severity": vuln.get("severity"),
"description": vuln.get("description"),
"fix_version": match.get("matchDetails", [{}])[0].get("fix", {}).get("suggestedVersion", "N/A"),
"locations": [loc.get("path") for loc in artifact.get("locations", [])]
})
# Statistics
severity_counts = {}
for v in vuln_mapping:
severity_counts[v['severity']] = severity_counts.get(v['severity'], 0) + 1
# HTML Template
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SBOM Vulnerability Report</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif; background:#f9fafb; color:#374151; line-height:1.6; }}
.container {{ max-width:1400px; margin:0 auto; padding:0 20px; }}
header {{ background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); color:white; padding:2rem 0; }}
.header-content {{ display:flex; justify-content:space-between; align-items:center; }}
.logo {{ display:flex; align-items:center; gap:15px; }}
.logo i {{ font-size:2.5rem; background:rgba(255,255,255,0.2); padding:15px; border-radius:50%; }}
.logo h1 {{ font-size:2.2rem; font-weight:600; }}
.stats-container, .table-container {{ background:white; border-radius:12px; padding:25px; margin:30px 0; box-shadow:0 4px 15px rgba(0,0,0,0.05); }}
.stats-title, .table-title {{ font-size:1.3rem; font-weight:600; margin-bottom:20px; color:#4b5563; display:flex; align-items:center; gap:10px; }}
.stats-grid {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:20px; }}
.stat-card {{ background:#f8fafc; border-radius:10px; padding:20px; text-align:center; border:1px solid #e5e7eb; }}
.stat-value {{ font-size:2.5rem; font-weight:700; margin-bottom:5px; }}
table {{ width:100%; border-collapse:collapse; }}
th {{ background:#f1f5f9; padding:16px 12px; text-align:left; font-weight:600; color:#475569; border-bottom:2px solid #e2e8f0; }}
td {{ padding:14px 12px; border-bottom:1px solid #e5e7eb; }}
tr:hover {{ background:#f8fafc; }}
.severity-badge {{ display:inline-block; padding:5px 12px; border-radius:20px; font-size:0.8rem; font-weight:600; text-transform:uppercase; }}
.severity-critical {{ background:#fee2e2; color:#991b1b; }}
.severity-high {{ background:#ffedd5; color:#9a3412; }}
.severity-medium {{ background:#fef3c7; color:#92400e; }}
.severity-low {{ background:#dcfce7; color:#166534; }}
footer {{ background:#f1f5f9; padding:25px 0; border-top:1px solid #e5e7eb; }}
.footer-content {{ display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:20px; }}
@media (max-width:768px) {{
.header-content, .footer-content {{ flex-direction:column; text-align:center; gap:20px; }}
th, td {{ padding:10px 8px; font-size:0.9rem; }}
}}
</style>
</head>
<body>
<header><div class="container">
<div class="header-content">
<div class="logo"><i class="fas fa-shield-alt"></i><div><h1>SBOM Vulnerability Report</h1><p>Software Bill of Materials Security Analysis</p></div></div>
<div><p><i class="far fa-calendar-alt"></i> Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
<p><i class="fas fa-file-alt"></i> Components: {len(components)}</p></div>
</div>
</div></header>
<main class="container">
<div class="stats-container">
<div class="stats-title"><i class="fas fa-chart-bar"></i> Vulnerability Overview</div>
<div class="stats-grid">
<div class="stat-card"><div class="stat-value">{len(vuln_mapping)}</div><div class="stat-label">Total Vulnerabilities</div></div>
<div class="stat-card"><div class="stat-value">{severity_counts.get('Critical',0)}</div><div class="stat-label">Critical</div></div>
<div class="stat-card"><div class="stat-value">{severity_counts.get('High',0)}</div><div class="stat-label">High</div></div>
<div class="stat-card"><div class="stat-value">{severity_counts.get('Medium',0)}</div><div class="stat-label">Medium</div></div>
<div class="stat-card"><div class="stat-value">{severity_counts.get('Low',0)}</div><div class="stat-label">Low</div></div>
</div>
</div>
<div class="table-container">
<div class="table-title"><i class="fas fa-list-ul"></i> Detailed Vulnerability Findings</div>
"""
if vuln_mapping:
html += """<table><thead><tr>
<th>Package</th><th>Version</th><th>Vulnerability ID</th><th>Severity</th><th>Description</th><th>Fix Version</th><th>Locations</th>
</tr></thead><tbody>"""
for v in vuln_mapping:
severity_class = f"severity-{v['severity'].lower()}"
html += f"""<tr>
<td><strong>{v['name']}</strong></td>
<td>{v['version']}</td>
<td><code>{v['vulnerability']}</code></td>
<td><span class="severity-badge {severity_class}">{v['severity']}</span></td>
<td>{v['description']}</td>
<td>{v['fix_version']}</td>
<td><small>{'<br>'.join(v['locations'])}</small></td>
</tr>"""
html += "</tbody></table>"
else:
html += """<div style="text-align:center; padding:50px 20px; color:#6b7280;">
<i class="fas fa-check-circle" style="font-size:3rem; margin-bottom:20px; color:#d1d5db;"></i>
<h2>No Vulnerabilities Found</h2><p>All components appear secure.</p></div>"""
html += f"""
</div>
</main>
<footer><div class="container">
<div class="footer-content">
<div style="display:flex; align-items:center; gap:10px; font-weight:600; color:#4b5563;">
<i class="fas fa-shield-alt"></i><span>SBOM Security Scanner</span>
</div>
<div style="color:#6b7280; font-size:0.9rem;">
<p>Report generated on {datetime.now().strftime('%B %d, %Y at %H:%M')}</p>
<p>For security inquiries, contact your security team.</p>
</div>
</div>
</div></footer>
</body></html>"""
# Save file
Path("vuln-report.html").write_text(html)
print(f"HTML report generated: vuln-report.html")
python3 generate_report.py
Output
vuln-report.html

The HTML report provides:
- Clean, readable layout
- Component‑wise vulnerability mapping
- Severity highlighting
- Better visibility for security teams and management
Benefits of This Approach
- Automated SBOM generation
- Standardized formats (CycloneDX, SPDX compatible)
- Accurate vulnerability detection
- CI/CD pipeline ready
- Human‑readable security reporting
- Strong compliance and audit support
Conclusion
By combining Syft, Grype, and a custom HTML reporting layer, you build a powerful and practical DevSecOps workflow.
This approach enables teams to:
- Understand their software supply chain
- Detect vulnerabilities early
- Meet compliance requirements
- Improve security posture before production
SBOMs are no longer optional — they are a security necessity.
메타데이터
- post_id
- dc15a5ff40e7
- slug
- end-to-end-sbom-vulnerability-scanning-with-syft-and-grype-dc15a5ff40e7
- url
- https://medium.com/@santhoshprabhakaran03/end-to-end-sbom-vulnerability-scanning-with-syft-and-grype-dc15a5ff40e7
- canonical_url
- https://medium.com/@santhoshprabhakaran03/end-to-end-sbom-vulnerability-scanning-with-syft-and-grype-dc15a5ff40e7
- author_url
- https://medium.com/@santhoshprabhakaran03
- status
- ok
- fetched_at
- 2026-07-13 10:22:01