diff --git a/.github/README.md b/.github/README.md
index 9548a15f6231ec2f3746386003564210a048d062..38b52eb4ca73b0b7a007dff8db3f4a233f0d7612 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -83,7 +83,7 @@ opensca-cli -db db.json -path ${project_path}
| `token` | `string` | Cloud service verification. You have to apply for it on the cloud service platform and use it with the `url` parameter. | `-token xxxxxxx` |
| `cache` | `bool` | This option is recommended. It can cache the downloaded files, for example, the `.pom` file, and save your time when detecting the same component next time. The downloaded files are saved in `.cache` under the same directory as opensca-cli. | `-cache` |
| `vuln` | `bool` | Show the vulnerabilities info only. Using this parameter, the component hierarchical architecture will **NOT** be included in the result. | `-vuln` |
-| `out` | `string` | Set the output file. The result defaults to json format. | `-out output.json` |
+| `out` | `string` | Set the output file. The result defaults to json format. Support the output of SBOM list in spdx format. | `-out output.json` |
| `db` | `string` | Set the local vulnerability database file. It helps when you prefer to use your own vulnerability database. The format of the vulnerability database is shown below. If the cloud and local vulnerability databases are both set, the result of detection will merge both. | `-db db.json` |
| `progress` | `bool` | Show the progress bar. | `-progress` |
| `dedup` | `bool` | Same result deduplication | `-dedup` |
diff --git a/README.md b/README.md
index 24a79fe1f01a287476973f2e1c2237e8c618becb..cdf70aad446814dc6939c583365f92377cc251f6 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
@@ -81,7 +80,7 @@ opensca-cli -db db.json -path ${project_path}
| `token` | `string` | 云服务验证 `token`,需要在云服务平台申请,与 `url` 参数一起使用 | `-token xxxxxxx` |
| `cache` | `bool` | 建议开启,缓存下载的文件(例如 `.pom` 文件),重复检测相同组件时会节省时间,下载的文件会保存到工具所在目录的.cache 目录下 | `-cache` |
| `vuln` | `bool` | 结果仅保留有漏洞信息的组件,使用该参数将不会保留组件层级结构 | `-vuln` |
-| `out` | `string` | 将检测结果保存到指定文件,根据后缀生成不同格式的文件,默认为 `json` 格式 | `-out output.json` |
+| `out` | `string` | 将检测结果保存到指定文件,根据后缀生成不同格式的文件,默认为 `json` 格式;支持以`spdx`格式展示`sbom`清单只需更换相应输出文件后缀即可 | `-out output.json` |
| `db` | `string` | 指定本地漏洞库文件,希望使用自己漏洞库时可用,漏洞库文件为 `json` 格式,具体格式会在之后给出;若同时使用云端漏洞库与本地漏洞库,漏洞查询结果取并集 | `-db db.json` |
| `progress` | `bool` | 显示进度条 | `-progress` |
| `dedup` | `bool` | 相同组件去重 | `-dedup` |
diff --git a/analyzer/engine/archive.go b/analyzer/engine/archive.go
index 88d97a922a04bd8c78843763284ddcb08b8f8448..d4882cadaefef5d422a5419a2be8f00662d41110 100644
--- a/analyzer/engine/archive.go
+++ b/analyzer/engine/archive.go
@@ -28,7 +28,8 @@ import (
// checkFile 检测是否为可检测的文件
func (e Engine) checkFile(filename string) bool {
for _, analyzer := range e.Analyzers {
- if analyzer.CheckFile(filename) {
+ if analyzer.CheckFile(filename) ||
+ filter.CheckLicense(filename) {
return true
}
}
diff --git a/analyzer/engine/parse.go b/analyzer/engine/parse.go
index 9049daa38b2357805036e1b5375fc307d02b6085..1f6fca1e4a4554dfaec2740370bb6e8263670a52 100644
--- a/analyzer/engine/parse.go
+++ b/analyzer/engine/parse.go
@@ -7,16 +7,25 @@ package engine
import (
"path"
+ "regexp"
"strings"
"util/filter"
"util/model"
)
+// copyright匹配优先级
+const (
+ low = iota
+ mid
+ high
+)
+
// parseDependency 解析依赖
func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) *model.DepTree {
if depRoot == nil {
depRoot = model.NewDepTree(nil)
}
+ var copyrightMess = make(map[string]string)
for _, analyzer := range e.Analyzers {
// 遍历目录树获取要检测的文件
files := []*model.FileInfo{}
@@ -30,11 +39,22 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree)
for _, f := range n.Files {
if analyzer.CheckFile(f.Name) {
files = append(files, f)
+ } else if filter.CheckLicense(f.Name) {
+ if _, ok := copyrightMess[path.Dir(f.Name)]; !ok {
+ // 记录解析到的copyrigh信息
+ copyrightMess[path.Dir(f.Name)] = parseCopyright(f)
+ }
}
}
}
// 从文件中解析依赖树
for _, d := range analyzer.ParseFiles(files) {
+ p := path.Dir(d.Path)
+ if _, ok := copyrightMess[p]; ok {
+ // 将copyright信息加入与其同一文件目录的依赖节点中
+ d.CopyrightText = copyrightMess[p]
+ delete(copyrightMess, p)
+ }
depRoot.Children = append(depRoot.Children, d)
d.Parent = depRoot
if d.Name != "" && !strings.ContainsAny(d.Vendor+d.Name, "${}") && d.Version.Ok() {
@@ -99,3 +119,47 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree)
}
return depRoot
}
+
+// 从文件中提取copyright信息
+func parseCopyright(f *model.FileInfo) string {
+ matchLevel := map[int]string{}
+ ct := string(f.Data)
+ if len(ct) == 0 {
+ return ""
+ }
+ pras := strings.Split(ct, "\n\n")
+ re := regexp.MustCompile(`^\d{4}$|^\d{4}-\d{4}$|^\(c\)$`)
+ for _, pra := range pras {
+ if !strings.Contains(strings.ToLower(pra), "copyright") {
+ continue
+ }
+ lines := strings.Split(pra, "\n")
+ line := strings.TrimSpace(lines[0])
+ if len(lines) == 0 {
+ continue
+ }
+ tks := strings.Fields(line)
+ if len(tks) == 0 {
+ continue
+ }
+ if strings.EqualFold("copyright", tks[0]) {
+ if re.MatchString(tks[1]) {
+ matchLevel[high] = line
+ }
+ matchLevel[mid] = line
+ }
+ for _, l := range lines {
+ if strings.HasPrefix(strings.TrimSpace(strings.ToLower(l)), "copyright") {
+ matchLevel[low] = strings.TrimSpace(l)
+ break
+ }
+ }
+
+ }
+ for i := high; i >= low; i-- {
+ if matchLevel[i] != "" {
+ return matchLevel[i]
+ }
+ }
+ return ""
+}
diff --git a/analyzer/golang/gomod.go b/analyzer/golang/gomod.go
index 119105280a62f27253a5022b412251a8d97b8c30..334bb6e25975931d67435a4dead4210f21df97d2 100644
--- a/analyzer/golang/gomod.go
+++ b/analyzer/golang/gomod.go
@@ -20,6 +20,7 @@ func parseGomod(dep *model.DepTree, file *model.FileInfo) {
sub := model.NewDepTree(dep)
sub.Name = strings.Trim(match[1], `'"`)
sub.Version = model.NewVersion(match[2])
+ sub.HomePage = "https://" + sub.Name
}
}
@@ -40,6 +41,7 @@ func parseGosum(dep *model.DepTree, file *model.FileInfo) {
sub := model.NewDepTree(dep)
sub.Name = strings.Trim(match[1], `'"`)
sub.Version = model.NewVersion(match[2])
+ sub.HomePage = sub.Name
exist[sub.Name] = struct{}{}
}
}
diff --git a/analyzer/javascript/package_json.go b/analyzer/javascript/package_json.go
index e5d2eed52815e68d69c9a4f5968f843bf1b3d8fa..2aec0c4a778e9af85540ad26250f9fd8cf551d99 100644
--- a/analyzer/javascript/package_json.go
+++ b/analyzer/javascript/package_json.go
@@ -22,11 +22,13 @@ import (
// package.json 文件结构
type PkgJson struct {
- Name string `json:"name"`
- Version string `json:"version"`
- License string `json:"license"`
- DevDeps map[string]string `json:"devDependencies"`
- Deps map[string]string `json:"dependencies"`
+ Name string `json:"name"`
+ Version string `json:"version"`
+ License string `json:"license"`
+ DevDeps map[string]string `json:"devDependencies"`
+ Deps map[string]string `json:"dependencies"`
+ HomePage string `json:"homepage"`
+ Repository map[string]string `json:"repository,omitempty"`
}
// npm下载文件结构
@@ -41,13 +43,15 @@ func parsePackage(root *model.DepTree, file *model.FileInfo, simulation bool) (d
pkg := PkgJson{}
if err := json.Unmarshal(file.Data, &pkg); err != nil {
logs.Error(err)
- return
}
if pkg.Name != "" {
root.Name = pkg.Name
}
- root.Version = model.NewVersion(pkg.Version)
+ if pkg.Version != "" {
+ root.Version = model.NewVersion(pkg.Version)
+ }
root.AddLicense(pkg.License)
+ root.HomePage = pkg.HomePage
// 依赖列表map[name]version
depMap := map[string]string{}
for name, version := range pkg.DevDeps {
@@ -81,7 +85,7 @@ func parsePackage(root *model.DepTree, file *model.FileInfo, simulation bool) (d
}
for !q.Empty() {
node := q.Pop().(*model.DepTree)
- for _, sub := range npmSimulation(node) {
+ for _, sub := range npmSimulation(node, exist) {
if _, ok := exist[sub.Name]; !ok {
bar.Npm.Add(1)
exist[sub.Name] = struct{}{}
@@ -93,7 +97,7 @@ func parsePackage(root *model.DepTree, file *model.FileInfo, simulation bool) (d
}
// npmSimulation 模拟npm获取详细依赖信息
-func npmSimulation(dep *model.DepTree) (subDeps []*model.DepTree) {
+func npmSimulation(dep *model.DepTree, exist map[string]struct{}) (subDeps []*model.DepTree) {
subDeps = []*model.DepTree{}
dep.Language = language.JavaScript
// 获取依赖数据
@@ -149,6 +153,9 @@ func npmSimulation(dep *model.DepTree) (subDeps []*model.DepTree) {
}
sort.Strings(names)
for _, name := range names {
+ if _, ok := exist[name]; ok {
+ continue
+ }
sub := model.NewDepTree(dep)
sub.Name = name
sub.Version = model.NewVersion(info.Deps[name])
diff --git a/analyzer/javascript/package_lock.go b/analyzer/javascript/package_lock.go
index 0a940580a1bd4b3e1cbd0dfee041e2147fee2a51..564f21ba056994f16947bb0d8141b3d10ce0803b 100644
--- a/analyzer/javascript/package_lock.go
+++ b/analyzer/javascript/package_lock.go
@@ -45,7 +45,7 @@ func parsePackageLock(root *model.DepTree, file *model.FileInfo, direct []string
}{}
if err := json.Unmarshal(file.Data, &lock); err != nil {
logs.Error(err)
- return
+ //return
}
if lock.Name != "" {
root.Name = lock.Name
diff --git a/analyzer/php/composer.go b/analyzer/php/composer.go
index b1e55f969903d4cde7e3c3177b4aa5006fb51632..0403888aabb3f7f0f447f700c1d4cd8aa3367a96 100644
--- a/analyzer/php/composer.go
+++ b/analyzer/php/composer.go
@@ -26,6 +26,8 @@ type Composer struct {
License string `json:"license"`
Require map[string]string `json:"require"`
RequireDev map[string]string `json:"require-dev"`
+ HomePage string `json:"homepage"`
+ Support map[string]string `json:"support"`
}
type ComposerRepo struct {
@@ -47,6 +49,8 @@ func parseComposer(root *model.DepTree, file *model.FileInfo, simulation bool) (
if composer.Name != "" {
root.Name = composer.Name
}
+ root.HomePage = composer.HomePage
+ root.DownloadLocation = composer.Support["source"]
// add license
if composer.License != "" {
root.AddLicense(composer.License)
@@ -84,7 +88,7 @@ func parseComposer(root *model.DepTree, file *model.FileInfo, simulation bool) (
}
for !q.Empty() {
node := q.Pop().(*model.DepTree)
- for _, sub := range composerSimulation(node) {
+ for _, sub := range composerSimulation(node, exist) {
if _, ok := exist[sub.Name]; !ok {
bar.Composer.Add(1)
exist[sub.Name] = struct{}{}
@@ -96,7 +100,7 @@ func parseComposer(root *model.DepTree, file *model.FileInfo, simulation bool) (
}
// composerSimulation composer simulation
-func composerSimulation(dep *model.DepTree) (subDeps []*model.DepTree) {
+func composerSimulation(dep *model.DepTree, exist map[string]struct{}) (subDeps []*model.DepTree) {
subDeps = []*model.DepTree{}
dep.Language = language.Php
data := cache.LoadCache(dep.Dependency)
@@ -148,6 +152,9 @@ func composerSimulation(dep *model.DepTree) (subDeps []*model.DepTree) {
if strings.EqualFold(name, "php") {
continue
}
+ if _, ok := exist[name]; ok {
+ continue
+ }
sub := model.NewDepTree(dep)
sub.Name = name
sub.Version = model.NewVersion(requires[name])
diff --git a/analyzer/php/composer_lock.go b/analyzer/php/composer_lock.go
index e752f0f4dd1fc68e18c9001cdc743289127ad4c4..e1beec6bb984321aa2186ffa9c2866c25dd3fa5c 100644
--- a/analyzer/php/composer_lock.go
+++ b/analyzer/php/composer_lock.go
@@ -8,6 +8,7 @@ package php
import (
"encoding/json"
"sort"
+ "strings"
"util/logs"
"util/model"
)
@@ -15,9 +16,11 @@ import (
// composer.lock
type ComposerLock struct {
Pkgs []struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Require map[string]string `json:"require"`
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Require map[string]string `json:"require"`
+ HomePage string `json:"homepage"`
+ Source map[string]string `json:"source"`
} `json:"packages"`
}
@@ -26,7 +29,7 @@ func parseComposerLock(root *model.DepTree, file *model.FileInfo, direct []strin
lock := ComposerLock{}
if err := json.Unmarshal(file.Data, &lock); err != nil {
logs.Error(err)
- return
+ //return
}
// 记录尚无Parent的依赖
depMap := map[string]*model.DepTree{}
@@ -37,6 +40,8 @@ func parseComposerLock(root *model.DepTree, file *model.FileInfo, direct []strin
dep.Name = cps.Name
dep.Version = model.NewVersion(cps.Version)
dep.Expand = cps.Require
+ dep.HomePage = cps.HomePage
+ dep.DownloadLocation = strings.ReplaceAll(cps.Source["url"], ".git", "")
depMap[cps.Name] = dep
directMap[cps.Name] = dep
}
diff --git a/analyzer/python/pipfile.go b/analyzer/python/pipfile.go
index 4b720c9cf0fdbde0101f5c813f4b378c9ccd5d79..71575227a5550be9e48757fc91325af2188ec9b9 100644
--- a/analyzer/python/pipfile.go
+++ b/analyzer/python/pipfile.go
@@ -2,6 +2,7 @@ package python
import (
"encoding/json"
+ "strings"
"util/logs"
"util/model"
@@ -20,12 +21,12 @@ func parsePipfile(root *model.DepTree, file *model.FileInfo) {
for name, version := range pip.Packages {
dep := model.NewDepTree(root)
dep.Name = name
- dep.Version = model.NewVersion(version)
+ dep.Version = model.NewVersion(formatVer(version))
}
for name, version := range pip.DevPackages {
dep := model.NewDepTree(root)
dep.Name = name
- dep.Version = model.NewVersion(version)
+ dep.Version = model.NewVersion(formatVer(version))
}
}
@@ -49,8 +50,17 @@ func parsePipfileLock(root *model.DepTree, file *model.FileInfo) {
if v != "" {
dep := model.NewDepTree(root)
dep.Name = n
- dep.Version = model.NewVersion(v)
+ dep.Version = model.NewVersion(formatVer(v))
}
}
return
}
+
+// 后续使用其他办法确定版本号
+func formatVer(v string) string {
+ res := strings.ReplaceAll(v, "==", "")
+ res = strings.ReplaceAll(res, "~=", "")
+ res = strings.ReplaceAll(res, ">=", "")
+ res = strings.ReplaceAll(res, "<=", "")
+ return res
+}
diff --git a/analyzer/python/setup.go b/analyzer/python/setup.go
index 5ce395056b901202229382a8902d2a69dbef5486..4f79b561efbf3f017dbb8b0e026857c0b3a7a827 100644
--- a/analyzer/python/setup.go
+++ b/analyzer/python/setup.go
@@ -56,7 +56,7 @@ func parseSetup(root *model.DepTree, file *model.FileInfo) {
logs.Warn(err)
}
root.Name = dep.Name
- root.Version = model.NewVersion(dep.Version)
+ root.Version = model.NewVersion(formatVer(dep.Version))
root.Licenses = append(root.Licenses, dep.License)
for _, pkg := range [][]string{dep.Packages, dep.InstallRequires, dep.Requires} {
for _, p := range pkg {
@@ -64,7 +64,7 @@ func parseSetup(root *model.DepTree, file *model.FileInfo) {
sub := model.NewDepTree(root)
if index > -1 {
sub.Name = p[:index]
- sub.Version = model.NewVersion(p[index:])
+ sub.Version = model.NewVersion(formatVer(p[index:]))
} else {
sub.Name = p
}
diff --git a/cli/go.mod b/cli/go.mod
index 9ad835ea1acac6f47013e6bdc7d9f19ac5e988c5..e88a59ba0f57d13510b5cf00affe712e25999e5a 100644
--- a/cli/go.mod
+++ b/cli/go.mod
@@ -1,3 +1,5 @@
module cli
go 1.18
+
+require github.com/Masterminds/semver/v3 v3.1.1
diff --git a/cli/go.sum b/cli/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..471bde90bb2e48c4593d65f12500b1d83585bfa8
--- /dev/null
+++ b/cli/go.sum
@@ -0,0 +1,2 @@
+github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
diff --git a/cli/main.go b/cli/main.go
index 796942c5ccb90170707e4ab3e4936b2e84ec3c96..ace61e3869f46dbc54ef66bee7284f4c80a0c0a8 100644
--- a/cli/main.go
+++ b/cli/main.go
@@ -9,6 +9,7 @@ import (
"flag"
"fmt"
"path"
+ "strings"
"util/args"
"util/logs"
"util/model"
@@ -33,11 +34,23 @@ func output(depRoot *model.DepTree, taskInfo report.TaskInfo) {
logs.Debug("\n" + depRoot.String())
// 输出结果
var reportFunc func(*model.DepTree, report.TaskInfo) []byte
- switch path.Ext(args.Config.Out) {
+ out := args.Config.Out
+ switch path.Ext(out) {
case ".html":
reportFunc = report.Html
case ".json":
+ if strings.HasSuffix(out, ".spdx.json") {
+ reportFunc = report.SpdxJson
+ break
+ }
reportFunc = report.Json
+ case ".spdx":
+ reportFunc = report.Spdx
+ case ".xml":
+ if strings.HasSuffix(out, ".spdx.xml") {
+ reportFunc = report.SpdxXml
+ break
+ }
default:
reportFunc = report.Json
}
diff --git a/util/filter/file.go b/util/filter/file.go
index 81ffe09f9b1cd99940ee01823340bf083a6e60be..55f1b7003f49ed2a9d442fd599161403bd587142 100644
--- a/util/filter/file.go
+++ b/util/filter/file.go
@@ -5,6 +5,9 @@
package filter
import (
+ "fmt"
+ "path"
+ "regexp"
"strings"
)
@@ -92,3 +95,24 @@ var (
PythonPipfile = filterFunc(strings.HasSuffix, "Pipfile")
PythonPipfileLock = filterFunc(strings.HasSuffix, "Pipfile.lock")
)
+
+// 用于筛选可能有copyright信息的文件
+var (
+ LicenseFileNames = []string{
+ "li[cs]en[cs]e(s?)",
+ "legal",
+ "copy(left|right|ing)",
+ "unlicense",
+ "l?gpl([-_ v]?)(\\d\\.?\\d)?",
+ "bsd",
+ "mit",
+ "apache",
+ }
+ LicenseFileRe = regexp.MustCompile(
+ fmt.Sprintf("^(|.*[-_. ])(%s)(|[-_. ].*)$",
+ strings.Join(LicenseFileNames, "|")))
+)
+
+func CheckLicense(name string) bool {
+ return LicenseFileRe.MatchString(strings.ToLower(path.Base(name)))
+}
diff --git a/util/model/dependency.go b/util/model/dependency.go
index cfcfede6aaacc038b56a5f80e237c556f764a3a2..e80df434522622cf1b7e5dcf8f94646c9f139e93 100644
--- a/util/model/dependency.go
+++ b/util/model/dependency.go
@@ -86,10 +86,19 @@ type DepTree struct {
// 许可证列表
licenseMap map[string]struct{} `json:"-"`
Licenses []string `json:"licenses,omitempty"`
+ // spdx相关字段
+ CopyrightText string `json:"copyrightText,omitempty"`
+ HomePage string `json:"-"`
+ DownloadLocation string `json:"-"`
+ CheckSum string `json:"-"`
// 子组件
Children []*DepTree `json:"children,omitempty"`
Expand interface{} `json:"-"`
}
+type CheckSum struct {
+ Algorithm string `json:"algorithm,omitempty"`
+ Value string `json:"value,omitempty"`
+}
// NewDepTree 创建DepTree
func NewDepTree(parent *DepTree) *DepTree {
@@ -103,6 +112,7 @@ func NewDepTree(parent *DepTree) *DepTree {
Children: []*DepTree{},
licenseMap: map[string]struct{}{},
Licenses: []string{},
+ CopyrightText: "",
}
if parent != nil {
parent.Children = append(parent.Children, dep)
@@ -124,6 +134,9 @@ func (dep *DepTree) Move(other *DepTree) {
if other == nil {
return
}
+ if other.CopyrightText == "" {
+ other.CopyrightText = dep.CopyrightText
+ }
// 从父节点中删除当前节点
if dep.Parent != nil {
for i, child := range dep.Parent.Children {
diff --git a/util/report/format.go b/util/report/format.go
index 4da0940e8058cdc48e7c79e614e68d918f4e4f5a..b23d439bc302bfae6a161fe1ec5eb5c5b8a0de16 100644
--- a/util/report/format.go
+++ b/util/report/format.go
@@ -52,6 +52,12 @@ func format(dep *model.DepTree) {
if d, ok := dm[k]; !ok {
dm[k] = n
} else {
+ // 临时解决部分组件homepage字段不显示问题
+ // 因为去重时刚好把解析到homepage字段的组件去掉了
+ // 其他字段可能也需要类似操作
+ if n.HomePage != "" {
+ d.HomePage = n.HomePage
+ }
// 已存在相同组件
d.Paths = append(d.Paths, n.Path)
// 从父组件中移除当前组件
diff --git a/util/report/spdx.go b/util/report/spdx.go
new file mode 100644
index 0000000000000000000000000000000000000000..47270c324e048580d477f4821a429f11bb44ed60
--- /dev/null
+++ b/util/report/spdx.go
@@ -0,0 +1,222 @@
+package report
+
+import (
+ "bytes"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "path"
+ "strings"
+ "text/template"
+ "time"
+ "util/logs"
+ "util/model"
+)
+
+// 记录节点名与pacakge的对应关系
+var nodePkg = make(map[*model.DepTree]Package)
+
+func init() {
+ replacers := []string{"_", "-", "/", "."}
+ replacer = strings.NewReplacer(replacers...)
+}
+func Spdx(dep *model.DepTree, taskInfo TaskInfo) []byte {
+ format(dep)
+ doc := buildDocument(dep, taskInfo)
+ addPkgToDoc(dep, doc)
+ addRelation(dep, doc)
+ tmpl := template.New("tagValue")
+ tmpl, err := tmpl.Parse(T)
+
+ if err != nil {
+ logs.Warn(err)
+ }
+ templateBuffer := new(bytes.Buffer)
+ err = tmpl.Execute(templateBuffer, doc)
+ if err != nil {
+ logs.Error(err)
+ }
+ return templateBuffer.Bytes()
+}
+func SpdxJson(dep *model.DepTree, taskInfo TaskInfo) []byte {
+ format(dep)
+ doc := buildDocument(dep, taskInfo)
+ addPkgToDoc(dep, doc)
+ addRelation(dep, doc)
+ type D struct {
+ Document `json:"document"`
+ }
+ d := D{*doc}
+ res, err := json.Marshal(d.Document)
+ if err != nil {
+ logs.Error(err)
+ }
+ return res
+}
+func SpdxXml(dep *model.DepTree, taskInfo TaskInfo) []byte {
+ format(dep)
+ doc := buildDocument(dep, taskInfo)
+ addPkgToDoc(dep, doc)
+ addRelation(dep, doc)
+ type D struct {
+ Document `xml:"document"`
+ }
+ d := D{*doc}
+ res, err := xml.Marshal(d.Document)
+ if err != nil {
+ logs.Error(err)
+ }
+ return res
+}
+
+// 为document添加relationship字段
+func addRelation(dep *model.DepTree, doc *Document) {
+ doc.Relationships = append(doc.Relationships, Relationship{
+ SPDXElementID: "SPDXRef-DOCUMENT",
+ RelatedSPDXElement: doc.DocumentName,
+ RelationshipType: "DESCRIBES",
+ })
+ q := []*model.DepTree{dep}
+ for len(q) > 0 {
+ n := q[0]
+ if pkg, ok := nodePkg[n]; ok {
+ if !pkg.RootPackage {
+ q = append(q[1:], n.Children...)
+ continue
+ }
+ for _, sub := range n.Children {
+ if subpkg, ok := nodePkg[sub]; ok {
+ doc.Relationships = append(doc.Relationships, Relationship{
+ SPDXElementID: pkg.SPDXID,
+ RelatedSPDXElement: subpkg.SPDXID,
+ RelationshipType: "DEPENDS_ON",
+ })
+ }
+ }
+ }
+ q = append(q[1:], n.Children...)
+ }
+}
+
+// 为document添加packages字段
+func addPkgToDoc(root *model.DepTree, doc *Document) {
+ if root.Name == "" {
+ root.Name = doc.DocumentName
+ }
+ q := []*model.DepTree{root}
+ for len(q) > 0 {
+ n := q[0]
+ q = append(q[1:], n.Children...)
+ doc.Packages = append(doc.Packages, buildPkg(n))
+ }
+}
+
+// 构建package
+func buildPkg(dep *model.DepTree) Package {
+ pkg := Package{
+ PackageName: setpkgName(dep),
+ SPDXID: "",
+ PackageVersion: setPkgVer(dep),
+ PackageSupplier: setPkgSup(dep),
+ PackageDownloadLocation: setPkgDownloadLoc(dep),
+ // FilesAnalyzed: false,
+ //PackageChecksums: nil,
+ PackageHomePage: setHomePage(dep),
+ PackageLicenseConcluded: setPkgLicenseCon(dep),
+ PackageLicenseDeclared: setPkgLicenseDec(dep),
+ PackageCopyrightText: setCopyrightCont(dep),
+ PackageLicenseComments: setPkgLicenseComments(dep),
+ PackageComment: setPkgComments(dep),
+ RootPackage: isParent(dep),
+ }
+ pkg.SPDXID = setPkgSPDXID(dep.Name, dep.VersionStr)
+ nodePkg[dep] = pkg
+ return pkg
+}
+
+// 初始化Document
+func buildDocument(root *model.DepTree, taskInfo TaskInfo) *Document {
+ return &Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "",
+ SPDXID: "SPDXRef-DOCUMENT",
+ DocumentName: path.Base(taskInfo.AppName),
+ DocumentNamespace: "",
+ CreationInfo: CreationInfo{
+ Creators: []string{"OpenSCA-Cli"},
+ Created: time.Now().Format("2006-01-02 15:04:05"),
+ },
+ Packages: []Package{},
+ Relationships: []Relationship{},
+ ExtractedLicensingInfos: []ExtractedLicensingInfo{},
+ }
+}
+
+func setPkgSPDXID(s, v string) string {
+ if v == "" {
+ return fmt.Sprintf("SPDXRef-Package-%s", replacer.Replace(s))
+ }
+ return fmt.Sprintf("SPDXRef-Package-%s-%s", replacer.Replace(s), v)
+}
+func setpkgName(dep *model.DepTree) string {
+ if dep.Name != "" {
+ return dep.Name
+ }
+ return ""
+}
+func setPkgVer(dep *model.DepTree) string {
+ if dep.VersionStr != "" {
+ return dep.VersionStr
+ }
+ return "NOASSERTION"
+}
+func setPkgSup(dep *model.DepTree) string {
+ if dep.Vendor != "" {
+ return dep.Vendor
+ }
+ return "NOASSERTION"
+}
+func setPkgDownloadLoc(dep *model.DepTree) string {
+ if dep.DownloadLocation != "" {
+ return dep.DownloadLocation
+ }
+ return "NOASSERTION"
+}
+func setHomePage(dep *model.DepTree) string {
+ if dep.HomePage != "" {
+ return dep.HomePage
+ }
+ return "NOASSERTION"
+}
+func setPkgLicenseCon(dep *model.DepTree) string {
+ if len(dep.Licenses) > 0 {
+ lic := ""
+ for _, v := range dep.Licenses {
+ if lic == "" {
+ lic = v
+ continue
+ }
+ lic = lic + " OR " + v
+ }
+ return lic
+ }
+ return "NOASSERTION"
+}
+func setPkgLicenseDec(dep *model.DepTree) string {
+ return "NOASSERTION"
+}
+func setCopyrightCont(dep *model.DepTree) string {
+ if dep.CopyrightText != "" {
+ return dep.CopyrightText
+ }
+ return "NOASSERTION"
+}
+func setPkgLicenseComments(dep *model.DepTree) string {
+ return "NOASSERTION"
+}
+func setPkgComments(dep *model.DepTree) string {
+ return "NOASSERTION"
+}
+func isParent(dep *model.DepTree) bool {
+ return len(dep.Children) > 0
+}
diff --git a/util/report/spdx_type.go b/util/report/spdx_type.go
new file mode 100644
index 0000000000000000000000000000000000000000..a57d94a0753048016afc2a69b95a73c0cd1c976d
--- /dev/null
+++ b/util/report/spdx_type.go
@@ -0,0 +1,103 @@
+package report
+
+import "strings"
+
+var replacer *strings.Replacer
+
+type HashAlgorithm string
+type Package struct {
+ PackageName string `json:"name,omitempty" xml:"name,omitempty"`
+ SPDXID string `json:"SPDXID,omitempty" xml:"SPDXID,omitempty"`
+ PackageVersion string `json:"versionInfo,omitempty" xml:"versionInfo,omitempty"`
+ PackageSupplier string `json:"supplier,omitempty" xml:"supplier,omitempty"`
+ PackageDownloadLocation string `json:"downloadLocation,omitempty" xml:"downloadLocation,omitempty"`
+ // FilesAnalyzed bool `json:"filesAnalyzed" xml:"filesAnalyzed"`
+ // PackageChecksums []PackageChecksum `json:"checksums,omitempty" xml:"checksums>checksum,omitempty"`
+ PackageHomePage string `json:"homepage,omitempty" xml:"homepage,omitempty"`
+ PackageLicenseConcluded string `json:"licenseConcluded,omitempty" xml:"licenseConcluded,omitempty"`
+ PackageLicenseDeclared string `json:"licenseDeclared,omitempty" xml:"licenseDeclared,omitempty"`
+ PackageCopyrightText string `json:"copyrightText,omitempty" xml:"copyrightText,omitempty"`
+ PackageLicenseComments string `json:"licenseComments,omitempty" xml:"licenseComments,omitempty"`
+ PackageComment string `json:"comment,omitempty" xml:"comment,omitempty"`
+ RootPackage bool `json:"-" xml:"-"`
+}
+
+type Document struct {
+ SPDXVersion string `json:"spdxVersion,omitempty" xml:"spdxVersion,omitempty"`
+ DataLicense string `json:"dataLicense,omitempty" xml:"dataLicense,omitempty"`
+ SPDXID string `json:"SPDXID,omitempty" xml:"SPDXID,omitempty"`
+ DocumentName string `json:"name,omitempty" xml:"name,omitempty"`
+ DocumentNamespace string `json:"documentNamespace,omitempty" xml:"documentNamespace,omitempty"`
+ CreationInfo CreationInfo `json:"creationInfo,omitempty" xml:"creationInfo,omitempty"`
+ Packages []Package `json:"packages,omitempty" xml:"packages>package,omitempty"`
+ Relationships []Relationship `json:"relationships,omitempty" xml:"relationships>relationship,omitempty"`
+ ExtractedLicensingInfos []ExtractedLicensingInfo `json:"hasExtractedLicensingInfos,omitempty" xml:"hasExtractedLicensingInfos>ExtractedLicensingInfo,omitempty"`
+}
+
+type CreationInfo struct {
+ Comment string `json:"comment,omitempty" xml:"comment,omitempty"`
+ Created string `json:"created,omitempty" xml:"created,omitempty"`
+ Creators []string `json:"creators,omitempty" xml:"creators>creator,omitempty"`
+ LicenceListVersion string `json:"licenseListVersion,omitempty" xml:"licenseListVersion,omitempty"`
+}
+
+type Relationship struct {
+ SPDXElementID string `json:"spdxElementId,omitempty" xml:"spdxElementId,omitempty"`
+ RelatedSPDXElement string `json:"relatedSpdxElement,omitempty" xml:"relatedSpdxElement,omitempty"`
+ RelationshipType string `json:"relationshipType,omitempty" xml:"relationshipType,omitempty"`
+}
+type ExtractedLicensingInfo struct {
+ LicenseID string `json:"licenseId,omitempty" xml:"licenseId,omitempty"`
+ ExtractedText string `json:"extractedText,omitempty" xml:"extractedText,omitempty"`
+ LicenseName string `json:"name,omitempty" xml:"name,omitempty"`
+ LicenseComment string `json:"comment,omitempty" xml:"comment,omitempty"`
+}
+type PackageChecksum struct {
+ Algorithm HashAlgorithm `json:"algorithm,omitempty" xml:"algorithm,omitempty"`
+ Value string `json:"checksumValue,omitempty" xml:"checksumValue,omitempty"`
+}
+
+const T = `SPDXVersion: {{ .SPDXVersion }}
+DataLicense: {{ .DataLicense }}
+SPDXID: {{ .SPDXID }}
+DocumentName: {{ .DocumentName }}
+DocumentNamespace: {{ .DocumentNamespace }}
+Creator: {{ range .CreationInfo.Creators }}{{ . -}} {{ end }}
+Created: {{ .CreationInfo.Created }}
+
+{{ range .Packages }}
+##### Package representing the {{.PackageName}}
+
+PackageName: {{ .PackageName }}
+SPDXID: {{ .SPDXID }}
+{{ with .PackageVersion -}}
+PackageVersion: {{ . }}
+{{- end }}
+PackageSupplier: {{ .PackageSupplier }}
+PackageDownloadLocation: {{ .PackageDownloadLocation }}
+PackageHomePage: {{ .PackageHomePage }}
+PackageLicenseConcluded: {{ .PackageLicenseConcluded }}
+PackageLicenseDeclared: {{ .PackageLicenseDeclared }}
+PackageCopyrightText: {{ .PackageCopyrightText }}
+PackageLicenseComments: {{ .PackageLicenseComments }}
+PackageComment: {{ .PackageComment }}
+{{ end }}
+{{- range .Relationships }}
+Relationship: {{ .SPDXElementID }} {{ .RelationshipType }} {{ .RelatedSPDXElement }}
+{{- end }}
+
+{{- with .ExtractedLicensingInfos -}}
+##### Non-standard license
+{{ range . }}
+LicenseID: {{ .LicenseID }}
+ExtractedText: {{ .ExtractedText }}
+LicenseName: {{ .LicenseName }}
+LicenseComment: {{ .LicenseComment }}
+{{- end -}}
+{{- end -}}`
+
+// {{- range .PackageChecksums }}
+// PackageChecksum: {{ .Algorithm }}: {{ .Value }}
+// {{- end }}
+
+// FilesAnalyzed: {{ .FilesAnalyzed }}