Skip to content

Commit 553f49b

Browse files
committed
snyk-sbom implementation
1 parent 1784322 commit 553f49b

File tree

10 files changed

+684
-0
lines changed

10 files changed

+684
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Snyk-SBOM
2+
3+
This component implements a [scanner](https://github.com/smithy-security/smithy/blob/main/sdk/component/component.go)
4+
that uses the command `snyk sbom` to generate an sbom for any tech snyk supports and send it to a waiting Dependency Track.
5+
This component does not do any other processing at this time.
6+
7+
## Parser Environment variables
8+
9+
The component uses environment variables for configuration.
10+
11+
It requires the component
12+
environment variables defined [here](https://github.com/smithy-security/smithy/blob/main/sdk/README.md#component) as well
13+
as the following:
14+
15+
| Environment Variable | Type | Required | Default | Description |
16+
|--------------------------|--------|----------|------------|---------------------------------------------------------|
17+
| RAW\_OUT\_FILE\_PATH | string | yes | - | The path where to find the snyk sarif report |
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"time"
7+
8+
"github.com/go-errors/errors"
9+
10+
"github.com/smithy-security/smithy/sdk/component"
11+
12+
"github.com/smithy-security/smithy/new-components/scanners/snyk/internal/transformer"
13+
)
14+
15+
func main() {
16+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
17+
defer cancel()
18+
19+
if err := Main(ctx); err != nil {
20+
log.Fatalf("unexpected error: %v", err)
21+
}
22+
}
23+
24+
func Main(ctx context.Context, opts ...component.RunnerOption) error {
25+
opts = append(opts, component.RunnerWithComponentName("semgrep"))
26+
27+
ocsfTransformer, err := transformer.New()
28+
if err != nil {
29+
return errors.Errorf("could not create transformer: %w", err)
30+
}
31+
32+
if err := component.RunScanner(ctx, ocsfTransformer, opts...); err != nil {
33+
return errors.Errorf("could not run scanner: %w", err)
34+
}
35+
36+
return nil
37+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: snyk-sbom
2+
description: "Runs `snyk sbom` then uploads findings to Dependency-Track"
3+
type: scanner
4+
parameters:
5+
- name: snyk_token
6+
type: string
7+
value: ""
8+
- name: dependency_track_api_key
9+
type: string
10+
value: ""
11+
- name: dependency_track_api_url
12+
type: string
13+
value: ""
14+
- name: dependency_track_project_name
15+
type: string
16+
value: ""
17+
- name: dependency_track_project_uuid
18+
type: string
19+
value: ""
20+
- name: dependency_track_project_version
21+
type: string
22+
value: ""
23+
- name: http_proxy
24+
type: string
25+
value: ""
26+
- name: https_proxy
27+
type: string
28+
value: ""
29+
steps:
30+
- name: run-snyk-sbom
31+
image: components/scanners/snyk-sbom/scanner
32+
env_vars:
33+
HTTP_PROXY: "{{.parameters.http_proxy}}"
34+
HTTPS_PROXY: "{{.parameters.https_proxy}}"
35+
SNYK_INTEGRATION_VERSION: docker
36+
SNYK_INTEGRATION_NAME: smithy
37+
SNYK_TOKEN: "{{.parameters.snyk_token}}"
38+
executable: /bin/snyk
39+
args:
40+
- sbom
41+
- --prune-repeated-subdependencies
42+
- --format=cyclonedx1.4+json
43+
- --all-projects
44+
- --json-file-output={{scratchWorkspace}}/snyk-sbom.out
45+
- "{{sourceCodeWorkspace}}/"
46+
- name: upload-sbom-to-dependency-track
47+
image: "components/scanners/snyk-sbom"
48+
executable: /bin/app
49+
env_vars:
50+
RAW_OUT_FILE_PATH: "{{scratchWorkspace}}/snyk-sbom.out"
51+
DEPENDENCY_TRACK_API_KEY: "{{.parameters.dependency_track_api_key}}"
52+
DEPENDENCY_TRACK_API_URL: "{{.parameters.dependency_track_api_url}}"
53+
DEPENDENCY_TRACK_PROJECT_NAME: "{{.parameters.dependency_track_project_name}}"
54+
DEPENDENCY_TRACK_PROJECT_UUID: "{{.parameters.dependency_track_project_uuid}}"
55+
DEPENDENCY_TRACK_PROJECT_VERSION: "{{.parameters.dependency_track_project_version}}"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package transformer
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"log"
7+
"log/slog"
8+
"net/http"
9+
"os"
10+
11+
dtrack "github.com/DependencyTrack/client-go"
12+
"github.com/go-errors/errors"
13+
"github.com/google/uuid"
14+
"github.com/smithy-security/pkg/env"
15+
16+
"github.com/smithy-security/smithy/sdk/component"
17+
18+
ocsf "github.com/smithy-security/smithy/sdk/gen/ocsf_schema/v1"
19+
)
20+
21+
type (
22+
// SnykTransformerOption allows customising the transformer.
23+
SnykTransformerOption func(g *snykTransformer) error
24+
25+
snykTransformer struct {
26+
rawOutFilePath string
27+
apiKey string
28+
apiURL string
29+
projectName string
30+
projectUUID string
31+
projectVersion string
32+
debug bool
33+
dtClient dtrack.Client
34+
}
35+
)
36+
37+
// SnykTransformerWithDTClient allows customising the underlying dependency track client.
38+
func SnykTransformerWithDTClient(client *dtrack.Client) SnykTransformerOption {
39+
return func(g *snykTransformer) error {
40+
if client == nil {
41+
return errors.Errorf("invalid nil client")
42+
}
43+
g.dtClient = *client
44+
return nil
45+
}
46+
}
47+
48+
// New returns a new snyk transformer.
49+
func New(opts ...SnykTransformerOption) (*snykTransformer, error) {
50+
rawOutFilePath, err := env.GetOrDefault(
51+
"RAW_OUT_FILE_PATH",
52+
"",
53+
env.WithDefaultOnError(false),
54+
)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
dtAPIKey, err := env.GetOrDefault(
60+
"DEPENDENCY_TRACK_API_KEY",
61+
"",
62+
env.WithDefaultOnError(false),
63+
)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
dtAPIURL, err := env.GetOrDefault(
69+
"DEPENDENCY_TRACK_API_URL",
70+
"",
71+
env.WithDefaultOnError(false),
72+
)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
dtProjectName, err := env.GetOrDefault(
78+
"DEPENDENCY_TRACK_PROJECT_NAME",
79+
"",
80+
env.WithDefaultOnError(false),
81+
)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
dtProjectUUID, err := env.GetOrDefault(
87+
"DEPENDENCY_TRACK_PROJECT_UUID",
88+
"",
89+
env.WithDefaultOnError(true),
90+
)
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
dtProjectVersion, err := env.GetOrDefault(
96+
"DEPENDENCY_TRACK_PROJECT_VERSION",
97+
"",
98+
env.WithDefaultOnError(true),
99+
)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
debug, err := env.GetOrDefault(
105+
"DEBUG",
106+
"",
107+
env.WithDefaultOnError(true),
108+
)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
t := snykTransformer{
114+
rawOutFilePath: rawOutFilePath,
115+
apiKey: dtAPIKey,
116+
apiURL: dtAPIURL,
117+
projectName: dtProjectName,
118+
projectUUID: dtProjectUUID,
119+
projectVersion: dtProjectVersion,
120+
debug: len(debug) > 0,
121+
}
122+
123+
for _, opt := range opts {
124+
if err := opt(&t); err != nil {
125+
return nil, errors.Errorf("failed to apply option: %w", err)
126+
}
127+
}
128+
129+
if t.rawOutFilePath == "" {
130+
return nil, errors.New("invalid empty raw output file")
131+
}
132+
return &t, nil
133+
}
134+
135+
// Transform uploads a Cyclonedx sbom to a waiting dependency track
136+
func (g *snykTransformer) Transform(ctx context.Context) ([]*ocsf.VulnerabilityFinding, error) {
137+
logger := component.
138+
LoggerFromContext(ctx)
139+
140+
logger.Debug("preparing to upload raw snyk output...")
141+
142+
b, err := os.ReadFile(g.rawOutFilePath)
143+
if err != nil {
144+
if os.IsNotExist(err) {
145+
return nil, errors.Errorf("raw output file '%s' not found", g.rawOutFilePath)
146+
}
147+
return nil, errors.Errorf("failed to read raw output file '%s': %w", g.rawOutFilePath, err)
148+
}
149+
if err := g.sendToDtrack(ctx, b); err != nil {
150+
return nil, errors.Errorf("failed to upload raw output file '%s': %w", g.rawOutFilePath, err)
151+
}
152+
return []*ocsf.VulnerabilityFinding{}, nil
153+
}
154+
155+
func (g *snykTransformer) sendToDtrack(ctx context.Context, sbom []byte) error {
156+
client, err := dtrack.NewClient(
157+
g.apiURL,
158+
dtrack.WithHttpClient(
159+
&http.Client{Transport: &http.Transport{
160+
TLSClientConfig: &tls.Config{
161+
InsecureSkipVerify: g.debug,
162+
},
163+
},
164+
}),
165+
dtrack.WithDebug(g.debug),
166+
dtrack.WithAPIKey(g.apiKey),
167+
)
168+
if err != nil {
169+
log.Fatalf("could not instantiate client err: %#v\n", err)
170+
}
171+
172+
_, err = client.About.Get(ctx)
173+
if err != nil {
174+
return errors.Errorf("cannot connect to Dependency Track at %s, err:'%w'", g.apiURL, err)
175+
}
176+
177+
slog.Info("Connection to Dependency Track successful")
178+
179+
var tokens []string
180+
181+
token, err := g.uploadBOM(string(sbom), g.projectVersion)
182+
if err != nil {
183+
return errors.Errorf("could not upload bom to dependency track, err:%w", err)
184+
}
185+
186+
slog.Debug("upload", "token", token)
187+
tokens = append(tokens, token)
188+
return nil
189+
}
190+
191+
func (g *snykTransformer) uploadBOM(bom string, projectVersion string) (string, error) {
192+
slog.Info("uploading BOM to Dependency Track", "projectName", g.projectName,
193+
"projectVersion", projectVersion)
194+
if projectVersion == "" {
195+
projectVersion = "Unknown"
196+
}
197+
projUUID, err := uuid.Parse(g.projectUUID)
198+
if err != nil {
199+
return "", err
200+
}
201+
token, err := g.dtClient.BOM.PostBom(context.TODO(), dtrack.BOMUploadRequest{
202+
ProjectName: g.projectName,
203+
ProjectVersion: projectVersion,
204+
ProjectUUID: &projUUID,
205+
AutoCreate: true,
206+
BOM: bom,
207+
})
208+
return string(token), err
209+
}

0 commit comments

Comments
 (0)