Skip to content

Commit d9247c3

Browse files
authored
chore(feature/go): Trivy Integration (#55)
* chore(feature/go): Trivy Integration Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix(debug): extract from wrapper function Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix(debug): extract json from zip Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix(debug): remove debug print of sbom Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix(aws): Some inputs are not longer required Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix(aws): Some inputs are not longer required Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: add trivy to config validation Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: add trivy to config validation Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: add trivy to config validation Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: ecr auth Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: trivy clickhouse table name Signed-off-by: Julio Jimenez <julio@clickhouse.com> * feat: ability to do application scope reports Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: i don't think org uuid is always required Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: if no projectUuids are provided Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: mend-project-uuids Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: maxDepthLevel Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: maxDepthLevel Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: maxDepthLevel Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: maxDepthLevel Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: maxDepthLevel Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: maxDepthLevel Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: maxDepthLevel Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: stuff Signed-off-by: Julio Jimenez <julio@clickhouse.com> * feat: add merge Signed-off-by: Julio Jimenez <julio@clickhouse.com> * feat: add merge Signed-off-by: Julio Jimenez <julio@clickhouse.com> * feat: add merge Signed-off-by: Julio Jimenez <julio@clickhouse.com> * feat: add merge Signed-off-by: Julio Jimenez <julio@clickhouse.com> * fix: lint Signed-off-by: Julio Jimenez <julio@clickhouse.com> --------- Signed-off-by: Julio Jimenez <julio@clickhouse.com>
1 parent 5b3636d commit d9247c3

File tree

15 files changed

+790
-173
lines changed

15 files changed

+790
-173
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ repos:
6565

6666
- id: go-cyclo
6767
name: Check cyclomatic complexity
68-
entry: gocyclo -over 25 .
68+
entry: gocyclo -over 26 .
6969
language: system
7070
pass_filenames: false
7171
files: \.go$

Dockerfile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# hadolint global ignore=DL3047,DL4001
1+
# hadolint global ignore=DL3047,DL4001,DL4006
22
# Multi-stage build for Go application
33
FROM golang:1.25.3-alpine3.22 AS builder
44

@@ -44,6 +44,12 @@ RUN apk add --no-cache curl unzip && \
4444
RUN wget -O /cyclonedx "https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.27.2/cyclonedx-linux-x64" && \
4545
chmod +x /cyclonedx
4646

47+
# Install Trivy
48+
# Download the static binary directly since we're using distroless
49+
RUN TRIVY_VERSION=$(wget -qO- "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') && \
50+
wget -qO- "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" | tar -xzf - -C /usr/local/bin trivy && \
51+
chmod +x /usr/local/bin/trivy
52+
4753
# Runtime stage - Distroless
4854
FROM gcr.io/distroless/static-debian12:nonroot
4955

@@ -56,6 +62,7 @@ LABEL maintainer="ClickHouse Security Team" \
5662
COPY --from=tools /usr/local/aws-cli /usr/local/aws-cli
5763
COPY --from=tools /usr/local/bin/aws /usr/local/bin/aws
5864
COPY --from=tools /cyclonedx /usr/local/bin/cyclonedx
65+
COPY --from=tools /usr/local/bin/trivy /usr/local/bin/trivy
5966

6067
# Copy the binary from builder
6168
COPY --from=builder /build/clickbom /app/clickbom
@@ -69,7 +76,8 @@ WORKDIR /app
6976
# distroless runs as nonroot user by default (UID 65532)
7077
# Set environment
7178
ENV PATH="/usr/local/bin:$PATH" \
72-
TEMP_DIR="/tmp"
79+
TEMP_DIR="/tmp" \
80+
TRIVY_CACHE_DIR="/tmp/.trivy"
7381

7482
# Run the application
7583
ENTRYPOINT ["/app/clickbom"]

action.yml

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: 'ClickBOM'
2-
description: 'Download SBOMs from GitHub, Mend, and Wiz. Convert to CycloneDX and SPDX formats. Upload to S3 and ClickHouse.'
2+
description: 'Download SBOMs from GitHub, Mend, Wiz, and Trivy. Convert to CycloneDX and SPDX formats. Upload to S3 and ClickHouse.'
33
author: 'ClickHouse, Inc.'
44
inputs:
55
# GitHub-specific inputs
@@ -59,17 +59,25 @@ inputs:
5959
wiz-report-id:
6060
description: 'Wiz report ID to download'
6161
required: false
62-
# AWS-specific inputs
63-
aws-access-key-id:
64-
description: 'AWS Access Key ID'
65-
required: true
66-
aws-secret-access-key:
67-
description: 'AWS Secret Access Key'
68-
required: true
69-
aws-region:
70-
description: 'AWS region'
62+
# Trivy-specific inputs
63+
trivy-image:
64+
description: 'Container image to scan with Trivy for SBOM generation (format: registry/repo:tag or ECR URI)'
65+
required: false
66+
trivy-ecr-account-id:
67+
description: 'AWS Account ID where ECR repository is located (for cross-account access)'
68+
required: false
69+
trivy-ecr-region:
70+
description: 'AWS region where ECR repository is located'
7171
required: false
7272
default: 'us-east-1'
73+
trivy-ecr-role-arn:
74+
description: 'IAM role ARN to assume for ECR access (for cross-account)'
75+
required: false
76+
trivy-format:
77+
description: 'Trivy SBOM output format: cyclonedx or spdxjson'
78+
required: false
79+
default: 'cyclonedx'
80+
# AWS-specific inputs
7381
s3-bucket:
7482
description: 'S3 bucket name'
7583
required: true
@@ -103,7 +111,7 @@ inputs:
103111
default: 'false'
104112
# General inputs
105113
sbom-source:
106-
description: 'SBOM source: github or mend'
114+
description: 'SBOM source: github, mend, wiz, or trivy'
107115
required: false
108116
default: 'github'
109117
sbom-format:
@@ -150,10 +158,13 @@ runs:
150158
WIZ_CLIENT_ID: ${{ inputs.wiz-client-id }}
151159
WIZ_CLIENT_SECRET: ${{ inputs.wiz-client-secret }}
152160
WIZ_REPORT_ID: ${{ inputs.wiz-report-id }}
161+
# Trivy-specific
162+
TRIVY_IMAGE: ${{ inputs.trivy-image }}
163+
TRIVY_ECR_ACCOUNT_ID: ${{ inputs.trivy-ecr-account-id }}
164+
TRIVY_ECR_REGION: ${{ inputs.trivy-ecr-region }}
165+
TRIVY_ECR_ROLE_ARN: ${{ inputs.trivy-ecr-role-arn }}
166+
TRIVY_FORMAT: ${{ inputs.trivy-format }}
153167
# AWS-specific
154-
AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }}
155-
AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }}
156-
AWS_DEFAULT_REGION: ${{ inputs.aws-region }}
157168
S3_BUCKET: ${{ inputs.s3-bucket }}
158169
S3_KEY: ${{ inputs.s3-key }}
159170
# ClickHouse-specific

cmd/clickbom/main.go

Lines changed: 139 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"os"
8+
"path"
89
"path/filepath"
910
"strings"
1011

@@ -45,7 +46,7 @@ func run() error {
4546
}()
4647

4748
// Initialize S3 client
48-
s3Client, err := storage.NewS3Client(ctx, cfg.AWSAccessKeyID, cfg.AWSSecretAccessKey, cfg.AWSRegion)
49+
s3Client, err := storage.NewS3Client(ctx)
4950
if err != nil {
5051
return fmt.Errorf("failed to create S3 client: %w", err)
5152
}
@@ -64,7 +65,7 @@ func handleNormalMode(ctx context.Context, cfg *config.Config, s3Client *storage
6465
extractedSBOM := filepath.Join(tempDir, "extracted_sbom.json")
6566
processedSBOM := filepath.Join(tempDir, "processed_sbom.json")
6667

67-
// Download SBOM based on source
68+
// Download/Generate SBOM based on source
6869
switch cfg.SBOMSource {
6970
case "github":
7071
logger.Info("Downloading SBOM from GitHub")
@@ -87,11 +88,21 @@ func handleNormalMode(ctx context.Context, cfg *config.Config, s3Client *storage
8788
return fmt.Errorf("failed to download Wiz SBOM: %w", err)
8889
}
8990

91+
case "trivy":
92+
logger.Info("Generating SBOM with Trivy")
93+
trivyClient, err := sbom.NewTrivyClient(ctx, cfg)
94+
if err != nil {
95+
return fmt.Errorf("failed to create Trivy client: %w", err)
96+
}
97+
if err := trivyClient.GenerateSBOM(ctx, originalSBOM); err != nil {
98+
return fmt.Errorf("failed to generate SBOM with Trivy: %w", err)
99+
}
100+
90101
default:
91102
return fmt.Errorf("unsupported SBOM source: %s", cfg.SBOMSource)
92103
}
93104

94-
// Extract from wrapper if needed
105+
// Extract SBOM from wrapper if needed (mainly for GitHub)
95106
if err := sbom.ExtractSBOMFromWrapper(originalSBOM, extractedSBOM); err != nil {
96107
return fmt.Errorf("failed to extract SBOM: %w", err)
97108
}
@@ -103,9 +114,9 @@ func handleNormalMode(ctx context.Context, cfg *config.Config, s3Client *storage
103114
}
104115
logger.Info("Detected SBOM format: %s", detectedFormat)
105116

106-
// Convert to desired format
107-
targetFormat := sbom.Format(cfg.SBOMFormat)
108-
if err := sbom.ConvertSBOM(extractedSBOM, processedSBOM, detectedFormat, targetFormat); err != nil {
117+
// Convert to desired format if needed
118+
desiredFormat := sbom.Format(cfg.SBOMFormat)
119+
if err := sbom.ConvertSBOM(extractedSBOM, processedSBOM, detectedFormat, desiredFormat); err != nil {
109120
return fmt.Errorf("failed to convert SBOM: %w", err)
110121
}
111122

@@ -115,28 +126,133 @@ func handleNormalMode(ctx context.Context, cfg *config.Config, s3Client *storage
115126
}
116127

117128
logger.Success("SBOM processing completed successfully!")
118-
logger.Info("SBOM available at: s3://%s/%s", cfg.S3Bucket, cfg.S3Key)
119129

120-
// ClickHouse operations
130+
// ClickHouse upload if configured
121131
if cfg.ClickHouseURL != "" {
122-
if err := handleClickHouse(ctx, cfg, processedSBOM); err != nil {
123-
return fmt.Errorf("ClickHouse error: %w", err)
132+
logger.Info("Uploading SBOM data to ClickHouse")
133+
134+
chClient, err := storage.NewClickHouseClient(cfg)
135+
if err != nil {
136+
return fmt.Errorf("failed to create ClickHouse client: %w", err)
137+
}
138+
139+
tableName := generateTableName(cfg)
140+
141+
if err := chClient.SetupTable(ctx, tableName); err != nil {
142+
return fmt.Errorf("failed to setup table: %w", err)
124143
}
144+
145+
if err := chClient.InsertSBOMData(ctx, processedSBOM, tableName, cfg.SBOMFormat); err != nil {
146+
return fmt.Errorf("failed to upload to ClickHouse: %w", err)
147+
}
148+
149+
logger.Success("ClickHouse operations completed successfully!")
125150
}
126151

127152
return nil
128153
}
129154

130-
func handleMergeMode(_ context.Context, _ *config.Config, _ *storage.S3Client, _ string) error {
155+
func handleMergeMode(ctx context.Context, cfg *config.Config, s3Client *storage.S3Client, tempDir string) error {
131156
logger.Info("Running in MERGE mode - merging all CycloneDX SBOMs from S3")
132157

133-
// Implementation for merge mode...
134-
// This would involve downloading all SBOMs from S3, merging them, and uploading
158+
// Create download directory
159+
downloadDir := filepath.Join(tempDir, "downloads")
160+
if err := os.MkdirAll(downloadDir, 0755); err != nil {
161+
return fmt.Errorf("failed to create download directory: %w", err)
162+
}
163+
164+
// Download all files from S3
165+
downloadedFiles, err := s3Client.DownloadAll(ctx, cfg.S3Bucket, "", downloadDir)
166+
if err != nil {
167+
return fmt.Errorf("failed to download files from S3: %w", err)
168+
}
169+
170+
logger.Info("Downloaded %d files from S3", len(downloadedFiles))
171+
172+
if len(downloadedFiles) == 0 {
173+
return fmt.Errorf("no files found in S3 bucket: %s", cfg.S3Bucket)
174+
}
175+
176+
// Filter and validate CycloneDX SBOMs
177+
cyclonedxFiles := make([]string, 0)
178+
179+
for _, file := range downloadedFiles {
180+
filename := filepath.Base(file)
181+
182+
// Apply include/exclude filters
183+
if !sbom.ShouldIncludeFile(filename, cfg.Include, cfg.Exclude) {
184+
logger.Debug("Skipping %s due to include/exclude filters", filename)
185+
continue
186+
}
187+
188+
// Check if file is valid CycloneDX
189+
format, err := sbom.DetectSBOMFormat(file)
190+
if err != nil {
191+
logger.Warning("Failed to detect format for %s: %v", filename, err)
192+
continue
193+
}
194+
195+
if format != sbom.FormatCycloneDX {
196+
logger.Debug("Skipping %s: not CycloneDX format (detected: %s)", filename, format)
197+
continue
198+
}
199+
200+
cyclonedxFiles = append(cyclonedxFiles, file)
201+
logger.Debug("Added %s to merge list", filename)
202+
}
203+
204+
logger.Info("Found %d valid CycloneDX SBOMs to merge", len(cyclonedxFiles))
205+
206+
if len(cyclonedxFiles) == 0 {
207+
return fmt.Errorf("no valid CycloneDX SBOMs found after filtering")
208+
}
209+
210+
// Merge all SBOMs
211+
mergedSBOM := filepath.Join(tempDir, "merged_sbom.json")
212+
if err := sbom.MergeSBOMs(cyclonedxFiles, mergedSBOM); err != nil {
213+
return fmt.Errorf("failed to merge SBOMs: %w", err)
214+
}
215+
216+
// Convert to desired format if needed
217+
finalSBOM := filepath.Join(tempDir, "final_sbom.json")
218+
desiredFormat := sbom.Format(cfg.SBOMFormat)
219+
if err := sbom.ConvertSBOM(mergedSBOM, finalSBOM, sbom.FormatCycloneDX, desiredFormat); err != nil {
220+
return fmt.Errorf("failed to convert merged SBOM: %w", err)
221+
}
222+
223+
// Upload merged SBOM back to S3
224+
if err := s3Client.Upload(ctx, finalSBOM, cfg.S3Bucket, cfg.S3Key, cfg.SBOMFormat); err != nil {
225+
return fmt.Errorf("failed to upload merged SBOM: %w", err)
226+
}
227+
228+
logger.Success("SBOM merging and upload completed successfully!")
229+
230+
// ClickHouse upload if configured
231+
if cfg.ClickHouseURL != "" {
232+
logger.Info("Uploading merged SBOM data to ClickHouse")
233+
234+
chClient, err := storage.NewClickHouseClient(cfg)
235+
if err != nil {
236+
return fmt.Errorf("failed to create ClickHouse client: %w", err)
237+
}
238+
239+
tableName := generateTableName(cfg)
240+
241+
if err := chClient.SetupTable(ctx, tableName); err != nil {
242+
return fmt.Errorf("failed to setup table: %w", err)
243+
}
244+
245+
if err := chClient.InsertSBOMData(ctx, finalSBOM, tableName, cfg.SBOMFormat); err != nil {
246+
return fmt.Errorf("failed to upload to ClickHouse: %w", err)
247+
}
248+
249+
logger.Success("ClickHouse operations completed successfully!")
250+
}
135251

136252
return nil
137253
}
138254

139-
func handleClickHouse(ctx context.Context, cfg *config.Config, sbomFile string) error {
255+
func handleClickHouse(ctx context.Context, cfg *config.Config, sbomFile string) error { // nolint: unused
140256
logger.Info("Starting ClickHouse operations")
141257

142258
chClient, err := storage.NewClickHouseClient(cfg)
@@ -159,6 +275,10 @@ func handleClickHouse(ctx context.Context, cfg *config.Config, sbomFile string)
159275
}
160276

161277
func generateTableName(cfg *config.Config) string {
278+
if cfg.Merge {
279+
replacer := strings.NewReplacer(".", "_", "-", "_")
280+
return fmt.Sprintf("merged_%s", replacer.Replace(cfg.S3Key))
281+
}
162282
switch cfg.SBOMSource {
163283
case "github":
164284
return strings.ReplaceAll(strings.ToLower(cfg.Repository), "/", "_")
@@ -170,6 +290,11 @@ func generateTableName(cfg *config.Config) string {
170290
return fmt.Sprintf("mend_%s", strings.ReplaceAll(uuid, "-", "_"))
171291
case "wiz":
172292
return fmt.Sprintf("wiz_%s", strings.ReplaceAll(cfg.WizReportID, "-", "_"))
293+
case "trivy":
294+
result := path.Base(cfg.TrivyImage)
295+
replacer := strings.NewReplacer(":", "_", ".", "_", "-", "_")
296+
result = replacer.Replace(result)
297+
return fmt.Sprintf("trivy_%s", result)
173298
default:
174299
return "sbom_data"
175300
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ go 1.25.3
55
require (
66
github.com/aws/aws-sdk-go-v2 v1.39.4
77
github.com/aws/aws-sdk-go-v2/config v1.31.15
8-
github.com/aws/aws-sdk-go-v2/credentials v1.18.19
98
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.7
9+
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9
10+
github.com/google/uuid v1.6.0
1011
)
1112

1213
require (
1314
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 // indirect
15+
github.com/aws/aws-sdk-go-v2/credentials v1.18.19 // indirect
1416
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 // indirect
1517
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 // indirect
1618
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 // indirect
@@ -22,6 +24,5 @@ require (
2224
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.11 // indirect
2325
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 // indirect
2426
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 // indirect
25-
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 // indirect
2627
github.com/aws/smithy-go v1.23.1 // indirect
2728
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 h1:Ekml5vGg6sHSZLZJQJagefnVe6Pm
3434
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9/go.mod h1:/e15V+o1zFHWdH3u7lpI3rVBcxszktIKuHKCY2/py+k=
3535
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
3636
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
37+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
38+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

0 commit comments

Comments
 (0)