17
17
package compose
18
18
19
19
import (
20
+ "bytes"
20
21
"context"
21
22
"crypto/sha256"
22
23
"errors"
23
24
"fmt"
25
+ "io"
24
26
"os"
25
27
28
+ "github.com/DefangLabs/secret-detector/pkg/scanner"
29
+ "github.com/DefangLabs/secret-detector/pkg/secrets"
30
+
26
31
"github.com/compose-spec/compose-go/v2/loader"
27
32
"github.com/compose-spec/compose-go/v2/types"
28
33
"github.com/distribution/reference"
@@ -226,15 +231,37 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje
226
231
return override .MarshalYAML ()
227
232
}
228
233
234
+ //nolint:gocyclo
229
235
func (s * composeService ) preChecks (project * types.Project , options api.PublishOptions ) (bool , error ) {
230
- if ok , err := s .checkOnlyBuildSection (project ); ! ok {
236
+ if ok , err := s .checkOnlyBuildSection (project ); ! ok || err != nil {
237
+ return false , err
238
+ }
239
+ if ok , err := s .checkForBindMount (project ); ! ok || err != nil {
231
240
return false , err
232
241
}
242
+ if options .AssumeYes {
243
+ return true , nil
244
+ }
245
+ detectedSecrets , err := s .checkForSensitiveData (project )
246
+ if err != nil {
247
+ return false , err
248
+ }
249
+ if len (detectedSecrets ) > 0 {
250
+ fmt .Println ("you are about to publish sensitive data within your OCI artifact.\n " +
251
+ "please double check that you are not leaking sensitive data" )
252
+ for _ , val := range detectedSecrets {
253
+ _ , _ = fmt .Fprintln (s .dockerCli .Out (), val .Type )
254
+ _ , _ = fmt .Fprintf (s .dockerCli .Out (), "%q: %s\n " , val .Key , val .Value )
255
+ }
256
+ if ok , err := acceptPublishSensitiveData (s .dockerCli ); err != nil || ! ok {
257
+ return false , err
258
+ }
259
+ }
233
260
envVariables , err := s .checkEnvironmentVariables (project , options )
234
261
if err != nil {
235
262
return false , err
236
263
}
237
- if ! options . AssumeYes && len (envVariables ) > 0 {
264
+ if len (envVariables ) > 0 {
238
265
fmt .Println ("you are about to publish environment variables within your OCI artifact.\n " +
239
266
"please double check that you are not leaking sensitive data" )
240
267
for key , val := range envVariables {
@@ -243,17 +270,10 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp
243
270
_ , _ = fmt .Fprintf (s .dockerCli .Out (), "%s=%v\n " , k , * v )
244
271
}
245
272
}
246
- return acceptPublishEnvVariables (s .dockerCli )
247
- }
248
-
249
- for name , config := range project .Services {
250
- for _ , volume := range config .Volumes {
251
- if volume .Type == types .VolumeTypeBind {
252
- return false , fmt .Errorf ("cannot publish compose file: service %q relies on bind-mount. You should use volumes" , name )
253
- }
273
+ if ok , err := acceptPublishEnvVariables (s .dockerCli ); err != nil || ! ok {
274
+ return false , err
254
275
}
255
276
}
256
-
257
277
return true , nil
258
278
}
259
279
@@ -299,6 +319,12 @@ func acceptPublishEnvVariables(cli command.Cli) (bool, error) {
299
319
return confirm , err
300
320
}
301
321
322
+ func acceptPublishSensitiveData (cli command.Cli ) (bool , error ) {
323
+ msg := "Are you ok to publish these sensitive data? [y/N]: "
324
+ confirm , err := prompt .NewPrompt (cli .In (), cli .Out ()).Confirm (msg , false )
325
+ return confirm , err
326
+ }
327
+
302
328
func envFileLayers (project * types.Project ) []ocipush.Pushable {
303
329
var layers []ocipush.Pushable
304
330
for _ , service := range project .Services {
@@ -334,3 +360,99 @@ func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, er
334
360
}
335
361
return true , nil
336
362
}
363
+
364
+ func (s * composeService ) checkForBindMount (project * types.Project ) (bool , error ) {
365
+ for name , config := range project .Services {
366
+ for _ , volume := range config .Volumes {
367
+ if volume .Type == types .VolumeTypeBind {
368
+ return false , fmt .Errorf ("cannot publish compose file: service %q relies on bind-mount. You should use volumes" , name )
369
+ }
370
+ }
371
+ }
372
+ return true , nil
373
+ }
374
+
375
+ func (s * composeService ) checkForSensitiveData (project * types.Project ) ([]secrets.DetectedSecret , error ) {
376
+ var allFindings []secrets.DetectedSecret
377
+ scan := scanner .NewDefaultScanner ()
378
+ // Check all compose files
379
+ for _ , file := range project .ComposeFiles {
380
+ in , err := composeFileAsByteReader (file , project )
381
+ if err != nil {
382
+ return nil , err
383
+ }
384
+
385
+ findings , err := scan .ScanReader (in )
386
+ if err != nil {
387
+ return nil , fmt .Errorf ("failed to scan compose file %s: %w" , file , err )
388
+ }
389
+ allFindings = append (allFindings , findings ... )
390
+ }
391
+ for _ , service := range project .Services {
392
+ // Check env files
393
+ for _ , envFile := range service .EnvFiles {
394
+ findings , err := scan .ScanFile (envFile .Path )
395
+ if err != nil {
396
+ return nil , fmt .Errorf ("failed to scan env file %s: %w" , envFile .Path , err )
397
+ }
398
+ allFindings = append (allFindings , findings ... )
399
+ }
400
+ }
401
+
402
+ // Check configs defined by files
403
+ for _ , config := range project .Configs {
404
+ if config .File != "" {
405
+ findings , err := scan .ScanFile (config .File )
406
+ if err != nil {
407
+ return nil , fmt .Errorf ("failed to scan config file %s: %w" , config .File , err )
408
+ }
409
+ allFindings = append (allFindings , findings ... )
410
+ }
411
+ }
412
+
413
+ // Check secrets defined by files
414
+ for _ , secret := range project .Secrets {
415
+ if secret .File != "" {
416
+ findings , err := scan .ScanFile (secret .File )
417
+ if err != nil {
418
+ return nil , fmt .Errorf ("failed to scan secret file %s: %w" , secret .File , err )
419
+ }
420
+ allFindings = append (allFindings , findings ... )
421
+ }
422
+ }
423
+
424
+ return allFindings , nil
425
+ }
426
+
427
+ func composeFileAsByteReader (filePath string , project * types.Project ) (io.Reader , error ) {
428
+ composeFile , err := os .ReadFile (filePath )
429
+ if err != nil {
430
+ return nil , fmt .Errorf ("failed to open compose file %s: %w" , filePath , err )
431
+ }
432
+ base , err := loader .LoadWithContext (context .TODO (), types.ConfigDetails {
433
+ WorkingDir : project .WorkingDir ,
434
+ Environment : project .Environment ,
435
+ ConfigFiles : []types.ConfigFile {
436
+ {
437
+ Filename : filePath ,
438
+ Content : composeFile ,
439
+ },
440
+ },
441
+ }, func (options * loader.Options ) {
442
+ options .SkipValidation = true
443
+ options .SkipExtends = true
444
+ options .SkipConsistencyCheck = true
445
+ options .ResolvePaths = true
446
+ options .SkipInterpolation = true
447
+ options .SkipResolveEnvironment = true
448
+ })
449
+ if err != nil {
450
+ return nil , err
451
+ }
452
+
453
+ in , err := base .MarshalYAML ()
454
+ if err != nil {
455
+ return nil , err
456
+ }
457
+ return bytes .NewBuffer (in ), nil
458
+ }
0 commit comments