diff --git a/cmd/format/dpsbom.go b/cmd/format/dpsbom.go new file mode 100644 index 00000000..59be656b --- /dev/null +++ b/cmd/format/dpsbom.go @@ -0,0 +1,117 @@ +package format + +import ( + "archive/zip" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "hash" + "io" + "path/filepath" + "strings" + + "github.com/xmirrorsecurity/opensca-cli/v3/cmd/detail" + "github.com/xmirrorsecurity/opensca-cli/v3/opensca/model" +) + +func DpSbomZip(report Report, out string) { + zipFile := out + if !strings.HasSuffix(out, ".zip") { + zipFile = out + ".zip" + } + jsonName := filepath.Base(out) + if !strings.HasSuffix(jsonName, ".json") { + jsonName = jsonName + ".json" + } + outWrite(zipFile, func(w io.Writer) error { + doc := pdSbomDoc(report) + if doc.Hashes.HashFile == "" { + return errors.New("hash file is required") + } + + var h hash.Hash + switch strings.ToLower(doc.Hashes.Algorithm) { + case "sha-256": + h = sha256.New() + case "sha-1": + h = sha1.New() + case "md5": + h = md5.New() + case "": + return errors.New("hash algorithm is required") + default: + return fmt.Errorf("unsupported hash algorithm: %s", doc.Hashes.Algorithm) + } + + tojson := func(w io.Writer) error { + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + return encoder.Encode(doc) + } + + zipfile := zip.NewWriter(w) + defer zipfile.Close() + + sbomfile, err := zipfile.Create(jsonName) + if err != nil { + return err + } + err = tojson(sbomfile) + if err != nil { + return err + } + + hashfile, err := zipfile.Create(doc.Hashes.HashFile) + if err != nil { + return err + } + err = tojson(h) + if err != nil { + return err + } + hashstr := hex.EncodeToString(h.Sum(nil)[:]) + hashfile.Write([]byte(hashstr)) + + return nil + }) +} + +func pdSbomDoc(report Report) *model.DpSbomDocument { + + doc := model.NewDpSbomDocument(report.TaskInfo.AppName, "opensca-cli") + + report.DepDetailGraph.ForEach(func(n *detail.DepDetailGraph) bool { + + if n.Name == "" { + return true + } + + lics := []string{} + for _, lic := range n.Licenses { + lics = append(lics, lic.ShortName) + } + doc.AppendComponents(func(dsp *model.DpSbomPackage) { + dsp.Identifier.Purl = n.Purl() + dsp.Name = n.Name + dsp.Version = n.Version + dsp.License = lics + }) + + children := []string{} + for _, c := range n.Children { + if c.Name == "" { + continue + } + children = append(children, c.Purl()) + } + doc.AppendDependencies(n.Purl(), children) + + return true + }) + + return doc +} diff --git a/cmd/format/save.go b/cmd/format/save.go index f6bb4488..8d2ee382 100644 --- a/cmd/format/save.go +++ b/cmd/format/save.go @@ -39,6 +39,12 @@ func Save(report Report, output string) { switch filepath.Ext(out) { case ".html": Html(genReport(report), out) + case ".zip": + if strings.HasSuffix(out, ".dpsbom.zip") { + DpSbomZip(report, out) + } else { + Json(genReport(report), out) + } case ".json": if strings.HasSuffix(out, ".spdx.json") { SpdxJson(report, out) @@ -48,9 +54,13 @@ func Save(report Report, output string) { CycloneDXJson(report, out) } else if strings.HasSuffix(out, ".swid.json") { SwidJson(report, out) + } else if strings.HasSuffix(out, ".dpsbom.json") { + DpSbomZip(report, out) } else { Json(genReport(report), out) } + case ".dpsbom": + DpSbomZip(report, out) case ".dsdx": Dsdx(report, out) case ".spdx": diff --git a/opensca/model/dpsbom.go b/opensca/model/dpsbom.go new file mode 100644 index 00000000..1786bbf4 --- /dev/null +++ b/opensca/model/dpsbom.go @@ -0,0 +1,112 @@ +package model + +import "time" + +type DpSbomDocument struct { + // 文档名称 + DocumentName string `json:"DocumentName"` + // 文档版本 + DocumentVersion string `json:"DocumentVersion"` + // 文档创建/更新时间 yyyy-MM-ddTHH:mm:ssTZD + DocumentTime string `json:"DocumentTime"` + // 文档格式 + BomFormat string `json:"BomFormat"` + // 生成工具 + Tool string `json:"tool"` + // sbom签名信息 + Hashes DpSbomHashes `json:"Hashes"` + // 组件列表 + Packages []DpSbomPackage `json:"Packages"` + // 依赖关系 + Dependencies []DpSbomDependencies `json:"Dependencies"` +} + +type DpSbomPackage struct { + Name string `json:"ComponentName"` + Version string `json:"ComponentVersion"` + + Identifier struct { + Purl string `json:"PURL"` + } `json:"ComponentIdentifier"` + + License []string `json:"License"` + + Author []map[string]string `json:"Author"` + Provider []map[string]string `json:"Provider"` + Hash DpSbomHash `json:"ComponentHash"` + + // 组件信息更新时间 yyyy-MM-ddTHH:mm:ssTZD + Timestamp string `json:"Timestamp"` +} + +type DpSbomDependencies struct { + Ref string `json:"Ref"` + DependsOn []struct { + Target string `json:"Target"` + } `json:"DependsOn"` +} + +func newDependencies(ref string, dependsOn []string) DpSbomDependencies { + deps := DpSbomDependencies{Ref: ref} + deps.DependsOn = make([]struct { + Target string "json:\"Target\"" + }, len(dependsOn)) + for i, d := range dependsOn { + deps.DependsOn[i].Target = d + } + return deps +} + +type DpSbomHashes struct { + Algorithm string `json:"Algorithm"` + HashFile string `json:"HashFile,omitempty"` + DigitalFile string `json:"DigitalFile,omitempty"` +} + +type DpSbomHash struct { + Algorithm string `json:"Algorithm,omitempty"` + Hash string `json:"Hash,omitempty"` +} + +func NewDpSbomDocument(name, creator string) *DpSbomDocument { + version := "1.0.0" + timestamp := time.Now().Format("2006-01-02T15:04:05MST") + return &DpSbomDocument{ + DocumentName: name, + DocumentVersion: version, + DocumentTime: timestamp, + BomFormat: "DP-SBOM-1.0", + Tool: creator, + Hashes: DpSbomHashes{ + Algorithm: "SHA-256", + HashFile: "sha256.txt", + }, + Dependencies: []DpSbomDependencies{}, + } +} + +func (doc *DpSbomDocument) AppendComponents(fn func(*DpSbomPackage)) { + c := DpSbomPackage{} + if fn != nil { + fn(&c) + } + if c.Timestamp == "" { + c.Timestamp = time.Now().Format("2006-01-02T15:04:05MST") + } + if c.Author == nil { + c.Author = []map[string]string{} + } + if c.Provider == nil { + c.Provider = []map[string]string{} + } + doc.Packages = append(doc.Packages, c) +} + +func (doc *DpSbomDocument) AppendDependencies(parentId string, childrenIds []string) { + if doc.Dependencies == nil { + doc.Dependencies = []DpSbomDependencies{} + } + if len(childrenIds) > 0 { + doc.Dependencies = append(doc.Dependencies, newDependencies(parentId, childrenIds)) + } +} diff --git a/opensca/sca/filter/filter.go b/opensca/sca/filter/filter.go index 35092b77..0c493640 100644 --- a/opensca/sca/filter/filter.go +++ b/opensca/sca/filter/filter.go @@ -69,10 +69,11 @@ var ( ) var ( - SbomSpdx = filterFunc(strings.HasSuffix, ".spdx") - SbomDsdx = filterFunc(strings.HasSuffix, ".dsdx") - SbomJson = filterFunc(strings.HasSuffix, ".json") - SbomXml = filterFunc(strings.HasSuffix, ".xml") + SbomSpdx = filterFunc(strings.HasSuffix, ".spdx") + SbomDsdx = filterFunc(strings.HasSuffix, ".dsdx") + SbomJson = filterFunc(strings.HasSuffix, ".json") + SbomXml = filterFunc(strings.HasSuffix, ".xml") + SbomDbSbom = filterFunc(strings.HasSuffix, ".dbsbom") // SbomRdf = filterFunc(strings.HasSuffix, ".rdf") ) diff --git a/opensca/sca/sbom/dpsbom.go b/opensca/sca/sbom/dpsbom.go new file mode 100644 index 00000000..c3a70c3f --- /dev/null +++ b/opensca/sca/sbom/dpsbom.go @@ -0,0 +1,65 @@ +package sbom + +import ( + "encoding/json" + "io" + + "github.com/xmirrorsecurity/opensca-cli/v3/opensca/model" +) + +func ParseDpSbomJson(f *model.File) *model.DepGraph { + doc := &model.DpSbomDocument{} + f.OpenReader(func(reader io.Reader) { + json.NewDecoder(reader).Decode(doc) + }) + return parseDpSbomDoc(f, doc) +} + +func parseDpSbomDoc(f *model.File, doc *model.DpSbomDocument) *model.DepGraph { + + if doc == nil { + return nil + } + + depIdMap := map[string]*model.DepGraph{} + _dep := model.NewDepGraphMap(func(s ...string) string { + return s[0] + }, func(s ...string) *model.DepGraph { + vendor, name, version, language := model.ParsePurl(s[0]) + return &model.DepGraph{ + Vendor: vendor, + Name: name, + Version: version, + Language: language, + } + }).LoadOrStore + + for _, pkg := range doc.Packages { + dep := _dep(pkg.Identifier.Purl) + dep.Licenses = pkg.License + depIdMap[pkg.Identifier.Purl] = dep + } + + for _, dependOn := range doc.Dependencies { + parent, ok := depIdMap[dependOn.Ref] + if !ok { + continue + } + for _, dep := range dependOn.DependsOn { + child, ok := depIdMap[dep.Target] + if !ok { + continue + } + parent.AppendChild(child) + } + } + + root := &model.DepGraph{Path: f.Relpath()} + for _, dep := range depIdMap { + if len(dep.Parents) == 0 { + root.AppendChild(dep) + } + } + + return root +} diff --git a/opensca/sca/sbom/sca.go b/opensca/sca/sbom/sca.go index 3950d955..8d0c41ce 100644 --- a/opensca/sca/sbom/sca.go +++ b/opensca/sca/sbom/sca.go @@ -25,10 +25,14 @@ func (sca Sca) Sca(ctx context.Context, parent *model.File, files []*model.File, if filter.SbomDsdx(file.Relpath()) { call(file, ParseDsdx(file)) } + if filter.SbomDbSbom(file.Relpath()) { + call(file, ParseDpSbomJson(file)) + } if filter.SbomJson(file.Relpath()) { call(file, ParseSpdxJson(file)) call(file, ParseCdxJson(file)) call(file, ParseDsdxJson(file)) + call(file, ParseDpSbomJson(file)) } if filter.SbomXml(file.Relpath()) { call(file, ParseSpdxXml(file))