Skip to content

Commit 05ee7bc

Browse files
committed
primer for generating components
1 parent af22c70 commit 05ee7bc

1 file changed

Lines changed: 274 additions & 0 deletions

File tree

docs/oss/generating-components.md

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
---
2+
title: "Generating Components"
3+
description: "Did you know you can create your own Smithy components using AI?"
4+
sidebar_position: 4
5+
---
6+
7+
# Generating Components
8+
9+
Smithy Security provides a variety of pre-built components for different security tools and workflows. However, you can also create your own custom components to fit your specific needs. One interesting approach to generating Smithy components is by leveraging AI tools like Cursor to help you draft the initial component definitions.
10+
11+
## The Prompt: Generate a Dependency Track Component with Cursor
12+
13+
The following prompt was used with Cursor to generate a new Smithy Security scanner component for Dependency Track:
14+
15+
```Text
16+
17+
I want you to generate a new scanner component. Please analyse the `components` folder to understand:
18+
- What components are
19+
- That they are built using an sdk and other helpful packages like pkg/env
20+
- That they are configured by a component.yaml. You can explore https://github.com/smithy-security/smithy/tree/main/smithyctl to get more context about components and their config based on the docs.
21+
22+
Once you are done, I want you to build a new dependency-track scanner that uploads a local cyclonedx SBOM to it and then returns. This has to be built using the mentioned SDK https://github.com/smithy-security/smithy/tree/main/sdk and helper packages like https://github.com/smithy-security/pkg/tree/main.I want you to think very deeply and work on each of the following steps in isolation. DO NOT work on all of them at the same time. Always ask for approval.Assume that:
23+
- You’ll find the input cyclonedx SBOM in the scratch workspace
24+
- We want minimal configuration for dependency track: I.e. API token etc based on what’s needed
25+
- You have to use GO. If there are packages to interact with cyclonedx stuff or dependency track, please use them
26+
- Always use the shown style used to build other components
27+
- Avoid useless comments. For the method signatures or exported things that’s fine.
28+
- If you are working on something new and you need a dependency, run: `go mod tidy && go mod vendor && go mod edit -toolchain=none`
29+
Steps:
30+
- create a new scanner called dependency-track and create a go.mod with go v1.23.4 for it.
31+
- Setup the internal/config package based on the needed config
32+
- Setup the internal/dependency-track client code.
33+
- Setup the internal/transformer package and: 1. Read the cyclonedx SBOM from disk; 2. Use the client to upload it to dependency-track. Do define interfaces where possible the closest to where you need them. For example, don’t define the interface to abstract the client in the client package itself. This is needed in the transformer package. I don’t expect the scanner to return any findings in this flow.
34+
- Plug everything together in the main
35+
- Create tools.go at the root and rely on uber’s go mock to generate mocks for the client interface. Output the mocks in the transformer_test package where the transformer code lives.
36+
- Now start writing unit tests for the transformer package from the transformer_test package. Use the generated mocks and comply with the available styles.
37+
- Bonus: if you can, also write unit tests for the dependency track client. Preferably using a Doer if their http client can be customised.
38+
- Bonus: use the pkg/retry package to have a retryable http client.
39+
- Write down the component.yaml and the README.
40+
41+
Again, work on each step in isolation and ask for approval.
42+
```
43+
44+
Let's go through it step by step!
45+
46+
### Step 1: Create the Scanner and go.mod
47+
48+
The first step is to create a new directory for the Dependency Track scanner component and initialize a Go module with the appropriate version.
49+
50+
It should result in a command like:
51+
```bash
52+
mkdir -p components/scanners/dependency-track
53+
cd components/scanners/dependency-track
54+
go mod init github.com/smithy-security/smithy/components/scanners/dependency-track
55+
go mod edit -go=1.23.4
56+
```
57+
58+
### Step 2: Setup the internal/config Package
59+
60+
Next, we need to create the `internal/config` package to handle the configuration parameters for the Dependency Track component, such as the API token and server URL. This package will define a struct to hold these parameters and methods to load them from environment variables or component parameters.
61+
We expect generated code similar to this:
62+
```go
63+
package config
64+
import (
65+
"github.com/smithy-security/pkg/env"
66+
)
67+
type Config struct {
68+
APIToken string
69+
ServerURL string
70+
}
71+
func LoadConfig() (*Config, error) {
72+
cfg := &Config{
73+
APIToken: env.GetString("DEPENDENCY_TRACK_API_TOKEN", ""),
74+
ServerURL: env.GetString("DEPENDENCY_TRACK_SERVER_URL", "http://localhost:8080"),
75+
}
76+
if cfg.APIToken == "" {
77+
return nil, fmt.Errorf("DEPENDENCY_TRACK_API_TOKEN is required")
78+
}
79+
return cfg, nil
80+
}
81+
```
82+
83+
### Step 3: Setup the internal/dependency-track Client Code
84+
85+
The next step is to create the `internal/dependency-track` package that will handle interactions with the Dependency Track API. This package will define a client struct and methods to upload the CycloneDX SBOM.
86+
We expect generated code similar to this:
87+
```go
88+
package dependencytrack
89+
90+
import (
91+
"context"
92+
"encoding/base64"
93+
94+
dtrack "github.com/DependencyTrack/client-go"
95+
"github.com/go-errors/errors"
96+
)
97+
98+
// Client represents a Dependency-Track API client using the official library.
99+
type Client struct {
100+
client *dtrack.Client
101+
}
102+
103+
// New creates a new Dependency-Track client using the official client-go library.
104+
func New(baseURL, apiToken string) (*Client, error) {
105+
if baseURL == "" {
106+
return nil, errors.New("base URL cannot be empty")
107+
}
108+
if apiToken == "" {
109+
return nil, errors.New("API token cannot be empty")
110+
}
111+
112+
// Create client using the official library
113+
client, err := dtrack.NewClient(baseURL, dtrack.WithAPIKey(apiToken))
114+
if err != nil {
115+
return nil, errors.Errorf("failed to create Dependency-Track client: %w", err)
116+
}
117+
118+
return &Client{
119+
client: client,
120+
}, nil
121+
}
122+
123+
// UploadSBOM uploads a CycloneDX SBOM to Dependency-Track using the official client.
124+
func (c *Client) UploadSBOM(ctx context.Context, sbomData []byte, projectName, projectVersion string) error {
125+
// Encode the SBOM data to base64 as required by the API
126+
encodedBOM := base64.StdEncoding.EncodeToString(sbomData)
127+
128+
// Upload the BOM using the official client
129+
uploadToken, err := c.client.BOM.Upload(ctx, dtrack.BOMUploadRequest{
130+
ProjectName: projectName,
131+
ProjectVersion: projectVersion,
132+
AutoCreate: true, // Automatically create project if it doesn't exist
133+
BOM: encodedBOM,
134+
})
135+
if err != nil {
136+
return errors.Errorf("failed to upload SBOM: %w", err)
137+
}
138+
139+
// The upload token can be used to track processing status if needed
140+
// For now, we just return success after upload
141+
_ = uploadToken
142+
143+
return nil
144+
}
145+
146+
```
147+
### Step 4: Setup the internal/transformer Package
148+
The `internal/transformer` package will contain the logic to read the CycloneDX SBOM from disk and use the Dependency Track client to upload it.
149+
We expect generated code similar to this:
150+
```go
151+
package transformer
152+
import (
153+
"io/ioutil"
154+
"path/filepath"
155+
"github.com/smithy-security/smithy/components/scanners/dependency-track/internal/config"
156+
"github.com/smithy-security/smithy/components/scanners/dependency-track/internal/dependencytrack"
157+
)
158+
type Transformer struct {
159+
client *dependencytrack.Client
160+
sbomPath string
161+
}
162+
func NewTransformer(cfg *config.Config, sbomPath string) *Transformer {
163+
client := dependencytrack.NewClient(cfg.APIToken, cfg.ServerURL)
164+
return &Transformer{client: client, sbomPath: sbomPath}
165+
}
166+
func (t *Transformer) UploadSBOM() error {
167+
sbom, err := ioutil.ReadFile(t.sbomPath)
168+
if err != nil { return err }
169+
return t.client.UploadSBOM(sbom)
170+
}
171+
```
172+
173+
### Step 5: Plug Everything Together in main.go
174+
175+
The `main.go` file will tie everything together by loading the configuration, creating the transformer, and invoking the upload process.
176+
We expect generated code similar to this:
177+
178+
```go
179+
package main
180+
import (
181+
"log"
182+
"path/filepath"
183+
"github.com/smithy-security/smithy/components/scanners/dependency-track/internal/config"
184+
"github.com/smithy-security/smithy/components/scanners/dependency-track/internal/transformer"
185+
)
186+
func main() {
187+
cfg, err := config.LoadConfig()
188+
if err != nil { log.Fatalf("failed to load config: %v", err) }
189+
sbomPath := filepath.Join(env.GetString("SCRATCH_WORKSPACE", "/tmp"), "sbom.cyclonedx.json")
190+
transformer := transformer.NewTransformer(cfg, sbomPath)
191+
if err := transformer.UploadSBOM(); err != nil {
192+
log.Fatalf("failed to upload SBOM: %v", err)
193+
}
194+
log.Println("SBOM uploaded successfully")
195+
}
196+
```
197+
198+
### Step 6: Create tools.go for Mocks
199+
At this state we can likely stop and just create a component.yaml and README. However, to ensure we are following good practices(and since it's free and easy to do and a requirement for getting components merged in the registry) we should provide some unit tests. For that we need to create a `tools.go` file at the root of the component to generate mocks for the Dependency Track client using Uber's GoMock.
200+
201+
The `tools.go` file will include the necessary directives to generate mocks for the Dependency Track client using Uber's GoMock.
202+
We expect generated code similar to this:
203+
204+
```go
205+
// +build tools
206+
package main
207+
import (
208+
_ "github.com/golang/mock/mockgen"
209+
)
210+
```
211+
### Step 7: Write Unit Tests for the transformer Package
212+
213+
Finally, we can write unit tests for the `transformer` package using the generated mocks to ensure that our upload logic works as expected.
214+
We expect generated code similar to this:
215+
216+
```go
217+
package transformer_test
218+
import (
219+
"testing"
220+
"github.com/golang/mock/gomock"
221+
"github.com/smithy-security/smithy/components/scanners/dependency-track/internal/transformer"
222+
"github.com/smithy-security/smithy/components/scanners/dependency-track/internal/dependencytrack/mocks"
223+
)
224+
func TestUploadSBOM(t *testing.T) {
225+
ctrl := gomock.NewController(t)
226+
defer ctrl.Finish()
227+
mockClient := mocks.NewMockClient(ctrl)
228+
transformer := &transformer.Transformer{
229+
client: mockClient,
230+
sbomPath: "testdata/valid_sbom.json",
231+
}
232+
mockClient.EXPECT().UploadSBOM(gomock.Any()).Return(nil)
233+
if err := transformer.UploadSBOM(); err != nil {
234+
t.Fatalf("expected no error, got %v", err)
235+
}
236+
}
237+
```
238+
239+
### Step 8: Write the component.yaml and README
240+
241+
Finally, we need to create the `component.yaml` file to define the Dependency Track scanner component and a `README.md` file to document its usage.
242+
243+
The `component.yaml` file will define the component's metadata, parameters, and steps to execute the scanner.
244+
245+
```yaml
246+
# dependency-track/component.yaml
247+
name: dependency-track
248+
description: "Uploads a CycloneDX SBOM to Dependency Track"
249+
type: scanner
250+
parameters:
251+
- name: api_token
252+
type: string
253+
value: "" # API token for Dependency Track
254+
- name: server_url
255+
type: string
256+
value: "http://localhost:8080" # Dependency Track server URL
257+
steps:
258+
- name: upload-sbom
259+
image: components/scanners/dependency-track
260+
executable: /app/dependency-track
261+
```
262+
263+
And we're done! By following these steps, we have successfully created a new Smithy Security scanner component for Dependency Track using AI assistance from Cursor. This component can now be used in Smithy workflows to upload CycloneDX SBOMs to a Dependency Track server.
264+
265+
266+
We can and should further enhance the component by adding more features, error handling, and configuration options as needed. e.g. adding retries using the `pkg/retry` package for the HTTP client.
267+
But this is just the beginning! As we continue to develop and improve the component, we can leverage the power of AI assistance to streamline our workflow and enhance our productivity.
268+
269+
*Hint:*
270+
You can explore other components in the [Smithy Security GitHub repository](https://github.com/smithy-security/smithy/components).
271+
272+
You can also refer to the [Smithy Security SDK documentation](https://docs.smithy.security/) for more information on building custom components and workflows.
273+
274+
This component is very useful for users of Dependency Track who do not want SBOM generation from CDXGEN or other tools but already have SBOMs generated from other parts of their toolchain and just want to upload them to Dependency Track for analysis. If you want to use cdxgen to generate SBOMs and upload them to Dependency Track, you can use the existing [cdxgen component](https://github.com/smithy-security/smithy/components/tree/main/components/scanners/cdxgen).

0 commit comments

Comments
 (0)