← Back to list

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

Introduction

Santhosh · 2026-01-14 04:32 · 1 claps · 5.2 min read
#sbom #devops #syft #grype
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔒 · Cybersecurity

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

  1. Place the Python script in the same directory as:
  • sbom.json
  • vuln-report.json
  1. Create the script file:
touch generate_report.py
  1. Paste your custom Python code into generate_report.py
  2. 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