Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API examples #2517

Merged
merged 5 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
wagoodman marked this conversation as resolved.
Show resolved Hide resolved
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
Loading