End-to-end supply chain security pipeline: Source to signed SBOM with Syft, Grype, and Cosign on AWS
- Introduction: The Supply Chain Is the New Attack Surface
- The Regulatory Landscape: Why SBOMs Are Now Mandatory
- What Is an SBOM? SPDX vs CycloneDX
- AWS Inspector SBOM: The Native Starting Point
- Syft Deep Dive: The Industry-Standard SBOM Generator
- Grype Deep Dive: Vulnerability Scanning Against SBOMs
- Complete CodePipeline Integration
- Cosign: Signing SBOMs and Container Images
- Comparison: AWS Inspector vs Syft/Grype
- Automated Compliance Reporting with S3 and Athena
- Best Practices for SBOM Lifecycle Management
- Implementation Roadmap
- Related Articles
- Conclusion
Introduction: The Supply Chain Is the New Attack Surface
Software supply chain attacks are no longer a theoretical risk. They are the dominant breach vector of 2025. The global cost of supply chain attacks reached $60 billion in 2025, up from $46 billion just two years prior, and projections place the figure at $138 billion by 2031. The 2025 Verizon Data Breach Investigations Report found that 30% of all breaches now involve a third party, a 100% increase from prior reporting periods. Attack frequency has doubled year-over-year, with October 2025 setting a record of 41 supply chain attacks in a single month.
The response from regulators has been decisive. The EU Cyber Resilience Act (CRA), NIST Executive Order 14028, and FDA cybersecurity requirements all converge on a single mandate: organizations must produce, maintain, and share Software Bills of Materials (SBOMs).
If you sell software to the EU market, you have until September 11, 2026 to comply with CRA vulnerability reporting obligations, and until December 11, 2027 for full compliance. If you sell to the US federal government, SBOM requirements are already in effect.
The good news: the open-source tooling for SBOM generation and vulnerability scanning is mature, battle-tested, and free. Syft and Grype from Anchore are the industry standard — even AWS Inspector uses Anchore-derived technology under the hood. Combined with Cosign for artifact signing and AWS CodePipeline for orchestration, you can build a fully automated, regulatory-compliant supply chain security pipeline without a single commercial license.
This guide walks you through exactly how to do it.
The Regulatory Landscape: Why SBOMs Are Now Mandatory
EU Cyber Resilience Act (CRA)
The CRA entered into force on December 10, 2024, establishing the most sweeping software security regulation in history. Key SBOM requirements include:
- Mandatory SBOM generation for all products with digital elements sold in the EU market
- Machine-readable format using a commonly used standard (SPDX or CycloneDX)
- Top-level dependency listing at minimum, with full dependency trees recommended
- Technical documentation that includes the SBOM, provided to market surveillance authorities on request
- Vulnerability reporting within 24 hours to ENISA and national CSIRTs for actively exploited vulnerabilities, effective September 2026
The penalties are severe: up to EUR 15 million or 2.5% of global annual turnover, whichever is higher. A draft horizontal standard is expected by mid-2026 to formalize the exact SBOM schema, but the law is clear — if you do not have SBOM generation and vulnerability management in place before September 2026, you cannot comply.
NIST and Executive Order 14028
Executive Order 14028 (May 2021) defined SBOMs as a “formal record containing the details and supply chain relationships of various components used in building software.” NIST guidance mandates:
- Machine-readable SBOMs in SPDX, CycloneDX, or SWID format
- Signed self-attestation of secure development practices aligned with NIST SP 800-218 (SSDF)
- Traceable and current SBOMs that are updated with each software release
- CISA minimum elements (2025 update) expanding required metadata for provenance and authenticity
Software vendors selling to the US federal government must comply today. The August 2025 consortium deadline for SSDF alignment guidance has passed, and procurement requirements are tightening.
FDA Cybersecurity Requirements
For medical device manufacturers, the FDA now requires SBOMs as part of premarket submissions under the Consolidated Appropriations Act (Section 524B). This applies to any device with software components.
The bottom line: If you ship software to any regulated market, SBOMs are not optional. The question is not whether to generate them, but how to automate the process reliably.
What Is an SBOM? SPDX vs CycloneDX
A Software Bill of Materials is a machine-readable inventory of every component in your software — libraries, frameworks, compilers, and their transitive dependencies. Think of it as a nutrition label for software.
SPDX (Software Package Data Exchange)
Developed by the Linux Foundation, SPDX is the only internationally recognized SBOM standard (ISO/IEC 5962:2021). Key characteristics:
- Primary focus: License compliance and intellectual property tracking
- Current version: SPDX 3.0 (though many tools still use 2.3)
- Formats: JSON, RDF, XML, YAML, tag-value
- Strengths: Comprehensive license expression support, ISO standardization, strong adoption with Microsoft and Linux Foundation projects
- Best for: Legal teams, IP due diligence, licensing compliance
CycloneDX
Developed by the OWASP community, CycloneDX is a lightweight, security-first SBOM format. Key characteristics:
- Primary focus: Security vulnerability tracking and risk assessment
- Current version: CycloneDX 1.6.1
- Formats: JSON, XML, Protocol Buffers
- Strengths: Native VEX (Vulnerability Exploitability eXchange) support, dependency tree representation, hashing, and attestation fields
- Best for: Security teams, DevSecOps pipelines, vulnerability management
Which Format Should You Use?
Both formats share significant overlap, and both Syft and AWS Inspector support generating either. The practical recommendation:
| Criteria | SPDX | CycloneDX |
|---|---|---|
| Regulatory compliance | Both accepted by EU CRA and NIST | Both accepted by EU CRA and NIST |
| License compliance | Preferred | Adequate |
| Security scanning | Adequate | Preferred |
| VEX support | Via companion docs | Native |
| ISO standardized | Yes (ISO/IEC 5962) | No (OWASP standard) |
| CI/CD integration | Good | Excellent |
| Tool ecosystem | Broad | Broad |
Our recommendation: Generate CycloneDX for your security pipeline (native VEX support, lighter weight, better CI/CD tooling) and SPDX for regulatory submissions and legal compliance. With Syft, generating both is a single flag change.
AWS Inspector SBOM: The Native Starting Point
AWS Inspector provides built-in SBOM capabilities through its SBOM Generator (Sbomgen) and the Inspector Scan API. If you are already running Inspector for vulnerability management, this is your zero-effort baseline.
What Inspector Offers
- Automated SBOM generation for monitored EC2 instances, Lambda functions, and ECR images
- Export in CycloneDX 1.4 and SPDX 2.3 formats
- Integrated vulnerability scanning against the Inspector vulnerability database
- Console and API access for SBOM export and scanning
Exporting SBOMs with the AWS CLI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Export SBOMs for all monitored resources
aws inspector2 create-sbom-export \
--report-format CYCLONEDX_1_4 \
--s3-destination \
bucketName=my-sbom-bucket,\
keyPrefix=inspector-exports/,\
kmsKeyArn=arn:aws:kms:us-east-1:123456789012:key/my-key-id
# Check export status
aws inspector2 get-sbom-export \
--report-id "report-id-from-create"
# Scan a single SBOM via the API
aws inspector-scan scan-sbom \
--sbom file://my-sbom.json \
--output-format INSPECTOR
Using Inspector Sbomgen Locally
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Download the Inspector SBOM Generator
curl -O https://amazon-inspector-sbomgen.s3.amazonaws.com/latest/linux/amd64/inspector-sbomgen
chmod +x inspector-sbomgen
# Generate SBOM for a container image
./inspector-sbomgen container \
--image my-app:latest \
--outfile sbom.json
# Generate SBOM for a directory
./inspector-sbomgen directory \
--path /app \
--outfile sbom.json
Inspector Limitations
While Inspector is a strong baseline, it has constraints that matter for serious supply chain security:
- Format versions: Limited to CycloneDX 1.4 and SPDX 2.3 (not the latest specifications)
- Ecosystem coverage: Focused on AWS-monitored resources; does not cover arbitrary CI/CD artifacts
- Portability: SBOMs are tied to the AWS ecosystem and Inspector’s vulnerability database
- Cost: Inspector pricing is per-assessment, which scales with resource count
- Customization: Limited control over scanning depth, exclusion rules, and output enrichment
This is where open-source tooling fills the gap.
Syft Deep Dive: The Industry-Standard SBOM Generator
Syft (v1.42.0 as of February 2026) is the most widely adopted open-source SBOM generator. It scans container images, filesystems, archives, and source code repositories to produce comprehensive, standards-compliant SBOMs.
Installation
1
2
3
4
5
# Install via curl (recommended for CI/CD)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Verify installation
syft version
Generating SBOMs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Scan a container image (CycloneDX JSON)
syft packages registry.example.com/my-app:v1.2.3 \
-o cyclonedx-json \
--file sbom-cyclonedx.json
# Scan a container image (SPDX JSON)
syft packages registry.example.com/my-app:v1.2.3 \
-o spdx-json \
--file sbom-spdx.json
# Scan a local directory (source code)
syft packages dir:/path/to/source \
-o cyclonedx-json \
--file sbom-source.json
# Scan a filesystem archive
syft packages file:application.tar.gz \
-o cyclonedx-json \
--file sbom-archive.json
# Scan an OCI image from ECR
syft packages \
123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest \
-o cyclonedx-json \
--file sbom-ecr.json
# Generate multiple formats simultaneously
syft packages my-app:latest \
-o cyclonedx-json=sbom-cdx.json \
-o spdx-json=sbom-spdx.json \
-o table
Supported Ecosystems
Syft detects and catalogs packages across a broad ecosystem:
- OS packages: Alpine (apk), Debian/Ubuntu (dpkg), RHEL/CentOS (rpm), Arch (pacman)
- Language ecosystems: npm, pip/pipenv/poetry, Maven/Gradle, Go modules, Ruby gems, Rust crates, PHP Composer, .NET NuGet, Swift CocoaPods, Dart pub, Haskell Cabal
- Container metadata: Dockerfile instructions, base image identification, layer analysis
- Binary analysis: Compiled Go and Rust binaries (extracts embedded dependency information)
Syft Configuration File
For consistent SBOM generation across your organization, use a .syft.yaml configuration file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# .syft.yaml - Organization-wide Syft configuration
output:
- cyclonedx-json
# Package cataloging options
package:
cataloger:
enabled: true
scope: all-layers # Scan all image layers, not just squashed
# Exclusion patterns
exclude:
- "**/test/**"
- "**/node_modules/.cache/**"
- "**/.git/**"
# Source metadata
source:
name: "my-application"
version: "${APP_VERSION}"
metadata:
image:
# Include base image info for full provenance
layers: true
Performance Characteristics
Syft is designed for CI/CD performance:
- Typical scan time: 10-30 seconds for a standard application container image
- Memory usage: 200-500MB depending on image size
- Parallelism: Scans multiple layers concurrently
- Caching: Supports local database caching for repeated scans
Grype Deep Dive: Vulnerability Scanning Against SBOMs
Grype (v0.107.1 as of February 2026) is the companion vulnerability scanner to Syft. It takes an SBOM as input and cross-references every component against multiple vulnerability databases.
Installation
1
2
3
4
5
6
7
8
# Install via curl
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Verify installation
grype version
# Update vulnerability database
grype db update
Scanning for Vulnerabilities
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Scan an SBOM directly
grype sbom:sbom-cyclonedx.json
# Scan a container image (generates SBOM internally)
grype registry.example.com/my-app:v1.2.3
# Scan with severity threshold (fail on Critical or High)
grype sbom:sbom-cyclonedx.json --fail-on high
# Output in JSON for CI/CD processing
grype sbom:sbom-cyclonedx.json -o json --file vuln-report.json
# Output as a SARIF report (for GitHub/GitLab integration)
grype sbom:sbom-cyclonedx.json -o sarif --file vuln-report.sarif
# Scan with only fixed vulnerabilities shown
grype sbom:sbom-cyclonedx.json --only-fixed
Vulnerability Database Sources
Grype aggregates vulnerability data from:
- National Vulnerability Database (NVD)
- GitHub Advisory Database (GHSA)
- Alpine SecDB
- Amazon Linux ALAS
- Debian Security Tracker
- Red Hat RHSA/CVE
- Ubuntu CVE Tracker
- SUSE CVRF
- Oracle Linux ELSA
- Wolfi SecDB
- Chainguard SecDB
Grype Configuration for CI/CD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# .grype.yaml - CI/CD vulnerability policy
fail-on-severity: high
# Ignore specific CVEs (with justification)
ignore:
- vulnerability: CVE-2023-XXXXX
reason: "Disputed, not exploitable in our configuration"
- vulnerability: GHSA-XXXX-XXXX-XXXX
reason: "Fixed in our forked dependency, awaiting upstream merge"
- package:
name: "test-utils"
type: "npm"
reason: "Development dependency, not shipped in production image"
# Match configuration
match:
java:
using-cpes: true
python:
using-cpes: true
javascript:
using-cpes: false # Reduce false positives for npm
Interpreting Grype Output
1
2
3
4
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
lodash 4.17.20 4.17.21 npm CVE-2021-23337 Critical
express 4.17.1 4.17.3 npm CVE-2022-24999 High
openssl 1.1.1k 1.1.1l deb CVE-2021-3711 High
Each finding includes the package name, installed version, the version that fixes the vulnerability, the package type, CVE identifier, and severity rating. The --fail-on flag lets you set a severity threshold that causes the scan to return a non-zero exit code, which breaks your CI/CD pipeline on policy violations.
Complete CodePipeline Integration
Here is the complete infrastructure to build an automated SBOM pipeline on AWS. We will use CodePipeline to orchestrate the workflow: build the application, generate the SBOM with Syft, scan for vulnerabilities with Grype, sign the artifact with Cosign, push to ECR, and store the SBOM in S3.
CodeBuild buildspec.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# buildspec.yml - SBOM generation and vulnerability scanning
version: 0.2
env:
variables:
APP_NAME: "my-application"
ECR_REPO: "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app"
SBOM_BUCKET: "my-sbom-storage-bucket"
SEVERITY_THRESHOLD: "high"
parameter-store:
COSIGN_KEY: "/my-app/prod/cosign-private-key"
phases:
install:
runtime-versions:
docker: 20
commands:
# Install Syft
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Install Grype
- curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Install Cosign
- curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 -o /usr/local/bin/cosign
- chmod +x /usr/local/bin/cosign
# Update Grype vulnerability database
- grype db update
pre_build:
commands:
- echo "Logging in to Amazon ECR..."
- aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $ECR_REPO
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG="${COMMIT_HASH:-latest}"
- TIMESTAMP=$(date +%Y%m%dT%H%M%S)
build:
commands:
- echo "Building Docker image..."
- docker build -t $APP_NAME:$IMAGE_TAG .
- docker tag $APP_NAME:$IMAGE_TAG $ECR_REPO:$IMAGE_TAG
- docker tag $APP_NAME:$IMAGE_TAG $ECR_REPO:latest
post_build:
commands:
# --- SBOM Generation ---
- echo "Generating SBOM with Syft..."
- syft packages $APP_NAME:$IMAGE_TAG
-o cyclonedx-json=sbom-cyclonedx.json
-o spdx-json=sbom-spdx.json
-o table
# --- Vulnerability Scanning ---
- echo "Scanning SBOM for vulnerabilities with Grype..."
- grype sbom:sbom-cyclonedx.json
-o json --file vuln-report.json
- grype sbom:sbom-cyclonedx.json
-o table
- echo "Enforcing severity policy..."
- grype sbom:sbom-cyclonedx.json
--fail-on $SEVERITY_THRESHOLD
|| { echo "VULNERABILITY POLICY VIOLATION - blocking deployment"; exit 1; }
# --- Push Image ---
- echo "Pushing image to ECR..."
- docker push $ECR_REPO:$IMAGE_TAG
- docker push $ECR_REPO:latest
# --- Sign Image and SBOM ---
- echo "Signing container image with Cosign..."
- cosign sign --key env://COSIGN_KEY $ECR_REPO:$IMAGE_TAG
- echo "Attaching SBOM to image..."
- cosign attach sbom --sbom sbom-cyclonedx.json $ECR_REPO:$IMAGE_TAG
# --- Store SBOMs in S3 ---
- echo "Uploading SBOMs to S3..."
- aws s3 cp sbom-cyclonedx.json
s3://$SBOM_BUCKET/$APP_NAME/$TIMESTAMP/sbom-cyclonedx.json
- aws s3 cp sbom-spdx.json
s3://$SBOM_BUCKET/$APP_NAME/$TIMESTAMP/sbom-spdx.json
- aws s3 cp vuln-report.json
s3://$SBOM_BUCKET/$APP_NAME/$TIMESTAMP/vuln-report.json
# --- Summary ---
- echo "Pipeline complete."
- echo "Image: $ECR_REPO:$IMAGE_TAG"
- echo "SBOMs: s3://$SBOM_BUCKET/$APP_NAME/$TIMESTAMP/"
- VULN_CRITICAL=$(cat vuln-report.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(len([m for m in d.get('matches',[]) if m.get('vulnerability',{}).get('severity')=='Critical']))")
- VULN_HIGH=$(cat vuln-report.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(len([m for m in d.get('matches',[]) if m.get('vulnerability',{}).get('severity')=='High']))")
- echo "Vulnerabilities - Critical: $VULN_CRITICAL, High: $VULN_HIGH"
artifacts:
files:
- sbom-cyclonedx.json
- sbom-spdx.json
- vuln-report.json
discard-paths: yes
reports:
vulnerability-report:
files:
- vuln-report.json
file-format: GENERICJSON
Terraform for CodePipeline with Security Stages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# main.tf - CodePipeline with SBOM security stages
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "skynet-tf-state-prod"
key = "sbom-pipeline/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Customer = "RedTeam"
Application = "SBOM-Pipeline"
Environment = "prod"
Owner = "jon"
Costcenter = "security"
}
}
}
# --- S3 Bucket for SBOM Storage ---
resource "aws_s3_bucket" "sbom_storage" {
bucket = "redteam-sbom-storage-prod"
}
resource "aws_s3_bucket_versioning" "sbom_storage" {
bucket = aws_s3_bucket.sbom_storage.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "sbom_storage" {
bucket = aws_s3_bucket.sbom_storage.id
rule {
id = "archive-old-sboms"
status = "Enabled"
transition {
days = 90
storage_class = "STANDARD_IA"
}
transition {
days = 365
storage_class = "GLACIER"
}
expiration {
days = 2555 # 7 years for compliance retention
}
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "sbom_storage" {
bucket = aws_s3_bucket.sbom_storage.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
# --- ECR Repository ---
resource "aws_ecr_repository" "app" {
name = "my-application"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
}
}
# --- CodeBuild Project ---
resource "aws_codebuild_project" "sbom_build" {
name = "sbom-security-build"
description = "Build, generate SBOM, scan vulnerabilities, sign artifacts"
build_timeout = 30
service_role = aws_iam_role.codebuild_role.arn
artifacts {
type = "CODEPIPELINE"
}
environment {
compute_type = "BUILD_GENERAL1_MEDIUM"
image = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
type = "LINUX_CONTAINER"
privileged_mode = true
image_pull_credentials_type = "CODEBUILD"
environment_variable {
name = "ECR_REPO"
value = aws_ecr_repository.app.repository_url
}
environment_variable {
name = "SBOM_BUCKET"
value = aws_s3_bucket.sbom_storage.id
}
environment_variable {
name = "SEVERITY_THRESHOLD"
value = "high"
}
environment_variable {
name = "COSIGN_KEY"
value = "/my-app/prod/cosign-private-key"
type = "PARAMETER_STORE"
}
}
source {
type = "CODEPIPELINE"
buildspec = "buildspec.yml"
}
logs_config {
cloudwatch_logs {
group_name = "/codebuild/sbom-security-build"
stream_name = "build-log"
}
}
}
# --- CodePipeline ---
resource "aws_codepipeline" "sbom_pipeline" {
name = "sbom-security-pipeline"
role_arn = aws_iam_role.codepipeline_role.arn
artifact_store {
location = aws_s3_bucket.pipeline_artifacts.id
type = "S3"
}
stage {
name = "Source"
action {
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
version = "1"
output_artifacts = ["source_output"]
configuration = {
RepositoryName = "my-application"
BranchName = "main"
}
}
}
stage {
name = "Build-SBOM-Scan-Sign"
action {
name = "BuildAndSecurityScan"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
input_artifacts = ["source_output"]
output_artifacts = ["build_output"]
version = "1"
configuration = {
ProjectName = aws_codebuild_project.sbom_build.name
}
}
}
stage {
name = "Approval"
action {
name = "SecurityApproval"
category = "Approval"
owner = "AWS"
provider = "Manual"
version = "1"
configuration = {
CustomData = "Review SBOM and vulnerability report before deployment"
}
}
}
stage {
name = "Deploy"
action {
name = "Deploy"
category = "Deploy"
owner = "AWS"
provider = "ECS"
input_artifacts = ["build_output"]
version = "1"
configuration = {
ClusterName = "production-cluster"
ServiceName = "my-application"
}
}
}
}
# --- S3 Bucket for Pipeline Artifacts ---
resource "aws_s3_bucket" "pipeline_artifacts" {
bucket = "redteam-pipeline-artifacts-prod"
}
# --- IAM Role for CodeBuild ---
resource "aws_iam_role" "codebuild_role" {
name = "sbom-codebuild-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "codebuild.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "codebuild_policy" {
role = aws_iam_role.codebuild_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:GetBucketLocation"
]
Resource = [
"${aws_s3_bucket.sbom_storage.arn}",
"${aws_s3_bucket.sbom_storage.arn}/*",
"${aws_s3_bucket.pipeline_artifacts.arn}",
"${aws_s3_bucket.pipeline_artifacts.arn}/*"
]
},
{
Effect = "Allow"
Action = [
"ssm:GetParameter",
"ssm:GetParameters"
]
Resource = "arn:aws:ssm:us-east-1:*:parameter/my-app/*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "*"
}
]
})
}
# --- IAM Role for CodePipeline ---
resource "aws_iam_role" "codepipeline_role" {
name = "sbom-codepipeline-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "codepipeline.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "codepipeline_policy" {
role = aws_iam_role.codepipeline_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:GetBucketVersioning"
]
Resource = [
"${aws_s3_bucket.pipeline_artifacts.arn}/*"
]
},
{
Effect = "Allow"
Action = [
"codebuild:BatchGetBuilds",
"codebuild:StartBuild"
]
Resource = aws_codebuild_project.sbom_build.arn
},
{
Effect = "Allow"
Action = [
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:UploadArchive",
"codecommit:GetUploadArchiveStatus",
"codecommit:CancelUploadArchive"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:DescribeTasks",
"ecs:ListTasks",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService"
]
Resource = "*"
}
]
})
}
Cosign: Signing SBOMs and Container Images
Cosign from the Sigstore project provides cryptographic signing for container images and SBOMs. Signing establishes provenance — proof that your artifacts were produced by your pipeline and have not been tampered with.
Generating a Cosign Key Pair
1
2
3
4
5
6
7
8
9
10
11
12
# Generate a key pair
cosign generate-key-pair
# Store the private key in SSM Parameter Store
aws ssm put-parameter \
--name "/my-app/prod/cosign-private-key" \
--value "$(cat cosign.key)" \
--type "SecureString" \
--tier "Standard"
# Store the public key in a known location for verification
aws s3 cp cosign.pub s3://redteam-sbom-storage-prod/keys/cosign.pub
Signing Workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Sign a container image
cosign sign --key cosign.key $ECR_REPO:$IMAGE_TAG
# Attach the SBOM as a cosign attestation
cosign attest --key cosign.key \
--predicate sbom-cyclonedx.json \
--type cyclonedx \
$ECR_REPO:$IMAGE_TAG
# Verify the signature (consumer side)
cosign verify --key cosign.pub $ECR_REPO:$IMAGE_TAG
# Verify the SBOM attestation
cosign verify-attestation --key cosign.pub \
--type cyclonedx \
$ECR_REPO:$IMAGE_TAG
Keyless Signing with Sigstore
For open-source projects or environments where key management is a burden, Cosign supports keyless signing via the Sigstore transparency log (Rekor):
1
2
3
4
5
# Keyless sign (uses OIDC identity)
COSIGN_EXPERIMENTAL=1 cosign sign $ECR_REPO:$IMAGE_TAG
# Keyless verify (checks Rekor transparency log)
COSIGN_EXPERIMENTAL=1 cosign verify $ECR_REPO:$IMAGE_TAG
This ties the signature to your CI/CD identity (e.g., an AWS IAM role or GitHub Actions OIDC token) without managing private keys.
Comparison: AWS Inspector vs Syft/Grype
| Capability | AWS Inspector | Syft + Grype |
|---|---|---|
| SBOM formats | CycloneDX 1.4, SPDX 2.3 | CycloneDX 1.6, SPDX 3.0, plus 20+ output formats |
| Scan targets | EC2, Lambda, ECR images | Any container image, filesystem, archive, binary, source directory |
| Vulnerability DBs | AWS-curated database | NVD, GHSA, Alpine, Debian, RHEL, Ubuntu, SUSE, Oracle, Amazon, Wolfi, Chainguard |
| CI/CD integration | Via API/CLI | Native CLI, exit codes, SARIF output, GitHub Actions |
| Artifact signing | Not included | Cosign integration |
| Cost | Per-assessment pricing | Free (open source) |
| Portability | AWS only | Any environment (local, GitHub Actions, GitLab, Jenkins, AWS, GCP, Azure) |
| Scan depth | Standard package detection | All image layers, binary analysis, compiled dependency extraction |
| VEX support | Limited | Native CycloneDX VEX, Grype ignore rules |
| Offline mode | No | Yes (cached vulnerability database) |
| Customization | Limited | Full configuration via YAML, custom matchers, exclusion rules |
Verdict: Use AWS Inspector as your always-on baseline for monitored resources. Use Syft and Grype as your CI/CD pipeline tool for deep scanning, multi-format output, signing, and cross-cloud portability. They are complementary, not competing.
Automated Compliance Reporting with S3 and Athena
SBOMs stored in S3 become a queryable compliance database when combined with Amazon Athena. This lets you answer questions like “which production images contain log4j?” across your entire fleet.
Python Script for SBOM Analysis and Reporting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#!/usr/bin/env python3
"""
sbom_compliance_reporter.py
Analyzes SBOMs stored in S3 and generates compliance reports.
Queries vulnerability data across all applications and versions.
"""
import boto3
import json
import csv
import io
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from dataclasses import dataclass, field
@dataclass
class VulnerabilityFinding:
"""Represents a single vulnerability finding from Grype output."""
app_name: str
image_tag: str
scan_date: str
package_name: str
installed_version: str
fixed_version: str
vulnerability_id: str
severity: str
description: str = ""
@dataclass
class ComplianceReport:
"""Aggregated compliance report across all applications."""
generated_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
total_applications: int = 0
total_sboms: int = 0
total_vulnerabilities: int = 0
critical_count: int = 0
high_count: int = 0
medium_count: int = 0
low_count: int = 0
applications_with_critical: List[str] = field(default_factory=list)
findings: List[VulnerabilityFinding] = field(default_factory=list)
class SBOMComplianceReporter:
"""Generates compliance reports from SBOMs stored in S3."""
def __init__(self, bucket_name: str, region: str = "us-east-1"):
self.s3 = boto3.client("s3", region_name=region)
self.bucket = bucket_name
def list_applications(self) -> List[str]:
"""List all applications with SBOMs in the bucket."""
response = self.s3.list_objects_v2(
Bucket=self.bucket,
Delimiter="/"
)
return [
prefix["Prefix"].rstrip("/")
for prefix in response.get("CommonPrefixes", [])
]
def get_latest_sbom(self, app_name: str) -> Optional[Dict]:
"""Retrieve the most recent SBOM for an application."""
response = self.s3.list_objects_v2(
Bucket=self.bucket,
Prefix=f"{app_name}/",
Delimiter="/"
)
# Get the latest timestamp directory
prefixes = sorted(
[p["Prefix"] for p in response.get("CommonPrefixes", [])],
reverse=True
)
if not prefixes:
return None
latest_prefix = prefixes[0]
# Download the CycloneDX SBOM
try:
obj = self.s3.get_object(
Bucket=self.bucket,
Key=f"{latest_prefix}sbom-cyclonedx.json"
)
return json.loads(obj["Body"].read().decode("utf-8"))
except self.s3.exceptions.NoSuchKey:
return None
def get_latest_vuln_report(self, app_name: str) -> Optional[Dict]:
"""Retrieve the most recent vulnerability report."""
response = self.s3.list_objects_v2(
Bucket=self.bucket,
Prefix=f"{app_name}/",
Delimiter="/"
)
prefixes = sorted(
[p["Prefix"] for p in response.get("CommonPrefixes", [])],
reverse=True
)
if not prefixes:
return None
latest_prefix = prefixes[0]
try:
obj = self.s3.get_object(
Bucket=self.bucket,
Key=f"{latest_prefix}vuln-report.json"
)
return json.loads(obj["Body"].read().decode("utf-8"))
except self.s3.exceptions.NoSuchKey:
return None
def generate_compliance_report(self) -> ComplianceReport:
"""Generate a full compliance report across all applications."""
report = ComplianceReport()
applications = self.list_applications()
report.total_applications = len(applications)
for app_name in applications:
sbom = self.get_latest_sbom(app_name)
if sbom:
report.total_sboms += 1
vuln_data = self.get_latest_vuln_report(app_name)
if not vuln_data:
continue
matches = vuln_data.get("matches", [])
has_critical = False
for match in matches:
vuln = match.get("vulnerability", {})
artifact = match.get("artifact", {})
severity = vuln.get("severity", "Unknown")
finding = VulnerabilityFinding(
app_name=app_name,
image_tag=vuln_data.get("source", {})
.get("target", {})
.get("tags", ["unknown"])[0],
scan_date=vuln_data.get("descriptor", {})
.get("timestamp", ""),
package_name=artifact.get("name", ""),
installed_version=artifact.get("version", ""),
fixed_version=vuln.get("fix", {})
.get("versions", ["N/A"])[0]
if vuln.get("fix", {}).get("versions")
else "N/A",
vulnerability_id=vuln.get("id", ""),
severity=severity,
description=vuln.get("description", "")[:200],
)
report.findings.append(finding)
report.total_vulnerabilities += 1
if severity == "Critical":
report.critical_count += 1
has_critical = True
elif severity == "High":
report.high_count += 1
elif severity == "Medium":
report.medium_count += 1
elif severity == "Low":
report.low_count += 1
if has_critical:
report.applications_with_critical.append(app_name)
return report
def export_csv(self, report: ComplianceReport) -> str:
"""Export findings as CSV for stakeholder reporting."""
output = io.StringIO()
writer = csv.writer(output)
writer.writerow([
"Application", "Image Tag", "Scan Date",
"Package", "Installed Version", "Fixed Version",
"Vulnerability ID", "Severity"
])
for finding in report.findings:
writer.writerow([
finding.app_name,
finding.image_tag,
finding.scan_date,
finding.package_name,
finding.installed_version,
finding.fixed_version,
finding.vulnerability_id,
finding.severity,
])
return output.getvalue()
def upload_report(self, report: ComplianceReport) -> str:
"""Upload compliance report to S3."""
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%S")
key = f"compliance-reports/{timestamp}/report.json"
self.s3.put_object(
Bucket=self.bucket,
Key=key,
Body=json.dumps({
"generated_at": report.generated_at,
"summary": {
"total_applications": report.total_applications,
"total_sboms": report.total_sboms,
"total_vulnerabilities": report.total_vulnerabilities,
"critical": report.critical_count,
"high": report.high_count,
"medium": report.medium_count,
"low": report.low_count,
"applications_with_critical":
report.applications_with_critical,
},
"findings": [
{
"app": f.app_name,
"package": f.package_name,
"version": f.installed_version,
"fixed": f.fixed_version,
"cve": f.vulnerability_id,
"severity": f.severity,
}
for f in report.findings
],
}, indent=2),
ContentType="application/json",
)
# Also upload CSV
csv_key = f"compliance-reports/{timestamp}/report.csv"
self.s3.put_object(
Bucket=self.bucket,
Key=csv_key,
Body=self.export_csv(report),
ContentType="text/csv",
)
return f"s3://{self.bucket}/{key}"
if __name__ == "__main__":
reporter = SBOMComplianceReporter(
bucket_name="redteam-sbom-storage-prod"
)
print("Generating compliance report...")
report = reporter.generate_compliance_report()
print(f"\n--- SBOM Compliance Summary ---")
print(f"Applications scanned: {report.total_applications}")
print(f"SBOMs on file: {report.total_sboms}")
print(f"Total vulnerabilities: {report.total_vulnerabilities}")
print(f" Critical: {report.critical_count}")
print(f" High: {report.high_count}")
print(f" Medium: {report.medium_count}")
print(f" Low: {report.low_count}")
if report.applications_with_critical:
print(f"\nApplications with CRITICAL vulnerabilities:")
for app in report.applications_with_critical:
print(f" - {app}")
location = reporter.upload_report(report)
print(f"\nReport uploaded to: {location}")
Athena Table for SBOM Queries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- Create an Athena table over SBOM data stored in S3
CREATE EXTERNAL TABLE IF NOT EXISTS sbom_vulnerabilities (
app_name STRING,
package_name STRING,
installed_version STRING,
fixed_version STRING,
vulnerability_id STRING,
severity STRING,
scan_date STRING
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://redteam-sbom-storage-prod/compliance-reports/'
TBLPROPERTIES ('has_encrypted_data'='true');
-- Find all critical vulnerabilities across your fleet
SELECT app_name, package_name, vulnerability_id, installed_version, fixed_version
FROM sbom_vulnerabilities
WHERE severity = 'Critical'
ORDER BY app_name;
-- Check if any application ships a specific vulnerable package
SELECT app_name, installed_version, vulnerability_id
FROM sbom_vulnerabilities
WHERE package_name = 'log4j-core'
AND severity IN ('Critical', 'High');
-- Compliance dashboard: vulnerability counts by application
SELECT
app_name,
COUNT(*) AS total_vulns,
SUM(CASE WHEN severity = 'Critical' THEN 1 ELSE 0 END) AS critical,
SUM(CASE WHEN severity = 'High' THEN 1 ELSE 0 END) AS high,
SUM(CASE WHEN severity = 'Medium' THEN 1 ELSE 0 END) AS medium
FROM sbom_vulnerabilities
GROUP BY app_name
ORDER BY critical DESC, high DESC;
Best Practices for SBOM Lifecycle Management
1. Generate SBOMs at Every Build
Do not treat SBOMs as a one-time artifact. Every CI/CD build should produce a fresh SBOM. Vulnerability databases are updated daily — an SBOM generated last week may miss newly disclosed CVEs.
2. Store SBOMs with Retention Policies
Regulatory frameworks require SBOM retention for auditing. The CRA requires technical documentation (including SBOMs) to be available for the product’s expected lifetime plus five years. Use S3 lifecycle policies to tier storage costs while maintaining compliance:
- 0-90 days: S3 Standard (frequent access for active development)
- 90-365 days: S3 Standard-IA (infrequent access, still queryable)
- 1-7 years: S3 Glacier (archive for regulatory retention)
3. Sign Everything
An unsigned SBOM has no provenance guarantee. Use Cosign to sign both the container image and the SBOM attestation. This creates a cryptographic chain: the SBOM was generated by your pipeline, for this specific image, at this specific time.
4. Scan Continuously, Not Just at Build Time
New vulnerabilities are disclosed daily. Implement a scheduled Lambda function that re-scans stored SBOMs against the latest Grype database:
1
2
3
4
5
6
# Cron job: re-scan all stored SBOMs weekly
grype db update
for sbom in $(aws s3 ls s3://redteam-sbom-storage-prod/ --recursive | grep sbom-cyclonedx.json); do
aws s3 cp "s3://redteam-sbom-storage-prod/$sbom" /tmp/sbom.json
grype sbom:/tmp/sbom.json --fail-on critical -o json --file "/tmp/rescan-$(basename $(dirname $sbom)).json"
done
5. Establish a Vulnerability Response SLA
Define clear SLAs for vulnerability remediation by severity:
| Severity | Response SLA | Remediation SLA |
|---|---|---|
| Critical | 4 hours | 24 hours |
| High | 24 hours | 7 days |
| Medium | 7 days | 30 days |
| Low | 30 days | Next release cycle |
6. Use VEX to Communicate Exploitability
Not every CVE is exploitable in your context. Vulnerability Exploitability eXchange (VEX) documents let you formally declare that a CVE is “not affected”, “under investigation”, or “fixed” in your deployment. CycloneDX supports VEX natively, and Grype’s ignore rules serve a similar purpose in your pipeline.
7. Monitor the SBOM for License Risk
SBOMs are not just for security. They also reveal license obligations. A single AGPL-licensed transitive dependency in a commercial product can create significant legal exposure. Use the SPDX output from Syft to audit license compliance alongside vulnerability scanning.
Implementation Roadmap
Adopting SBOM-based supply chain security does not have to be a big-bang project. Here is a phased approach:
- Week 1-2: Install Syft and Grype locally, generate SBOMs for your top three production images, review the output
- Week 3-4: Add SBOM generation to one CodeBuild project, store results in S3, set severity threshold to
criticalonly - Month 2: Roll out to all pipelines, lower threshold to
high, implement Cosign signing - Month 3: Deploy the compliance reporting script, create Athena queries, establish vulnerability SLAs
- Month 4-6: Implement continuous re-scanning, VEX workflows, and license compliance auditing
- Ongoing: Review and update SBOM policies quarterly as regulatory standards evolve
Related Articles
- AI Supply Chain Security: Model Poisoning Defense — Extends supply chain security concepts to AI/ML model pipelines
- AWS DevSecOps Pipeline Security Automation — Broader DevSecOps pipeline security patterns including SAST, DAST, and IaC scanning
- AWS Cloud Security Best Practices Implementation Guide — Foundational AWS security controls that complement supply chain security
Conclusion
Software supply chain security is no longer a nice-to-have — it is a regulatory requirement with real enforcement deadlines and substantial penalties. The EU CRA’s September 2026 vulnerability reporting obligations are less than six months away, and NIST requirements are already in effect for federal suppliers.
The path forward is clear: Syft generates comprehensive, multi-format SBOMs. Grype scans those SBOMs against the broadest vulnerability database available. Cosign signs everything for provenance. AWS CodePipeline orchestrates the workflow. S3 and Athena provide long-term storage and fleet-wide querying. And AWS Inspector serves as your always-on baseline for monitored resources.
Every tool in this stack is open source (except Inspector). You own the data, you control the pipeline, and you are not locked into any vendor. That is the way supply chain security should work.
Start with one pipeline. Generate one SBOM. Scan it. Fix what you find. Then scale.
Jon Price is a DevSecOps consultant specializing in AWS security architecture and automated compliance. Connect with him on LinkedIn to discuss supply chain security strategy for your organization.