@@ -3,6 +3,7 @@ package main
3
3
import (
4
4
"bytes"
5
5
"context"
6
+ "encoding/json"
6
7
"fmt"
7
8
"io"
8
9
"mime/multipart"
21
22
v * viper.Viper
22
23
)
23
24
25
+ type Tag struct {
26
+ Name string `json:"name"`
27
+ }
28
+
29
+ type Project struct {
30
+ UUID string `json:"uuid,omitempty"`
31
+ Name string `json:"name"`
32
+ Classifier string `json:"classifier,omitempty"`
33
+ Version string `json:"version,omitempty"`
34
+ Description string `json:"description,omitempty"`
35
+ Active bool `json:"active,omitempty"`
36
+ Tags []Tag `json:"tags,omitempty"`
37
+ Children []Project `json:"children,omitempty"`
38
+ Parent * Project `json:"parent,omitempty"`
39
+ CollectionLogic string `json:"collectionLogic,omitempty"`
40
+ }
41
+
24
42
func main () {
25
43
rootCmd := & cobra.Command {
26
44
Use : "sbom-uploader" ,
@@ -48,27 +66,76 @@ func main() {
48
66
}
49
67
}
50
68
51
- func runUploader (cmd * cobra.Command , args []string ) error {
69
+ func createParent (dependencyTrackUrl string , dependencyTrackKey string , parentName string , tags string ) error {
70
+ url := fmt .Sprintf ("%s/api/v1/project" , strings .TrimRight (dependencyTrackUrl , "/" ))
71
+ newProject := & Project {
72
+ Name : parentName ,
73
+ Classifier : "APPLICATION" ,
74
+ CollectionLogic : "AGGREGATE_LATEST_VERSION_CHILDREN" ,
75
+ Tags : []Tag {},
76
+ }
52
77
53
- dependencyTrackUrl := v .GetString ("url" )
54
- dependencyTrackKey := v .GetString ("api-key" )
55
- projectName := v .GetString ("name" )
56
- projectVersion := v .GetString ("version" )
57
- sbomFilePath := v .GetString ("sbom" )
58
- // Check required inputs
59
- if dependencyTrackUrl == "" {
60
- return fmt .Errorf ("missing required inputs: url (via flags or env)" )
78
+ for _ , tag := range strings .Split (tags , "," ) {
79
+ newProject .Tags = append (newProject .Tags , Tag {Name : tag })
61
80
}
62
- if dependencyTrackKey == "" {
63
- return fmt .Errorf ("missing required inputs: api-key (via flags or env)" )
81
+
82
+ jsonBody , err := json .Marshal (newProject )
83
+ if err != nil {
84
+ return fmt .Errorf ("failed to marshal request body: %w" , err )
64
85
}
65
- if projectName == "" {
66
- return fmt .Errorf ("missing required inputs: name (via flags or env)" )
86
+ reqBody := bytes .NewBuffer (jsonBody )
87
+
88
+ req , err := retryablehttp .NewRequest ("PUT" , url , reqBody )
89
+ if err != nil {
90
+ return fmt .Errorf ("failed to create request: %w" , err )
67
91
}
68
- if projectVersion == "" {
69
- return fmt .Errorf ("missing required inputs: version (via flags or env)" )
92
+ req .Header .Set ("X-Api-Key" , dependencyTrackKey )
93
+ req .Header .Set ("Content-Type" , "application/json" )
94
+ retryClient := retryablehttp .NewClient ()
95
+ retryClient .RetryMax = 20
96
+ resp , err := retryClient .Do (req )
97
+ if err != nil {
98
+ return fmt .Errorf ("HTTP request failed: %w" , err )
99
+ }
100
+ defer func (Body io.ReadCloser ) {
101
+ _ = Body .Close ()
102
+ }(resp .Body )
103
+
104
+ respBody , _ := io .ReadAll (resp .Body )
105
+
106
+ print (respBody )
107
+ return nil
108
+ }
109
+
110
+ func ensureParentExists (dependencyTrackUrl string , dependencyTrackKey string , parentName string , tags string ) error {
111
+ url := fmt .Sprintf ("%s/api/v1/project/lookup?name=%s" , strings .TrimRight (dependencyTrackUrl , "/" ), parentName )
112
+ req , err := retryablehttp .NewRequest ("GET" , url , nil )
113
+ if err != nil {
114
+ return fmt .Errorf ("failed to create request: %w" , err )
115
+ }
116
+ req .Header .Set ("X-Api-Key" , dependencyTrackKey )
117
+
118
+ // Execute request
119
+ retryClient := retryablehttp .NewClient ()
120
+ retryClient .RetryMax = 20
121
+ resp , err := retryClient .Do (req )
122
+ if err != nil {
123
+ return fmt .Errorf ("HTTP request failed: %w" , err )
70
124
}
125
+ var project Project
126
+ err = json .NewDecoder (resp .Body ).Decode (& project )
127
+ if project .Name == "" {
128
+ fmt .Println ("Parent project not found... creating a new one" )
129
+ err := createParent (dependencyTrackUrl , dependencyTrackKey , parentName , tags )
130
+ if err != nil {
131
+ return err
132
+ }
133
+ }
134
+
135
+ return nil
136
+ }
71
137
138
+ func uploadSbom (dependencyTrackUrl string , dependencyTrackKey string , projectName string , parentName string , projectVersion string , sbomFilePath string , tags string ) error {
72
139
// Read SBOM from file or stdin
73
140
var sbomContent []byte
74
141
var err error
@@ -102,18 +169,14 @@ func runUploader(cmd *cobra.Command, args []string) error {
102
169
103
170
// Add metadata fields
104
171
_ = writer .WriteField ("projectName" , projectName )
172
+ _ = writer .WriteField ("parentName" , parentName )
105
173
_ = writer .WriteField ("projectVersion" , projectVersion )
106
174
_ = writer .WriteField ("autoCreate" , "true" )
175
+ _ = writer .WriteField ("tags" , tags )
107
176
108
177
if v .GetBool ("latest" ) {
109
178
_ = writer .WriteField ("isLatest" , "true" )
110
179
}
111
- if v := v .GetString ("parent" ); v != "" {
112
- _ = writer .WriteField ("parentName" , v )
113
- }
114
- if projectTags := v .GetString ("tags" ); projectTags != "" {
115
- _ = writer .WriteField ("projectTags" , projectTags )
116
- }
117
180
118
181
// Close writer to finalize body
119
182
if err := writer .Close (); err != nil {
@@ -141,10 +204,45 @@ func runUploader(cmd *cobra.Command, args []string) error {
141
204
_ = Body .Close ()
142
205
}(resp .Body )
143
206
144
- respBody , _ := io .ReadAll (resp .Body )
207
+ return nil
208
+ }
209
+
210
+ func runUploader (cmd * cobra.Command , args []string ) error {
211
+
212
+ dependencyTrackUrl := v .GetString ("url" )
213
+ dependencyTrackKey := v .GetString ("api-key" )
214
+ projectName := v .GetString ("name" )
215
+ parentName := v .GetString ("parent" )
216
+ projectVersion := v .GetString ("version" )
217
+ sbomFilePath := v .GetString ("sbom" )
218
+ // Check required inputs
219
+ if dependencyTrackUrl == "" {
220
+ return fmt .Errorf ("missing required inputs: url (via flags or env)" )
221
+ }
222
+ if dependencyTrackKey == "" {
223
+ return fmt .Errorf ("missing required inputs: api-key (via flags or env)" )
224
+ }
225
+ if projectName == "" {
226
+ return fmt .Errorf ("missing required inputs: name (via flags or env)" )
227
+ }
228
+ if parentName == "" {
229
+ return fmt .Errorf ("missing required inputs: name (via flags or env)" )
230
+ }
231
+ if projectVersion == "" {
232
+ return fmt .Errorf ("missing required inputs: version (via flags or env)" )
233
+ }
234
+ tags := ""
235
+ if projectTags := v .GetString ("tags" ); projectTags != "" {
236
+ tags = projectTags
237
+ }
145
238
146
- if resp .StatusCode != http .StatusAccepted && resp .StatusCode != http .StatusOK {
147
- return fmt .Errorf ("upload failed (%d): %s" , resp .StatusCode , string (respBody ))
239
+ err := ensureParentExists (dependencyTrackUrl , dependencyTrackKey , parentName , tags )
240
+ if err != nil {
241
+ return err
242
+ }
243
+ err = uploadSbom (dependencyTrackUrl , dependencyTrackKey , projectName , parentName , projectVersion , sbomFilePath , tags )
244
+ if err != nil {
245
+ return err
148
246
}
149
247
150
248
fmt .Println ("✅ SBOM upload successful." )
0 commit comments