diff --git a/.github/README.md b/.github/README.md index e437c59df19afc808972029341201c5cc01fbcfa..c8b1046b0922c24b0f5c2241b9c8e2be90c74fec 100644 --- a/.github/README.md +++ b/.github/README.md @@ -26,7 +26,7 @@ OpenSCA is now capable of parsing configuration files in the listed programming | ------------ | --------------- | ---------------------------------------------- | | `Java` | `Maven` | `pom.xml` | | `JavaScript` | `Npm` | `package-lock.json` `package.json` `yarn.lock` | -| `PHP` | `Composer` | `composer.json` | +| `PHP` | `Composer` | `composer.json` `composer.lock` | | `Ruby` | `gem` | `gemfile.lock` | | `Golang` | `gomod` | `go.mod` `go.sum` | | `Rust` | `cargo` | `Cargo.lock` | diff --git a/README.md b/README.md index 5e87d151ee5092cf71b834a5dcd6e4995b67b6db..bc274800ec10cd166ac76c40f1fceb26beaac4f7 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,15 @@ ## 检测能力 `OpenSCA`现已支持以下编程语言相关的配置文件解析及对应的包管理器,后续会逐步支持更多的编程语言,丰富相关配置文件的解析。 -| 支持语言 | 包管理器 | 解析文件 | -| ------------ | ---------- | ------------------------------------------------------ | -| `Java` | `Maven` | `pom.xml` | -| `JavaScript` | `Npm` | `package-lock.json``package.json``yarn.lock` | -| `PHP` | `Composer` | `composer.json` | -| `Ruby` | `gem` | `gemfile.lock` | -| `Golang` | `gomod` | `go.mod``go.sum` | -| `Rust` | `cargo` | `Cargo.lock` | -| `Erlang` | `Rebar` | `rebar.lock` | +| 支持语言 | 包管理器 | 解析文件 | +| ------------ | ---------- | ---------------------------------------------- | +| `Java` | `Maven` | `pom.xml` | +| `JavaScript` | `Npm` | `package-lock.json` `package.json` `yarn.lock` | +| `PHP` | `Composer` | `composer.json` `composer.lock` | +| `Ruby` | `gem` | `gemfile.lock` | +| `Golang` | `gomod` | `go.mod` `go.sum` | +| `Rust` | `cargo` | `Cargo.lock` | +| `Erlang` | `Rebar` | `rebar.lock` | ## 下载安装 diff --git a/analyzer/engine/engine.go b/analyzer/engine/engine.go index 45201f7066545281db4ab276d0c629480f6f28ef..ed67841be19483501b74bd0d37b8b8ddc9e8b0fc 100644 --- a/analyzer/engine/engine.go +++ b/analyzer/engine/engine.go @@ -10,16 +10,17 @@ import ( "os" "path" "strings" + "time" "util/args" "util/filter" "util/logs" "util/model" + "util/report" "util/vuln" "analyzer/analyzer" "analyzer/erlang" "analyzer/golang" - "analyzer/groovy" "analyzer/java" "analyzer/javascript" "analyzer/php" @@ -47,13 +48,23 @@ func NewEngine() Engine { } // ParseFile 解析一个目录或文件 -func (e Engine) ParseFile(filepath string) (*model.DepTree, error) { +func (e Engine) ParseFile(filepath string) (depRoot *model.DepTree, taskInfo report.TaskInfo) { // 目录树 dirRoot := model.NewDirTree() - depRoot := model.NewDepTree(nil) + depRoot = model.NewDepTree(nil) + taskInfo = report.TaskInfo{ + AppName: filepath, + StartTime: time.Now().Format("2006-01-02 15:04:05"), + } + s := time.Now() + defer func() { + taskInfo.CostTime = time.Since(s).Seconds() + taskInfo.EndTime = time.Now().Format("2006-01-02 15:04:05") + }() if f, err := os.Stat(filepath); err != nil { + taskInfo.Error = err logs.Error(err) - return depRoot, err + return depRoot, taskInfo } else { if f.IsDir() { // 目录 @@ -61,8 +72,13 @@ func (e Engine) ParseFile(filepath string) (*model.DepTree, error) { // 尝试解析mvn依赖 java.MvnDepTree(filepath, depRoot) // 尝试解析gradle依赖 - groovy.GradleDepTree(filepath, depRoot) + java.GradleDepTree(filepath, depRoot) } else if filter.AllPkg(filepath) { + if f, err := os.Stat(filepath); err != nil { + logs.Warn(err) + } else { + taskInfo.Size = f.Size() + } // 压缩包 dirRoot = e.unArchiveFile(filepath) } else if e.checkFile(filepath) { @@ -79,7 +95,7 @@ func (e Engine) ParseFile(filepath string) (*model.DepTree, error) { // 解析目录树获取依赖树 e.parseDependency(dirRoot, depRoot) // 获取漏洞 - err := vuln.SearchVuln(depRoot) + taskInfo.Error = vuln.SearchVuln(depRoot) // 是否仅保留漏洞组件 if args.OnlyVuln { root := model.NewDepTree(nil) @@ -116,5 +132,5 @@ func (e Engine) ParseFile(filepath string) (*model.DepTree, error) { dep.IndirectVulnerabilities = len(vulnExist) } } - return depRoot, err + return depRoot, taskInfo } diff --git a/analyzer/engine/parse.go b/analyzer/engine/parse.go index b6964a12561e977b90889d4153562ab913db1466..5f61b5dd6c9b55da630eb65763b72c0421b35667 100644 --- a/analyzer/engine/parse.go +++ b/analyzer/engine/parse.go @@ -39,6 +39,12 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) if d.Name != "" && d.Version.Ok() { d.Path = path.Join(d.Path, d.Dependency.String()) } + // 标识为直接依赖 + d.Direct = true + for _, c := range d.Children { + c.Direct = true + } + // 填充路径 q := []*model.DepTree{d} s := map[int64]struct{}{} for len(q) > 0 { diff --git a/analyzer/groovy/analyzer.go b/analyzer/groovy/analyzer.go deleted file mode 100644 index 4e7d15a5288a13017d9b3744851eaddb883e9cb3..0000000000000000000000000000000000000000 --- a/analyzer/groovy/analyzer.go +++ /dev/null @@ -1,39 +0,0 @@ -package groovy - -import ( - "util/enum/language" - "util/filter" - "util/model" -) - -type Analyzer struct{} - -func New() Analyzer { - return Analyzer{} -} - -// GetLanguage get language of analyzer -func (a Analyzer) GetLanguage() language.Type { - return language.Groovy -} - -// CheckFile check parsable file -func (a Analyzer) CheckFile(filename string) bool { - return false - // groovy 文件无法解析依赖层级,暂不处理 - // return filter.GroovyFile(filename) -} - -// ParseFiles parse dependency from file -func (a Analyzer) ParseFiles(files []*model.FileInfo) []*model.DepTree { - deps := []*model.DepTree{} - for _, f := range files { - dep := model.NewDepTree(nil) - dep.Path = f.Name - if filter.GroovyFile(f.Name) { - parseGroovyFile(dep, f) - } - deps = append(deps, dep) - } - return deps -} diff --git a/analyzer/groovy/grape.go b/analyzer/groovy/grape.go deleted file mode 100644 index abc0cccb686670383ee206cf35386a2f30341f22..0000000000000000000000000000000000000000 --- a/analyzer/groovy/grape.go +++ /dev/null @@ -1,25 +0,0 @@ -package groovy - -import ( - "regexp" - "util/model" -) - -// parseGroovyFile parse deps in groovy file -func parseGroovyFile(root *model.DepTree, file *model.FileInfo) { - // repo: @GrabResolver(name='mvnRepository', root='http://central.maven.org/maven2/') - regs := []*regexp.Regexp{ - // @Grab('org.springframework:spring-orm:3.2.5.RELEASE') - // @Grab('org.neo4j:neo4j-cypher:2.1.4;transitive=false') - regexp.MustCompile(``), - // @Grab(group='org.restlet', module='org.restlet', version='1.1.6') - // @Grab(group='org.restlet', module='org.restlet', version='1.1.6', classifier='jdk15') - regexp.MustCompile(``), - // Grape.grab(group:'org.slf4j', module:'slf4j-api', version:'1.7.25') - // Grape.grab(groupId:'com.jidesoft', artifactId:'jide-oss', version:'[2.2.1,2.3)', classLoader:loader) - } - for _, reg := range regs { - _ = reg - // match := reg.FindAllStringSubmatch(string(file.Data), -1) - } -} diff --git a/analyzer/java/ext.go b/analyzer/java/ext.go index eb381a40bf3830d0225701c7fd64287cc8dc90e7..cc4478087f948fdfb3d5e3d955f0061f0b5a8a47 100644 --- a/analyzer/java/ext.go +++ b/analyzer/java/ext.go @@ -48,6 +48,7 @@ func MvnDepTree(path string, root *model.DepTree) { start := 0 // 标记是否在依赖范围内树 tree := false + root.Direct = true // 获取mvn依赖树 for i, line := range lines { if title.MatchString(line) { @@ -58,6 +59,9 @@ func MvnDepTree(path string, root *model.DepTree) { if tree && strings.Trim(line, "-") == "" { tree = false buildMvnDepTree(root, lines[start+1:i]) + for _, c := range root.Children { + c.Direct = true + } continue } } diff --git a/analyzer/groovy/gradle.go b/analyzer/java/gradle.go similarity index 92% rename from analyzer/groovy/gradle.go rename to analyzer/java/gradle.go index 1d1ae4be65725fb974a7c79c3856e891f59a70fa..51f5e57171ae20b26ea729857910948f22b0ef67 100644 --- a/analyzer/groovy/gradle.go +++ b/analyzer/java/gradle.go @@ -1,4 +1,4 @@ -package groovy +package java import ( "bytes" @@ -45,6 +45,7 @@ func GradleDepTree(dirpath string, root *model.DepTree) { // 获取 gradle 解析内容 startTag := `ossDepStart` endTag := `ossDepEnd` + root.Direct = true for { startIndex, endIndex := bytes.Index(out, []byte(startTag)), bytes.Index(out, []byte(endTag)) if startIndex > -1 && endIndex > -1 { @@ -62,12 +63,15 @@ func GradleDepTree(dirpath string, root *model.DepTree) { d.Vendor = n.GroupId d.Name = n.ArtifactId d.Version = model.NewVersion(n.Version) - d.Language = language.Groovy + d.Language = language.Java for _, c := range n.Children { c.MapDep = model.NewDepTree(d) } q = append(q[1:], n.Children...) } + for _, c := range gdep.MapDep.Children { + c.Direct = true + } } else { break } diff --git a/analyzer/groovy/oss.gradle b/analyzer/java/oss.gradle similarity index 100% rename from analyzer/groovy/oss.gradle rename to analyzer/java/oss.gradle diff --git a/cli/main.go b/cli/main.go index fce14beaa2bd850a53508a8665c866e0cb0432e8..6d36bf58b2bbc20d11d2f836409b1b85f3fc822f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -25,25 +25,22 @@ func main() { } // output 输出结果 -func output(depRoot *model.DepTree, err error) { - // 整理错误信息 - errInfo := "" - if err != nil { - errInfo = err.Error() - } +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.Out) { + case ".html": + reportFunc = report.Html + case ".json": + reportFunc = report.Json + default: + reportFunc = report.Json + } if args.Out != "" { - switch path.Ext(args.Out) { - case ".html": - report.SaveHtml(depRoot, errInfo, args.Out) - case ".json": - report.SaveJson(depRoot, errInfo, args.Out) - default: - report.SaveJson(depRoot, errInfo, args.Out) - } + report.Save(reportFunc(depRoot, taskInfo), args.Out) } else { - fmt.Println(string(report.Json(depRoot, errInfo))) + fmt.Println(string(reportFunc(depRoot, taskInfo))) } } diff --git a/util/model/dependency.go b/util/model/dependency.go index 14d13e7562c32850816b5c143d596c3d4d667f3c..f6c5ba7223a25fc833694ba8854de008bcf25b08 100644 --- a/util/model/dependency.go +++ b/util/model/dependency.go @@ -72,20 +72,22 @@ func (dep Dependency) String() string { // DepTree 依赖树 type DepTree struct { Dependency - Vulnerabilities []*Vuln `json:"vulnerabilities,omitempty"` - IndirectVulnerabilities int `json:"indirect_vulnerabilities,omitempty"` + // 是否为直接依赖 + Direct bool `json:"direct"` // 依赖路径 Path string `json:"path,omitempty"` // 唯一的组件id,用来标识不同组件 ID int64 `json:"-"` // 父组件 - Parent *DepTree `json:"-"` - // 子组件 - Children []*DepTree `json:"children,omitempty"` + Parent *DepTree `json:"-"` + Vulnerabilities []*Vuln `json:"vulnerabilities,omitempty"` + IndirectVulnerabilities int `json:"indirect_vulnerabilities,omitempty"` // 许可证列表 licenseMap map[string]struct{} `json:"-"` Licenses []string `json:"licenses,omitempty"` - Expand interface{} `json:"-"` + // 子组件 + Children []*DepTree `json:"children,omitempty"` + Expand interface{} `json:"-"` } // NewDepTree 创建DepTree diff --git a/util/report/format.go b/util/report/format.go index ca0fd826fb113e4874cc5a615cf2a6e56ee628d8..346973110b1773655dea34659ea48057256ab794 100644 --- a/util/report/format.go +++ b/util/report/format.go @@ -1,11 +1,23 @@ package report import ( + "os" "strings" "util/enum/language" + "util/logs" "util/model" ) +// 任务检查信息 +type TaskInfo struct { + AppName string `json:"app_name"` + Size int64 `json:"size"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + CostTime float64 `json:"cost_time"` + Error error `json:"error,omitempty"` +} + // format 按照输出内容格式化(不可逆) func format(dep *model.DepTree) { q := []*model.DepTree{dep} @@ -23,3 +35,15 @@ func format(dep *model.DepTree) { node.Version = nil } } + +// Save 保存结果文件 +func Save(data []byte, filepath string) { + if len(data) > 0 { + if f, err := os.Create(filepath); err != nil { + logs.Error(err) + } else { + defer f.Close() + f.Write(data) + } + } +} diff --git a/util/report/html.go b/util/report/html.go index acca6bf874474efff99d802c6e01bdacdc285223..3a7fbfb5aa500ab7d6f45ae0880ed7ec3b05e370 100644 --- a/util/report/html.go +++ b/util/report/html.go @@ -1,13 +1,81 @@ package report import ( - "errors" + "bytes" + _ "embed" + "encoding/json" "util/logs" "util/model" ) -// SaveHtml 将结果保存为html文件 -func SaveHtml(dep *model.DepTree, err, filepath string) { +//go:embed index.html +var index []byte + +// Html 获取html格式报告数据 +func Html(dep *model.DepTree, taskInfo TaskInfo) []byte { + // html组件字段 + type htmlDep struct { + *model.DepTree + SecId int `json:"security_level_id,omitempty"` + Statis map[int]int `json:"vuln_statis"` + } + deps := []htmlDep{} + // html统计信息 + type htmlStatis struct { + Component map[int]int `json:"component"` + Vuln map[int]int `json:"vuln"` + } + statis := htmlStatis{ + Component: map[int]int{}, + Vuln: map[int]int{}, + } + vulnMap := map[string]int{} + // 遍历所有组件 format(dep) - logs.Error(errors.New("not implement")) + q := []*model.DepTree{dep} + for len(q) > 0 { + n := q[0] + q = append(q[1:], n.Children...) + // 删除不需要的数据 + n.Children = nil + n.IndirectVulnerabilities = 0 + // 计算组件风险 + secid := 5 + vuln_statis := map[int]int{} + for _, v := range n.Vulnerabilities { + vulnMap[v.Id] = v.SecurityLevelId + vuln_statis[v.SecurityLevelId]++ + if secid > v.SecurityLevelId { + secid = v.SecurityLevelId + } + } + if n.Name != "" { + statis.Component[secid]++ + deps = append(deps, htmlDep{ + n, + secid, + vuln_statis, + }) + } + } + // 统计漏洞风险 + for _, secid := range vulnMap { + statis.Vuln[secid]++ + } + // 生成html报告需要的json数据 + if data, err := json.Marshal(struct { + TaskInfo TaskInfo `json:"task_info"` + Statis htmlStatis `json:"statis"` + Components []htmlDep `json:"components"` + }{ + TaskInfo: taskInfo, + Statis: statis, + Components: deps, + }); err != nil { + logs.Warn(err) + return []byte{} + } else { + // 替换模板数据 + return bytes.Replace(index, []byte("N$}"), append(data, '}'), 1) + } } diff --git a/util/report/index.html b/util/report/index.html new file mode 100644 index 0000000000000000000000000000000000000000..76160e38fb06631d2daff19d699eb09efd00bfcc --- /dev/null +++ b/util/report/index.html @@ -0,0 +1,2 @@ +