-
Notifications
You must be signed in to change notification settings - Fork 593
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* [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
Showing
10 changed files
with
5,490 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
127
examples/create_custom_sbom/alpine_configuration_cataloger.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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... | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.