-
Notifications
You must be signed in to change notification settings - Fork 591
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
458 additions
and
0 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
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,183 @@ | ||
package github | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"time" | ||
|
||
"github.com/mholt/archiver/v3" | ||
|
||
"github.com/anchore/packageurl-go" | ||
"github.com/anchore/syft/internal" | ||
"github.com/anchore/syft/internal/log" | ||
"github.com/anchore/syft/internal/version" | ||
"github.com/anchore/syft/syft/pkg" | ||
"github.com/anchore/syft/syft/sbom" | ||
"github.com/anchore/syft/syft/source" | ||
) | ||
|
||
// toGithubModel converts the provided SBOM to a GitHub dependency model | ||
func toGithubModel(s *sbom.SBOM) DependencySnapshot { | ||
scanTime := time.Now().Format(time.RFC3339) // TODO is there a record of this somewhere? | ||
v := version.FromBuild().Version | ||
if v == "[not provided]" { | ||
v = "0.0.0-dev" | ||
} | ||
return DependencySnapshot{ | ||
Version: 0, | ||
// TODO allow property input to specify the Job, Sha, and Ref | ||
Detector: DetectorMetadata{ | ||
Name: internal.ApplicationName, | ||
URL: "https://github.com/anchore/syft", | ||
Version: v, | ||
}, | ||
Metadata: toSnapshotMetadata(s), | ||
Manifests: toGithubManifests(s), | ||
Scanned: scanTime, | ||
} | ||
} | ||
|
||
// toSnapshotMetadata captures the linux distribution information and other metadata | ||
func toSnapshotMetadata(s *sbom.SBOM) Metadata { | ||
out := Metadata{} | ||
|
||
if s.Artifacts.LinuxDistribution != nil { | ||
d := s.Artifacts.LinuxDistribution | ||
qualifiers := packageurl.Qualifiers{} | ||
if len(d.IDLike) > 0 { | ||
qualifiers = append(qualifiers, packageurl.Qualifier{ | ||
Key: "like", | ||
Value: strings.Join(d.IDLike, ","), | ||
}) | ||
} | ||
purl := packageurl.NewPackageURL("generic", "", d.ID, d.VersionID, qualifiers, "") | ||
out["syft:distro"] = purl.ToString() | ||
} | ||
|
||
return out | ||
} | ||
|
||
func filesystem(p pkg.Package) string { | ||
if len(p.Locations) > 0 { | ||
return p.Locations[0].FileSystemID | ||
} | ||
return "" | ||
} | ||
|
||
// isArchive returns true if the path appears to be an archive | ||
func isArchive(path string) bool { | ||
_, err := archiver.ByExtension(path) | ||
return err == nil | ||
} | ||
|
||
// toPath Generates a string representation of the package location, optionally including the layer hash | ||
func toPath(s source.Metadata, p pkg.Package) string { | ||
inputPath := strings.TrimPrefix(s.Path, "./") | ||
if inputPath == "." { | ||
inputPath = "" | ||
} | ||
if len(p.Locations) > 0 { | ||
location := p.Locations[0] | ||
packagePath := location.RealPath | ||
if location.VirtualPath != "" { | ||
packagePath = location.VirtualPath | ||
} | ||
packagePath = strings.TrimPrefix(packagePath, "/") | ||
switch s.Scheme { | ||
case source.ImageScheme: | ||
image := strings.ReplaceAll(s.ImageMetadata.UserInput, ":/", "//") | ||
return fmt.Sprintf("%s:/%s", image, packagePath) | ||
case source.FileScheme: | ||
if isArchive(inputPath) { | ||
return fmt.Sprintf("%s:/%s", inputPath, packagePath) | ||
} | ||
return inputPath | ||
case source.DirectoryScheme: | ||
if inputPath != "" { | ||
return fmt.Sprintf("%s/%s", inputPath, packagePath) | ||
} | ||
return packagePath | ||
} | ||
} | ||
return fmt.Sprintf("%s%s", inputPath, s.ImageMetadata.UserInput) | ||
} | ||
|
||
// toGithubManifests manifests, each of which represents a specific location that has dependencies | ||
func toGithubManifests(s *sbom.SBOM) Manifests { | ||
manifests := map[string]*Manifest{} | ||
|
||
for _, p := range s.Artifacts.PackageCatalog.Sorted() { | ||
path := toPath(s.Source, p) | ||
manifest, ok := manifests[path] | ||
if !ok { | ||
manifest = &Manifest{ | ||
Name: path, | ||
File: FileInfo{ | ||
SourceLocation: path, | ||
}, | ||
Resolved: DependencyGraph{}, | ||
} | ||
fs := filesystem(p) | ||
if fs != "" { | ||
manifest.Metadata = Metadata{ | ||
"syft:filesystem": fs, | ||
} | ||
} | ||
manifests[path] = manifest | ||
} | ||
|
||
name := dependencyName(p) | ||
manifest.Resolved[name] = DependencyNode{ | ||
Purl: p.PURL, | ||
Metadata: toDependencyMetadata(p), | ||
Relationship: toDependencyRelationshipType(p), | ||
Scope: toDependencyScope(p), | ||
Dependencies: toDependencies(s, p), | ||
} | ||
} | ||
|
||
out := Manifests{} | ||
for k, v := range manifests { | ||
out[k] = *v | ||
} | ||
return out | ||
} | ||
|
||
// dependencyName to make things a little nicer to read; this might end up being lossy | ||
func dependencyName(p pkg.Package) string { | ||
purl, err := packageurl.FromString(p.PURL) | ||
if err != nil { | ||
log.Warnf("Invalid PURL for package: '%s' PURL: '%s' (%w)", p.Name, p.PURL, err) | ||
return "" | ||
} | ||
// don't use qualifiers for this | ||
purl.Qualifiers = nil | ||
return purl.ToString() | ||
} | ||
|
||
func toDependencyScope(_ pkg.Package) DependencyScope { | ||
return DependencyScopeRuntime | ||
} | ||
|
||
func toDependencyRelationshipType(_ pkg.Package) DependencyRelationship { | ||
return DependencyRelationshipDirect | ||
} | ||
|
||
func toDependencyMetadata(_ pkg.Package) Metadata { | ||
// We have limited properties: up to 8 with reasonably small values | ||
// For now, we are encoding the location as part of the key, we are encoding PURLs with most | ||
// of the other information Grype might need; and the distro information at the top level | ||
// so we don't need anything here yet | ||
return Metadata{} | ||
} | ||
|
||
func toDependencies(s *sbom.SBOM, p pkg.Package) (out []string) { | ||
for _, r := range s.Relationships { | ||
if r.From.ID() == p.ID() { | ||
if p, ok := r.To.(pkg.Package); ok { | ||
out = append(out, dependencyName(p)) | ||
} | ||
} | ||
} | ||
return | ||
} |
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,161 @@ | ||
package github | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/anchore/packageurl-go" | ||
"github.com/anchore/syft/syft/linux" | ||
"github.com/anchore/syft/syft/pkg" | ||
"github.com/anchore/syft/syft/sbom" | ||
"github.com/anchore/syft/syft/source" | ||
) | ||
|
||
func Test_toGithubModel(t *testing.T) { | ||
s := sbom.SBOM{ | ||
Source: source.Metadata{ | ||
Scheme: source.ImageScheme, | ||
ImageMetadata: source.ImageMetadata{ | ||
UserInput: "ubuntu:18.04", | ||
Architecture: "amd64", | ||
}, | ||
}, | ||
Artifacts: sbom.Artifacts{ | ||
LinuxDistribution: &linux.Release{ | ||
ID: "ubuntu", | ||
VersionID: "18.04", | ||
IDLike: []string{"debian"}, | ||
}, | ||
PackageCatalog: pkg.NewCatalog(), | ||
}, | ||
} | ||
for _, p := range []pkg.Package{ | ||
{ | ||
Name: "pkg-1", | ||
Version: "1.0.1", | ||
Locations: []source.Location{{ | ||
Coordinates: source.Coordinates{ | ||
RealPath: "/usr/lib", | ||
FileSystemID: "fsid-1", | ||
}, | ||
}}, | ||
}, | ||
{ | ||
Name: "pkg-2", | ||
Version: "2.0.2", | ||
Locations: []source.Location{{ | ||
Coordinates: source.Coordinates{ | ||
RealPath: "/usr/lib", | ||
FileSystemID: "fsid-1", | ||
}, | ||
}}, | ||
}, | ||
{ | ||
Name: "pkg-3", | ||
Version: "3.0.3", | ||
Locations: []source.Location{{ | ||
Coordinates: source.Coordinates{ | ||
RealPath: "/etc", | ||
FileSystemID: "fsid-1", | ||
}, | ||
}}, | ||
}, | ||
} { | ||
p.PURL = packageurl.NewPackageURL( | ||
"generic", | ||
"", | ||
p.Name, | ||
p.Version, | ||
nil, | ||
"", | ||
).ToString() | ||
s.Artifacts.PackageCatalog.Add(p) | ||
} | ||
|
||
actual := toGithubModel(&s) | ||
|
||
expected := DependencySnapshot{ | ||
Version: 0, | ||
Detector: DetectorMetadata{ | ||
Name: "syft", | ||
Version: "0.0.0-dev", | ||
URL: "https://github.com/anchore/syft", | ||
}, | ||
Metadata: Metadata{ | ||
"syft:distro": "pkg:generic/[email protected]?like=debian", | ||
}, | ||
Scanned: actual.Scanned, | ||
Manifests: Manifests{ | ||
"ubuntu:18.04:/usr/lib": Manifest{ | ||
Name: "ubuntu:18.04:/usr/lib", | ||
File: FileInfo{ | ||
SourceLocation: "ubuntu:18.04:/usr/lib", | ||
}, | ||
Metadata: Metadata{ | ||
"syft:filesystem": "fsid-1", | ||
}, | ||
Resolved: DependencyGraph{ | ||
"pkg:generic/[email protected]": DependencyNode{ | ||
Purl: "pkg:generic/[email protected]", | ||
Scope: DependencyScopeRuntime, | ||
Relationship: DependencyRelationshipDirect, | ||
}, | ||
"pkg:generic/[email protected]": DependencyNode{ | ||
Purl: "pkg:generic/[email protected]", | ||
Scope: DependencyScopeRuntime, | ||
Relationship: DependencyRelationshipDirect, | ||
}, | ||
}, | ||
}, | ||
"ubuntu:18.04:/etc": Manifest{ | ||
Name: "ubuntu:18.04:/etc", | ||
File: FileInfo{ | ||
SourceLocation: "ubuntu:18.04:/etc", | ||
}, | ||
Metadata: Metadata{ | ||
"syft:filesystem": "fsid-1", | ||
}, | ||
Resolved: DependencyGraph{ | ||
"pkg:generic/[email protected]": DependencyNode{ | ||
Purl: "pkg:generic/[email protected]", | ||
Scope: DependencyScopeRuntime, | ||
Relationship: DependencyRelationshipDirect, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
// just using JSONEq because it gives a comprehensible diff | ||
s1, _ := json.Marshal(expected) | ||
s2, _ := json.Marshal(actual) | ||
assert.JSONEq(t, string(s1), string(s2)) | ||
|
||
// Just test the other schemes: | ||
s.Source.Path = "." | ||
s.Source.Scheme = source.DirectoryScheme | ||
actual = toGithubModel(&s) | ||
assert.Equal(t, "etc", actual.Manifests["etc"].Name) | ||
|
||
s.Source.Path = "./artifacts" | ||
s.Source.Scheme = source.DirectoryScheme | ||
actual = toGithubModel(&s) | ||
assert.Equal(t, "artifacts/etc", actual.Manifests["artifacts/etc"].Name) | ||
|
||
s.Source.Path = "/artifacts" | ||
s.Source.Scheme = source.DirectoryScheme | ||
actual = toGithubModel(&s) | ||
assert.Equal(t, "/artifacts/etc", actual.Manifests["/artifacts/etc"].Name) | ||
|
||
s.Source.Path = "./executable" | ||
s.Source.Scheme = source.FileScheme | ||
actual = toGithubModel(&s) | ||
assert.Equal(t, "executable", actual.Manifests["executable"].Name) | ||
|
||
s.Source.Path = "./archive.tar.gz" | ||
s.Source.Scheme = source.FileScheme | ||
actual = toGithubModel(&s) | ||
assert.Equal(t, "archive.tar.gz:/etc", actual.Manifests["archive.tar.gz:/etc"].Name) | ||
} |
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,29 @@ | ||
package github | ||
|
||
import ( | ||
"encoding/json" | ||
"io" | ||
|
||
"github.com/anchore/syft/syft/sbom" | ||
) | ||
|
||
const ID sbom.FormatID = "github-0-json" | ||
|
||
func Format() sbom.Format { | ||
return sbom.NewFormat( | ||
ID, | ||
func(writer io.Writer, sbom sbom.SBOM) error { | ||
bom := toGithubModel(&sbom) | ||
|
||
bytes, err := json.MarshalIndent(bom, "", " ") | ||
if err != nil { | ||
return err | ||
} | ||
_, err = writer.Write(bytes) | ||
|
||
return err | ||
}, | ||
nil, | ||
nil, | ||
) | ||
} |
Oops, something went wrong.