Skip to content
Open
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
2 changes: 1 addition & 1 deletion cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func init() {
generateCmd.Flags().StringVarP(&documentName, "name", "n", "sbomit-sbom", "Name for the SBOM document")
generateCmd.Flags().StringVarP(&documentVersion, "version", "v", "0.0.1", "Version for the SBOM document")
generateCmd.Flags().StringSliceVar(&authors, "author", []string{}, "Document authors (can be specified multiple times)")
generateCmd.Flags().StringSliceVar(&attestationTypes, "types", []string{"material", "command-run", "product", "network-trace"}, "Attestation types to parse (comma-separated).")
generateCmd.Flags().StringSliceVar(&attestationTypes, "types", []string{"material", "command-run", "product", "network-trace", "maven"}, "Attestation types to parse (comma-separated).")
generateCmd.Flags().StringVarP(&catalog, "catalog", "c", "", "Cataloger to run before processing attestations (supported: syft)")
generateCmd.Flags().StringVar(&projectDir, "project-dir", "", "Project directory to scan with the cataloger (default: current directory)")
}
Expand Down
73 changes: 72 additions & 1 deletion pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func DefaultOptions() *Options {
DocumentName: "sbomit-generated-sbom",
DocumentVersion: "0.0.1",
Authors: []string{},
AttestationTypes: []string{"material", "command-run", "product", "network-trace"},
AttestationTypes: []string{"material", "command-run", "product", "network-trace", "maven"},
OutputFormat: "spdx23",
Catalog: "",
ProjectDir: "",
Expand Down Expand Up @@ -133,6 +133,10 @@ func (g *Generator) GenerateFromAttestations(attestations []attestation.TypedAtt
networkPkgs := g.networkChain.ResolveAll(networkConns)
result = mergeNetworkPackages(result, networkPkgs)

// Resolve packages declared in Maven attestor (structured dependency list)
mavenPkgs := extractMavenAttestorPackages(attestations)
result = mergeNewPackages(result, mavenPkgs)

attDoc := g.createDocument(result)

if baseDoc != nil {
Expand Down Expand Up @@ -495,6 +499,73 @@ func sanitizeID(s string) string {
return result
}

func extractMavenAttestorPackages(attestations []attestation.TypedAttestation) []resolver.PackageInfo {
var packages []resolver.PackageInfo
seen := make(map[string]struct{})

for _, att := range attestations {
if att.Type != "maven" {
continue
}

depsRaw, ok := att.Data["dependencies"].([]interface{})
if !ok {
continue
}

for _, d := range depsRaw {
dep, ok := d.(map[string]interface{})
if !ok {
continue
}

groupID, _ := dep["groupid"].(string)
artifactID, _ := dep["artifactid"].(string)
version, _ := dep["version"].(string)

if groupID == "" || artifactID == "" || version == "" {
continue
}

purl := "pkg:maven/" + groupID + "/" + artifactID + "@" + version
if _, already := seen[purl]; already {
continue
}
seen[purl] = struct{}{}

packages = append(packages, resolver.PackageInfo{
Name: artifactID,
Version: version,
Ecosystem: "maven",
PURL: purl,
FoundBy: "attestation:maven",
})
}
}

return packages
}

func mergeNewPackages(result resolver.ResolverResult, newPkgs []resolver.PackageInfo) resolver.ResolverResult {
if len(newPkgs) == 0 {
return result
}

existingByPURL := make(map[string]struct{}, len(result.Packages))
for _, pkg := range result.Packages {
existingByPURL[pkg.PURL] = struct{}{}
}

for _, pkg := range newPkgs {
if _, found := existingByPURL[pkg.PURL]; !found {
result.Packages = append(result.Packages, pkg)
existingByPURL[pkg.PURL] = struct{}{}
}
}

return result
}

func generateUUID() string {
now := time.Now().UnixNano()
return fmt.Sprintf("%x-%x-%x-%x-%x",
Expand Down
144 changes: 144 additions & 0 deletions pkg/resolver/java.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package resolver

import (
"path"
"regexp"
"strings"
)

type JavaResolver struct {
mavenRe *regexp.Regexp
gradleRe *regexp.Regexp
}

func NewJavaResolver() *JavaResolver {
return &JavaResolver{
mavenRe: regexp.MustCompile(`\.m2/repository/(.+)/([^/]+)/([^/]+)/[^/]+-[^/]+\.(jar|pom|aar|war|ear)$`),
gradleRe: regexp.MustCompile(`\.gradle/caches/modules-2/files-2\.1/([^/]+)/([^/]+)/([^/]+)/[^/]+/[^/]+\.(jar|pom|aar)$`),
}
}

func (r *JavaResolver) Name() string {
return "java"
}

func (r *JavaResolver) Resolve(files []FileInfo) (packages []PackageInfo, remainingFiles []FileInfo) {
seen := make(map[string]int)

for _, f := range files {
np := path.Clean(f.Path)

if !r.isJavaPath(np) {
remainingFiles = append(remainingFiles, f)
continue
}

groupID, artifactID, version, ok := r.extractCoordinates(np)
if !ok {
remainingFiles = append(remainingFiles, f)
continue
}

key := groupID + ":" + artifactID + "@" + version
if idx, already := seen[key]; already {
for k, v := range f.Hashes {
if _, exists := packages[idx].Hashes[k]; !exists {
if packages[idx].Hashes == nil {
packages[idx].Hashes = make(map[string]string)
}
packages[idx].Hashes[k] = v
}
}
continue
}

purl := "pkg:maven/" + groupID + "/" + artifactID + "@" + version
pkg := PackageInfo{
Name: artifactID,
Version: version,
Ecosystem: "maven",
PURL: purl,
Hashes: f.Hashes,
FoundBy: "attestation:java",
}
seen[key] = len(packages)
packages = append(packages, pkg)
}

return packages, remainingFiles
}

func (r *JavaResolver) CreateFileFilters(packages []PackageInfo) []PackageFileFilter {
var filters []PackageFileFilter

for _, pkg := range packages {
if pkg.Ecosystem != "maven" {
continue
}

rest := strings.TrimPrefix(pkg.PURL, "pkg:maven/")
rest = strings.SplitN(rest, "@", 2)[0]
parts := strings.SplitN(rest, "/", 2)
if len(parts) != 2 {
continue
}

filters = append(filters, &javaPackageFilter{
groupID: parts[0],
artifactID: parts[1],
version: pkg.Version,
})
}

return filters
}

type javaPackageFilter struct {
groupID string
artifactID string
version string
}

func (f *javaPackageFilter) Matches(p string) bool {
np := path.Clean(p)
npLower := strings.ToLower(np)

if !strings.Contains(npLower, "/.m2/repository/") && !strings.Contains(npLower, "/.gradle/caches/") {
return false
}

groupPathLower := strings.ReplaceAll(strings.ToLower(f.groupID), ".", "/")
artifactLower := strings.ToLower(f.artifactID)
versionLower := strings.ToLower(f.version)

if strings.Contains(npLower, "/.m2/repository/"+groupPathLower+"/"+artifactLower+"/"+versionLower+"/") {
return true
}

if strings.Contains(npLower, "/.gradle/caches/modules-2/files-2.1/"+strings.ToLower(f.groupID)+"/"+artifactLower+"/"+versionLower+"/") {
return true
}

return false
}

func (r *JavaResolver) isJavaPath(p string) bool {
return strings.Contains(p, "/.m2/repository/") ||
strings.Contains(p, "/.gradle/caches/modules-2/files-2.1/")
}

func (r *JavaResolver) extractCoordinates(p string) (groupID, artifactID, version string, ok bool) {
if matches := r.mavenRe.FindStringSubmatch(p); len(matches) >= 4 {
group := strings.ReplaceAll(matches[1], "/", ".")
if group == "" || matches[2] == "" || matches[3] == "" {
return "", "", "", false
}
return group, matches[2], matches[3], true
}

if matches := r.gradleRe.FindStringSubmatch(p); len(matches) >= 4 {
return matches[1], matches[2], matches[3], true
}

return "", "", "", false
}
Loading