Skip to content

Commit

Permalink
800. Add sbom section to deploy configure/review (#1713)
Browse files Browse the repository at this point in the history
## Description
#800 



## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [x] Test, docs, adr added or updated as needed
- [ ] [Contributor Guide
Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow)
followed

---------

Co-authored-by: Wayne Starr <[email protected]>
Co-authored-by: Wayne Starr <[email protected]>
  • Loading branch information
3 people authored May 22, 2023
1 parent becbfd7 commit 4983505
Show file tree
Hide file tree
Showing 27 changed files with 381 additions and 75 deletions.
3 changes: 2 additions & 1 deletion src/cmd/tools/archiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"
"strings"

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/config/lang"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/mholt/archiver/v3"
Expand Down Expand Up @@ -55,7 +56,7 @@ var archiverDecompressCmd = &cobra.Command{
if strings.HasSuffix(path, ".tar") {
dst := filepath.Join(strings.TrimSuffix(path, ".tar"), "..")
// Unpack sboms.tar differently since it has a different folder structure than components
if info.Name() == "sboms.tar" {
if info.Name() == config.ZarfSBOMTar {
dst = strings.TrimSuffix(path, ".tar")
}
err := archiver.Unarchive(path, dst)
Expand Down
3 changes: 3 additions & 0 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ const (
ZarfImageCacheDir = "images"

ZarfYAML = "zarf.yaml"
ZarfYAMLSignature = "zarf.yaml.sig"
ZarfChecksumsTxt = "checksums.txt"
ZarfSBOMDir = "zarf-sbom"
ZarfSBOMTar = "sboms.tar"
ZarfPackagePrefix = "zarf-package-"

ZarfInClusterContainerRegistryNodePort = 31999
Expand Down
33 changes: 17 additions & 16 deletions src/internal/api/packages/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@
package packages

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/internal/api/common"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/types"
"github.com/go-chi/chi/v5"
goyaml "github.com/goccy/go-yaml"
"github.com/mholt/archiver/v3"
)

Expand All @@ -35,26 +33,29 @@ func Read(w http.ResponseWriter, r *http.Request) {

// internal function to read a package from the local filesystem.
func readPackage(path string) (pkg types.APIZarfPackage, err error) {
var file []byte

pkg.Path, err = url.QueryUnescape(path)
if err != nil {
return pkg, err
}

tmpDir, err := utils.MakeTempDir(config.CommonOptions.TempDirectory)
if err != nil {
return pkg, fmt.Errorf("unable to create tmpdir: %w", err)
}
defer os.RemoveAll(tmpDir)

// Extract the archive
err = archiver.Extract(pkg.Path, config.ZarfYAML, tmpDir)
// Check for zarf.yaml in the package and read into file
err = archiver.Walk(pkg.Path, func(f archiver.File) error {
if f.Name() == config.ZarfYAML {
file, err = ioutil.ReadAll(f)
if err != nil {
return err
}
return archiver.ErrStopWalk
}

return nil
})
if err != nil {
return pkg, err
}

// Read the Zarf yaml
configPath := filepath.Join(tmpDir, config.ZarfYAML)
err = utils.ReadYaml(configPath, &pkg.ZarfPackage)

err = goyaml.Unmarshal(file, &pkg.ZarfPackage)
return pkg, err
}
134 changes: 134 additions & 0 deletions src/internal/api/packages/sbom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package packages provides api functions for managing Zarf packages.
package packages

import (
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/internal/api/common"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/types"
"github.com/go-chi/chi/v5"
"github.com/mholt/archiver/v3"
)

var signalChan = make(chan os.Signal, 1)

// ExtractSBOM Extracts the SBOM from the package and returns the path to the SBOM
func ExtractSBOM(w http.ResponseWriter, r *http.Request) {
path := chi.URLParam(r, "path")

sbom, err := extractSBOM(path)

if err != nil {
message.ErrorWebf(err, w, err.Error())
} else {
common.WriteJSONResponse(w, sbom, http.StatusOK)
}

}

// DeleteSBOM removes the SBOM directory
func DeleteSBOM(w http.ResponseWriter, _ *http.Request) {
err := cleanupSBOM()
if err != nil {
message.ErrorWebf(err, w, err.Error())
return
}
common.WriteJSONResponse(w, nil, http.StatusOK)
}

// cleanupSBOM removes the SBOM directory
func cleanupSBOM() error {
err := os.RemoveAll(config.ZarfSBOMDir)
if err != nil {
return err
}
return nil
}

// Extracts the SBOM from the package and returns the path to the SBOM
func extractSBOM(escapedPath string) (sbom types.APIPackageSBOM, err error) {
path, err := url.QueryUnescape(escapedPath)
if err != nil {
return sbom, err
}

// Ensure we can get the cwd
cwd, err := os.Getwd()
if err != nil {
return sbom, err
}

// ensure the package exists
if _, err := os.Stat(path); os.IsNotExist(err) {
return sbom, err
}

// Join the current working directory with the zarf-sbom directory
sbomPath := filepath.Join(cwd, config.ZarfSBOMDir)

// ensure the zarf-sbom directory is empty
if _, err := os.Stat(sbomPath); !os.IsNotExist(err) {
cleanupSBOM()
}

// Create the Zarf SBOM directory
err = utils.CreateDirectory(sbomPath, 0700)
if err != nil {
return sbom, err
}

// Extract the SBOM tar.gz from the package
err = archiver.Extract(path, config.ZarfSBOMTar, sbomPath)
if err != nil {
cleanupSBOM()
return sbom, err
}

// Unarchive the SBOM tar.gz
err = archiver.Unarchive(filepath.Join(sbomPath, config.ZarfSBOMTar), sbomPath)
if err != nil {
cleanupSBOM()
return sbom, err
}

// Get the SBOM viewer files
sbom, err = getSbomViewFiles(sbomPath)
if err != nil {
cleanupSBOM()
return sbom, err
}

// Cleanup the temp directory on exit
go func() {
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
// Wait for a signal to be received
<-signalChan

cleanupSBOM()

// Exit the program
os.Exit(0)
}()

return sbom, err
}

func getSbomViewFiles(sbomPath string) (sbom types.APIPackageSBOM, err error) {
sbomViewFiles, err := filepath.Glob(filepath.Join(sbomPath, "sbom-viewer-*"))
if len(sbomViewFiles) > 0 {
sbom.Path = sbomViewFiles[0]
sbom.SBOMS = sbomViewFiles
}
return sbom, err
}
27 changes: 27 additions & 0 deletions src/internal/api/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ func LaunchAPIServer() {
r.Delete("/{pkg}/disconnect/{name}", packages.DisconnectTunnel)
r.Get("/{pkg}/connections", packages.ListPackageConnections)
r.Get("/connections", packages.ListConnections)
r.Get("/sbom/{path}", packages.ExtractSBOM)
r.Delete("/sbom", packages.DeleteSBOM)
})

r.Route("/components", func(r chi.Router) {
Expand All @@ -115,6 +117,30 @@ func LaunchAPIServer() {
message.Infof("Zarf UI connection: http://127.0.0.1:%s/auth?token=%s", devPort, token)
}

// Setup the static SBOM server
sbomSub := os.DirFS(config.ZarfSBOMDir)
sbomFs := http.FileServer(http.FS(sbomSub))

// Serve the SBOM viewer files
router.Get("/sbom-viewer/*", func(w http.ResponseWriter, r *http.Request) {
message.Debug("api.LaunchAPIServer() - /sbom-viewer/*")

// Extract the file name from the URL
file := strings.TrimPrefix(r.URL.Path, "/sbom-viewer/")

// Ensure SBOM file exists in the config.ZarfSBOMDir
if test, err := sbomSub.Open(file); err != nil {
// If the file doesn't exist, redirect to the homepage
r.URL.Path = "/"
http.Redirect(w, r, "/", http.StatusFound)
} else {
// If the file exists, close the file and serve it
test.Close()
}
r.URL.Path = file
sbomFs.ServeHTTP(w, r)
})

// Load the static UI files
if sub, err := fs.Sub(config.UIAssets, "build/ui"); err != nil {
message.Error(err, "Unable to load the embedded ui assets")
Expand All @@ -124,6 +150,7 @@ func LaunchAPIServer() {

// Catch all routes
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
message.Debug("api.LaunchAPIServer() - /*")
// If the request is not a real file, serve the index.html instead
if test, err := sub.Open(strings.TrimPrefix(r.URL.Path, "/")); err != nil {
r.URL.Path = "/"
Expand Down
3 changes: 1 addition & 2 deletions src/internal/packager/sbom/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import (

// ViewSBOMFiles opens a browser to view the SBOM files and pauses for user input.
func ViewSBOMFiles(tmp types.TempPaths) {
sbomFilePath := filepath.Join(tmp.Base, "sboms")
sbomViewFiles, _ := filepath.Glob(filepath.Join(sbomFilePath, "sbom-viewer-*"))
sbomViewFiles, _ := filepath.Glob(filepath.Join(tmp.Sboms, "sbom-viewer-*"))

if len(sbomViewFiles) > 0 {
link := sbomViewFiles[0]
Expand Down
8 changes: 4 additions & 4 deletions src/pkg/packager/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,11 @@ func createPaths() (paths types.TempPaths, err error) {
SeedImages: filepath.Join(basePath, "seed-images"),
Images: filepath.Join(basePath, "images"),
Components: filepath.Join(basePath, "components"),
SbomTar: filepath.Join(basePath, "sboms.tar"),
SbomTar: filepath.Join(basePath, config.ZarfSBOMTar),
Sboms: filepath.Join(basePath, "sboms"),
Checksums: filepath.Join(basePath, "checksums.txt"),
Checksums: filepath.Join(basePath, config.ZarfChecksumsTxt),
ZarfYaml: filepath.Join(basePath, config.ZarfYAML),
ZarfSig: filepath.Join(basePath, "zarf.yaml.sig"),
ZarfSig: filepath.Join(basePath, config.ZarfYAMLSignature),
}

return paths, err
Expand Down Expand Up @@ -456,7 +456,7 @@ func (p *Packager) validatePackageChecksums() error {
filepathMap[p.tmp.ZarfSig] = true

// Load the contents of the checksums file
checksumsFile, err := os.Open(filepath.Join(p.tmp.Base, "checksums.txt"))
checksumsFile, err := os.Open(filepath.Join(p.tmp.Base, config.ZarfChecksumsTxt))
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/packager/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ func generatePackageChecksums(basePath string) (string, error) {
}

// Create the checksums file
checksumsFilePath := filepath.Join(basePath, "checksums.txt")
checksumsFilePath := filepath.Join(basePath, config.ZarfChecksumsTxt)
if err := utils.WriteFile(checksumsFilePath, []byte(checksumsData)); err != nil {
return "", err
}
Expand Down
4 changes: 2 additions & 2 deletions src/pkg/packager/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ func (p *Packager) Inspect(includeSBOM bool, outputSBOM string, inspectPublicKey

layersToPull := []string{config.ZarfYAML}
if pullSBOM {
layersToPull = append(layersToPull, "sboms.tar")
layersToPull = append(layersToPull, config.ZarfSBOMTar)
}
if pullZarfSig {
layersToPull = append(layersToPull, "zarf.yaml.sig")
layersToPull = append(layersToPull, config.ZarfYAMLSignature)
}

message.Debugf("Pulling layers %v from %s", layersToPull, p.cfg.DeployOpts.PackagePath)
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/packager/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (p *Packager) handleOciPackage(url string, out string, concurrency int, lay
copyOpts.PostCopy = copyOpts.OnCopySkipped
isPartialPull := len(layers) > 0
if isPartialPull {
alwaysPull := []string{"zarf.yaml", "checksums.txt", "zarf.yaml.sig"}
alwaysPull := []string{config.ZarfYAML, config.ZarfChecksumsTxt, config.ZarfYAMLSignature}
layers = append(layers, alwaysPull...)
copyOpts.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
nodes, err := content.Successors(ctx, fetcher, desc)
Expand Down
11 changes: 9 additions & 2 deletions src/test/ui/02_initialize_cluster.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,21 @@ test.describe('initialize a zarf cluster', () => {
// Find first init package deploy button.
const deployInit = page.getByTitle('init').first();
// click the init package deploy button.
await deployInit.click();
deployInit.click();

// Validate that the SBOM has been loaded
const sbomInfo = await page.waitForSelector('#sbom-info', { timeout: 10000 });
expect(await sbomInfo.innerText()).toMatch(/[0-9]+ artifacts to be reviewed/);

// Components (check most functionaliy with k3s component)
const k3s = page.locator('.accordion:has-text("k3s")');
await expect(k3s.locator('.deploy-component-toggle')).toHaveAttribute('aria-pressed', 'false');
await k3s.locator('text=Deploy').click();
await expect(k3s.locator('.deploy-component-toggle')).toHaveAttribute('aria-pressed', 'true');
await expect(
page.locator('.component-accordion-header:has-text("*** REQUIRES ROOT (not sudo) *** Install K3s")')
page.locator(
'.component-accordion-header:has-text("*** REQUIRES ROOT (not sudo) *** Install K3s")'
)
).toBeVisible();
await expect(k3s.locator('code')).toBeHidden();
await k3s.locator('.accordion-toggle').click();
Expand Down
7 changes: 7 additions & 0 deletions src/types/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type RestAPI struct {
APIZarfPackageConnection APIDeployedPackageConnection `json:"apiZarfPackageConnection"`
APIDeployedPackageConnections APIDeployedPackageConnections `json:"apiZarfPackageConnections"`
APIConnections APIConnections `json:"apiConnections"`
APIPackageSBOM APIPackageSBOM `json:"apiPackageSBOM"`
}

// ClusterSummary contains the summary of a cluster for the API.
Expand All @@ -48,6 +49,12 @@ type APIZarfDeployPayload struct {
InitOpts *ZarfInitOptions `json:"initOpts,omitempty"`
}

// APIPackageSBOM represents the SBOM viewer files for a package
type APIPackageSBOM struct {
Path string `json:"path"`
SBOMS []string `json:"sboms"`
}

// APIConnections represents all of the existing connections
type APIConnections map[string]APIDeployedPackageConnections

Expand Down
Loading

0 comments on commit 4983505

Please sign in to comment.