Skip to content

Commit

Permalink
feat: write sbom result to disk (ko-build#822)
Browse files Browse the repository at this point in the history
Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>

Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
  • Loading branch information
developer-guy authored Sep 20, 2022
1 parent 01f28ab commit 5e0452a
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 13 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ jobs:
KO_DOCKER_REPO="" go run ./ build --push=false ./test | grep ":latest@sha256:"
KO_DOCKER_REPO="" go run ./ build --push=false -t test ./test | grep ":test@sha256:"
KO_DOCKER_REPO="" go run ./ build --push=false -t test --tag-only ./test | grep ":test$"
# Check that using sbom-dir works.
KO_DOCKER_REPO="" go run ./ build -t test --push=false --sbom-dir ./sbom-data ./test
jq . ./sbom-data/test.spdx.json
export PLATFORM=${GOOS}/${GOARCH}
if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ From v0.9+, `ko` generates and uploads an SBOM for every image it produces by de
`ko` will generate an SBOM in the [SPDX](https://spdx.dev/) format by default, but you can select the [CycloneDX](https://cyclonedx.org/) format instead with the `--sbom=cyclonedx` flag. To disable SBOM generation, pass `--sbom=none`.

These SBOMs can be downloaded using the [`cosign download sbom`](https://github.com/sigstore/cosign/blob/main/doc/cosign_download_sbom.md) command.

But you can prefer to write SBOM to disk by giving the `--sbom-dir` flag instead of uploading it to registry, and `ko` will decide the extension based on the format you defined in the `--sbom` flag and file name based on the import path of your project.

## Static Assets

`ko` can also bundle static assets into the images it produces.
Expand Down
1 change: 1 addition & 0 deletions doc/ko_apply.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ ko apply -f FILENAME [flags]
--push Push images to KO_DOCKER_REPO (default true)
-R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.
--sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, cyclonedx, go.version-m). (default "spdx")
--sbom-dir string Path to file where the SBOM will be written.
-l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)
--tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated.
-t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest])
Expand Down
1 change: 1 addition & 0 deletions doc/ko_build.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ ko build IMPORTPATH... [flags]
-P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO.
--push Push images to KO_DOCKER_REPO (default true)
--sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, cyclonedx, go.version-m). (default "spdx")
--sbom-dir string Path to file where the SBOM will be written.
--tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated.
-t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest])
--tarball string File to save images tarballs
Expand Down
1 change: 1 addition & 0 deletions doc/ko_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ ko create -f FILENAME [flags]
--push Push images to KO_DOCKER_REPO (default true)
-R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.
--sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, cyclonedx, go.version-m). (default "spdx")
--sbom-dir string Path to file where the SBOM will be written.
-l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)
--tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated.
-t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest])
Expand Down
1 change: 1 addition & 0 deletions doc/ko_resolve.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ ko resolve -f FILENAME [flags]
--push Push images to KO_DOCKER_REPO (default true)
-R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.
--sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, cyclonedx, go.version-m). (default "spdx")
--sbom-dir string Path to file where the SBOM will be written.
-l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)
--tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated.
-t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest])
Expand Down
1 change: 1 addition & 0 deletions doc/ko_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ko run IMPORTPATH [flags]
-P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO.
--push Push images to KO_DOCKER_REPO (default true)
--sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, cyclonedx, go.version-m). (default "spdx")
--sbom-dir string Path to file where the SBOM will be written.
--tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated.
-t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest])
--tarball string File to save images tarballs
Expand Down
71 changes: 60 additions & 11 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type GetBase func(context.Context, string) (name.Reference, Result, error)

type builder func(context.Context, string, string, v1.Platform, Config) (string, error)

type sbomber func(context.Context, string, string, oci.SignedEntity) ([]byte, types.MediaType, error)
type sbomber func(context.Context, string, string, string, oci.SignedEntity, string) ([]byte, types.MediaType, error)

type platformMatcher struct {
spec []string
Expand All @@ -77,6 +77,7 @@ type gobuild struct {
kodataCreationTime v1.Time
build builder
sbom sbomber
sbomDir string
disableOptimizations bool
trimpath bool
buildConfigs map[string]Config
Expand All @@ -98,6 +99,7 @@ type gobuildOpener struct {
kodataCreationTime v1.Time
build builder
sbom sbomber
sbomDir string
disableOptimizations bool
trimpath bool
buildConfigs map[string]Config
Expand Down Expand Up @@ -125,6 +127,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) {
kodataCreationTime: gbo.kodataCreationTime,
build: gbo.build,
sbom: gbo.sbom,
sbomDir: gbo.sbomDir,
disableOptimizations: gbo.disableOptimizations,
trimpath: gbo.trimpath,
buildConfigs: gbo.buildConfigs,
Expand Down Expand Up @@ -301,7 +304,7 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con
return file, nil
}

func goversionm(ctx context.Context, file string, appPath string, se oci.SignedEntity) ([]byte, types.MediaType, error) {
func goversionm(ctx context.Context, file string, appPath string, appFileName string, se oci.SignedEntity, dir string) ([]byte, types.MediaType, error) {
switch se.(type) {
case oci.SignedImage:
sbom := bytes.NewBuffer(nil)
Expand All @@ -314,7 +317,13 @@ func goversionm(ctx context.Context, file string, appPath string, se oci.SignedE

// In order to get deterministics SBOMs replace our randomized
// file name with the path the app will get inside of the container.
return []byte(strings.Replace(sbom.String(), file, appPath, 1)), "application/vnd.go.version-m", nil
s := []byte(strings.Replace(sbom.String(), file, appPath, 1))

if err := writeSBOM(s, appFileName, dir, "go.version-m"); err != nil {
return nil, "", err
}

return s, "application/vnd.go.version-m", nil

case oci.SignedImageIndex:
return nil, "", nil
Expand All @@ -325,10 +334,10 @@ func goversionm(ctx context.Context, file string, appPath string, se oci.SignedE
}

func spdx(version string) sbomber {
return func(ctx context.Context, file string, appPath string, se oci.SignedEntity) ([]byte, types.MediaType, error) {
return func(ctx context.Context, file string, appPath string, appFileName string, se oci.SignedEntity, dir string) ([]byte, types.MediaType, error) {
switch obj := se.(type) {
case oci.SignedImage:
b, _, err := goversionm(ctx, file, appPath, obj)
b, _, err := goversionm(ctx, file, appPath, appFileName, obj, "")
if err != nil {
return nil, "", err
}
Expand All @@ -337,10 +346,23 @@ func spdx(version string) sbomber {
if err != nil {
return nil, "", err
}

if err := writeSBOM(b, appFileName, dir, "spdx.json"); err != nil {
return nil, "", err
}

return b, ctypes.SPDXJSONMediaType, nil

case oci.SignedImageIndex:
b, err := sbom.GenerateIndexSPDX(version, obj)
if err != nil {
return nil, "", err
}

if err := writeSBOM(b, appFileName, dir, "spdx.json"); err != nil {
return nil, "", err
}

return b, ctypes.SPDXJSONMediaType, err

default:
Expand All @@ -349,11 +371,24 @@ func spdx(version string) sbomber {
}
}

func writeSBOM(sbom []byte, appFileName, dir, ext string) error {
if dir != "" {
sbomDir := filepath.Clean(dir)
if err := os.MkdirAll(sbomDir, os.ModePerm); err != nil {
return err
}
sbomPath := filepath.Join(sbomDir, appFileName+"."+ext)
log.Printf("Writing SBOM to %s", sbomPath)
return os.WriteFile(sbomPath, sbom, 0644)
}
return nil
}

func cycloneDX() sbomber {
return func(ctx context.Context, file string, appPath string, se oci.SignedEntity) ([]byte, types.MediaType, error) {
return func(ctx context.Context, file string, appPath string, appFileName string, se oci.SignedEntity, dir string) ([]byte, types.MediaType, error) {
switch obj := se.(type) {
case oci.SignedImage:
b, _, err := goversionm(ctx, file, appPath, obj)
b, _, err := goversionm(ctx, file, appPath, appFileName, obj, "")
if err != nil {
return nil, "", err
}
Expand All @@ -362,10 +397,23 @@ func cycloneDX() sbomber {
if err != nil {
return nil, "", err
}

if err := writeSBOM(b, appFileName, dir, "cyclone.json"); err != nil {
return nil, "", err
}

return b, ctypes.CycloneDXJSONMediaType, nil

case oci.SignedImageIndex:
b, err := sbom.GenerateIndexCycloneDX(obj)
if err != nil {
return nil, "", err
}

if err := writeSBOM(b, appFileName, dir, "cyclonedx.json"); err != nil {
return nil, "", err
}

return b, ctypes.SPDXJSONMediaType, err

default:
Expand Down Expand Up @@ -794,7 +842,8 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
})

appDir := "/ko-app"
appPath := path.Join(appDir, appFilename(ref.Path()))
appFileName := appFilename(ref.Path())
appPath := path.Join(appDir, appFileName)

miss := func() (v1.Layer, error) {
return buildLayer(appPath, file, platform, layerMediaType)
Expand Down Expand Up @@ -833,7 +882,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
cfg.Config.Entrypoint = []string{appPath}
cfg.Config.Cmd = nil
if platform.OS == "windows" {
cfg.Config.Entrypoint = []string{`C:\ko-app\` + appFilename(ref.Path())}
cfg.Config.Entrypoint = []string{`C:\ko-app\` + appFileName}
updatePath(cfg, `C:\ko-app`)
cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`)
} else {
Expand Down Expand Up @@ -862,7 +911,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
si := signed.Image(image)

if g.sbom != nil {
sbom, mt, err := g.sbom(ctx, file, appPath, si)
sbom, mt, err := g.sbom(ctx, file, appPath, appFileName, si, g.sbomDir)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1067,7 +1116,7 @@ func (g *gobuild) buildAll(ctx context.Context, ref string, baseRef name.Referen
adds...)

if g.sbom != nil {
sbom, mt, err := g.sbom(ctx, "", "", idx)
sbom, mt, err := g.sbom(ctx, "", "", "", idx, g.sbomDir)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ func nilGetBase(context.Context, string) (name.Reference, Result, error) {
const wantSBOM = "This is our fake SBOM"

// A helper method we use to substitute for the default "build" method.
func fauxSBOM(context.Context, string, string, oci.SignedEntity) ([]byte, types.MediaType, error) {
func fauxSBOM(context.Context, string, string, string, oci.SignedEntity, string) ([]byte, types.MediaType, error) {
return []byte(wantSBOM), "application/vnd.garbage", nil
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/build/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,11 @@ func WithJobs(jobs int) Option {
return nil
}
}

// WithSBOMDir is a functional option for overriding the directory
func WithSBOMDir(dir string) Option {
return func(gbo *gobuildOpener) error {
gbo.sbomDir = dir
return nil
}
}
3 changes: 3 additions & 0 deletions pkg/commands/options/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type BuildOptions struct {
ConcurrentBuilds int
DisableOptimizations bool
SBOM string
SBOMDir string
Platforms []string
Labels []string
// UserAgent enables overriding the default value of the `User-Agent` HTTP
Expand All @@ -76,6 +77,8 @@ func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) {
"Disable optimizations when building Go code. Useful when you want to interactively debug the created container.")
cmd.Flags().StringVar(&bo.SBOM, "sbom", "spdx",
"The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, cyclonedx, go.version-m).")
cmd.Flags().StringVar(&bo.SBOMDir, "sbom-dir", "",
"Path to file where the SBOM will be written.")
cmd.Flags().StringSliceVar(&bo.Platforms, "platform", []string{},
"Which platform to use when pulling a multi-platform base. Format: all | <os>[/<arch>[/<variant>]][,platform]*")
cmd.Flags().StringSliceVar(&bo.Labels, "image-label", []string{},
Expand Down
4 changes: 4 additions & 0 deletions pkg/commands/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) {
opts = append(opts, build.WithConfig(bo.BuildConfigs))
}

if bo.SBOMDir != "" {
opts = append(opts, build.WithSBOMDir(bo.SBOMDir))
}

return opts, nil
}

Expand Down

0 comments on commit 5e0452a

Please sign in to comment.