Skip to content

Commit

Permalink
Add API examples (#2517)
Browse files Browse the repository at this point in the history
* [wip] initial syft api examples

Signed-off-by: Alex Goodman <[email protected]>

* smooth over some rough edges in the API

Signed-off-by: Alex Goodman <[email protected]>

* embed example file

Signed-off-by: Alex Goodman <[email protected]>

* address review comments

Signed-off-by: Alex Goodman <[email protected]>

* change name of builder function

Signed-off-by: Alex Goodman <[email protected]>

---------

Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman authored Feb 2, 2024
1 parent b7a6d5e commit 3da6790
Show file tree
Hide file tree
Showing 10 changed files with 5,490 additions and 2 deletions.
15 changes: 15 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Syft API Examples

This directory contains examples of how to use the Syft API.

- `create_simple_sbom`: Create a simple SBOM from scratch
- `create_custom_sbom`: Create an SBOM using as much custom configuration as possible, including a custom cataloger implementation
- `decode_sbom`: Take an existing SBOM file (of arbitrary format) and decode it into a Syft SBOM object
- `source_detection`: Shows how to detect what to catalog automatically from a user string (e.g. container image vs directory)
- `source_from_image`: Construct a source from a only a container image

You can run any of these examples from this directory with:

```bash
go run ./DIRECTORY_NAME
```
127 changes: 127 additions & 0 deletions examples/create_custom_sbom/alpine_configuration_cataloger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"context"
"fmt"
"io"
"path"

"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)

/*
This is a contrived cataloger that attempts to capture useful APK files from the image as if it were a package.
This isn't a real cataloger, but it is a good example of how to use API elements to create a custom cataloger.
*/

var _ pkg.Cataloger = (*alpineConfigurationCataloger)(nil)

type alpineConfigurationCataloger struct {
}

func newAlpineConfigurationCataloger() pkg.Cataloger {
return alpineConfigurationCataloger{}
}

func (m alpineConfigurationCataloger) Name() string {
return "apk-configuration-cataloger"
}

func (m alpineConfigurationCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
version, versionLocations, err := getVersion(resolver)
if err != nil {
return nil, nil, fmt.Errorf("unable to get alpine version: %w", err)
}
if len(versionLocations) == 0 {
// this doesn't mean we should stop cataloging, just that we don't have a version to use, thus no package to raise up
return nil, nil, nil
}

metadata, metadataLocations, err := newAlpineConfiguration(resolver)
if err != nil {
return nil, nil, err
}

var locations []file.Location
locations = append(locations, versionLocations...)
locations = append(locations, metadataLocations...)

p := newPackage(version, *metadata, locations...)

return []pkg.Package{p}, nil, nil
}

func newPackage(version string, metadata AlpineConfiguration, locations ...file.Location) pkg.Package {
return pkg.Package{
Name: "alpine-configuration",
Version: version,
Locations: file.NewLocationSet(locations...),
Type: pkg.Type("system-configuration"), // you can make up your own package type here or use an existing one
Metadata: metadata,
}
}

func newAlpineConfiguration(resolver file.Resolver) (*AlpineConfiguration, []file.Location, error) {
var locations []file.Location

keys, keyLocations, err := getAPKKeys(resolver)
if err != nil {
return nil, nil, err
}

locations = append(locations, keyLocations...)

return &AlpineConfiguration{
APKKeys: keys,
}, locations, nil

}

func getVersion(resolver file.Resolver) (string, []file.Location, error) {
locations, err := resolver.FilesByPath("/etc/alpine-release")
if err != nil {
return "", nil, fmt.Errorf("unable to get alpine version: %w", err)
}
if len(locations) == 0 {
return "", nil, nil
}

reader, err := resolver.FileContentsByLocation(locations[0])
if err != nil {
return "", nil, fmt.Errorf("unable to read alpine version: %w", err)
}

version, err := io.ReadAll(reader)
if err != nil {
return "", nil, fmt.Errorf("unable to read alpine version: %w", err)
}

return string(version), locations, nil
}

func getAPKKeys(resolver file.Resolver) (map[string]string, []file.Location, error) {
// name-to-content values
keyContent := make(map[string]string)

locations, err := resolver.FilesByGlob("/etc/apk/keys/*.rsa.pub")
if err != nil {
return nil, nil, fmt.Errorf("unable to get apk keys: %w", err)
}
for _, location := range locations {
basename := path.Base(location.RealPath)
reader, err := resolver.FileContentsByLocation(location)
content, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to read apk key content at %s: %w", location.RealPath, err)
}
keyContent[basename] = string(content)
}
return keyContent, locations, nil
}

type AlpineConfiguration struct {
APKKeys map[string]string `json:"apkKeys" yaml:"apkKeys"`
// Add more data you want to capture as part of the package metadata here...
}
138 changes: 138 additions & 0 deletions examples/create_custom_sbom/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package main

import (
"context"
"crypto"
"fmt"
"os"

"gopkg.in/yaml.v3"

"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/cataloging/filecataloging"
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)

const defaultImage = "alpine:3.19"

func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())

// will catalog the given source and return a SBOM keeping in mind several configurable options
sbom := getSBOM(src)

// show a simple package summary
summarize(sbom)

// show the alpine-configuration cataloger results
showAlpineConfiguration(sbom)
}

func imageReference() string {
// read an image string reference from the command line or use a default
if len(os.Args) > 1 {
return os.Args[1]
}
return defaultImage
}

func getSource(input string) source.Source {
fmt.Println("detecting source type for input:", input, "...")

detection, err := source.Detect(input,
source.DetectConfig{
DefaultImageSource: "docker",
},
)

if err != nil {
panic(err)
}

src, err := detection.NewSource(source.DefaultDetectionSourceConfig())

if err != nil {
panic(err)
}

return src
}

func getSBOM(src source.Source) sbom.SBOM {
fmt.Println("creating SBOM...")

cfg := syft.DefaultCreateSBOMConfig().
// run the catalogers in parallel (5 at a time concurrently max)
WithParallelism(5).
// bake a specific tool name and version into the SBOM
WithTool("my-tool", "v1.0").
// catalog all files with 3 digests
WithFilesConfig(
filecataloging.DefaultConfig().
WithSelection(file.AllFilesSelection).
WithHashers(
crypto.MD5,
crypto.SHA1,
crypto.SHA256,
),
).
// only use OS related catalogers that would have been used with the kind of
// source type (container image or directory), but also add a specific python cataloger
WithCatalogerSelection(
pkgcataloging.NewSelectionRequest().
WithSubSelections("os").
WithAdditions("python-package-cataloger"),
).
// which relationships to include
WithRelationshipsConfig(
cataloging.RelationshipsConfig{
PackageFileOwnership: true,
PackageFileOwnershipOverlap: true,
ExcludeBinaryPackagesWithFileOwnershipOverlap: true,
},
).
// add your own cataloger to the mix
WithCatalogers(
pkgcataloging.NewAlwaysEnabledCatalogerReference(
newAlpineConfigurationCataloger(),
),
)

s, err := syft.CreateSBOM(context.Background(), src, cfg)
if err != nil {
panic(err)
}

return *s
}

func summarize(s sbom.SBOM) {
fmt.Printf("Cataloged %d packages:\n", s.Artifacts.Packages.PackageCount())
for _, p := range s.Artifacts.Packages.Sorted() {
fmt.Printf(" - %s@%s (%s)\n", p.Name, p.Version, p.Type)
}
fmt.Println()
}

func showAlpineConfiguration(s sbom.SBOM) {
pkgs := s.Artifacts.Packages.PackagesByName("alpine-configuration")
if len(pkgs) == 0 {
fmt.Println("no alpine-configuration package found")
return
}

p := pkgs[0]

fmt.Printf("All 'alpine-configuration' packages: %s\n", p.Version)
meta, err := yaml.Marshal(p.Metadata)
if err != nil {
panic(err)
}
fmt.Println(string(meta))

}
74 changes: 74 additions & 0 deletions examples/create_simple_sbom/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"context"
"fmt"
"os"

"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)

const defaultImage = "alpine:3.19"

func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())

// catalog the given source and return a SBOM
sbom := getSBOM(src)

// take the SBOM object and encode it into the syft-json representation
bytes := formatSBOM(sbom)

// show the SBOM!
fmt.Println(string(bytes))
}

func imageReference() string {
// read an image string reference from the command line or use a default
if len(os.Args) > 1 {
return os.Args[1]
}
return defaultImage
}

func getSource(input string) source.Source {
detection, err := source.Detect(input,
source.DetectConfig{
DefaultImageSource: "docker",
},
)

if err != nil {
panic(err)
}

src, err := detection.NewSource(source.DefaultDetectionSourceConfig())

if err != nil {
panic(err)
}

return src
}

func getSBOM(src source.Source) sbom.SBOM {
s, err := syft.CreateSBOM(context.Background(), src, nil)
if err != nil {
panic(err)
}

return *s
}

func formatSBOM(s sbom.SBOM) []byte {
bytes, err := format.Encode(s, syftjson.NewFormatEncoder())
if err != nil {
panic(err)
}
return bytes
}
Loading

0 comments on commit 3da6790

Please sign in to comment.