mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
Add API examples (#2517)
* [wip] initial syft api examples Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * smooth over some rough edges in the API Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * embed example file Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address review comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * change name of builder function Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
b7a6d5e946
commit
3da679066e
15
examples/README.md
Normal file
15
examples/README.md
Normal file
@ -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
examples/create_custom_sbom/alpine_configuration_cataloger.go
Normal file
127
examples/create_custom_sbom/alpine_configuration_cataloger.go
Normal file
@ -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
examples/create_custom_sbom/main.go
Normal file
138
examples/create_custom_sbom/main.go
Normal file
@ -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
examples/create_simple_sbom/main.go
Normal file
74
examples/create_simple_sbom/main.go
Normal file
@ -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
|
||||
}
|
||||
4964
examples/decode_sbom/alpine.syft.json
Normal file
4964
examples/decode_sbom/alpine.syft.json
Normal file
File diff suppressed because it is too large
Load Diff
53
examples/decode_sbom/main.go
Normal file
53
examples/decode_sbom/main.go
Normal file
@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/format"
|
||||
)
|
||||
|
||||
//go:embed alpine.syft.json
|
||||
var sbomContents string
|
||||
|
||||
func main() {
|
||||
// decode the SBOM
|
||||
fmt.Println("decoding SBOM...")
|
||||
sbom, sbomFormat, formatVersion, err := format.Decode(sbomReader())
|
||||
if err != nil {
|
||||
fmt.Printf("failed to decode sbom: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("SBOM format: %s@%s\n", sbomFormat, formatVersion)
|
||||
|
||||
// print packages found...
|
||||
fmt.Println("\nPackages found:")
|
||||
for _, pkg := range sbom.Artifacts.Packages.Sorted() {
|
||||
fmt.Printf(" %s : %s@%s (%s)\n", pkg.ID(), pkg.Name, pkg.Version, pkg.Type)
|
||||
}
|
||||
|
||||
// print files found...
|
||||
fmt.Println("\nFiles found:")
|
||||
for c, f := range sbom.Artifacts.FileMetadata {
|
||||
fmt.Printf(" %s : %s\n", c.ID(), f.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func sbomReader() io.Reader {
|
||||
// read file from sys args (or use the default)
|
||||
var reader io.Reader
|
||||
if len(os.Args) < 2 {
|
||||
reader = strings.NewReader(sbomContents)
|
||||
} else {
|
||||
var err error
|
||||
reader, err = os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return reader
|
||||
}
|
||||
62
examples/source_detection/main.go
Normal file
62
examples/source_detection/main.go
Normal file
@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
/*
|
||||
This example demonstrates how to create a source object from a generic string input.
|
||||
|
||||
Example inputs:
|
||||
alpine:3.19 pull an image from the docker daemon, podman, or the registry (based on what's available)
|
||||
./my.tar interpret a local archive as an OCI archive, docker save archive, or raw file from disk to catalog
|
||||
docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
podman:yourrepo/yourimage:tag explicitly use the Podman daemon
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
|
||||
dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
|
||||
*/
|
||||
|
||||
const defaultImage = "alpine:3.19"
|
||||
|
||||
func main() {
|
||||
detection, err := source.Detect(
|
||||
imageReference(),
|
||||
source.DetectConfig{
|
||||
DefaultImageSource: "docker",
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Show a basic description of the source to the screen
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(src.Describe()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
50
examples/source_from_image/main.go
Normal file
50
examples/source_from_image/main.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
/*
|
||||
This shows how to create a source from an image reference. This is useful when you are programmatically always
|
||||
expecting to catalog a container image and always from the same source (e.g. docker daemon, podman, registry, etc).
|
||||
*/
|
||||
|
||||
const defaultImage = "alpine:3.19"
|
||||
|
||||
func main() {
|
||||
platform, err := image.NewPlatform("linux/amd64")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
src, err := source.NewFromStereoscopeImage(
|
||||
source.StereoscopeImageConfig{
|
||||
Reference: imageReference(),
|
||||
From: image.OciRegistrySource, // always use the registry, there are several other "Source" options here
|
||||
Platform: platform,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Show a basic description of the source to the screen
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(src.Describe()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -72,7 +72,12 @@ func (cfg Config) WithSelection(selection file.Selection) Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg Config) WithHashers(hashers []crypto.Hash) Config {
|
||||
func (cfg Config) WithHashers(hashers ...crypto.Hash) Config {
|
||||
cfg.Hashers = intFile.NormalizeHashes(hashers)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg Config) WithContentConfig(content filecontent.Config) Config {
|
||||
cfg.Content = content
|
||||
return cfg
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
|
||||
{
|
||||
name: "no file digest cataloger",
|
||||
src: imgSrc,
|
||||
cfg: DefaultCreateSBOMConfig().WithFilesConfig(filecataloging.DefaultConfig().WithHashers(nil)),
|
||||
cfg: DefaultCreateSBOMConfig().WithFilesConfig(filecataloging.DefaultConfig().WithHashers()),
|
||||
wantTaskNames: [][]string{
|
||||
environmentCatalogerNames(),
|
||||
pkgCatalogerNamesWithTagOrName(t, "image"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user