From 8f1753d57a461a6e754b7e714467825657b2fa8e Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Fri, 25 Nov 2022 10:42:07 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=EF=BC=9A=E5=AF=B9=E6=8E=A5saas=E6=96=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E3=80=82=E8=AF=B7=E6=B1=82=E5=93=8D=E5=BA=94=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8F=98=E5=8A=A8=EF=BC=8C=E8=AF=B7=E6=B1=82=E4=BC=9A=E4=BC=A0?= =?UTF-8?q?=E9=80=92=E9=A1=B9=E7=9B=AE=E4=BE=9D=E8=B5=96=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E3=80=81=E9=A1=B9=E7=9B=AE=E5=93=88=E5=B8=8C=E3=80=81=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=90=8D=E7=A7=B0=E3=80=81=E9=A1=B9=E7=9B=AE=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E7=AD=89=20BUG=E4=BF=AE=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8F=90=E5=8F=96copyright=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E6=97=B6=E5=8F=AF=E8=83=BD=E6=8A=A5=E9=94=99=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analyzer/engine/engine.go | 5 ++ analyzer/engine/parse.go | 119 +++++++++++++++++++++++++++++++++++++- util/args/args.go | 3 + util/client/client.go | 86 +++++++++++++++++++++++++++ util/model/component.go | 99 +++++++++++++++++++++++++++++++ util/model/dependency.go | 119 +++++++++++++++++++++++++++++++++++++- util/report/format.go | 49 +++++++++------- util/vuln/server.go | 16 +++++ util/vuln/vuln.go | 20 ++++++- 9 files changed, 491 insertions(+), 25 deletions(-) create mode 100644 util/model/component.go diff --git a/analyzer/engine/engine.go b/analyzer/engine/engine.go index 4f6c7d4..0822e04 100644 --- a/analyzer/engine/engine.go +++ b/analyzer/engine/engine.go @@ -12,6 +12,7 @@ import ( "strings" "time" "util/args" + "util/client" "util/filter" "util/logs" "util/model" @@ -61,6 +62,7 @@ func (e Engine) ParseFile(filepath string) (depRoot *model.DepTree, taskInfo rep AppName: strings.TrimSuffix(path.Base(filepath), path.Ext(path.Base(filepath))), StartTime: time.Now().Format("2006-01-02 15:04:05"), } + client.PackageName = taskInfo.AppName s := time.Now() defer func() { taskInfo.CostTime = time.Since(s).Seconds() @@ -72,6 +74,9 @@ func (e Engine) ParseFile(filepath string) (depRoot *model.DepTree, taskInfo rep return depRoot, taskInfo } else { if f.IsDir() { + //如果是目录则不用去除后缀 + taskInfo.AppName = path.Base(filepath) + client.PackageName = taskInfo.AppName // 目录 dirRoot = e.opendir(filepath) // 尝试解析mvn依赖 diff --git a/analyzer/engine/parse.go b/analyzer/engine/parse.go index 1f6fca1..e8ea212 100644 --- a/analyzer/engine/parse.go +++ b/analyzer/engine/parse.go @@ -6,10 +6,18 @@ package engine import ( + "analyzer/java" + "crypto/md5" + "encoding/base64" + "encoding/json" + "fmt" "path" "regexp" "strings" + "util/client" + "util/enum/language" "util/filter" + "util/logs" "util/model" ) @@ -20,12 +28,28 @@ const ( high ) +// PackageVersionFile 需要解析包版本的文件数据 +type PackageVersionFile struct { + Language language.Type + FileData *model.FileInfo +} + +// PackageBase package.json、composer.json提取需要的字段 +type PackageBase struct { + PackageName string `json:"name"` + PackageVersion string `json:"version"` +} + // 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) + var h = md5.New() + var toHash = false + var packageVersionFiles []PackageVersionFile + var packageVersionFileCount = 0 for _, analyzer := range e.Analyzers { // 遍历目录树获取要检测的文件 files := []*model.FileInfo{} @@ -39,6 +63,50 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) for _, f := range n.Files { if analyzer.CheckFile(f.Name) { files = append(files, f) + //计算匹配到的文件hash、保存版本提取的文件 + //文件顺序会影响hash值,比如文件重命名后顺序改变 + h.Write(f.Data) + if !toHash { + toHash = true + } + if strings.HasSuffix(f.Name, dirRoot.Path+"/"+path.Base(f.Name)) { + packageVersionFileCount = packageVersionFileCount + 1 + toParseVersion := false + switch analyzer.GetLanguage() { + //java只保存一个包文件的信息,如果存在多个则不提取版本 + case language.Java: + if packageVersionFileCount == 1 { + toParseVersion = true + } + //python、php最多有两个 + case language.Python, language.Php: + if packageVersionFileCount <= 2 { + toParseVersion = true + } + //js最多有3个 + case language.JavaScript: + if packageVersionFileCount <= 3 { + toParseVersion = true + } + } + if toParseVersion { + if analyzer.GetLanguage() == language.JavaScript { + //js只处理package.json + if !filter.JavaScriptPackage(f.Name) { + continue + } + } else if analyzer.GetLanguage() == language.Php { + //php只处理composer.json + if !filter.PhpComposer(f.Name) { + continue + } + } + packageVersionFile := PackageVersionFile{Language: analyzer.GetLanguage(), FileData: f} + packageVersionFiles = append(packageVersionFiles, packageVersionFile) + } else { + packageVersionFiles = nil + } + } } else if filter.CheckLicense(f.Name) { if _, ok := copyrightMess[path.Dir(f.Name)]; !ok { // 记录解析到的copyrigh信息 @@ -47,6 +115,7 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) } } } + // 从文件中解析依赖树 for _, d := range analyzer.ParseFiles(files) { p := path.Dir(d.Path) @@ -89,6 +158,13 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) } } } + if toHash { + client.PackageHash = base64.StdEncoding.EncodeToString(h.Sum(nil)) + } + //根目录只有一个匹配到的文件时才提取包名和版本号 + if client.PackageVersion == "" && len(packageVersionFiles) >= 1 { + parsePackageVersion(packageVersionFiles) + } // 删除依赖树空节点 q := []*model.DepTree{depRoot} for len(q) > 0 { @@ -142,7 +218,7 @@ func parseCopyright(f *model.FileInfo) string { if len(tks) == 0 { continue } - if strings.EqualFold("copyright", tks[0]) { + if strings.EqualFold("copyright", tks[0]) && len(tks) > 1 { if re.MatchString(tks[1]) { matchLevel[high] = line } @@ -163,3 +239,44 @@ func parseCopyright(f *model.FileInfo) string { } return "" } + +// 从特定文件中提取包名、版本信息 +func parsePackageVersion(packageVersionFiles []PackageVersionFile) { + for _, packageVersionFile := range packageVersionFiles { + switch packageVersionFile.Language { + case language.Java: + if filter.JavaPom(packageVersionFile.FileData.Name) { + p := java.ReadPom(packageVersionFile.FileData.Data) + client.PackageVersion = p.Version + if p.ArtifactId != "" { + client.PackageName = p.ArtifactId + } + } else if strings.HasSuffix(packageVersionFile.FileData.Name, ".gradle") { + //基本没有声明包名和版本号? 暂不解析 + } + case language.JavaScript, language.Php: + packageBase := PackageBase{} + err := json.Unmarshal(packageVersionFile.FileData.Data, &packageBase) + if err != nil { + logs.Warn(err) + } else { + if packageBase.PackageName != "" { + client.PackageName = packageBase.PackageName + } + if packageBase.PackageVersion != "" { + client.PackageVersion = packageBase.PackageVersion + } + } + case language.Ruby: + fmt.Println("Ruby") + case language.Golang: + fmt.Println("Golang") + case language.Rust: + fmt.Println("Rust") + case language.Erlang: + fmt.Println("Erlang") + case language.Python: + fmt.Println("Python") + } + } +} diff --git a/util/args/args.go b/util/args/args.go index 1348a64..7d279db 100644 --- a/util/args/args.go +++ b/util/args/args.go @@ -28,6 +28,8 @@ var ( // remote vuldb Url string `json:"url"` Token string `json:"token"` + // remote v2 + V2 bool `json:"v2"` // local vuldb VulnDB string `json:"db"` // prvate repository @@ -46,6 +48,7 @@ func init() { flag.StringVar(&Config.Path, "path", Config.Path, "(必须) 指定要检测的文件或目录路径,例: -path ./foo 或 -path ./foo.zip") flag.StringVar(&Config.Url, "url", Config.Url, "(可选,与token需一起使用) 从云漏洞库查询漏洞,指定要连接云服务的地址,例:-url https://opensca.xmirror.cn") flag.StringVar(&Config.Token, "token", Config.Token, "(可选,与url需一起使用) 云服务验证token,需要在云服务平台申请") + flag.BoolVar(&Config.V2, "v2", Config.V2, "(可选,与url需一起使用) 是否使用新的v2版本的云服务接口") flag.BoolVar(&Config.Cache, "cache", Config.Cache, "(可选,建议开启) 缓存下载的文件(例如pom文件),重复检测相同组件时会节省时间,下载的文件会保存到工具所在目录的.cache目录下") flag.BoolVar(&Config.OnlyVuln, "vuln", Config.OnlyVuln, "(可选) 结果仅保留有漏洞信息的组件,使用该参数不会保留组件层级结构") flag.StringVar(&Config.Out, "out", Config.Out, "(可选) 将检测结果保存到指定文件,根据后缀生成不同格式的文件,默认为json格式,例: -out output.json") diff --git a/util/client/client.go b/util/client/client.go index c5a7660..fccadbd 100644 --- a/util/client/client.go +++ b/util/client/client.go @@ -20,11 +20,19 @@ import ( "regexp" "util/args" "util/logs" + "util/model" "util/temp" "github.com/pkg/errors" ) +const V2Type = 10 //新的saas端定义的类型,来自cli的type固定为10 +var ( + PackageName string + PackageVersion string + PackageHash string +) + // 消息响应格式 type SaasReponse struct { // 错误消息 @@ -57,6 +65,22 @@ type DetectRequst struct { ClientId string `json:"clientId"` } +// DetectRequstV2 检测任务请求格式 v2 +type DetectRequstV2 struct { + // 16位 大写字母 + //ClientId string `json:"clientId"` + // 在saas注册 + Token string `json:"token"` + ComponentInfoAddDTOList []*model.CompTree `json:"componentInfoAddDTOList"` + Type int `json:"type"` + PackageName string `json:"packageName"` + PackageVersion string `json:"packageVersion"` + PackageHash string `json:"packageHash"` + // 是否需要saas端进行依赖检测,只传直接依赖时才检测,DeepLimit是依赖层数限制 + //Check bool `json:"check"` + //DeepLimit int `json:"deepLimit"` +} + // GetClientId 获取客户端id func GetClientId() string { // 默认id @@ -163,6 +187,68 @@ func Detect(reqbody []byte) (repbody []byte, err error) { } } +// DetectV2 发送任务解析请求 v2版本 +func DetectV2(root *model.DepTree) (repbody []byte, err error) { + repbody = []byte{} + // 转为新版本的组件格式 + dep := root.ToDetectComponents() + if len(dep.Children) == 0 { + return repbody, err + } + // 构建请求 + url := args.Config.Url + "/oss-saas/api-v1/component/task/add" + // 添加参数 + param := DetectRequstV2{} + //param.ClientId = GetClientId() + param.Type = V2Type + param.Token = args.Config.Token + param.PackageName = PackageName + param.PackageVersion = PackageVersion + param.PackageHash = PackageHash + param.ComponentInfoAddDTOList = dep.Children + data, err := json.Marshal(param) + //return repbody, err + if err != nil { + return repbody, err + } + // 发送数据 + rep, err := http.Post(url, "application/json", bytes.NewReader(data)) + if err != nil { + return repbody, err + } + defer rep.Body.Close() + if rep.StatusCode == 200 { + repbody, err = ioutil.ReadAll(rep.Body) + if err != nil { + logs.Error(err) + return + } else { + // 解析响应 + saasrep := SaasReponse{} + err = json.Unmarshal(repbody, &saasrep) + if err != nil { + logs.Error(err) + } + if saasrep.Code != 0 { + // 出现错误 + logs.Warn(fmt.Sprintf("url:%s code:%d message: %s", url, saasrep.Code, saasrep.Message)) + err = errors.New(saasrep.Message) + return + } else { + data, err = json.Marshal(saasrep.Data) + detect := DetectReponse{} + err = json.Unmarshal([]byte(data), &detect) + if err != nil { + logs.Error(err) + } + return + } + } + } else { + return repbody, fmt.Errorf("%s status code: %d", url, rep.StatusCode) + } +} + // getAesKey 获取aes-key func getAesKey() (key []byte, err error) { u, err := url.Parse(args.Config.Url + "/oss-saas/api-v1/open-sca-client/aes-key") diff --git a/util/model/component.go b/util/model/component.go new file mode 100644 index 0000000..72001e9 --- /dev/null +++ b/util/model/component.go @@ -0,0 +1,99 @@ +/* + * @Descripation: 依赖相关数据结构 + * @Date: 2022-11-16 10:41:37 + */ + +package model + +import ( + "fmt" + "util/enum/language" +) + +// 参考dependency做了细微改动 + +// component 组件依赖 +type component struct { + Vendor string `json:"vendor,omitempty"` + Name string `json:"name,omitempty"` + Version *Version `json:"ver,omitempty"` + Language language.Type `json:"lan,omitempty"` + + // 仅在v2请求时赋值 + LanguageStr string `json:"language"` + //ComponentId int64 `json:"componentId"` + ComponentAuthor string `json:"componentAuthor"` + ComponentName string `json:"componentName"` + ComponentVersion string `json:"componentVersion"` + FilePath string `json:"filePath"` +} + +// NewComponent 创建Component +func NewComponent() component { + dep := component{ + Vendor: "", + Name: "", + Version: NewVersion(""), + Language: language.None, + } + return dep +} + +// String 获取用于展示的Component字符串 +func (dep component) String() string { + if len(dep.Vendor) == 0 { + return fmt.Sprintf("[%s:%s]", dep.Name, dep.Version.Org) + } else { + return fmt.Sprintf("[%s:%s:%s]", dep.Vendor, dep.Name, dep.Version.Org) + } +} + +// CompTree 依赖树 +type CompTree struct { + component + // 是否为直接依赖 + Direct bool `json:"direct"` + // 依赖路径 + Path string `json:"path,omitempty"` + Paths []string `json:"paths,omitempty"` + // 唯一的组件id,用来标识不同组件 + ID int64 `json:"id,omitempty"` + // 父组件 + Parent *CompTree `json:"-"` + Vulnerabilities []*Vuln `json:"vulnerabilities,omitempty"` + IndirectVulnerabilities int `json:"indirect_vulnerabilities,omitempty"` + // 许可证列表 + Licenses []string `json:"licenses,omitempty"` + // spdx相关字段 + CopyrightText string `json:"copyrightText,omitempty"` + // 子组件 + Children []*CompTree `json:"children"` +} + +// NewCompTree 创建CompTree +func NewCompTree(parent *CompTree) *CompTree { + dep := &CompTree{ + ID: getId(), + component: NewComponent(), + Path: "", + Paths: nil, + Parent: parent, + Children: []*CompTree{}, + } + if parent != nil { + parent.Children = append(parent.Children, dep) + } + return dep +} + +// Delete [移位法]删除子节点中指定索引的节点 +func (comp *CompTree) Delete(diMap map[int]int) { + j := 0 + for i, v := range comp.Children { + if _, ok := diMap[i]; !ok { + comp.Children[j] = v + j++ + } + } + comp.Children = comp.Children[:j] +} diff --git a/util/model/dependency.go b/util/model/dependency.go index e80df43..ee20916 100644 --- a/util/model/dependency.go +++ b/util/model/dependency.go @@ -6,11 +6,14 @@ package model import ( + "encoding/json" "fmt" "strings" "sync" "time" "util/enum/language" + "util/filter" + "util/logs" ) // 用于id生成 @@ -75,10 +78,10 @@ type DepTree struct { // 是否为直接依赖 Direct bool `json:"direct"` // 依赖路径 - Path string `json:"-"` + Path string `json:"path,omitempty"` Paths []string `json:"paths,omitempty"` // 唯一的组件id,用来标识不同组件 - ID int64 `json:"-"` + ID int64 `json:"id,omitempty"` // 父组件 Parent *DepTree `json:"-"` Vulnerabilities []*Vuln `json:"vulnerabilities,omitempty"` @@ -189,3 +192,115 @@ func (root *DepTree) String() string { } return res } + +// ToDetectComponents 转为检测依赖组件的格式 +func (root *DepTree) ToDetectComponents() (comp *CompTree) { + // 利用json.Unmarshal来实现深拷贝 + comp = NewCompTree(nil) + if data, err := json.Marshal(root); err != nil { + logs.Error(err) + } else { + err = json.Unmarshal(data, &comp) + if err != nil { + logs.Error(err) + } else { + // 参考format,改了下顺序,先去重再转换 + // 恢复转换后缺失的父节点 + for _, c := range comp.Children { + c.Parent = comp + } + // 去重 + q := []*CompTree{comp} + dm := map[string]*CompTree{} + for len(q) > 0 { + n := q[0] + q = append(q[1:], n.Children...) + // 去重 + k := fmt.Sprintf("%s:%s@%s#%s", n.Vendor, n.Name, n.Version.Org, strings.ToLower(n.Language.String())) + if d, ok := dm[k]; !ok { + dm[k] = n + } else { + // 已存在相同组件,但是某些字段可能不一样 + + // 当d.Path为空或者等于n.Path时才进行合并处理 + if d.Path == "" { + if n.Path != "" { + d.Path = n.Path + } + if n.Direct { + d.Direct = n.Direct + } + } else if d.Path != n.Path { + continue + } + // 从父组件中移除当前组件 + if n.Parent != nil { + for i, c := range n.Parent.Children { + if c.ID == n.ID { + n.Parent.Children = append(n.Parent.Children[:i], n.Parent.Children[i+1:]...) + break + } + } + } + // 将当前组件的子组件转移到已存在组件的子依赖中 + d.Children = append(d.Children, n.Children...) + for _, c := range n.Children { + c.Parent = d + } + } + } + + //应该只有pom的解析存在不需要一级节点这种情况。 + //先将二级子节点复制到一级,并记录需要删除的索引 + deleteIndex := make(map[int]int) + for i, c := range comp.Children { + if filter.JavaPom(strings.TrimSuffix(c.Path, "/"+c.String())) { + deleteIndex[i] = i + c.Parent.Children = append(c.Parent.Children, c.Children...) + } + } + //再删除对应的一级节点 + if len(deleteIndex) > 0 { + comp.Delete(deleteIndex) + } + + // 保留要导出的数据 + q = []*CompTree{comp} + for len(q) > 0 { + n := q[0] + q = append(q[1:], n.Children...) + if n.Language != language.None { + n.LanguageStr = strings.ToLower(n.Language.String()) + n.Language = language.None + } + if n.Version != nil { + n.ComponentVersion = n.Version.Org + n.Version = nil + } + if n.Vendor != "" { + n.ComponentAuthor = n.Vendor + n.Vendor = "" + } + if n.Name != "" { + n.ComponentName = n.Name + n.Name = "" + } + if n.Path != "" { + n.FilePath = n.Path + n.Path = "" + } + //n.ComponentId = n.ID + // 不展示的字段置空 + //n.ID = 0 + n.Paths = nil + n.Licenses = nil + n.CopyrightText = "" + n.Vulnerabilities = nil + n.IndirectVulnerabilities = 0 + } + + } + } + + return comp +} diff --git a/util/report/format.go b/util/report/format.go index b23d439..f15884d 100644 --- a/util/report/format.go +++ b/util/report/format.go @@ -23,42 +23,30 @@ type TaskInfo struct { // format 按照输出内容格式化(不可逆) func format(dep *model.DepTree) { - q := []*model.DepTree{dep} - // 保留要导出的数据 - for len(q) > 0 { - n := q[0] - q = append(q[1:], n.Children...) - if n.Language != language.None { - n.LanguageStr = n.Language.String() - } - if n.Version != nil { - n.VersionStr = n.Version.Org - } - if n.Path != "" { - n.Paths = []string{n.Path} - } - n.Language = language.None - n.Version = nil - } // 去重 if args.Config.Dedup { - q = []*model.DepTree{dep} + q := []*model.DepTree{dep} dm := map[string]*model.DepTree{} for len(q) > 0 { n := q[0] q = append(q[1:], n.Children...) // 去重 - k := fmt.Sprintf("%s:%s@%s#%s", n.Vendor, n.Name, n.VersionStr, n.LanguageStr) + k := fmt.Sprintf("%s:%s@%s#%s", n.Vendor, n.Name, n.Version.Org, n.Language.String()) if d, ok := dm[k]; !ok { dm[k] = n } else { + // 已存在相同组件,但是某些字段可能不一样 + // 临时解决部分组件homepage字段不显示问题 // 因为去重时刚好把解析到homepage字段的组件去掉了 // 其他字段可能也需要类似操作 if n.HomePage != "" { d.HomePage = n.HomePage } - // 已存在相同组件 + // 是否有直接依赖 + if n.Direct { + d.Direct = n.Direct + } d.Paths = append(d.Paths, n.Path) // 从父组件中移除当前组件 if n.Parent != nil { @@ -77,6 +65,27 @@ func format(dep *model.DepTree) { } } } + + // 保留要导出的数据 + q := []*model.DepTree{dep} + for len(q) > 0 { + n := q[0] + q = append(q[1:], n.Children...) + if n.Language != language.None { + n.LanguageStr = n.Language.String() + } + if n.Version != nil { + n.VersionStr = n.Version.Org + } + if !args.Config.Dedup && n.Path != "" { + n.Paths = []string{n.Path} + } + //不展示的字段置空 + n.Path = "" + n.Language = language.None + n.Version = nil + n.ID = 0 + } } // Save 保存结果文件 diff --git a/util/vuln/server.go b/util/vuln/server.go index a2fe354..25e8aa3 100644 --- a/util/vuln/server.go +++ b/util/vuln/server.go @@ -32,3 +32,19 @@ func GetServerVuln(deps []model.Dependency) (vulns [][]*model.Vuln, err error) { } return } + +// GetServerVulnV2 从云服务获取漏洞 v2 +func GetServerVulnV2(root *model.DepTree) (vulns map[int64][]*model.Vuln, err error) { + vulns = make(map[int64][]*model.Vuln) + data, err := client.DetectV2(root) + if err != nil { + return vulns, err + } + if len(data) > 0 { + err = json.Unmarshal(data, &vulns) + if err != nil { + logs.Error(err) + } + } + return +} diff --git a/util/vuln/vuln.go b/util/vuln/vuln.go index 2a17832..33bf694 100644 --- a/util/vuln/vuln.go +++ b/util/vuln/vuln.go @@ -25,6 +25,7 @@ func SearchVuln(root *model.DepTree) (err error) { } localVulns := [][]*model.Vuln{} serverVulns := [][]*model.Vuln{} + serverVulnsV2 := make(map[int64][]*model.Vuln) ds := make([]model.Dependency, len(deps)) for i, d := range deps { ds[i] = d.Dependency @@ -33,7 +34,11 @@ func SearchVuln(root *model.DepTree) (err error) { localVulns = GetLocalVulns(ds) } if args.Config.Url != "" && args.Config.Token != "" { - serverVulns, err = GetServerVuln(ds) + if args.Config.V2 { + serverVulnsV2, err = GetServerVulnV2(root) + } else { + serverVulns, err = GetServerVuln(ds) + } } else if args.Config.VulnDB == "" && args.Config.Url == "" && args.Config.Token != "" { err = errors.New("url is null") } else if args.Config.VulnDB == "" && args.Config.Url != "" && args.Config.Token == "" { @@ -50,7 +55,18 @@ func SearchVuln(root *model.DepTree) (err error) { } } } - if len(serverVulns) != 0 { + if args.Config.V2 { + if len(serverVulnsV2) != 0 { + if _, ok := serverVulnsV2[dep.ID]; ok { + for _, vuln := range serverVulnsV2[dep.ID] { + if _, ok := exist[vuln.Id]; !ok { + exist[vuln.Id] = struct{}{} + dep.Vulnerabilities = append(dep.Vulnerabilities, vuln) + } + } + } + } + } else if len(serverVulns) != 0 { for _, vuln := range serverVulns[i] { if _, ok := exist[vuln.Id]; !ok { exist[vuln.Id] = struct{}{} -- Gitee From 9e6d2f4bb82a8d20235fc8486058efa37b43869b Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Fri, 25 Nov 2022 10:48:16 +0800 Subject: [PATCH 02/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E4=BC=98=E5=8C=96python=20setup.py=E7=9A=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=EF=BC=8C=E4=B8=8D=E5=9C=A8=E4=B8=B4=E6=97=B6=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E4=B8=AD=E8=A7=A3=E6=9E=90=EF=BC=8C=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E9=A1=B9=E7=9B=AE=E7=9B=AE=E5=BD=95=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E8=A7=A3=E6=9E=90=EF=BC=9B=20BUG=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=E4=BF=AE=E5=A4=8D=E8=A7=A3=E6=9E=90python=20setup.py?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E4=BE=9D=E8=B5=96=E6=97=B6=EF=BC=8C~?= =?UTF-8?q?=E7=AC=A6=E5=8F=B7=E6=B2=A1=E8=80=83=E8=99=91=E5=88=B0=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=8C=85=E5=90=8D=E5=92=8C=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=9A=84bug=EF=BC=9B=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=8E=BB=E9=87=8D=E6=97=B6=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=B8=BA=E7=A9=BA=E7=9A=84bug=20=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=96=B0=E5=A2=9E=EF=BC=9A=E5=AE=8C=E6=88=90=E5=A4=A7?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=8C=85=E7=AE=A1=E7=90=86=E5=99=A8=E7=9A=84?= =?UTF-8?q?=E7=89=B9=E5=AE=9A=E6=96=87=E4=BB=B6=E4=B8=AD=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=8C=85=E5=90=8D=E5=92=8C=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analyzer/engine/engine.go | 5 +- analyzer/engine/parse.go | 168 +++++++++++++++++++++++++++++++++--- analyzer/python/analyzer.go | 7 +- analyzer/python/oss.py | 1 + analyzer/python/req.go | 18 ++-- analyzer/python/setup.go | 70 ++++++++++++++- analyzer/ruby/analyzer.go | 4 +- analyzer/rust/analyzer.go | 2 +- cli/main.go | 8 +- util/args/args.go | 6 +- util/client/client.go | 15 ++-- util/ex/python.go | 12 +-- util/filter/file.go | 7 +- util/report/format.go | 6 +- 14 files changed, 279 insertions(+), 50 deletions(-) diff --git a/analyzer/engine/engine.go b/analyzer/engine/engine.go index 0822e04..320d8c3 100644 --- a/analyzer/engine/engine.go +++ b/analyzer/engine/engine.go @@ -58,8 +58,9 @@ func (e Engine) ParseFile(filepath string) (depRoot *model.DepTree, taskInfo rep dirRoot := model.NewDirTree() depRoot = model.NewDepTree(nil) filepath = strings.ReplaceAll(filepath, `\`, `/`) + client.PackageRootDir = path.Base(filepath) taskInfo = report.TaskInfo{ - AppName: strings.TrimSuffix(path.Base(filepath), path.Ext(path.Base(filepath))), + AppName: strings.TrimSuffix(client.PackageRootDir, path.Ext(client.PackageRootDir)), StartTime: time.Now().Format("2006-01-02 15:04:05"), } client.PackageName = taskInfo.AppName @@ -75,7 +76,7 @@ func (e Engine) ParseFile(filepath string) (depRoot *model.DepTree, taskInfo rep } else { if f.IsDir() { //如果是目录则不用去除后缀 - taskInfo.AppName = path.Base(filepath) + taskInfo.AppName = client.PackageRootDir client.PackageName = taskInfo.AppName // 目录 dirRoot = e.opendir(filepath) diff --git a/analyzer/engine/parse.go b/analyzer/engine/parse.go index e8ea212..eb2d7d0 100644 --- a/analyzer/engine/parse.go +++ b/analyzer/engine/parse.go @@ -11,6 +11,8 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/ioutil" + "os" "path" "regexp" "strings" @@ -63,23 +65,24 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) for _, f := range n.Files { if analyzer.CheckFile(f.Name) { files = append(files, f) - //计算匹配到的文件hash、保存版本提取的文件 - //文件顺序会影响hash值,比如文件重命名后顺序改变 + //计算匹配到的文件hash。文件顺序会影响hash值,比如文件重命名后顺序改变 h.Write(f.Data) if !toHash { toHash = true } - if strings.HasSuffix(f.Name, dirRoot.Path+"/"+path.Base(f.Name)) { + //只保存根目录下可以版本提取的文件 + if strings.HasSuffix(f.Name, client.PackageRootDir+"/"+path.Base(f.Name)) { packageVersionFileCount = packageVersionFileCount + 1 toParseVersion := false + //如果不满足条件则可能不是单一语言的包,不提取版本 switch analyzer.GetLanguage() { - //java只保存一个包文件的信息,如果存在多个则不提取版本 + //java最多只有1个 case language.Java: if packageVersionFileCount == 1 { toParseVersion = true } - //python、php最多有两个 - case language.Python, language.Php: + //php、ruby、go最多有两个 + case language.Php, language.Ruby, language.Golang, language.Rust: if packageVersionFileCount <= 2 { toParseVersion = true } @@ -88,6 +91,11 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) if packageVersionFileCount <= 3 { toParseVersion = true } + //python最多有5个 + case language.Python: + if packageVersionFileCount <= 5 { + toParseVersion = true + } } if toParseVersion { if analyzer.GetLanguage() == language.JavaScript { @@ -100,6 +108,26 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) if !filter.PhpComposer(f.Name) { continue } + } else if analyzer.GetLanguage() == language.Python { + //python只处理setup.py、pyproject.toml + if !filter.PythonSetup(f.Name) && !filter.PythonPyproject(f.Name) { + continue + } + } else if analyzer.GetLanguage() == language.Ruby { + //ruby只处理Gemfile + if !filter.RubyGemfile(f.Name) { + continue + } + } else if analyzer.GetLanguage() == language.Golang { + //go只处理go.mod + if !filter.GoMod(f.Name) { + continue + } + } else if analyzer.GetLanguage() == language.Rust { + //rust只处理Cargo.toml + if !filter.RustCargoToml(f.Name) { + continue + } } packageVersionFile := PackageVersionFile{Language: analyzer.GetLanguage(), FileData: f} packageVersionFiles = append(packageVersionFiles, packageVersionFile) @@ -161,7 +189,6 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) if toHash { client.PackageHash = base64.StdEncoding.EncodeToString(h.Sum(nil)) } - //根目录只有一个匹配到的文件时才提取包名和版本号 if client.PackageVersion == "" && len(packageVersionFiles) >= 1 { parsePackageVersion(packageVersionFiles) } @@ -268,15 +295,130 @@ func parsePackageVersion(packageVersionFiles []PackageVersionFile) { } } case language.Ruby: - fmt.Println("Ruby") + //根路径是否存在gemspec + gemspecPath := "" + rootDir := path.Dir(packageVersionFile.FileData.Name) + files, err := ioutil.ReadDir(rootDir) + if err != nil { + continue + } + for _, file := range files { + if !file.IsDir() { + if strings.HasSuffix(file.Name(), "gemspec") { + gemspecPath = path.Join(rootDir, file.Name()) + if data, err := ioutil.ReadFile(gemspecPath); err == nil { + pkgMatch := regexp.MustCompile(`\s*spec\.name\s*=\s*["'](.*)["']\r?\n`) + r := pkgMatch.FindSubmatch(data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageName = string(r[1]) + } + } + pkgVerMatch := regexp.MustCompile(`\s*spec\.version\s*=\s*["'](.*)["']\r?\n`) + r = pkgVerMatch.FindSubmatch(data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageVersion = string(r[1]) + } + } else { + pkgVerPathMatch := regexp.MustCompile(`require\s*["'](.*version)["']\r?\n`) + r = pkgVerPathMatch.FindSubmatch(data) + if len(r) == 2 { + if string(r[1]) != "" { + //一般在lib目录 + pkgVersionPath := fmt.Sprintf("%s/lib/%s.rb", rootDir, string(r[1])) + if f, err := os.Stat(pkgVersionPath); err == nil { + if !f.IsDir() { + if data, err := ioutil.ReadFile(pkgVersionPath); err == nil { + pkgVerMatch = regexp.MustCompile(`\s*VERSION\s*=\s*["'](.*)["']\r?\n`) + r = pkgVerMatch.FindSubmatch(data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageVersion = string(r[1]) + } + } + } + } + } + } + } + } + } + return + } + } + } case language.Golang: - fmt.Println("Golang") + //只能提取包名 + pkgMatch := regexp.MustCompile(`module\s*(.*)\r?\n`) + r := pkgMatch.FindSubmatch(packageVersionFile.FileData.Data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageName = string(r[1]) + } + } case language.Rust: - fmt.Println("Rust") - case language.Erlang: - fmt.Println("Erlang") + pkgNameMatch := regexp.MustCompile(`\nname\s*=\s*['"](.*)['"]\r?\n`) + pkgVerMatch := regexp.MustCompile(`\nversion\s*=\s*['"](.*)['"]\r?\n`) + r := pkgNameMatch.FindSubmatch(packageVersionFile.FileData.Data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageName = string(r[1]) + } + } + r = pkgVerMatch.FindSubmatch(packageVersionFile.FileData.Data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageVersion = string(r[1]) + return + } + } case language.Python: - fmt.Println("Python") + //先从特定文件中尝试提取包名和版本 + for _, filepath := range []string{ + path.Dir(packageVersionFile.FileData.Name) + "/PKG-INFO", + path.Dir(packageVersionFile.FileData.Name) + "/METADATA", + } { + if f, err := os.Stat(filepath); err == nil { + if !f.IsDir() { + if data, err := ioutil.ReadFile(filepath); err == nil { + pkgNameMatch := regexp.MustCompile(`\nName: (.*)\r?\n`) + r := pkgNameMatch.FindSubmatch(data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageName = string(r[1]) + } + } + pkgVerMatch := regexp.MustCompile(`\nVersion: (.*)\r?\n`) + r = pkgVerMatch.FindSubmatch(data) + if len(r) == 2 { + if string(r[1]) != "" { + client.PackageVersion = string(r[1]) + return + } + } + } + } + } + } + pkgVerMatch := ®exp.Regexp{} + //可能存在name和version不是相邻的,暂时不考虑 + if filter.PythonSetup(packageVersionFile.FileData.Name) { + pkgVerMatch = regexp.MustCompile(`setup\(\s*name\s*=\s*["'](.*)["'],\s*version\s*=\s*["'](.*)["'],`) + } else if filter.PythonPyproject(packageVersionFile.FileData.Name) { + pkgVerMatch = regexp.MustCompile(`\[tool.poetry\]\s*name\s*=\s*"(.*)"\s*version\s*=\s*"(.*)"\r?\n`) + } + if pkgVerMatch.String() != "" { + r := pkgVerMatch.FindSubmatch(packageVersionFile.FileData.Data) + if len(r) == 3 { + if string(r[1]) != "" { + client.PackageName = string(r[1]) + } + if string(r[2]) != "" { + client.PackageVersion = string(r[2]) + } + } + } } } } diff --git a/analyzer/python/analyzer.go b/analyzer/python/analyzer.go index 807dacb..5a2d6a0 100644 --- a/analyzer/python/analyzer.go +++ b/analyzer/python/analyzer.go @@ -24,7 +24,8 @@ func (Analyzer) CheckFile(filename string) bool { filter.PythonPipfile(filename) || filter.PythonPipfileLock(filename) || filter.PythonRequirementsTxt(filename) || - filter.PythonRequirementsIn(filename) + filter.PythonRequirementsIn(filename) || + filter.PythonPyproject(filename) } // ParseFiles parse dependency from file @@ -34,6 +35,7 @@ func (Analyzer) ParseFiles(files []*model.FileInfo) []*model.DepTree { dep := model.NewDepTree(nil) dep.Path = f.Name if filter.PythonSetup(f.Name) { + //parseSetupTmp(dep, f) parseSetup(dep, f) } else if filter.PythonPipfile(f.Name) { parsePipfile(dep, f) @@ -41,6 +43,9 @@ func (Analyzer) ParseFiles(files []*model.FileInfo) []*model.DepTree { parsePipfileLock(dep, f) } else if filter.PythonRequirementsTxt(f.Name) || filter.PythonRequirementsIn(f.Name) { parseRequirementsin(dep, f) + } else if filter.PythonPyproject(f.Name) { + //暂未支持该文件的依赖解析 + //parsePythonPyproject(dep, f) } deps = append(deps, dep) } diff --git a/analyzer/python/oss.py b/analyzer/python/oss.py index 4179e66..d7f28cf 100644 --- a/analyzer/python/oss.py +++ b/analyzer/python/oss.py @@ -1,3 +1,4 @@ +#coding:utf-8 import re import sys import json diff --git a/analyzer/python/req.go b/analyzer/python/req.go index 87b7f23..a1c47ae 100644 --- a/analyzer/python/req.go +++ b/analyzer/python/req.go @@ -21,7 +21,7 @@ var replacer *strings.Replacer func init() { reg1 = regexp.MustCompile(`^\w`) regGit = regexp.MustCompile(`\/([\w-]+)\.git`) - replacer = strings.NewReplacer("# via","","\r",""," ","","#","") + replacer = strings.NewReplacer("# via", "", "\r", "", " ", "", "#", "") } func parseRequirementsin(root *model.DepTree, file *model.FileInfo) { @@ -32,7 +32,7 @@ func parseRequirementsin(root *model.DepTree, file *model.FileInfo) { strArry := []string{} temp.DoInTempDir(func(tempdir string) { // 安装piptools - if _, err := ex.Do(ex.PipinstallPiptoos, tempdir); err != nil { + if _, err := ex.Do(ex.PipinstallPiptools, tempdir); err != nil { logs.Error(err) return } @@ -89,7 +89,7 @@ func parseOutData(root *model.DepTree, strs []string) { depMap[cur.Name] = cur nodes = append(nodes, cur.Name) for _, name := range nodes { - if _,ok := depMap[name]; !ok { + if _, ok := depMap[name]; !ok { continue } parNames := parentsMap[name] @@ -99,13 +99,13 @@ func parseOutData(root *model.DepTree, strs []string) { directMap[dep.Name] = dep } } - if _,ok := depMap[parName]; !ok { + if _, ok := depMap[parName]; !ok { continue } parent := depMap[parName] dep := depMap[name] - if m,ok := childMap[dep]; ok { - if _,ok := m[dep.Name];ok { + if m, ok := childMap[dep]; ok { + if _, ok := m[dep.Name]; ok { continue } m[dep.Name] = struct{}{} @@ -115,11 +115,11 @@ func parseOutData(root *model.DepTree, strs []string) { } } } - withRoot(root,directMap) + withRoot(root, directMap) } // 所有直接依赖连接至root -func withRoot(root *model.DepTree,directMap map[string]*model.DepTree) { +func withRoot(root *model.DepTree, directMap map[string]*model.DepTree) { direct := []*model.DepTree{} for _, n := range directMap { direct = append(direct, n) @@ -180,4 +180,4 @@ func getSingleModStr(reqpath string, elem string) string { } else { return str } -} \ No newline at end of file +} diff --git a/analyzer/python/setup.go b/analyzer/python/setup.go index e48dc73..2ff9882 100644 --- a/analyzer/python/setup.go +++ b/analyzer/python/setup.go @@ -6,7 +6,9 @@ import ( "os" "os/exec" "path" + "path/filepath" "strings" + "util/client" "util/logs" "util/model" "util/temp" @@ -25,8 +27,8 @@ type setupDep struct { Requires []string `json:"requires"` } -// parseSetup 解析 setup.py 文件 -func parseSetup(root *model.DepTree, file *model.FileInfo) { +// parseSetupTmp 在临时目录中解析setup.py文件 有的项目不止需要setup.py +func parseSetupTmp(root *model.DepTree, file *model.FileInfo) { temp.DoInTempDir(func(tempdir string) { ossfile := path.Join(tempdir, "oss.py") setupfile := path.Join(tempdir, "setup.py") @@ -41,8 +43,11 @@ func parseSetup(root *model.DepTree, file *model.FileInfo) { return } // 解析 setup.py + pwd := temp.GetPwd() + os.Chdir(tempdir) cmd := exec.Command("python", ossfile, setupfile) out, _ := cmd.CombinedOutput() + os.Chdir(pwd) startTag, endTag := `oss_start<<`, `>>oss_end` startIndex, endIndex := strings.Index(string(out), startTag), strings.Index(string(out), endTag) if startIndex == -1 || endIndex == -1 { @@ -72,3 +77,64 @@ func parseSetup(root *model.DepTree, file *model.FileInfo) { } }) } + +// parseSetup 解析setup.py文件 +func parseSetup(root *model.DepTree, file *model.FileInfo) { + setupfile, err := filepath.Abs(file.Name) + if err != nil { + logs.Warn(err) + return + } + setupfile = strings.ReplaceAll(setupfile, `\`, `/`) + workdir := path.Dir(setupfile) + //名称尽量特殊点,避免原项目存在同名文件 + ossfile := path.Join(workdir, "oss-xm1rr0r.py") + // 创建 oss.py + if err := os.WriteFile(ossfile, ossPy, 0444); err != nil { + logs.Warn(err) + return + } + defer os.Remove(ossfile) + // 解析 setup.py + pwd := temp.GetPwd() + os.Chdir(workdir) + cmd := exec.Command("python", ossfile, setupfile) + out, _ := cmd.CombinedOutput() + os.Chdir(pwd) + startTag, endTag := `oss_start<<`, `>>oss_end` + startIndex, endIndex := strings.Index(string(out), startTag), strings.Index(string(out), endTag) + if startIndex == -1 || endIndex == -1 { + return + } else { + out = out[startIndex+len(startTag) : endIndex] + } + // 获取解析结果 + var dep setupDep + if err := json.Unmarshal(out, &dep); err != nil { + logs.Warn(err) + } + // 运行时提取的包名和版本号,只提取根目录的 + if strings.HasSuffix(file.Name, client.PackageRootDir+"/"+path.Base(file.Name)) { + if dep.Name != "" { + client.PackageName = dep.Name + } + if dep.Version != "" { + client.PackageVersion = dep.Version + } + } + root.Name = dep.Name + root.Version = model.NewVersion(dep.Version) + root.Licenses = append(root.Licenses, dep.License) + for _, pkg := range [][]string{dep.Packages, dep.InstallRequires, dep.Requires} { + for _, p := range pkg { + index := strings.IndexAny(p, "~=<>") + sub := model.NewDepTree(root) + if index > -1 { + sub.Name = p[:index] + sub.Version = model.NewVersion(p[index:]) + } else { + sub.Name = p + } + } + } +} diff --git a/analyzer/ruby/analyzer.go b/analyzer/ruby/analyzer.go index 6343f80..8b0e61b 100644 --- a/analyzer/ruby/analyzer.go +++ b/analyzer/ruby/analyzer.go @@ -25,7 +25,7 @@ func (a Analyzer) GetLanguage() language.Type { // CheckFile Check if it is a parsable file func (a Analyzer) CheckFile(filename string) bool { - return filter.RubyGemfileLock(filename) + return filter.RubyGemfileLock(filename) || filter.RubyGemfile(filename) } // ParseFiles Parse the file @@ -36,6 +36,8 @@ func (a Analyzer) ParseFiles(files []*model.FileInfo) (deps []*model.DepTree) { dep.Path = f.Name if filter.RubyGemfileLock(f.Name) { parseGemfileLock(dep, f) + } else if filter.RubyGemfile(f.Name) { + //暂不支持 } deps = append(deps, dep) } diff --git a/analyzer/rust/analyzer.go b/analyzer/rust/analyzer.go index 1fb3bb1..c794c5c 100644 --- a/analyzer/rust/analyzer.go +++ b/analyzer/rust/analyzer.go @@ -19,7 +19,7 @@ func (a Analyzer) GetLanguage() language.Type { // CheckFile Check if it is a parsable file func (a Analyzer) CheckFile(filename string) bool { - return filter.RustCargoLock(filename) + return filter.RustCargoLock(filename) || filter.RustCargoToml(filename) } // ParseFiles Parse the file diff --git a/cli/main.go b/cli/main.go index ace61e3..092b4b9 100644 --- a/cli/main.go +++ b/cli/main.go @@ -16,11 +16,13 @@ import ( "util/report" ) -var version string +const VERSION = "v1.1.0" func main() { args.Parse() - if len(args.Config.Path) > 0 { + if args.ShowVersion { + fmt.Println(VERSION) + } else if len(args.Config.Path) > 0 { output(engine.NewEngine().ParseFile(args.Config.Path)) } else { flag.PrintDefaults() @@ -29,7 +31,7 @@ func main() { // output 输出结果 func output(depRoot *model.DepTree, taskInfo report.TaskInfo) { - taskInfo.ToolVersion = version + taskInfo.ToolVersion = VERSION // 记录依赖 logs.Debug("\n" + depRoot.String()) // 输出结果 diff --git a/util/args/args.go b/util/args/args.go index 7d279db..b3fa94f 100644 --- a/util/args/args.go +++ b/util/args/args.go @@ -16,8 +16,9 @@ import ( ) var ( - ConfigPath string - Config = struct { + ConfigPath string + ShowVersion bool + Config = struct { // detect option Path string `json:"path"` Out string `json:"out"` @@ -55,6 +56,7 @@ func init() { flag.StringVar(&Config.VulnDB, "db", Config.VulnDB, "(可选) 指定本地漏洞库文件,希望使用自己漏洞库时可用,漏洞库文件为json格式,具体格式会在开源项目文档中给出;若同时使用云端漏洞库与本地漏洞库,漏洞查询结果取并集,例: -db db.json") flag.BoolVar(&Config.Bar, "progress", Config.Bar, "(可选) 显示进度条") flag.BoolVar(&Config.Dedup, "dedup", Config.Dedup, "(可选) 相同组件去重") + flag.BoolVar(&ShowVersion, "version", false, "显示客户端版本") } func Parse() { diff --git a/util/client/client.go b/util/client/client.go index fccadbd..aeb9073 100644 --- a/util/client/client.go +++ b/util/client/client.go @@ -28,12 +28,13 @@ import ( const V2Type = 10 //新的saas端定义的类型,来自cli的type固定为10 var ( + PackageRootDir string PackageName string PackageVersion string PackageHash string ) -// 消息响应格式 +// SaasReponse 消息响应格式 type SaasReponse struct { // 错误消息 Message string `json:"message"` @@ -43,7 +44,7 @@ type SaasReponse struct { Data interface{} `json:"data"` } -// 检测结果响应格式 +// DetectReponse 检测结果响应格式 type DetectReponse struct { // 加密后的消息 Message string `json:"aesMessage"` @@ -51,7 +52,7 @@ type DetectReponse struct { Nonce string `json:"aesNonce"` } -// 检测任务请求格式 +// DetectRequst 检测任务请求格式 type DetectRequst struct { // 16位byte base64编码 Tag string `json:"aesTag"` @@ -193,10 +194,11 @@ func DetectV2(root *model.DepTree) (repbody []byte, err error) { // 转为新版本的组件格式 dep := root.ToDetectComponents() if len(dep.Children) == 0 { + logs.Debug("依赖节点为空") return repbody, err } // 构建请求 - url := args.Config.Url + "/oss-saas/api-v1/component/task/add" + url := args.Config.Url + "/oss-saas/api-v1/component/task/cli/add" // 添加参数 param := DetectRequstV2{} //param.ClientId = GetClientId() @@ -207,7 +209,6 @@ func DetectV2(root *model.DepTree) (repbody []byte, err error) { param.PackageHash = PackageHash param.ComponentInfoAddDTOList = dep.Children data, err := json.Marshal(param) - //return repbody, err if err != nil { return repbody, err } @@ -236,10 +237,10 @@ func DetectV2(root *model.DepTree) (repbody []byte, err error) { return } else { data, err = json.Marshal(saasrep.Data) - detect := DetectReponse{} - err = json.Unmarshal([]byte(data), &detect) if err != nil { logs.Error(err) + } else { + repbody = data } return } diff --git a/util/ex/python.go b/util/ex/python.go index 797a9b4..81620d9 100644 --- a/util/ex/python.go +++ b/util/ex/python.go @@ -9,12 +9,12 @@ import ( ) const ( - Python string = "python" - PipinstallPiptoos string = "pipenv install pip-tools --skip-lock" - PipCompilein string = "pipenv run pip-compile requirements.in" - PipCompileCfg string = "pipenv run pip-compile setup.cfg -o temp.txt" - PipcompileSetup string = "pipenv run pip-compile setup.py" - RemoveVirtualCmd string = "pipenv --rm" + Python string = "python" + PipinstallPiptools string = "pipenv install pip-tools --skip-lock" + PipCompilein string = "pipenv run pip-compile requirements.in" + PipCompileCfg string = "pipenv run pip-compile setup.cfg -o temp.txt" + PipcompileSetup string = "pipenv run pip-compile setup.py" + RemoveVirtualCmd string = "pipenv --rm" ) type CmdOpts struct { diff --git a/util/filter/file.go b/util/filter/file.go index 5160836..a956d46 100644 --- a/util/filter/file.go +++ b/util/filter/file.go @@ -64,6 +64,7 @@ var ( // ruby相关 var ( + RubyGemfile = filterFunc(strings.HasSuffix, "Gemfile") RubyGemfileLock = filterFunc(strings.HasSuffix, "Gemfile.lock", "gems.locked") ) @@ -76,6 +77,7 @@ var ( // rust var ( RustCargoLock = filterFunc(strings.HasSuffix, "Cargo.lock") + RustCargoToml = filterFunc(strings.HasSuffix, "Cargo.toml") ) // erlang @@ -92,11 +94,14 @@ var ( // python var ( PythonSetup = filterFunc(strings.HasSuffix, "setup.py") + PythonPyproject = filterFunc(strings.HasSuffix, "pyproject.toml") PythonPipfile = filterFunc(strings.HasSuffix, "Pipfile") PythonPipfileLock = filterFunc(strings.HasSuffix, "Pipfile.lock") PythonRequirementsTxt = func(filename string) bool { return filterFunc(strings.HasSuffix, ".txt")(filename) && - filterFunc(strings.Contains, "requirements")(path.Base(filename)) && !filterFunc(strings.Contains, "test")(path.Base(filename)) + filterFunc(strings.Contains, "requirements")(path.Base(filename)) && + !filterFunc(strings.Contains, "test")(path.Base(filename)) && + !filterFunc(strings.Contains, "dev")(path.Base(filename)) } PythonRequirementsIn = filterFunc(strings.HasSuffix, "requirements.in") // PythonSetupCfg = filterFunc(strings.HasSuffix, "setup.cfg") diff --git a/util/report/format.go b/util/report/format.go index f15884d..a8b7d56 100644 --- a/util/report/format.go +++ b/util/report/format.go @@ -77,8 +77,10 @@ func format(dep *model.DepTree) { if n.Version != nil { n.VersionStr = n.Version.Org } - if !args.Config.Dedup && n.Path != "" { - n.Paths = []string{n.Path} + if len(n.Paths) == 0 { + if n.Path != "" { + n.Paths = []string{n.Path} + } } //不展示的字段置空 n.Path = "" -- Gitee From fbd926dee498d78a95e56a0d6b4ed74e7cbd1980 Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Fri, 25 Nov 2022 10:51:18 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0README=EF=BC=9A?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=82=E6=95=B0=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++++++ util/args/args.go | 2 +- util/client/client.go | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 15837f6..e6a67c1 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ opensca-cli -path ${project_path} opensca-cli -url ${url} -token ${token} -path ${project_path} ``` +连接新的云平台接口(新的漏洞库,新的检测逻辑) + +```shell +opensca-cli -url ${url} -token ${token} -v2 -path ${project_path} +``` + 或使用本地漏洞库 ```shell @@ -80,12 +86,14 @@ opensca-cli -db db.json -path ${project_path} | `path` | `string` | 指定要检测的文件或目录路径 | `-path ./foo` | | `url` | `string` | 从云漏洞库查询漏洞,指定要连接云服务的地址,与 `token` 参数一起使用 | `-url https://opensca.xmirror.cn` | | `token` | `string` | 云服务验证 `token`,需要在云服务平台申请,与 `url` 参数一起使用 | `-token xxxxxxx` | +| `v2` | `bool` | 是否使用新的v2版本的云服务接口 | `-v2` | | `cache` | `bool` | 建议开启,缓存下载的文件(例如 `.pom` 文件),重复检测相同组件时会节省时间,下载的文件会保存到工具所在目录的.cache 目录下 | `-cache` | | `vuln` | `bool` | 结果仅保留有漏洞信息的组件,使用该参数将不会保留组件层级结构 | `-vuln` | | `out` | `string` | 将检测结果保存到指定文件,根据后缀生成不同格式的文件,默认为 `json` 格式;支持以`spdx`格式展示`sbom`清单,只需更换相应输出文件后缀即可 | `-out output.json` | | `db` | `string` | 指定本地漏洞库文件,希望使用自己漏洞库时可用,漏洞库文件为 `json` 格式,具体格式会在之后给出;若同时使用云端漏洞库与本地漏洞库,漏洞查询结果取并集 | `-db db.json` | | `progress` | `bool` | 显示进度条 | `-progress` | | `dedup` | `bool` | 相同组件去重 | `-dedup` | +| `version` | `bool` | 显示客户端版本 | `-version` | --- diff --git a/util/args/args.go b/util/args/args.go index b3fa94f..d5df5ca 100644 --- a/util/args/args.go +++ b/util/args/args.go @@ -49,7 +49,7 @@ func init() { flag.StringVar(&Config.Path, "path", Config.Path, "(必须) 指定要检测的文件或目录路径,例: -path ./foo 或 -path ./foo.zip") flag.StringVar(&Config.Url, "url", Config.Url, "(可选,与token需一起使用) 从云漏洞库查询漏洞,指定要连接云服务的地址,例:-url https://opensca.xmirror.cn") flag.StringVar(&Config.Token, "token", Config.Token, "(可选,与url需一起使用) 云服务验证token,需要在云服务平台申请") - flag.BoolVar(&Config.V2, "v2", Config.V2, "(可选,与url需一起使用) 是否使用新的v2版本的云服务接口") + flag.BoolVar(&Config.V2, "v2", Config.V2, "(可选,与url、token需一起使用) 是否使用新的v2版本的云服务接口") flag.BoolVar(&Config.Cache, "cache", Config.Cache, "(可选,建议开启) 缓存下载的文件(例如pom文件),重复检测相同组件时会节省时间,下载的文件会保存到工具所在目录的.cache目录下") flag.BoolVar(&Config.OnlyVuln, "vuln", Config.OnlyVuln, "(可选) 结果仅保留有漏洞信息的组件,使用该参数不会保留组件层级结构") flag.StringVar(&Config.Out, "out", Config.Out, "(可选) 将检测结果保存到指定文件,根据后缀生成不同格式的文件,默认为json格式,例: -out output.json") diff --git a/util/client/client.go b/util/client/client.go index aeb9073..5e1024f 100644 --- a/util/client/client.go +++ b/util/client/client.go @@ -215,6 +215,7 @@ func DetectV2(root *model.DepTree) (repbody []byte, err error) { // 发送数据 rep, err := http.Post(url, "application/json", bytes.NewReader(data)) if err != nil { + logs.Error(err) return repbody, err } defer rep.Body.Close() -- Gitee From c5dda6ced734375fd95a218f3bb7342e4851b2f8 Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Wed, 7 Dec 2022 14:15:24 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=EF=BC=9A=E6=96=B0=E5=A2=9ECHANGELOG.md=20README=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=EF=BC=9A=E5=A2=9E=E5=8A=A0maven=E7=A7=81=E6=9C=8D?= =?UTF-8?q?=E5=BA=93=E7=9A=84=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?saas=E6=96=B0=E6=8E=A5=E5=8F=A3=E4=BC=A0=E9=80=92=E7=9A=84?= =?UTF-8?q?=E5=8C=85=E5=90=8D=E6=A0=BC=E5=BC=8F=E4=B8=BA=EF=BC=9A`[?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=B7=AF=E5=BE=84=E5=90=8D]=E5=8C=85?= =?UTF-8?q?=E5=90=8D`=20=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96=EF=BC=9Apytho?= =?UTF-8?q?n=E8=A7=A3=E6=9E=90setup.py=E9=94=99=E8=AF=AF=E6=97=B6=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +++++++++++++ README.md | 22 ++++++++++++++++++---- analyzer/engine/engine.go | 6 +++--- analyzer/engine/parse.go | 2 +- analyzer/python/setup.go | 3 ++- util/client/client.go | 10 +++++----- 6 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5f441d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## v1.1.0 + +* 文件新增:新增CHANGELOG.md +* 代码优化:优化saas新接口传递的包名格式为:`[基础路径名]包名` +* 代码优化:python解析setup.py错误时增加日志输出 +* BUG修复:fix 错误修复[解析python requirement时windows和linux打开文件的权限差异问题] +* README更新:新增参数说明,增加maven私服库的使用说明等 +* 功能优化:优化python setup.py的解析,不在临时目录中解析,直接进入项目目录进行解析; +* BUG修复:修复解析python setup.py中的依赖时,~符号没考虑到导致包名和版本号错误的bug;修复依赖去重时导致路径为空的bug +* 功能新增:完成大部分包管理器的特定文件中提取包名和版本号 +* 功能新增:对接saas新接口。请求响应格式变动,请求会传递项目依赖结构、项目哈希、项目名称、项目版本号等 +* BUG修复:修复提取copyright信息时可能报错的bug +* BUG修复:针对log模块中空指针问题进行修改 \ No newline at end of file diff --git a/README.md b/README.md index e6a67c1..ec49c0e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ | `Golang` | `gomod` | `go.mod` `go.sum` | | `Rust` | `cargo` | `Cargo.lock` | | `Erlang` | `Rebar` | `rebar.lock` | -| `Python` | `Pip` | `Pipfile` `Pipfile.lock` `setup.py` `requirements.txt` `requirements.in` (后两者的解析需要具备pipenv环境,需要联网。) | +| `Python` | `Pip` | `Pipfile` `Pipfile.lock` `setup.py` `requirements.txt` `requirements.in` (有的解析会对python版本有要求,后两者的解析需要具备pipenv环境,需要联网。) | ## 下载安装 @@ -86,17 +86,31 @@ opensca-cli -db db.json -path ${project_path} | `path` | `string` | 指定要检测的文件或目录路径 | `-path ./foo` | | `url` | `string` | 从云漏洞库查询漏洞,指定要连接云服务的地址,与 `token` 参数一起使用 | `-url https://opensca.xmirror.cn` | | `token` | `string` | 云服务验证 `token`,需要在云服务平台申请,与 `url` 参数一起使用 | `-token xxxxxxx` | -| `v2` | `bool` | 是否使用新的v2版本的云服务接口 | `-v2` | +| `v2` | `bool` | 是否使用新的v2版本的云服务接口 | `-v2` | | `cache` | `bool` | 建议开启,缓存下载的文件(例如 `.pom` 文件),重复检测相同组件时会节省时间,下载的文件会保存到工具所在目录的.cache 目录下 | `-cache` | | `vuln` | `bool` | 结果仅保留有漏洞信息的组件,使用该参数将不会保留组件层级结构 | `-vuln` | -| `out` | `string` | 将检测结果保存到指定文件,根据后缀生成不同格式的文件,默认为 `json` 格式;支持以`spdx`格式展示`sbom`清单,只需更换相应输出文件后缀即可 | `-out output.json` | +| `out` | `string` | 将检测结果保存到指定文件,根据后缀生成不同格式的文件,默认为 `json` 格式;支持以`spdx`格式展示`sbom`清单,只需更换相应输出文件后缀即可 | `-out output.json` | | `db` | `string` | 指定本地漏洞库文件,希望使用自己漏洞库时可用,漏洞库文件为 `json` 格式,具体格式会在之后给出;若同时使用云端漏洞库与本地漏洞库,漏洞查询结果取并集 | `-db db.json` | | `progress` | `bool` | 显示进度条 | `-progress` | | `dedup` | `bool` | 相同组件去重 | `-dedup` | -| `version` | `bool` | 显示客户端版本 | `-version` | +| `version` | `bool` | 显示客户端版本 | `-version` | --- +如果要配置maven私服库,需要在配置文件里进行配置,格式如下: + +```json +{ + "maven": [ + { + "repo": "url", + "user": "user", + "password": "password" + } + ] +} +``` + ### 漏洞库文件格式 ```json diff --git a/analyzer/engine/engine.go b/analyzer/engine/engine.go index 320d8c3..dcd8860 100644 --- a/analyzer/engine/engine.go +++ b/analyzer/engine/engine.go @@ -58,9 +58,9 @@ func (e Engine) ParseFile(filepath string) (depRoot *model.DepTree, taskInfo rep dirRoot := model.NewDirTree() depRoot = model.NewDepTree(nil) filepath = strings.ReplaceAll(filepath, `\`, `/`) - client.PackageRootDir = path.Base(filepath) + client.PackageBasePath = path.Base(filepath) taskInfo = report.TaskInfo{ - AppName: strings.TrimSuffix(client.PackageRootDir, path.Ext(client.PackageRootDir)), + AppName: strings.TrimSuffix(client.PackageBasePath, path.Ext(client.PackageBasePath)), StartTime: time.Now().Format("2006-01-02 15:04:05"), } client.PackageName = taskInfo.AppName @@ -76,7 +76,7 @@ func (e Engine) ParseFile(filepath string) (depRoot *model.DepTree, taskInfo rep } else { if f.IsDir() { //如果是目录则不用去除后缀 - taskInfo.AppName = client.PackageRootDir + taskInfo.AppName = client.PackageBasePath client.PackageName = taskInfo.AppName // 目录 dirRoot = e.opendir(filepath) diff --git a/analyzer/engine/parse.go b/analyzer/engine/parse.go index eb2d7d0..dc760cf 100644 --- a/analyzer/engine/parse.go +++ b/analyzer/engine/parse.go @@ -71,7 +71,7 @@ func (e Engine) parseDependency(dirRoot *model.DirTree, depRoot *model.DepTree) toHash = true } //只保存根目录下可以版本提取的文件 - if strings.HasSuffix(f.Name, client.PackageRootDir+"/"+path.Base(f.Name)) { + if strings.HasSuffix(f.Name, client.PackageBasePath+"/"+path.Base(f.Name)) { packageVersionFileCount = packageVersionFileCount + 1 toParseVersion := false //如果不满足条件则可能不是单一语言的包,不提取版本 diff --git a/analyzer/python/setup.go b/analyzer/python/setup.go index 2ff9882..d83955b 100644 --- a/analyzer/python/setup.go +++ b/analyzer/python/setup.go @@ -104,6 +104,7 @@ func parseSetup(root *model.DepTree, file *model.FileInfo) { startTag, endTag := `oss_start<<`, `>>oss_end` startIndex, endIndex := strings.Index(string(out), startTag), strings.Index(string(out), endTag) if startIndex == -1 || endIndex == -1 { + logs.Debug(string(out)) return } else { out = out[startIndex+len(startTag) : endIndex] @@ -114,7 +115,7 @@ func parseSetup(root *model.DepTree, file *model.FileInfo) { logs.Warn(err) } // 运行时提取的包名和版本号,只提取根目录的 - if strings.HasSuffix(file.Name, client.PackageRootDir+"/"+path.Base(file.Name)) { + if strings.HasSuffix(file.Name, client.PackageBasePath+"/"+path.Base(file.Name)) { if dep.Name != "" { client.PackageName = dep.Name } diff --git a/util/client/client.go b/util/client/client.go index 5e1024f..cad2dfe 100644 --- a/util/client/client.go +++ b/util/client/client.go @@ -28,10 +28,10 @@ import ( const V2Type = 10 //新的saas端定义的类型,来自cli的type固定为10 var ( - PackageRootDir string - PackageName string - PackageVersion string - PackageHash string + PackageBasePath string + PackageName string + PackageVersion string + PackageHash string ) // SaasReponse 消息响应格式 @@ -204,7 +204,7 @@ func DetectV2(root *model.DepTree) (repbody []byte, err error) { //param.ClientId = GetClientId() param.Type = V2Type param.Token = args.Config.Token - param.PackageName = PackageName + param.PackageName = fmt.Sprintf("[%s]%s", PackageBasePath, PackageName) param.PackageVersion = PackageVersion param.PackageHash = PackageHash param.ComponentInfoAddDTOList = dep.Children -- Gitee From 7cb755654f8c76922387260908609932ab02c116 Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Wed, 7 Dec 2022 14:46:28 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E6=97=A5=E5=BF=97=E9=87=8C=E5=B0=BD=E9=87=8F=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E8=AE=B0=E5=BD=95token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + util/client/client.go | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f441d8..7a60490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v1.1.0 +* 代码优化:日志里尽量避免记录token * 文件新增:新增CHANGELOG.md * 代码优化:优化saas新接口传递的包名格式为:`[基础路径名]包名` * 代码优化:python解析setup.py错误时增加日志输出 diff --git a/util/client/client.go b/util/client/client.go index cad2dfe..6a9ff93 100644 --- a/util/client/client.go +++ b/util/client/client.go @@ -18,6 +18,7 @@ import ( "os" "path" "regexp" + "strings" "util/args" "util/logs" "util/model" @@ -262,20 +263,24 @@ func getAesKey() (key []byte, err error) { param.Set("clientId", GetClientId()) param.Set("ossToken", args.Config.Token) u.RawQuery = param.Encode() + // 日志里尽量避免记录token + logUrl := strings.ReplaceAll(u.String(), args.Config.Token, "[TOKEN]") // 发送请求 rep, err := http.Get(u.String()) if err != nil { + err = fmt.Errorf("%s", strings.ReplaceAll(err.Error(), args.Config.Token, "[TOKEN]")) logs.Error(err) return } if rep.StatusCode != 200 { - err = fmt.Errorf("url: %s,status code:%d", u.String(), rep.StatusCode) + err = fmt.Errorf("url: %s,status code:%d", logUrl, rep.StatusCode) logs.Error(err) return } else { defer rep.Body.Close() data, err := ioutil.ReadAll(rep.Body) if err != nil { + err = fmt.Errorf("%s", strings.ReplaceAll(err.Error(), args.Config.Token, "[TOKEN]")) logs.Error(err) return key, err } @@ -284,7 +289,7 @@ func getAesKey() (key []byte, err error) { json.Unmarshal(data, &saasrep) if saasrep.Code != 0 { // 出现错误 - logs.Warn(fmt.Sprintf("url:%s code:%d message: %s", u.String(), saasrep.Code, saasrep.Message)) + logs.Warn(fmt.Sprintf("url:%s code:%d message: %s", logUrl, saasrep.Code, saasrep.Message)) err = errors.New(saasrep.Message) return key, err } else { -- Gitee From 742f6354cab3e9989db531d36df0d98e92591b07 Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Fri, 9 Dec 2022 11:12:32 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E6=A3=80=E6=B5=8B=E7=9B=AE=E5=BD=95=E6=97=B6=E8=BF=87?= =?UTF-8?q?=E6=BB=A4.git=E7=AD=89=E7=89=B9=E6=AE=8A=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=9A=84=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++- analyzer/engine/archive.go | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a60490..ba3a48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ ## v1.1.0 +* 功能优化:检测目录时过滤.git等特殊目录的检测 * 代码优化:日志里尽量避免记录token * 文件新增:新增CHANGELOG.md * 代码优化:优化saas新接口传递的包名格式为:`[基础路径名]包名` * 代码优化:python解析setup.py错误时增加日志输出 * BUG修复:fix 错误修复[解析python requirement时windows和linux打开文件的权限差异问题] * README更新:新增参数说明,增加maven私服库的使用说明等 -* 功能优化:优化python setup.py的解析,不在临时目录中解析,直接进入项目目录进行解析; +* 功能优化:优化python setup.py的解析,不在临时目录中解析,直接进入项目目录进行解析 * BUG修复:修复解析python setup.py中的依赖时,~符号没考虑到导致包名和版本号错误的bug;修复依赖去重时导致路径为空的bug * 功能新增:完成大部分包管理器的特定文件中提取包名和版本号 * 功能新增:对接saas新接口。请求响应格式变动,请求会传递项目依赖结构、项目哈希、项目名称、项目版本号等 diff --git a/analyzer/engine/archive.go b/analyzer/engine/archive.go index d4882ca..6b4d98b 100644 --- a/analyzer/engine/archive.go +++ b/analyzer/engine/archive.go @@ -25,6 +25,9 @@ import ( "github.com/pkg/errors" ) +// IgnoreDirMap 忽略的目录,用map方便判断 +var IgnoreDirMap = map[string]bool{".git": true, ".idea": true, ".vscode": true, ".svn": true} + // checkFile 检测是否为可检测的文件 func (e Engine) checkFile(filename string) bool { for _, analyzer := range e.Analyzers { @@ -130,8 +133,10 @@ func (e Engine) opendir(dirpath string) (dir *model.DirTree) { filename := file.Name() filepath := path.Join(dirpath, filename) if file.IsDir() { - dir.DirList = append(dir.DirList, filename) - dir.SubDir[filename] = e.opendir(filepath) + if _, ok := IgnoreDirMap[filename]; !ok { + dir.DirList = append(dir.DirList, filename) + dir.SubDir[filename] = e.opendir(filepath) + } } else { if filter.AllPkg(filename) { dir.DirList = append(dir.DirList, filename) -- Gitee From 5b394ce545ece87a42d9aae981c26493508dbd55 Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Thu, 15 Dec 2022 16:54:21 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9Ahtml=E6=8A=A5=E5=91=8A=E6=B7=BB=E5=8A=A0=E5=88=86?= =?UTF-8?q?=E9=A1=B5=EF=BC=9B=E4=BC=98=E5=8C=96html=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E4=B8=AD=E6=A3=80=E6=B5=8B=E6=97=B6=E9=95=BF=E4=B8=BA0.x?= =?UTF-8?q?=E7=A7=92=E6=97=B6=E7=9A=84=E7=B2=BE=E5=BA=A6(=E4=B9=8B?= =?UTF-8?q?=E5=89=8D0.x=E7=A7=92=E6=98=BE=E7=A4=BA=E4=B8=BA=E7=A9=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + util/report/html.go | 2 +- util/report/html_tpl | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba3a48b..5378c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v1.1.0 +* 功能优化:html报告添加分页;优化html报告中检测时长为0.x秒时的精度(之前0.x秒显示为空) * 功能优化:检测目录时过滤.git等特殊目录的检测 * 代码优化:日志里尽量避免记录token * 文件新增:新增CHANGELOG.md diff --git a/util/report/html.go b/util/report/html.go index 527e079..7d10127 100644 --- a/util/report/html.go +++ b/util/report/html.go @@ -76,6 +76,6 @@ func Html(dep *model.DepTree, taskInfo TaskInfo) []byte { return []byte{} } else { // 替换模板数据 - return bytes.Replace(index, []byte("N$}"), append(data, '}'), 1) + return bytes.Replace(index, []byte("DATA_REPLACE_HERE"), data, 1) } } diff --git a/util/report/html_tpl b/util/report/html_tpl index 90a4ce3..7b8033f 100644 --- a/util/report/html_tpl +++ b/util/report/html_tpl @@ -1,2 +1,2 @@ -OpenSCA开源组件检测报告
\ No newline at end of file +OpenSCA开源组件检测报告
\ No newline at end of file -- Gitee From 64f587f953af19f4c25f0563f5bd626632099fb3 Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Mon, 19 Dec 2022 14:49:30 +0800 Subject: [PATCH 08/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E4=BC=98=E5=8C=96=E5=AF=B9=E4=BA=8Ejava=20pom?= =?UTF-8?q?=E7=9A=84=E8=A7=A3=E6=9E=90=EF=BC=8C=E8=BF=87=E6=BB=A4scope?= =?UTF-8?q?=E4=B8=BAtest=E6=97=B6=E7=9A=84=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analyzer/java/ext.go | 11 +++++++++-- analyzer/java/mvn.go | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/analyzer/java/ext.go b/analyzer/java/ext.go index 9a82e38..e45ce2f 100644 --- a/analyzer/java/ext.go +++ b/analyzer/java/ext.go @@ -39,9 +39,16 @@ func MvnDepTree(path string, root *model.DepTree) { out = bytes.ReplaceAll(out, []byte("\r"), []byte("\n")) // 获取mvn解析内容 lines := strings.Split(string(out), "\n") - for i := range lines { - lines[i] = strings.TrimPrefix(lines[i], "[INFO] ") + i := 0 + for _, v := range lines { + tags := strings.Split(v, ":") + // 去除scope为test的依赖 + if len(tags) < 5 || tags[4] != "test" { + lines[i] = strings.TrimPrefix(v, "[INFO] ") + i++ + } } + lines = lines[:i] // 捕获依赖树起始位置 title := regexp.MustCompile(`--- [^\n]+ ---`) // 记录依赖树起始位置行号 diff --git a/analyzer/java/mvn.go b/analyzer/java/mvn.go index b87af0d..ede3c82 100644 --- a/analyzer/java/mvn.go +++ b/analyzer/java/mvn.go @@ -200,10 +200,10 @@ func (m *Mvn) MvnSimulation() []*Pom { } for i := range p.Dependencies { dp := &p.Dependencies[i] - if dp.Scope == "provided" { + if dp.Scope == "provided" || dp.Scope == "test" { continue } - if (dp.Scope == "test" || dp.Optional == "true") && p.Deep() > 0 { + if dp.Optional == "true" && p.Deep() > 0 { continue } p.Complete(dp) -- Gitee From fbc7e1770511c4c589986ed23d6e8b2551d3eec1 Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Mon, 19 Dec 2022 15:22:40 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E4=BC=98=E5=8C=96java=20mvn=E7=9A=84=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E4=BE=9D=E8=B5=96=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ analyzer/java/ext.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5378c1e..34cbf6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## v1.1.0 +* 功能优化:优化java mvn的直接依赖设置 +* 功能优化:优化对于java pom的解析,过滤scope为test时的依赖 * 功能优化:html报告添加分页;优化html报告中检测时长为0.x秒时的精度(之前0.x秒显示为空) * 功能优化:检测目录时过滤.git等特殊目录的检测 * 代码优化:日志里尽量避免记录token diff --git a/analyzer/java/ext.go b/analyzer/java/ext.go index e45ce2f..0782760 100644 --- a/analyzer/java/ext.go +++ b/analyzer/java/ext.go @@ -68,6 +68,10 @@ func MvnDepTree(path string, root *model.DepTree) { buildMvnDepTree(root, lines[start+1:i]) for _, c := range root.Children { c.Direct = true + // 第二层才是直接依赖?因为顶层为空 + for _, cc := range c.Children { + cc.Direct = true + } } continue } -- Gitee From 8bdbc99be5f01443070ce1706779da228b832e7f Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Thu, 22 Dec 2022 15:20:28 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E6=96=B0=E5=A2=9E=E5=86=85=E9=83=A8=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E6=8F=92=E4=BB=B6=E5=8F=82=E6=95=B0=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=9B=B4=E6=96=B0=EF=BC=9AREADME=E7=AD=89=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=9B=B4=E6=96=B0=EF=BC=8C=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- README.md | 25 +++++++++++++----- cli/main.go | 2 +- util/args/args.go | 2 ++ util/client/client.go | 11 ++++++-- ...1\346\211\213\345\276\256\344\277\241.jpg" | Bin 0 -> 40681 bytes 6 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 "\345\260\217\345\212\251\346\211\213\345\276\256\344\277\241.jpg" diff --git a/CHANGELOG.md b/CHANGELOG.md index 34cbf6d..37b42d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## v1.1.0 +## v2.0.0 * 功能优化:优化java mvn的直接依赖设置 * 功能优化:优化对于java pom的解析,过滤scope为test时的依赖 diff --git a/README.md b/README.md index ec49c0e..a953ac0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ --- ## 检测能力 + `OpenSCA`现已支持以下编程语言相关的配置文件解析及对应的包管理器,后续会逐步支持更多的编程语言,丰富相关配置文件的解析。 | 支持语言 | 包管理器 | 解析文件 | @@ -58,22 +59,22 @@ opensca-cli -path ${project_path} ``` -连接云平台 +使用本地漏洞库 ```shell -opensca-cli -url ${url} -token ${token} -path ${project_path} +opensca-cli -db db.json -path ${project_path} ``` -连接新的云平台接口(新的漏洞库,新的检测逻辑) +使用v1.0.9及以下版本仅使用云漏洞库服务 ```shell -opensca-cli -url ${url} -token ${token} -v2 -path ${project_path} +opensca-cli -url ${url} -token ${token} -path ${project_path} ``` -或使用本地漏洞库 +使用v2.0.0及以上版本连接SaaS服务,检测漏洞信息、获取资产清单、生成数据看板并进行项目管理 ```shell -opensca-cli -db db.json -path ${project_path} +opensca-cli -url ${url} -token ${token} -v2 -path ${project_path} ``` ## 参数说明 @@ -161,12 +162,24 @@ opensca-cli -db db.json -path ${project_path} | `exploit_level_id` | 漏洞利用评级(0:不可利用,1:可利用) | 否 | ## 贡献者 + - 张涛 - 张弛 - 陈钟 - 刘恩炙 - 宁戈 +## 问题反馈&联系我们 + +微信技术交流群:(扫码添加小助手邀您入群) + + + +QQ技术交流群:832039395 + +官方邮箱:opensca@anpro-tech.com + + ## 向我们贡献 **OpenSCA** 是一款开源的软件成分分析工具,项目成员期待您的贡献。 diff --git a/cli/main.go b/cli/main.go index 092b4b9..9587ed6 100644 --- a/cli/main.go +++ b/cli/main.go @@ -16,7 +16,7 @@ import ( "util/report" ) -const VERSION = "v1.1.0" +const VERSION = "v2.0.0-rc" func main() { args.Parse() diff --git a/util/args/args.go b/util/args/args.go index d5df5ca..bd5eab6 100644 --- a/util/args/args.go +++ b/util/args/args.go @@ -18,6 +18,7 @@ import ( var ( ConfigPath string ShowVersion bool + PluginName string Config = struct { // detect option Path string `json:"path"` @@ -57,6 +58,7 @@ func init() { flag.BoolVar(&Config.Bar, "progress", Config.Bar, "(可选) 显示进度条") flag.BoolVar(&Config.Dedup, "dedup", Config.Dedup, "(可选) 相同组件去重") flag.BoolVar(&ShowVersion, "version", false, "显示客户端版本") + flag.StringVar(&PluginName, "plugin", "", "内部使用,用于控制插件类型") } func Parse() { diff --git a/util/client/client.go b/util/client/client.go index 6a9ff93..96b77ab 100644 --- a/util/client/client.go +++ b/util/client/client.go @@ -27,7 +27,10 @@ import ( "github.com/pkg/errors" ) -const V2Type = 10 //新的saas端定义的类型,来自cli的type固定为10 +const CliType = 10 //新的saas端定义的类型,来自cli的type固定为10 +var PluginType = map[string]int{ + "idea": 3, //新的saas端定义的类型,来自idea的type固定为3 +} var ( PackageBasePath string PackageName string @@ -202,8 +205,12 @@ func DetectV2(root *model.DepTree) (repbody []byte, err error) { url := args.Config.Url + "/oss-saas/api-v1/component/task/cli/add" // 添加参数 param := DetectRequstV2{} + if _, ok := PluginType[args.PluginName]; ok { + param.Type = PluginType[args.PluginName] + } else { + param.Type = CliType + } //param.ClientId = GetClientId() - param.Type = V2Type param.Token = args.Config.Token param.PackageName = fmt.Sprintf("[%s]%s", PackageBasePath, PackageName) param.PackageVersion = PackageVersion diff --git "a/\345\260\217\345\212\251\346\211\213\345\276\256\344\277\241.jpg" "b/\345\260\217\345\212\251\346\211\213\345\276\256\344\277\241.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..af044bcab3dbe672d71344dcffb82e47b2b8fe56 GIT binary patch literal 40681 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 literal 0 HcmV?d00001 -- Gitee From 22fed020f0f98c6ea006ce9c95b8090ec4f4799e Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Thu, 29 Dec 2022 15:28:03 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E4=BE=9D=E8=B5=96=E8=8A=82=E7=82=B9=E4=B8=BA=E7=A9=BA?= =?UTF-8?q?=E4=B9=9F=E4=BC=9A=E5=90=8C=E6=AD=A5=E5=88=B0SaaS=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E4=B8=8A=20=E6=96=87=E4=BB=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E8=8B=B1=E6=96=87=E7=89=88README=E7=AD=89=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/README.md | 344 ++++++++++-------- CHANGELOG.md | 4 + README.md | 10 +- cli/main.go | 2 +- util/client/client.go | 1 - wechat.jpg | Bin 0 -> 44879 bytes ...1\346\211\213\345\276\256\344\277\241.jpg" | Bin 40681 -> 0 bytes 7 files changed, 202 insertions(+), 159 deletions(-) create mode 100644 wechat.jpg delete mode 100644 "\345\260\217\345\212\251\346\211\213\345\276\256\344\277\241.jpg" diff --git a/.github/README.md b/.github/README.md index 830d4f2..5995228 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,152 +1,192 @@ - - -

- logo -

-

OpenSCA-Cli

- -

- - -

- - - -## Introduction - -OpenSCA is intended for scanning the third-party component dependencies and vulnerabilities. - ------- - -## Detection Ability - -OpenSCA is now capable of parsing configuration files in the listed programming languages and correspondent package managers. The project team is now dedicated to introducing more languages and enriching the parsing of relevant configuration files gradually. - -| LANGUAGE | PACKAGE MANAGER | FILE | -| ------------ | --------------- | ---------------------------------------------- | -| `Java` | `Maven` | `pom.xml` | -| `Java` | `Gradle` | `.gradle` `.gradle.kts` | -| `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` | -| `Python` | `Pip` | `Pipfile` `Pipfile.lock` `setup.py` | - -## Download and Deployment - -1. Download the appropriate executable file according to your system architecture from [release](https://github.com/XmirrorSecurity/OpenSCA-cli/releases). - -2. Or download the source code and compile (go 1.18 and above is needed) - - ``` - git clone https://github.com/XmirrorSecurity/OpenSCA-cli.git opensca - cd opensca - go work init cli analyzer util - go build -o opensca-cli cli - ``` - - The default option is to generate the program of the current system architecture. If you want to try it for other system architectures, you can set the following environment variables before compiling. - - - Disable `CGO_ENABLED` `CGO_ENABLED=0` - - Set the operating system `GOOS=${OS} \\ darwin,freebsd,liunx,windows` - - Set the architecture `GOARCH=${arch} \\ 386,amd64,arm` - -## Samples - -For detecting the component information only: - -``` -opensca-cli -path ${project_path} -``` - -For connecting to the cloud platform: - -``` -opensca-cli -url ${url} -token ${token} -path ${project_path} -``` - -Or for using the local vulnerability database: - -``` -opensca-cli -db db.json -path ${project_path} -``` - -## Parameters - -**You can either configure the parameters in configuration files or input the parameters in the command-line. When the two conflict with each other, the input parameters will be prioritized.** - -| PARAMETER | TYPE | DESCRIPTION | SAMPLE | -| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -| `config` | `string` | Set the configuration file path, when the program runs, the parameter of the configuration file will be used as the startup parameters. If the configuration parameter conflicts with the command-line input parameter, the latter will be taken. | `-config config.json` | -| `path` | `string` | Set the file or directory path to be detected. | `-path ./foo` | -| `url` | `string` | Check the vulnerabilities from the cloud vulnerability database, set the address of the cloud service. It needs to be used with the `token` parameter. | `-url https://opensca.xmirror.cn` | -| `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. 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` | - ------- - -### The Format of the Vulnerability Database File - -``` -[ - { - "vendor": "org.apache.logging.log4j", - "product": "log4j-core", - "version": "[2.0-beta9,2.12.2)||[2.13.0,2.15.0)", - "language": "java", - "name": "Apache Log4j2 远程代码执行漏洞", - "id": "XMIRROR-2021-44228", - "cve_id": "CVE-2021-44228", - "cnnvd_id": "CNNVD-202112-799", - "cnvd_id": "CNVD-2021-95914", - "cwe_id": "CWE-502,CWE-400,CWE-20", - "description": "Apache Log4j是美国阿帕奇(Apache)基金会的一款基于Java的开源日志记录工具。\r\nApache Log4J 存在代码问题漏洞,攻击者可设计一个数据请求发送给使用 Apache Log4j工具的服务器,当该请求被打印成日志时就会触发远程代码执行。", - "description_en": "Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0, this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.", - "suggestion": "2.12.1及以下版本可以更新到2.12.2,其他建议更新至2.15.0或更高版本,漏洞详情可参考:https://github.com/apache/logging-log4j2/pull/608 \r\n1、临时解决方案,适用于2.10及以上版本:\r\n\t(1)设置jvm参数:“-Dlog4j2.formatMsgNoLookups=true”;\r\n\t(2)设置参数:“log4j2.formatMsgNoLookups=True”;", - "attack_type": "远程", - "release_date": "2021-12-10", - "security_level_id": 1, - "exploit_level_id": 1 - }, - {} -] -``` - -#### Explanations of Vulnerability Database Fields - -| FIELD | DESCRIPTION | REQUIRED OR NOT | -| ------------------- | ----------------------------------------------------------------- | --------------- | -| `vendor` | the manufacturer of the component | N | -| `product` | the name of the component | Y | -| `version` | the versions of the component affected by the vulnerability | Y | -| `language` | the programming language of the component | Y | -| `name` | the name of the vulnerability | N | -| `id` | custom identifier | Y | -| `cve_id` | cve identifier | N | -| `cnnvd_id` | cnnvd identifier | N | -| `cnvd_id` | cnvd identifier | N | -| `cwe_id` | cwe identifier | N | -| `description` | the description of the vulnerability | N | -| `description_en` | the description of the vulnerability in English | N | -| `suggestion` | the suggestion for fixing the vulnerability | N | -| `attack_type` | the type of attack | N | -| `release_date` | the release date of the vulnerability | N | -| `security_level_id` | the security level of the vulnerability (diminishing from 1 to 4) | N | -| `exploit_level_id` | the exploit level of the vulnerability (0-N/A 1-Available) | N | - -## Contributing - -OpenSCA is an open source project, we appreciate your help! - -To contribute, please read our [Contributing Guideline](../docs/Contributing%20Guideline-en%20v1.0.md). - - - -*For the Chinese version of this document, please check [README](../README.md). + + +

+ logo +

+

OpenSCA-Cli

+ +

+ + +

+ + + +## Introduction + +OpenSCA is intended for scanning the third-party component dependencies and vulnerabilities. + +------ + +## Detection Ability + +OpenSCA is now capable of parsing configuration files in the listed programming languages and correspondent package managers. The project team is now dedicated to supporting more languages and enriching the parsing of relevant configuration files gradually. + +| LANGUAGE | PACKAGE MANAGER | FILE | +| ------------ | --------------- | ------------------------------------------------------------ | +| `Java` | `Maven` | `pom.xml` | +| `Java` | `Gradle` | `.gradle` `.gradle.kts` | +| `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` | +| `Python` | `Pip` | `Pipfile` `Pipfile.lock` `setup.py` `requirements.txt` `requirements.in` (The version of Python may have impacts to the parsing. To parse the latter two, pipenv environment & internet connection is a must.) | + +## Download and Deployment + +1. Download the appropriate executable file according to your system architecture from [releases](https://github.com/XmirrorSecurity/OpenSCA-cli/releases). + +2. Or download the source code and compile (go 1.18 and above is needed) + + ``` + git clone https://github.com/XmirrorSecurity/OpenSCA-cli.git opensca + cd opensca + go work init cli analyzer util + go build -o opensca-cli cli/main.go + ``` + + The default option is to generate the program of the current system architecture. If you want to try it for other system architectures, you can set the following environment variables before compiling. + + - Disable `CGO_ENABLED` `CGO_ENABLED=0` + - Set the operating system `GOOS=${OS} \\ darwin,freebsd,liunx,windows` + - Set the architecture `GOARCH=${arch} \\ 386,amd64,arm` + +## Samples + +Detect the component only: + +``` +opensca-cli -path ${project_path} +``` + +Use the local vulnerability database: + +``` +opensca-cli -db db.json -path ${project_path} +``` + +Connect to the cloud vulnerability database only: + +```shell +opensca-cli -url ${url} -token ${token} -path ${project_path} +``` + +Use v2.0.0 and above to connect to OpenSCA SaaS, get vulnerabilities, assets, and dashboard, and manage all the projects: + +```shell +opensca-cli -url ${url} -token ${token} -v2 -path ${project_path} +``` + + + +## Parameters + +**You can either configure the parameters in configuration files or input the parameters in the command-line. When the two conflict with each other, the input parameters will be prioritized.** + +| PARAMETER | TYPE | DESCRIPTION | SAMPLE | +| ---------- | -------- | ------------------------------------------------------------ | --------------------------------- | +| `config` | `string` | Set the configuration file path, when the program runs, the parameter of the configuration file will be used as the startup parameters. If the configuration parameter conflicts with the command-line input parameter, the latter will be taken. | `-config config.json` | +| `path` | `string` | Set the file or directory path to be detected. | `-path ./foo` | +| `url` | `string` | Check the vulnerabilities from the cloud vulnerability database, set the address of the cloud service. It needs to be used with the `token` parameter. | `-url https://opensca.xmirror.cn` | +| `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` | +| `v2` | `bool` | Connect to the API of OpenSCA SaaS service. | `-v2` | +| `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 is json 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` | Deduplicate same components | `-dedup` | +| `version` | `bool` | Show the client version | `-version` | + +------ + +Local maven component database can be configured in the following format to configuration files: + +```json +{ + "maven": [ + { + "repo": "url", + "user": "user", + "password": "password" + } + ] +} +``` + +### The Format of the Vulnerability Database File + +``` +[ + { + "vendor": "org.apache.logging.log4j", + "product": "log4j-core", + "version": "[2.0-beta9,2.12.2)||[2.13.0,2.15.0)", + "language": "java", + "name": "Apache Log4j2 远程代码执行漏洞", + "id": "XMIRROR-2021-44228", + "cve_id": "CVE-2021-44228", + "cnnvd_id": "CNNVD-202112-799", + "cnvd_id": "CNVD-2021-95914", + "cwe_id": "CWE-502,CWE-400,CWE-20", + "description": "Apache Log4j是美国阿帕奇(Apache)基金会的一款基于Java的开源日志记录工具。\r\nApache Log4J 存在代码问题漏洞,攻击者可设计一个数据请求发送给使用 Apache Log4j工具的服务器,当该请求被打印成日志时就会触发远程代码执行。", + "description_en": "Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0, this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.", + "suggestion": "2.12.1及以下版本可以更新到2.12.2,其他建议更新至2.15.0或更高版本,漏洞详情可参考:https://github.com/apache/logging-log4j2/pull/608 \r\n1、临时解决方案,适用于2.10及以上版本:\r\n\t(1)设置jvm参数:“-Dlog4j2.formatMsgNoLookups=true”;\r\n\t(2)设置参数:“log4j2.formatMsgNoLookups=True”;", + "attack_type": "远程", + "release_date": "2021-12-10", + "security_level_id": 1, + "exploit_level_id": 1 + }, + {} +] +``` + +#### Explanations of Vulnerability Database Fields + +| FIELD | DESCRIPTION | REQUIRED OR NOT | +| ------------------- | ------------------------------------------------------------ | --------------- | +| `vendor` | the manufacturer of the component | N | +| `product` | the name of the component | Y | +| `version` | the versions of the component affected by the vulnerability | Y | +| `language` | the programming language of the component | Y | +| `name` | the name of the vulnerability | N | +| `id` | custom identifier | Y | +| `cve_id` | cve identifier | N | +| `cnnvd_id` | cnnvd identifier | N | +| `cnvd_id` | cnvd identifier | N | +| `cwe_id` | cwe identifier | N | +| `description` | the description of the vulnerability | N | +| `description_en` | the description of the vulnerability in English | N | +| `suggestion` | the suggestion for fixing the vulnerability | N | +| `attack_type` | the type of attack | N | +| `release_date` | the release date of the vulnerability | N | +| `security_level_id` | the security level of the vulnerability (diminishing from 1 to 4) | N | +| `exploit_level_id` | the exploit level of the vulnerability (0-N/A 1-Available) | N | + +## Contributors + +- Tao Zhang +- Chi Zhang +- Zhong Chen +- Enzhi Liu +- Ge Ning + +## Contact us + +Wechat Group: add our assistant to join the group + + + +QQ Group: 832039395 + +email: opensca@anpro-tech.com + +## Contributing + +OpenSCA is an open source project, we appreciate your help! + +To contribute, please read our [Contributing Guideline](https://github.com/XmirrorSecurity/OpenSCA-cli/blob/master/docs/Contributing%20Guideline-en%20v1.0.md). + +*For the Chinese version of this document, please check [README](https://github.com/XmirrorSecurity/OpenSCA-cli/blob/master/README.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b42d6..0e8f25d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## v2.0.0 +* 功能优化:依赖节点为空也会同步到SaaS平台上 +* 文件更新:英文版README等文件更新 +* 功能优化:新增内部使用的插件参数 +* 文件更新:README等文件更新 * 功能优化:优化java mvn的直接依赖设置 * 功能优化:优化对于java pom的解析,过滤scope为test时的依赖 * 功能优化:html报告添加分页;优化html报告中检测时长为0.x秒时的精度(之前0.x秒显示为空) diff --git a/README.md b/README.md index a953ac0..1f938e8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- logo + logo

OpenSCA-Cli

@@ -31,12 +31,12 @@ ## 下载安装 -1. 从 [releases](https://github.com/XmirrorSecurity/OpenSCA-cli/releases) 下载对应系统架构的可执行文件压缩包 +1. 从 [releases](https://gitee.com/XmirrorSecurity/OpenSCA-cli/releases) 下载对应系统架构的可执行文件压缩包 2. 或者下载源码编译(需要 `go 1.18` 及以上版本) ```shell - git clone https://github.com/XmirrorSecurity/OpenSCA-cli.git opensca + git clone https://gitee.com/XmirrorSecurity/OpenSCA-cli.git opensca cd opensca go work init cli analyzer util go build -o opensca-cli cli/main.go @@ -65,7 +65,7 @@ opensca-cli -path ${project_path} opensca-cli -db db.json -path ${project_path} ``` -使用v1.0.9及以下版本仅使用云漏洞库服务 +仅使用云漏洞库服务 ```shell opensca-cli -url ${url} -token ${token} -path ${project_path} @@ -173,7 +173,7 @@ opensca-cli -url ${url} -token ${token} -v2 -path ${project_path} 微信技术交流群:(扫码添加小助手邀您入群) - + QQ技术交流群:832039395 diff --git a/cli/main.go b/cli/main.go index 9587ed6..5a9d6e7 100644 --- a/cli/main.go +++ b/cli/main.go @@ -16,7 +16,7 @@ import ( "util/report" ) -const VERSION = "v2.0.0-rc" +const VERSION = "v2.0.0" func main() { args.Parse() diff --git a/util/client/client.go b/util/client/client.go index 96b77ab..efb0704 100644 --- a/util/client/client.go +++ b/util/client/client.go @@ -199,7 +199,6 @@ func DetectV2(root *model.DepTree) (repbody []byte, err error) { dep := root.ToDetectComponents() if len(dep.Children) == 0 { logs.Debug("依赖节点为空") - return repbody, err } // 构建请求 url := args.Config.Url + "/oss-saas/api-v1/component/task/cli/add" diff --git a/wechat.jpg b/wechat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..14d23844df810f5ce4f94cbd9b22ebf2ea671b00 GIT binary patch literal 44879 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 BVvGO) literal 0 HcmV?d00001 diff --git "a/\345\260\217\345\212\251\346\211\213\345\276\256\344\277\241.jpg" "b/\345\260\217\345\212\251\346\211\213\345\276\256\344\277\241.jpg" deleted file mode 100644 index af044bcab3dbe672d71344dcffb82e47b2b8fe56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40681 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 -- Gitee From 1192b5eced6272d36c2d30b5c72d92e1d001bb5b Mon Sep 17 00:00:00 2001 From: Xmirror-FN Date: Sat, 28 Jan 2023 09:39:11 +0800 Subject: [PATCH 12/12] =?UTF-8?q?BUG=E4=BF=AE=E5=A4=8D=EF=BC=9A=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=9C=AC=E5=9C=B0=E6=BC=8F=E6=B4=9E=E5=BA=93=E6=97=B6?= =?UTF-8?q?=E6=8A=A5=E7=A9=BA=E6=8C=87=E9=92=88=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 43 ++++++++++++++++++++++--------------------- util/vuln/local.go | 8 +++++++- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8f25d..7bef248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,23 @@ -## v2.0.0 - -* 功能优化:依赖节点为空也会同步到SaaS平台上 -* 文件更新:英文版README等文件更新 -* 功能优化:新增内部使用的插件参数 -* 文件更新:README等文件更新 -* 功能优化:优化java mvn的直接依赖设置 -* 功能优化:优化对于java pom的解析,过滤scope为test时的依赖 -* 功能优化:html报告添加分页;优化html报告中检测时长为0.x秒时的精度(之前0.x秒显示为空) -* 功能优化:检测目录时过滤.git等特殊目录的检测 -* 代码优化:日志里尽量避免记录token -* 文件新增:新增CHANGELOG.md -* 代码优化:优化saas新接口传递的包名格式为:`[基础路径名]包名` -* 代码优化:python解析setup.py错误时增加日志输出 -* BUG修复:fix 错误修复[解析python requirement时windows和linux打开文件的权限差异问题] -* README更新:新增参数说明,增加maven私服库的使用说明等 -* 功能优化:优化python setup.py的解析,不在临时目录中解析,直接进入项目目录进行解析 -* BUG修复:修复解析python setup.py中的依赖时,~符号没考虑到导致包名和版本号错误的bug;修复依赖去重时导致路径为空的bug -* 功能新增:完成大部分包管理器的特定文件中提取包名和版本号 -* 功能新增:对接saas新接口。请求响应格式变动,请求会传递项目依赖结构、项目哈希、项目名称、项目版本号等 -* BUG修复:修复提取copyright信息时可能报错的bug +## v2.0.0 + +* BUG修复:使用本地漏洞库时报空指针错误 +* 功能优化:依赖节点为空也会同步到SaaS平台上 +* 文件更新:英文版README等文件更新 +* 功能优化:新增内部使用的插件参数 +* 文件更新:README等文件更新 +* 功能优化:优化java mvn的直接依赖设置 +* 功能优化:优化对于java pom的解析,过滤scope为test时的依赖 +* 功能优化:html报告添加分页;优化html报告中检测时长为0.x秒时的精度(之前0.x秒显示为空) +* 功能优化:检测目录时过滤.git等特殊目录的检测 +* 代码优化:日志里尽量避免记录token +* 文件新增:新增CHANGELOG.md +* 代码优化:优化saas新接口传递的包名格式为:`[基础路径名]包名` +* 代码优化:python解析setup.py错误时增加日志输出 +* BUG修复:fix 错误修复[解析python requirement时windows和linux打开文件的权限差异问题] +* README更新:新增参数说明,增加maven私服库的使用说明等 +* 功能优化:优化python setup.py的解析,不在临时目录中解析,直接进入项目目录进行解析 +* BUG修复:修复解析python setup.py中的依赖时,~符号没考虑到导致包名和版本号错误的bug;修复依赖去重时导致路径为空的bug +* 功能新增:完成大部分包管理器的特定文件中提取包名和版本号 +* 功能新增:对接saas新接口。请求响应格式变动,请求会传递项目依赖结构、项目哈希、项目名称、项目版本号等 +* BUG修复:修复提取copyright信息时可能报错的bug * BUG修复:针对log模块中空指针问题进行修改 \ No newline at end of file diff --git a/util/vuln/local.go b/util/vuln/local.go index 90d62f8..68ce53a 100644 --- a/util/vuln/local.go +++ b/util/vuln/local.go @@ -36,8 +36,14 @@ func loadVulnDB() { } else { // 解析本地漏洞 db := []vulnInfo{} - json.Unmarshal(data, &db) + err := json.Unmarshal(data, &db) + if err != nil { + logs.Error(err) + } for _, info := range db { + if info.Vuln == nil { + continue + } // 有中文描述则省略英文描述 if info.Description != "" { info.DescriptionEn = "" -- Gitee