feat: init nightly vuls for blackhat

This commit is contained in:
MaineK00n
2022-11-15 11:26:26 +09:00
parent 1d97e91341
commit 3605645ff6
234 changed files with 6172 additions and 54872 deletions

22
pkg/cmd/config/config.go Normal file
View File

@@ -0,0 +1,22 @@
package config
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
configInitCmd "github.com/future-architect/vuls/pkg/cmd/config/init"
)
func NewCmdConfig() *cobra.Command {
cmd := &cobra.Command{
Use: "config <subcommand>",
Short: "Vuls Config Operation",
Example: heredoc.Doc(`
$ vuls config init > config.json
`),
}
cmd.AddCommand(configInitCmd.NewCmdInit())
return cmd
}

101
pkg/cmd/config/init/init.go Normal file
View File

@@ -0,0 +1,101 @@
package init
import (
"os"
"path/filepath"
"text/template"
"github.com/MakeNowJust/heredoc"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func NewCmdInit() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "generate vuls config template",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
return generateConfigTemplate()
},
Example: heredoc.Doc(`
$ vuls config init > config.json
`),
}
return cmd
}
func generateConfigTemplate() error {
pwd, err := os.Getwd()
if err != nil {
pwd = os.TempDir()
}
home, err := os.UserHomeDir()
if err != nil {
home = "/home/vuls"
}
create := func(name, t string) *template.Template {
return template.Must(template.New(name).Parse(t))
}
t := create("config template",
`{
"server": {
"listen": "127.0.0.1:5515",
"path": "{{.dbpath}}"
},
"hosts": {
"local": {
"type": "local",
"scan": {
"ospkg": {
"root": false
}
},
"detect": {
"path": "{{.dbpath}}",
"result_dir": "{{.results}}"
}
},
"remote": {
"type": "remote",
"host": "127.0.0.1",
"port": "22",
"user": "vuls",
"ssh_config": "{{.sshconfig}}",
"ssh_key": "{{.sshkey}}",
"scan": {
"ospkg": {
"root": false
}
},
"detect": {
"path": "{{.dbpath}}",
"result_dir": "{{.results}}"
}
},
"cpe": {
"type": "local",
"scan": {
"cpe": [
{
"cpe": "cpe:2.3:a:apache:log4j:2.3:*:*:*:*:*:*:*"
}
]
},
"detect": {
"path": "{{.dbpath}}",
"result_dir": "{{.results}}"
}
}
}
}
`)
if err := t.Execute(os.Stdout, map[string]string{"dbpath": filepath.Join(pwd, "vuls.db"), "results": filepath.Join(pwd, "results"), "sshconfig": filepath.Join(home, ".ssh", "config"), "sshkey": filepath.Join(home, ".ssh", "id_rsa")}); err != nil {
return errors.Wrap(err, "output config template")
}
return nil
}

413
pkg/cmd/db/create/create.go Normal file
View File

@@ -0,0 +1,413 @@
package create
import (
"encoding/json"
"fmt"
"io/fs"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/future-architect/vuls/pkg/cmd/db/create/vulnsrc"
"github.com/future-architect/vuls/pkg/db"
"github.com/future-architect/vuls/pkg/util"
)
type DBCreateOption struct {
Path string
}
func NewCmdCreate() *cobra.Command {
opts := &DBCreateOption{
Path: "vuls.db",
}
cmd := &cobra.Command{
Use: "create",
Short: "Create Vuls DB",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
return create(args[0], opts.Path)
},
Example: heredoc.Doc(`
$ vuls db create https://github.com/vulsio/vuls-data.git
$ vuls db create /home/MaineK00n/.cache/vuls
`),
}
cmd.Flags().StringVarP(&opts.Path, "path", "p", "vuls.db", "path to create Vuls DB")
return cmd
}
func create(src, dbpath string) error {
datapath := src
if u, err := url.Parse(src); err == nil && u.Scheme != "" {
cloneDir := filepath.Join(util.CacheDir(), "clone")
if err := exec.Command("git", "clone", "--depth", "1", src, cloneDir).Run(); err != nil {
return errors.Wrapf(err, "git clone --depth 1 %s %s", src, cloneDir)
}
datapath = cloneDir
}
if _, err := os.Stat(datapath); err != nil {
return errors.Wrapf(err, "%s not found", datapath)
}
db, err := db.Open("boltdb", dbpath, false)
if err != nil {
return errors.Wrap(err, "open db")
}
defer db.Close()
if err := filepath.WalkDir(datapath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
p := strings.TrimPrefix(strings.TrimPrefix(path, datapath), string(os.PathSeparator))
srcType, p, found := strings.Cut(p, string(os.PathSeparator))
if !found {
return nil
}
advType, p, found := strings.Cut(p, string(os.PathSeparator))
if !found {
return errors.Errorf(`unexpected filepath. expected: "%s/["official", ...]/["vulnerability", "os", "library", "cpe"]/...", actual: "%s"`, datapath, path)
}
bs, err := util.Read(path)
if err != nil {
return errors.Wrapf(err, "read %s", path)
}
if len(bs) == 0 {
return nil
}
switch advType {
case "vulnerability":
var src vulnsrc.Vulnerability
if err := json.Unmarshal(bs, &src); err != nil {
return errors.Wrapf(err, "unmarshal json. path: %s", path)
}
if err := db.PutVulnerability(srcType, fmt.Sprintf("vulnerability:%s", src.ID), vulnsrc.ToVulsVulnerability(src)); err != nil {
return errors.Wrap(err, "put vulnerability")
}
case "os":
advType, err := toAdvType(p)
if err != nil {
return errors.Wrap(err, "path to adv type")
}
bucket, err := toAdvBucket(p)
if err != nil {
return errors.Wrap(err, "path to adv bucket")
}
switch advType {
case "redhat_oval":
if strings.Contains(p, "repository_to_cpe.json") {
var src vulnsrc.RepositoryToCPE
if err := json.Unmarshal(bs, &src); err != nil {
return errors.Wrapf(err, "unmarshal json. path: %s", path)
}
if err := db.PutRedHatRepoToCPE(srcType, bucket, vulnsrc.ToVulsRepositoryToCPE(src)); err != nil {
return errors.Wrap(err, "put repository to cpe")
}
break
}
var src vulnsrc.DetectPackage
if err := json.Unmarshal(bs, &src); err != nil {
return errors.Wrapf(err, "unmarshal json. path: %s", path)
}
pkgs, err := vulnsrc.ToVulsPackage(src, advType)
if err != nil {
return errors.Wrap(err, "to vuls package")
}
if err := db.PutPackage(srcType, bucket, pkgs); err != nil {
return errors.Wrap(err, "put package")
}
case "windows":
if strings.Contains(p, "supercedence.json") {
var supercedences []vulnsrc.Supercedence
if err := json.Unmarshal(bs, &supercedences); err != nil {
return errors.Wrapf(err, "unnmarshal json. path: %s", path)
}
if err := db.PutWindowsSupercedence(srcType, bucket, vulnsrc.ToVulsSupercedences(supercedences)); err != nil {
return errors.Wrap(err, "put supercedence")
}
break
}
var src vulnsrc.DetectPackage
if err := json.Unmarshal(bs, &src); err != nil {
return errors.Wrapf(err, "unmarshal json. path: %s", path)
}
pkgs, err := vulnsrc.ToVulsPackage(src, advType)
if err != nil {
return errors.Wrap(err, "to vuls package")
}
if err := db.PutPackage(srcType, bucket, pkgs); err != nil {
return errors.Wrap(err, "put package")
}
default:
var src vulnsrc.DetectPackage
if err := json.Unmarshal(bs, &src); err != nil {
return errors.Wrapf(err, "unmarshal json. path: %s", path)
}
pkgs, err := vulnsrc.ToVulsPackage(src, advType)
if err != nil {
return errors.Wrap(err, "to vuls package")
}
if err := db.PutPackage(srcType, bucket, pkgs); err != nil {
return errors.Wrap(err, "put package")
}
}
case "library":
case "cpe":
var src vulnsrc.DetectCPE
if err := json.Unmarshal(bs, &src); err != nil {
return errors.Wrapf(err, "unmarshal json. path: %s", path)
}
advType, err := toAdvType(p)
if err != nil {
return errors.Wrap(err, "path to adv type")
}
cs, err := vulnsrc.ToVulsCPEConfiguration(src, advType)
if err != nil {
return errors.Wrap(err, "to vuls cpe configuration")
}
bucket, err := toAdvBucket(p)
if err != nil {
return errors.Wrap(err, "path to adv bucket")
}
if err := db.PutCPEConfiguration(srcType, bucket, cs); err != nil {
return errors.Wrap(err, "put cpe configuration")
}
}
return nil
}); err != nil {
return err
}
return nil
}
func toAdvType(path string) (string, error) {
ss := strings.Split(path, string(os.PathSeparator))
if len(ss) < 3 && ss[0] != "windows" {
return "", errors.Errorf(`unexpected path. accepts: "[<os name>, <library name>, "nvd", "jvn"]/**/*.json*", received: "%s"`, path)
}
switch ss[0] {
case "alma", "alpine", "amazon", "epel", "fedora", "oracle", "rocky":
return fmt.Sprintf("%s:%s", ss[0], ss[1]), nil
case "arch", "freebsd", "gentoo", "windows", "conan", "erlang", "nvd", "jvn":
return ss[0], nil
case "debian":
switch ss[1] {
case "oval":
return "debian_oval", nil
case "tracker":
return "debian_security_tracker", nil
default:
return "", errors.Errorf(`unexpected debian advisory type. accepts: ["oval", "tracker"], received: "%s"`, ss[1])
}
case "redhat":
switch ss[1] {
case "api":
return "redhat_security_api", nil
case "oval":
return "redhat_oval", nil
default:
return "", errors.Errorf(`unexpected redhat advisory type. accepts: ["api", "oval"], received: "%s"`, ss[1])
}
case "suse":
switch ss[1] {
case "cvrf":
return "suse_cvrf", nil
case "oval":
return "suse_oval", nil
default:
return "", errors.Errorf(`unexpected suse advisory type. accepts: ["cvrf", "oval"], received: "%s"`, ss[1])
}
case "ubuntu":
switch ss[1] {
case "oval":
return "ubuntu_oval", nil
case "tracker":
return "ubuntu_security_tracker", nil
default:
return "", errors.Errorf(`unexpected debian advisory type. accepts: ["oval", "tracker"], received: "%s"`, ss[1])
}
case "cargo":
switch ss[1] {
case "db":
return "cargo_db", nil
case "ghsa":
return "cargo_ghsa", nil
case "osv":
return "cargo_osv", nil
default:
return "", errors.Errorf(`unexpected cargo advisory type. accepts: ["db", "ghsa", "osv"], received: "%s"`, ss[1])
}
case "composer":
switch ss[1] {
case "db":
return "composer_db", nil
case "ghsa":
return "composer_ghsa", nil
case "glsa":
return "composer_glsa", nil
default:
return "", errors.Errorf(`unexpected composer advisory type. accepts: ["db", "ghsa", "glsa"], received: "%s"`, ss[1])
}
case "golang":
switch ss[1] {
case "db":
return "golang_db", nil
case "ghsa":
return "golang_ghsa", nil
case "glsa":
return "golang_glsa", nil
case "govulndb":
return "golang_govulndb", nil
case "osv":
return "golang_osv", nil
default:
return "", errors.Errorf(`unexpected golang advisory type. accepts: ["db", "ghsa", "glsa", "govulndb", "osv"], received: "%s"`, ss[1])
}
case "maven":
switch ss[1] {
case "ghsa":
return "maven_ghsa", nil
case "glsa":
return "maven_glsa", nil
default:
return "", errors.Errorf(`unexpected maven advisory type. accepts: ["ghsa", "glsa"], received: "%s"`, ss[1])
}
case "npm":
switch ss[1] {
case "db":
return "npm_db", nil
case "ghsa":
return "npm_ghsa", nil
case "glsa":
return "npm_glsa", nil
case "osv":
return "npm_osv", nil
default:
return "", errors.Errorf(`unexpected npm advisory type. accepts: ["db", "ghsa", "glsa", "osv"], received: "%s"`, ss[1])
}
case "nuget":
switch ss[1] {
case "ghsa":
return "nuget_ghsa", nil
case "glsa":
return "nuget_glsa", nil
case "osv":
return "nuget_osv", nil
default:
return "", errors.Errorf(`unexpected nuget advisory type. accepts: ["ghsa", "glsa", "osv"], received: "%s"`, ss[1])
}
case "pip":
switch ss[1] {
case "db":
return "pip_db", nil
case "ghsa":
return "pip_ghsa", nil
case "glsa":
return "pip_glsa", nil
case "osv":
return "pip_osv", nil
default:
return "", errors.Errorf(`unexpected pip advisory type. accepts: ["db", "ghsa", "glsa", "osv"], received: "%s"`, ss[1])
}
case "rubygems":
switch ss[1] {
case "db":
return "rubygems_db", nil
case "ghsa":
return "rubygems_ghsa", nil
case "glsa":
return "rubygems_glsa", nil
case "osv":
return "rubygems_osv", nil
default:
return "", errors.Errorf(`unexpected rubygems advisory type. accepts: ["db", "ghsa", "glsa", "osv"], received: "%s"`, ss[1])
}
default:
return "", errors.Errorf(`unexpected os or library or cpe. accepts: ["alma", "alpine", "amazon", "arch", "debian", "epel", "fedora", "freebsd", "gentoo", "oracle", "redhat", "rocky", "suse", "ubuntu", "windows", "cargo", "composer", "conan", "erlang", "golang", "maven", "npm", "nuget", "pip", "rubygems", "nvd", "jvn"], received: "%s"`, ss[0])
}
}
func toAdvBucket(path string) (string, error) {
ss := strings.Split(path, string(os.PathSeparator))
if len(ss) < 3 && ss[0] != "windows" {
return "", errors.Errorf(`unexpected path. accepts: "[<os name>, <library name>, "nvd", "jvn"]/**/*.json*", received: "%s"`, path)
}
switch ss[0] {
case "alma", "alpine", "amazon", "epel", "fedora", "oracle", "rocky":
return fmt.Sprintf("%s:%s", ss[0], ss[1]), nil
case "arch", "freebsd", "gentoo", "cargo", "composer", "conan", "erlang", "golang", "maven", "npm", "nuget", "pip", "rubygems":
return ss[0], nil
case "debian":
switch ss[1] {
case "oval", "tracker":
return fmt.Sprintf("%s:%s", ss[0], ss[2]), nil
default:
return "", errors.Errorf(`unexpected debian advisory type. accepts: ["oval", "tracker"], received: "%s"`, ss[1])
}
case "redhat":
switch ss[1] {
case "api":
return fmt.Sprintf("%s:%s", ss[0], ss[2]), nil
case "oval":
if len(ss) < 4 {
return "", errors.Errorf(`unexpected path. accepts: "redhat/oval/<os version>/<stream>/yyyy/*.json*", received: "%s"`, path)
}
if strings.Contains(path, "repository_to_cpe.json") {
return fmt.Sprintf("%s_cpe:%s", ss[0], ss[3]), nil
}
return fmt.Sprintf("%s:%s", ss[0], ss[3]), nil
default:
return "", errors.Errorf(`unexpected redhat advisory type. accepts: ["api", "oval"], received: "%s"`, ss[1])
}
case "suse":
switch ss[1] {
case "cvrf", "oval":
if len(ss) < 4 {
return "", errors.Errorf(`unexpected path. accepts: "suse/[cvrf, oval]/<os>/<version>/yyyy/*.json*", received: "%s"`, path)
}
return fmt.Sprintf("%s:%s", ss[2], ss[3]), nil
default:
return "", errors.Errorf(`unexpected suse advisory type. accepts: ["cvrf", "oval"], received: "%s"`, ss[1])
}
case "ubuntu":
switch ss[1] {
case "oval", "tracker":
return fmt.Sprintf("%s:%s", ss[0], ss[2]), nil
default:
return "", errors.Errorf(`unexpected debian advisory type. accepts: ["oval", "tracker"], received: "%s"`, ss[1])
}
case "windows":
if strings.Contains(path, "supercedence.json") {
return "windows_supercedence", nil
}
return fmt.Sprintf("%s:%s", ss[0], ss[1]), nil
case "nvd", "jvn":
return "cpe", nil
default:
return "", errors.Errorf(`unexpected os or library or cpe. accepts: ["alma", "alpine", "amazon", "arch", "debian", "epel", "fedora", "freebsd", "gentoo", "oracle", "redhat", "rocky", "suse", "ubuntu", "windows", "cargo", "composer", "conan", "erlang", "golang", "maven", "npm", "nuget", "pip", "rubygems", "nvd", "jvn"], received: "%s"`, ss[0])
}
}

View File

@@ -0,0 +1,269 @@
package vulnsrc
// https://github.com/MaineK00n/vuls-data-update/blob/main/pkg/build/types.go
import "time"
type Vulnerability struct {
ID string `json:"id,omitempty"`
Advisory *Advisories `json:"advisory,omitempty"`
Title *Titles `json:"title,omitempty"`
Description *Descriptions `json:"description,omitempty"`
CVSS *CVSSes `json:"cvss,omitempty"`
EPSS *EPSS `json:"epss,omitempty"`
CWE *CWEs `json:"cwe,omitempty"`
Metasploit []Metasploit `json:"metasploit,omitempty"`
Exploit *Exploit `json:"exploit,omitempty"`
KEV *KEV `json:"kev,omitempty"`
Mitigation *Mitigation `json:"mitigation,omitempty"`
Published *Publisheds `json:"published,omitempty"`
Modified *Modifieds `json:"modified,omitempty"`
References *References `json:"references,omitempty"`
}
type Advisories struct {
MITRE *Advisory `json:"mitre,omitempty"`
NVD *Advisory `json:"nvd,omitempty"`
JVN []Advisory `json:"jvn,omitempty"`
Alma map[string][]Advisory `json:"alma,omitempty"`
Alpine map[string]Advisory `json:"alpine,omitempty"`
Amazon map[string][]Advisory `json:"amazon,omitempty"`
Arch []Advisory `json:"arch,omitempty"`
DebianOVAL map[string][]Advisory `json:"debian_oval,omitempty"`
DebianSecurityTracker map[string]Advisory `json:"debian_security_tracker,omitempty"`
FreeBSD []Advisory `json:"freebsd,omitempty"`
Oracle map[string][]Advisory `json:"oracle,omitempty"`
RedHatOVAL map[string][]Advisory `json:"redhat_oval,omitempty"`
SUSEOVAL map[string][]Advisory `json:"suse_oval,omitempty"`
SUSECVRF *Advisory `json:"suse_cvrf,omitempty"`
UbuntuOVAL map[string][]Advisory `json:"ubuntu_oval,omitempty"`
UbuntuSecurityTracker *Advisory `json:"ubuntu_security_tracker,omitempty"`
}
type Advisory struct {
ID string `json:"id,omitempty"`
URL string `json:"url,omitempty"`
}
type Titles struct {
MITRE string `json:"mitre,omitempty"`
NVD string `json:"nvd,omitempty"`
JVN map[string]string `json:"jvn,omitempty"`
Alma map[string]map[string]string `json:"alma,omitempty"`
Alpine map[string]string `json:"alpine,omitempty"`
Amazon map[string]map[string]string `json:"amazon,omitempty"`
Arch map[string]string `json:"arch,omitempty"`
DebianOVAL map[string]map[string]string `json:"debian_oval,omitempty"`
DebianSecurityTracker map[string]string `json:"debian_security_tracker,omitempty"`
FreeBSD map[string]string `json:"freebsd,omitempty"`
Oracle map[string]map[string]string `json:"oracle,omitempty"`
RedHatOVAL map[string]map[string]string `json:"redhat_oval,omitempty"`
SUSEOVAL map[string]map[string]string `json:"suse_oval,omitempty"`
SUSECVRF string `json:"suse_cvrf,omitempty"`
UbuntuOVAL map[string]map[string]string `json:"ubuntu_oval,omitempty"`
UbuntuSecurityTracker string `json:"ubuntu_security_tracker,omitempty"`
}
type Descriptions struct {
MITRE string `json:"mitre,omitempty"`
NVD string `json:"nvd,omitempty"`
JVN map[string]string `json:"jvn,omitempty"`
Alma map[string]map[string]string `json:"alma,omitempty"`
Amazon map[string]map[string]string `json:"amazon,omitempty"`
DebianOVAL map[string]map[string]string `json:"debian_oval,omitempty"`
DebianSecurityTracker map[string]string `json:"debian_security_tracker,omitempty"`
FreeBSD map[string]string `json:"freebsd,omitempty"`
Oracle map[string]map[string]string `json:"oracle,omitempty"`
RedHatOVAL map[string]map[string]string `json:"redhat_oval,omitempty"`
SUSEOVAL map[string]map[string]string `json:"suse_oval,omitempty"`
SUSECVRF string `json:"suse_cvrf,omitempty"`
UbuntuOVAL map[string]map[string]string `json:"ubuntu_oval,omitempty"`
UbuntuSecurityTracker string `json:"ubuntu_security_tracker,omitempty"`
}
type CVSSes struct {
NVD []CVSS `json:"nvd,omitempty"`
JVN map[string][]CVSS `json:"jvn,omitempty"`
Alma map[string]map[string][]CVSS `json:"alma,omitempty"`
Amazon map[string]map[string][]CVSS `json:"amazon,omitempty"`
Arch map[string][]CVSS `json:"arch,omitempty"`
Oracle map[string]map[string][]CVSS `json:"oracle,omitempty"`
RedHatOVAL map[string]map[string][]CVSS `json:"redhat_oval,omitempty"`
SUSEOVAL map[string]map[string][]CVSS `json:"suse_oval,omitempty"`
SUSECVRF []CVSS `json:"suse_cvrf,omitempty"`
UbuntuOVAL map[string]map[string][]CVSS `json:"ubuntu_oval,omitempty"`
UbuntuSecurityTracker []CVSS `json:"ubuntu_security_tracker,omitempty"`
}
type CVSS struct {
Version string `json:"version,omitempty"`
Source string `json:"source,omitempty"`
Vector string `json:"vector,omitempty"`
Score *float64 `json:"score,omitempty"`
Severity string `json:"severity,omitempty"`
}
type EPSS struct {
EPSS *float64 `json:"epss,omitempty"`
Percentile *float64 `json:"percentile,omitempty"`
}
type CWEs struct {
NVD []string `json:"nvd,omitempty"`
JVN map[string][]string `json:"jvn,omitempty"`
RedHatOVAL map[string]map[string][]string `json:"redhat_oval,omitempty"`
}
type Metasploit struct {
Name string `json:"name,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
URLs []string `json:"urls,omitempty"`
}
type Exploit struct {
NVD []string `json:"nvd,omitempty"`
ExploitDB []ExploitDB `json:"exploit_db,omitempty"`
GitHub []GitHub `json:"github,omitempty"`
InTheWild []InTheWild `json:"inthewild,omitempty"`
Trickest *Trickest `json:"trickest,omitempty"`
}
type ExploitDB struct {
ID string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
URL string `json:"url,omitempty"`
FileURL string `json:"file_url,omitempty"`
}
type GitHub struct {
Name string `json:"name,omitempty"`
Stars int `json:"stars"`
Forks int `json:"forks"`
Watches int `json:"watches"`
URL string `json:"url,omitempty"`
}
type InTheWild struct {
Source string `json:"source,omitempty"`
URL string `json:"url,omitempty"`
}
type Trickest struct {
Description string `json:"description,omitempty"`
PoC *TrickestPoc `json:"poc,omitempty"`
}
type TrickestPoc struct {
Reference []string `json:"reference,omitempty"`
GitHub []string `json:"github,omitempty"`
}
type KEV struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
RequiredAction string `json:"required_action,omitempty"`
DueDate *time.Time `json:"due_date,omitempty"`
}
type Mitigation struct {
NVD []string `json:"nvd,omitempty"`
UbuntuSecurityTracker string `json:"ubuntu_security_tracker,omitempty"`
}
type Publisheds struct {
MITRE *time.Time `json:"mitre,omitempty"`
NVD *time.Time `json:"nvd,omitempty"`
JVN map[string]*time.Time `json:"jvn,omitempty"`
Alma map[string]map[string]*time.Time `json:"alma,omitempty"`
Amazon map[string]map[string]*time.Time `json:"amazon,omitempty"`
FreeBSD map[string]*time.Time `json:"freebsd,omitempty"`
Oracle map[string]map[string]*time.Time `json:"oracle,omitempty"`
RedHatOVAL map[string]map[string]*time.Time `json:"redhat_oval,omitempty"`
SUSEOVAL map[string]map[string]*time.Time `json:"suse_oval,omitempty"`
SUSECVRF *time.Time `json:"suse_cvrf,omitempty"`
UbuntuOVAL map[string]map[string]*time.Time `json:"ubuntu_oval,omitempty"`
UbuntuSecurityTracker *time.Time `json:"ubuntu_security_tracker,omitempty"`
}
type Modifieds struct {
MITRE *time.Time `json:"mitre,omitempty"`
NVD *time.Time `json:"nvd,omitempty"`
JVN map[string]*time.Time `json:"jvn,omitempty"`
Alma map[string]map[string]*time.Time `json:"alma,omitempty"`
Amazon map[string]map[string]*time.Time `json:"amazon,omitempty"`
FreeBSD map[string]*time.Time `json:"freebsd,omitempty"`
RedHatOVAL map[string]map[string]*time.Time `json:"redhat_oval,omitempty"`
SUSEOVAL map[string]map[string]*time.Time `json:"suse_oval,omitempty"`
SUSECVRF *time.Time `json:"suse_cvrf,omitempty"`
}
type References struct {
MITRE []Reference `json:"mitre,omitempty"`
NVD []Reference `json:"nvd,omitempty"`
JVN map[string][]Reference `json:"jvn,omitempty"`
Alma map[string]map[string][]Reference `json:"alma,omitempty"`
Amazon map[string]map[string][]Reference `json:"amazon,omitempty"`
Arch map[string][]Reference `json:"arch,omitempty"`
DebianOVAL map[string]map[string][]Reference `json:"debian_oval,omitempty"`
DebianSecurityTracker map[string][]Reference `json:"debian_security_tracker,omitempty"`
FreeBSD map[string][]Reference `json:"freebsd,omitempty"`
Oracle map[string]map[string][]Reference `json:"oracle,omitempty"`
RedHatOVAL map[string]map[string][]Reference `json:"redhat_oval,omitempty"`
SUSEOVAL map[string]map[string][]Reference `json:"suse_oval,omitempty"`
SUSECVRF []Reference `json:"suse_cvrf,omitempty"`
UbuntuOVAL map[string]map[string][]Reference `json:"ubuntu_oval,omitempty"`
UbuntuSecurityTracker []Reference `json:"ubuntu_security_tracker,omitempty"`
}
type Reference struct {
Source string `json:"source,omitempty"`
Name string `json:"name,omitempty"`
Tags []string `json:"tags,omitempty"`
URL string `json:"url,omitempty"`
}
type DetectCPE struct {
ID string `json:"id,omitempty"`
Configurations map[string][]CPEConfiguration `json:"configurations,omitempty"`
}
type CPEConfiguration struct {
Vulnerable []CPE `json:"vulnerable,omitempty"`
RunningOn []CPE `json:"running_on,omitempty"`
}
type CPE struct {
CPEVersion string `json:"cpe_version,omitempty"`
CPE string `json:"cpe,omitempty"`
Version []Version `json:"version,omitempty"`
}
type DetectPackage struct {
ID string `json:"id,omitempty"`
Packages map[string][]Package `json:"packages,omitempty"`
}
type Package struct {
Name string `json:"name,omitempty"`
Status string `json:"status,omitempty"`
Version [][]Version `json:"version,omitempty"`
ModularityLabel string `json:"modularity_label,omitempty"`
Arch []string `json:"arch,omitempty"`
Repository string `json:"repository,omitempty"`
CPE []string `json:"cpe,omitempty"`
}
type Version struct {
Operator string `json:"operator,omitempty"`
Version string `json:"version,omitempty"`
}
type RepositoryToCPE map[string][]string
type Supercedence struct {
KBID string `json:"KBID,omitempty"`
Supersededby struct {
KBIDs []string `json:"KBIDs,omitempty"`
} `json:"Supersededby,omitempty"`
}

View File

@@ -0,0 +1,619 @@
package vulnsrc
import (
"fmt"
"strings"
"time"
"golang.org/x/exp/maps"
"github.com/knqyf263/go-cpe/common"
"github.com/knqyf263/go-cpe/naming"
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/db/types"
)
func ToVulsVulnerability(src Vulnerability) types.Vulnerability {
return types.Vulnerability{
ID: src.ID,
Advisory: toVulsAdvisory(src.Advisory),
Title: toVulsTitle(src.Title),
Description: toVulsDescription(src.Description),
CVSS: toVulsCVSS(src.CVSS),
EPSS: toVulsEPSS(src.EPSS),
CWE: toVulsCWE(src.CWE),
Metasploit: toVulsMetasploit(src.Metasploit),
Exploit: toVulsExploit(src.Exploit),
KEV: src.KEV != nil,
Published: toVulsPublished(src.Published),
Modified: toVulsModified(src.Modified),
Reference: toVulsReference(src.References),
}
}
func toVulsAdvisory(src *Advisories) []string {
if src == nil {
return nil
}
var advs []string
if src.MITRE != nil {
advs = append(advs, "mitre")
}
if src.NVD != nil {
advs = append(advs, "nvd")
}
for _, a := range src.JVN {
advs = append(advs, fmt.Sprintf("jvn:%s", a.ID))
}
for v, as := range src.Alma {
for _, a := range as {
advs = append(advs, fmt.Sprintf("alma:%s:%s", v, a.ID))
}
}
for v, a := range src.Alpine {
advs = append(advs, fmt.Sprintf("alpine:%s:%s", v, a.ID))
}
for v, as := range src.Amazon {
for _, a := range as {
advs = append(advs, fmt.Sprintf("amazon:%s:%s", v, a.ID))
}
}
for _, a := range src.Arch {
advs = append(advs, fmt.Sprintf("arch:%s", a.ID))
}
for v, as := range src.DebianOVAL {
for _, a := range as {
advs = append(advs, fmt.Sprintf("debian_oval:%s:%s", v, a.ID))
}
}
for v, a := range src.DebianSecurityTracker {
advs = append(advs, fmt.Sprintf("debian_security_tracker:%s:%s", v, a.ID))
}
for _, a := range src.FreeBSD {
advs = append(advs, fmt.Sprintf("freebsd:%s", a.ID))
}
for v, as := range src.Oracle {
for _, a := range as {
advs = append(advs, fmt.Sprintf("oracle:%s:%s", v, a.ID))
}
}
for v, as := range src.RedHatOVAL {
for _, a := range as {
advs = append(advs, fmt.Sprintf("redhat_oval:%s:%s", v, a.ID))
}
}
for v, as := range src.SUSEOVAL {
for _, a := range as {
advs = append(advs, fmt.Sprintf("suse_oval:%s:%s", v, a.ID))
}
}
if src.SUSECVRF != nil {
advs = append(advs, "suse_cvrf")
}
for v, as := range src.UbuntuOVAL {
for _, a := range as {
advs = append(advs, fmt.Sprintf("ubuntu_oval:%s:%s", v, a.ID))
}
}
if src.UbuntuSecurityTracker != nil {
advs = append(advs, "ubuntu_security_tracker")
}
return advs
}
func toVulsTitle(src *Titles) string {
if src == nil {
return ""
}
if src.NVD != "" {
return src.NVD
}
if src.MITRE != "" {
return src.MITRE
}
return ""
}
func toVulsDescription(src *Descriptions) string {
if src == nil {
return ""
}
if src.NVD != "" {
return src.NVD
}
if src.MITRE != "" {
return src.MITRE
}
return ""
}
func toVulsCVSS(src *CVSSes) []types.CVSS {
if src == nil {
return nil
}
var cvsses []types.CVSS
for _, c := range src.NVD {
cvsses = append(cvsses, types.CVSS{
Source: "nvd",
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
for id, cs := range src.JVN {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("jvn:%s", id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
for v, idcs := range src.Alma {
for id, cs := range idcs {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("alma:%s:%s", v, id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
}
for v, idcs := range src.Amazon {
for id, cs := range idcs {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("amazon:%s:%s", v, id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
}
for id, cs := range src.Arch {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("arch:%s", id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
for v, idcs := range src.Oracle {
for id, cs := range idcs {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("oracle:%s:%s", v, id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
}
for v, idcs := range src.RedHatOVAL {
for id, cs := range idcs {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("redhat_oval:%s:%s", v, id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
}
for v, idcs := range src.SUSEOVAL {
for id, cs := range idcs {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("suse_oval:%s:%s", v, id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
}
for _, c := range src.SUSECVRF {
cvsses = append(cvsses, types.CVSS{
Source: "suse_cvrf",
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
for v, idcs := range src.UbuntuOVAL {
for id, cs := range idcs {
for _, c := range cs {
cvsses = append(cvsses, types.CVSS{
Source: fmt.Sprintf("ubuntu_oval:%s:%s", v, id),
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
}
}
for _, c := range src.UbuntuSecurityTracker {
cvsses = append(cvsses, types.CVSS{
Source: "ubuntu_security_tracker",
Version: c.Version,
Vector: c.Vector,
Score: c.Score,
Severity: c.Severity,
})
}
return cvsses
}
func toVulsEPSS(src *EPSS) *types.EPSS {
if src == nil {
return nil
}
return &types.EPSS{EPSS: src.EPSS, Percentile: src.Percentile}
}
func toVulsCWE(src *CWEs) []types.CWE {
if src == nil {
return nil
}
m := map[string][]string{}
for _, c := range src.NVD {
m[c] = append(m[c], "nvd")
}
for id, cs := range src.JVN {
for _, c := range cs {
m[c] = append(m[c], fmt.Sprintf("jvn:%s", id))
}
}
for v, idcs := range src.RedHatOVAL {
for id, cs := range idcs {
for _, c := range cs {
m[c] = append(m[c], fmt.Sprintf("redhat_oval:%s:%s", v, id))
}
}
}
var cwes []types.CWE
for id, srcs := range m {
cwes = append(cwes, types.CWE{
Source: srcs,
ID: id,
})
}
return cwes
}
func toVulsMetasploit(src []Metasploit) []types.Metasploit {
ms := make([]types.Metasploit, 0, len(src))
for _, m := range src {
ms = append(ms, types.Metasploit{
Title: m.Title,
URL: m.URLs[0],
})
}
return ms
}
func toVulsExploit(src *Exploit) []types.Exploit {
if src == nil {
return nil
}
m := map[string][]string{}
for _, e := range src.NVD {
m[e] = append(m[e], "nvd")
}
for _, e := range src.ExploitDB {
m[e.URL] = append(m[e.URL], "exploit-db")
}
for _, e := range src.GitHub {
m[e.URL] = append(m[e.URL], "github")
}
for _, e := range src.InTheWild {
m[e.URL] = append(m[e.URL], "inthewild")
}
if src.Trickest != nil {
if src.Trickest.PoC != nil {
for _, e := range src.Trickest.PoC.Reference {
m[e] = append(m[e], "trickest")
}
for _, e := range src.Trickest.PoC.GitHub {
m[e] = append(m[e], "trickest")
}
}
}
var es []types.Exploit
for u, srcs := range m {
es = append(es, types.Exploit{
Source: srcs,
URL: u,
})
}
return es
}
func toVulsPublished(src *Publisheds) *time.Time {
if src == nil {
return nil
}
if src.NVD != nil {
return src.NVD
}
if src.MITRE != nil {
return src.MITRE
}
return nil
}
func toVulsModified(src *Modifieds) *time.Time {
if src == nil {
return nil
}
if src.NVD != nil {
return src.NVD
}
if src.MITRE != nil {
return src.MITRE
}
return nil
}
func toVulsReference(src *References) []string {
if src == nil {
return nil
}
m := map[string]struct{}{}
for _, r := range src.MITRE {
m[r.URL] = struct{}{}
}
for _, r := range src.NVD {
m[r.URL] = struct{}{}
}
for _, rs := range src.JVN {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
for _, idrs := range src.Alma {
for _, rs := range idrs {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
}
for _, idrs := range src.Amazon {
for _, rs := range idrs {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
}
for _, rs := range src.Arch {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
for _, idrs := range src.DebianOVAL {
for _, rs := range idrs {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
}
for _, rs := range src.DebianSecurityTracker {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
for _, rs := range src.FreeBSD {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
for _, idrs := range src.Oracle {
for _, rs := range idrs {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
}
for _, idrs := range src.RedHatOVAL {
for _, rs := range idrs {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
}
for _, idrs := range src.SUSEOVAL {
for _, rs := range idrs {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
}
for _, r := range src.SUSECVRF {
m[r.URL] = struct{}{}
}
for _, idrs := range src.UbuntuOVAL {
for _, rs := range idrs {
for _, r := range rs {
m[r.URL] = struct{}{}
}
}
}
for _, r := range src.UbuntuSecurityTracker {
m[r.URL] = struct{}{}
}
return maps.Keys(m)
}
func ToVulsPackage(src DetectPackage, advType string) (map[string]types.Packages, error) {
m := map[string]types.Packages{}
for id, ps := range src.Packages {
id = fmt.Sprintf("%s:%s", advType, id)
for _, p := range ps {
name, err := toVulsPackageName(p.Name, p.ModularityLabel)
if err != nil {
return nil, errors.Wrap(err, "to vuls package name")
}
base, ok := m[name]
if !ok {
base = types.Packages{
ID: src.ID,
Package: map[string]types.Package{},
}
}
vers := make([][]types.Version, 0, len(p.Version))
for _, vs := range p.Version {
vss := make([]types.Version, 0, len(vs))
for _, v := range vs {
vss = append(vss, types.Version{
Operator: v.Operator,
Version: v.Version,
})
}
vers = append(vers, vss)
}
base.Package[id] = types.Package{
Status: p.Status,
Version: vers,
Arch: p.Arch,
Repository: p.Repository,
CPE: p.CPE,
}
m[name] = base
}
}
return m, nil
}
func toVulsPackageName(name, modularitylabel string) (string, error) {
if modularitylabel == "" {
return name, nil
}
ss := strings.Split(modularitylabel, ":")
if len(ss) < 2 {
return name, errors.Errorf(`[WARN] unexpected modularitylabel. accepts: "<module name>:<stream>(:<version>:<context>:<arch>)", received: "%s"`, modularitylabel)
}
return fmt.Sprintf("%s:%s::%s", ss[0], ss[1], name), nil
}
func ToVulsCPEConfiguration(src DetectCPE, advType string) (map[string]types.CPEConfigurations, error) {
m := map[string][]types.CPEConfiguration{}
for id, cs := range src.Configurations {
id = fmt.Sprintf("%s:%s", advType, id)
for _, c := range cs {
rs := make([]types.CPE, 0, len(c.RunningOn))
for _, r := range c.RunningOn {
rs = append(rs, toVulnsrcCPEtoVulsCPE(r))
}
for _, v := range c.Vulnerable {
m[id] = append(m[id], types.CPEConfiguration{
Vulnerable: toVulnsrcCPEtoVulsCPE(v),
RunningOn: rs,
})
}
}
}
m2 := map[string]types.CPEConfigurations{}
for id, cs := range m {
for _, c := range cs {
pvp, err := toVulsCPEConfigurationName(c.Vulnerable.CPEVersion, c.Vulnerable.CPE)
if err != nil {
return nil, errors.Wrap(err, "to vuls cpe configuration name")
}
base, ok := m2[pvp]
if !ok {
base = types.CPEConfigurations{
ID: src.ID,
Configuration: map[string][]types.CPEConfiguration{},
}
}
base.Configuration[id] = append(base.Configuration[id], c)
m2[pvp] = base
}
}
return m2, nil
}
func toVulsCPEConfigurationName(version string, cpe string) (string, error) {
var (
wfn common.WellFormedName
err error
)
switch version {
case "2.3":
wfn, err = naming.UnbindFS(cpe)
default:
wfn, err = naming.UnbindURI(cpe)
}
if err != nil {
return "", errors.Wrapf(err, "unbind %s", cpe)
}
return fmt.Sprintf("%s:%s:%s", wfn.GetString(common.AttributePart), wfn.GetString(common.AttributeVendor), wfn.GetString(common.AttributeProduct)), nil
}
func toVulnsrcCPEtoVulsCPE(s CPE) types.CPE {
d := types.CPE{
CPEVersion: s.CPEVersion,
CPE: s.CPE,
}
for _, v := range s.Version {
d.Version = append(d.Version, types.Version{
Operator: v.Operator,
Version: v.Version,
})
}
return d
}
func ToVulsRepositoryToCPE(src RepositoryToCPE) types.RepositoryToCPE {
return types.RepositoryToCPE(src)
}
func ToVulsSupercedences(src []Supercedence) types.Supercedence {
ss := types.Supercedence{}
for _, s := range src {
ss[s.KBID] = s.Supersededby.KBIDs
}
return ss
}

37
pkg/cmd/db/db.go Normal file
View File

@@ -0,0 +1,37 @@
package db
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
dbCreateCmd "github.com/future-architect/vuls/pkg/cmd/db/create"
dbEditCmd "github.com/future-architect/vuls/pkg/cmd/db/edit"
dbFetchCmd "github.com/future-architect/vuls/pkg/cmd/db/fetch"
dbSearchCmd "github.com/future-architect/vuls/pkg/cmd/db/search"
dbUploadCmd "github.com/future-architect/vuls/pkg/cmd/db/upload"
)
func NewCmdDB() *cobra.Command {
cmd := &cobra.Command{
Use: "db <subcommand>",
Short: "Vuls DB Operation",
Example: heredoc.Doc(`
$ vuls db create https://github.com/vulsio/vuls-data.git
$ vuls db create /home/MaineK00n/.cache/vuls
$ vuls db edit ubuntu 22.04 openssl
$ vuls db edit vulnerability CVE-2022-3602
$ vuls db fetch
$ vuls db fetch ghcr.io/vuls/db
$ vuls db search ubuntu 22.04 openssl
$ vuls db search vulnerability CVE-2022-3602
`),
}
cmd.AddCommand(dbCreateCmd.NewCmdCreate())
cmd.AddCommand(dbEditCmd.NewCmdEdit())
cmd.AddCommand(dbFetchCmd.NewCmdFetch())
cmd.AddCommand(dbSearchCmd.NewCmdSearch())
cmd.AddCommand(dbUploadCmd.NewCmdUpload())
return cmd
}

23
pkg/cmd/db/edit/edit.go Normal file
View File

@@ -0,0 +1,23 @@
package edit
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
func NewCmdEdit() *cobra.Command {
cmd := &cobra.Command{
Use: "edit",
Short: "Edit Data in Vuls DB",
Args: cobra.RangeArgs(2, 3),
RunE: func(_ *cobra.Command, _ []string) error {
return nil
},
Example: heredoc.Doc(`
$ vuls db edit ubuntu 22.04 openssl
$ vuls db edit vulnerability CVE-2022-3602
`),
}
return cmd
}

152
pkg/cmd/db/fetch/fetch.go Normal file
View File

@@ -0,0 +1,152 @@
package fetch
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"github.com/MakeNowJust/heredoc"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/registry/remote"
)
type DBFetchOption struct {
Path string
PlainHTTP bool
}
const (
defaultVulsDBRepository = "ghcr.io/mainek00n/vuls-data/vuls-db"
defaultTag = "latest"
vulsDBConfigMediaType = "application/vnd.vuls.vuls.db"
vulsDBLayerMediaType = "application/vnd.vuls.vuls.db.layer.v1.tar+gzip"
)
func NewCmdFetch() *cobra.Command {
opts := &DBFetchOption{
Path: "vuls.db",
}
cmd := &cobra.Command{
Use: "fetch",
Short: "Fetch Vuls DB",
Args: cobra.MaximumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
p := defaultVulsDBRepository
if len(args) > 0 {
p = args[0]
}
return fetch(context.Background(), p, opts.Path, opts.PlainHTTP)
},
Example: heredoc.Doc(`
$ vuls db fetch
$ vuls db fetch ghcr.io/vuls/db
`),
}
cmd.Flags().StringVarP(&opts.Path, "path", "p", "vuls.db", "path to fetch Vuls DB")
cmd.Flags().BoolVarP(&opts.PlainHTTP, "plain-http", "", false, "container registry is provided with plain http")
return cmd
}
func fetch(ctx context.Context, ref, dbpath string, plainHTTP bool) error {
repo, err := remote.NewRepository(ref)
if err != nil {
return errors.WithStack(err)
}
if plainHTTP {
repo.PlainHTTP = true
}
desc, err := repo.Resolve(ctx, defaultTag)
if err != nil {
return errors.WithStack(err)
}
pulledBlob, err := content.FetchAll(ctx, repo, desc)
if err != nil {
return errors.WithStack(err)
}
var manifest ocispec.Manifest
if err := json.Unmarshal(pulledBlob, &manifest); err != nil {
return errors.WithStack(err)
}
if manifest.Config.MediaType != vulsDBConfigMediaType {
return errors.New("not vuls repository")
}
for _, l := range manifest.Layers {
if l.MediaType != vulsDBLayerMediaType {
continue
}
desc, err := repo.Blobs().Resolve(ctx, l.Digest.String())
if err != nil {
return errors.WithStack(err)
}
rc, err := repo.Fetch(ctx, desc)
if err != nil {
return errors.WithStack(err)
}
defer rc.Close()
bs, err := content.ReadAll(rc, desc)
if err != nil {
return errors.WithStack(err)
}
gr, err := gzip.NewReader(bytes.NewReader(bs))
if err != nil {
return errors.WithStack(err)
}
defer gr.Close()
tr := tar.NewReader(gr)
for {
header, err := tr.Next()
if err != nil {
if err == io.EOF {
break
}
return errors.Wrap(err, "Next()")
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(filepath.Join(dbpath, header.Name), header.FileInfo().Mode()); err != nil {
return errors.WithStack(err)
}
case tar.TypeReg:
if err := func() error {
f, err := os.OpenFile(dbpath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(header.Mode))
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
if _, err := io.Copy(f, tr); err != nil {
return errors.WithStack(err)
}
return nil
}(); err != nil {
return err
}
default:
return errors.Errorf("unknown type: %s in %s", header.Typeflag, header.Name)
}
}
}
return nil
}

View File

@@ -0,0 +1,23 @@
package search
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
func NewCmdSearch() *cobra.Command {
cmd := &cobra.Command{
Use: "search",
Short: "Search Vulnerabilty/Package in Vuls DB",
Args: cobra.RangeArgs(2, 3),
RunE: func(_ *cobra.Command, _ []string) error {
return nil
},
Example: heredoc.Doc(`
$ vuls db search ubuntu 22.04 openssl
$ vuls db search vulnerability CVE-2022-3602
`),
}
return cmd
}

View File

@@ -0,0 +1,23 @@
package upload
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
func NewCmdUpload() *cobra.Command {
cmd := &cobra.Command{
Use: "upload",
Short: "Upload Vuls DB",
Args: cobra.MaximumNArgs(1),
RunE: func(_ *cobra.Command, _ []string) error {
return nil
},
Example: heredoc.Doc(`
$ vuls db upload
$ vuls db upload ghcr.io/vuls/db
`),
}
return cmd
}

183
pkg/cmd/detect/detect.go Normal file
View File

@@ -0,0 +1,183 @@
package detect
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"github.com/future-architect/vuls/pkg/config"
"github.com/future-architect/vuls/pkg/detect"
"github.com/future-architect/vuls/pkg/log"
"github.com/future-architect/vuls/pkg/types"
)
type DetectOptions struct {
Config string
}
func NewCmdDetect() *cobra.Command {
opts := &DetectOptions{
Config: "config.json",
}
cmd := &cobra.Command{
Use: "detect ([\"host\"])",
Short: "Vuls detect vulnerabilities",
RunE: func(_ *cobra.Command, args []string) error {
if err := exec(context.Background(), opts.Config, args); err != nil {
return errors.Wrap(err, "failed to detect")
}
return nil
},
Example: heredoc.Doc(`
$ vuls detect
$ vuls detect results/**/host.json
`),
}
cmd.Flags().StringVarP(&opts.Config, "config", "c", "config.json", "vuls config file path")
return cmd
}
func exec(ctx context.Context, path string, args []string) error {
logger, err := zap.NewProduction()
if err != nil {
return errors.Wrap(err, "create logger")
}
ctx = log.ContextWithLogger(ctx, logger)
c, err := config.Open(path)
if err != nil {
return errors.Wrapf(err, "open %s as config", path)
}
if len(args) == 0 {
pwd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "get working direcotry")
}
fs, err := os.ReadDir(filepath.Join(pwd, "results"))
if err != nil {
return errors.Wrapf(err, "read %s", filepath.Join(pwd, "results"))
}
var ds []time.Time
for _, f := range fs {
if !f.IsDir() {
continue
}
t, err := time.Parse("2006-01-02T150405-0700", f.Name())
if err != nil {
continue
}
ds = append(ds, t)
}
if len(ds) == 0 {
return errors.Wrapf(err, "result dir not found")
}
slices.SortFunc(ds, func(e1, e2 time.Time) bool {
return e1.After(e2)
})
args = append(args, filepath.Join(pwd, "results", ds[0].Format("2006-01-02T150405-0700")))
}
type result struct {
error string
nCVEs int
}
detectCVEs := map[string]result{}
for _, arg := range args {
if err := filepath.WalkDir(arg, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
f, err := os.OpenFile(p, os.O_RDWR, 0644)
if err != nil {
return errors.Wrapf(err, "open %s", p)
}
defer f.Close()
var host types.Host
if err := json.NewDecoder(f).Decode(&host); err != nil {
return errors.Wrapf(err, "decode %s", p)
}
hc, ok := c.Hosts[host.Name]
if !ok {
return errors.Wrapf(err, "not found %s in %s", host.Name, path)
}
host.Config.Detect = &hc.Detect
host.ScannedCves = nil
host.DetectError = ""
if err := detect.Detect(ctx, &host); err != nil {
host.DetectError = err.Error()
}
name := host.Name
if host.Family != "" && host.Release != "" {
name = fmt.Sprintf("%s (%s %s)", host.Name, host.Family, host.Release)
}
errstr := host.DetectError
if host.ScanError != "" {
errstr = fmt.Sprintf("scan error: %s", host.ScanError)
}
detectCVEs[name] = result{
error: errstr,
nCVEs: len(host.ScannedCves),
}
if err := f.Truncate(0); err != nil {
return errors.Wrap(err, "truncate file")
}
if _, err := f.Seek(0, 0); err != nil {
return errors.Wrap(err, "set offset")
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(host); err != nil {
return errors.Wrap(err, "encode json")
}
return nil
}); err != nil {
return errors.Wrapf(err, "walk %s", arg)
}
}
fmt.Println("Detect Summary")
fmt.Println("==============")
for name, r := range detectCVEs {
if r.error != "" {
fmt.Printf("%s : error msg: %s\n", name, r.error)
continue
}
fmt.Printf("%s : success %d CVEs detected\n", name, r.nCVEs)
}
return nil
}

245
pkg/cmd/report/report.go Normal file
View File

@@ -0,0 +1,245 @@
package report
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"github.com/future-architect/vuls/pkg/log"
"github.com/future-architect/vuls/pkg/types"
)
type ReportOptions struct {
Format string
}
func NewCmdReport() *cobra.Command {
opts := &ReportOptions{
Format: "oneline",
}
cmd := &cobra.Command{
Use: "report (<result path>)",
Short: "Vuls report vulnerabilities",
RunE: func(_ *cobra.Command, args []string) error {
if err := exec(context.Background(), opts.Format, args); err != nil {
return errors.Wrap(err, "failed to report")
}
return nil
},
Example: heredoc.Doc(`
$ vuls report
$ vuls report results
$ vuls report resutls/2022-11-05T01:08:44+09:00/local/localhost.json
`),
}
cmd.Flags().StringVarP(&opts.Format, "format", "f", "oneline", "stdout format")
return cmd
}
type affectedPackage struct {
name string
status string
source string
}
type result struct {
cveid string
cvssVector string
cvssScore *float64
epss *float64
kev bool
packages []affectedPackage
}
func exec(ctx context.Context, format string, args []string) error {
logger, err := zap.NewProduction()
if err != nil {
return errors.Wrap(err, "create logger")
}
ctx = log.ContextWithLogger(ctx, logger)
if len(args) == 0 {
pwd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "get working direcotry")
}
fs, err := os.ReadDir(filepath.Join(pwd, "results"))
if err != nil {
return errors.Wrapf(err, "read %s", filepath.Join(pwd, "results"))
}
var ds []time.Time
for _, f := range fs {
if !f.IsDir() {
continue
}
t, err := time.Parse("2006-01-02T150405-0700", f.Name())
if err != nil {
continue
}
ds = append(ds, t)
}
if len(ds) == 0 {
return errors.Wrapf(err, "result dir not found")
}
slices.SortFunc(ds, func(e1, e2 time.Time) bool {
return e1.After(e2)
})
args = append(args, filepath.Join(pwd, "results", ds[0].Format("2006-01-02T150405-0700")))
}
rs := map[string][]result{}
for _, arg := range args {
if err := filepath.WalkDir(arg, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
f, err := os.Open(path)
if err != nil {
return errors.Wrapf(err, "open %s", path)
}
defer f.Close()
var host types.Host
if err := json.NewDecoder(f).Decode(&host); err != nil {
return errors.Wrapf(err, "decode %s", path)
}
name := host.Name
if host.Family != "" && host.Release != "" {
name = fmt.Sprintf("%s (%s %s)", host.Name, host.Family, host.Release)
}
for id, vinfo := range host.ScannedCves {
r := result{
cveid: id,
}
if officialCont, ok := vinfo.Content["official"]; ok {
for _, c := range officialCont.CVSS {
if c.Source != "nvd" || strings.HasPrefix(c.Version, "3") {
continue
}
r.cvssVector = c.Vector
r.cvssScore = c.Score
}
if officialCont.EPSS != nil {
r.epss = officialCont.EPSS.EPSS
}
r.kev = officialCont.KEV
}
for _, p := range vinfo.AffectedPackages {
r.packages = append(r.packages, affectedPackage{
name: p.Name,
status: p.Status,
source: p.Source,
})
}
rs[name] = append(rs[name], r)
}
return nil
}); err != nil {
return errors.Wrapf(err, "walk %s", arg)
}
}
switch format {
case "oneline":
formatOneline(rs)
case "list":
formatList(rs)
default:
return errors.Errorf("%s is not implemented format", format)
}
return nil
}
func formatOneline(rs map[string][]result) {
for name, lines := range rs {
fmt.Println(name)
fmt.Println(strings.Repeat("=", len(name)))
status := map[string]int{}
for _, l := range lines {
for _, p := range l.packages {
s := p.status
if p.status == "" {
s = "(none)"
}
status[s]++
}
}
var ss []string
for s, num := range status {
ss = append(ss, fmt.Sprintf("%s: %d", s, num))
}
fmt.Printf("%d CVEs detected. package status: %s\n\n", len(lines), strings.Join(ss, ", "))
}
}
func formatList(rs map[string][]result) {
for name, lines := range rs {
slices.SortFunc(lines, func(l1, l2 result) bool {
s1, s2 := 0.0, 0.0
if l1.cvssScore != nil {
s1 = *l1.cvssScore
}
if l2.cvssScore != nil {
s2 = *l2.cvssScore
}
return s1 > s2
})
fmt.Println(name)
fmt.Println(strings.Repeat("=", len(name)))
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"CVEID", "Vector", "CVSS", "EPSS", "KEV", "Package", "Status", "Source"})
table.SetAutoMergeCells(true)
table.SetRowLine(true)
for _, l := range lines {
for _, p := range l.packages {
var score string
if l.cvssScore != nil {
score = fmt.Sprintf("%.1f", *l.cvssScore)
}
var epss string
if l.epss != nil {
epss = fmt.Sprintf("%f", *l.epss)
}
source, _, _ := strings.Cut(p.source, ":")
table.Append([]string{l.cveid, l.cvssVector, score, epss, fmt.Sprintf("%v", l.kev), p.name, p.status, source})
}
}
table.Render()
fmt.Println()
}
}

44
pkg/cmd/root/root.go Normal file
View File

@@ -0,0 +1,44 @@
package root
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
configCmd "github.com/future-architect/vuls/pkg/cmd/config"
dbCmd "github.com/future-architect/vuls/pkg/cmd/db"
detectCmd "github.com/future-architect/vuls/pkg/cmd/detect"
reportCmd "github.com/future-architect/vuls/pkg/cmd/report"
scanCmd "github.com/future-architect/vuls/pkg/cmd/scan"
serverCmd "github.com/future-architect/vuls/pkg/cmd/server"
tuiCmd "github.com/future-architect/vuls/pkg/cmd/tui"
versionCmd "github.com/future-architect/vuls/pkg/cmd/version"
)
func NewCmdRoot() *cobra.Command {
cmd := &cobra.Command{
Use: "vuls <command>",
Short: "Vuls",
Long: "Vulnerability Scanner: Vuls",
SilenceErrors: true,
SilenceUsage: true,
Example: heredoc.Doc(`
$ vuls config init
$ vuls db fetch
$ vuls scan
$ vuls detect
$ vuls report
$ vuls tui
`),
}
cmd.AddCommand(configCmd.NewCmdConfig())
cmd.AddCommand(dbCmd.NewCmdDB())
cmd.AddCommand(detectCmd.NewCmdDetect())
cmd.AddCommand(reportCmd.NewCmdReport())
cmd.AddCommand(scanCmd.NewCmdScan())
cmd.AddCommand(serverCmd.NewCmdServer())
cmd.AddCommand(tuiCmd.NewCmdTUI())
cmd.AddCommand(versionCmd.NewCmdVersion())
return cmd
}

136
pkg/cmd/scan/scan.go Normal file
View File

@@ -0,0 +1,136 @@
package scan
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.uber.org/zap"
"golang.org/x/exp/maps"
"github.com/future-architect/vuls/pkg/config"
"github.com/future-architect/vuls/pkg/log"
"github.com/future-architect/vuls/pkg/scan"
"github.com/future-architect/vuls/pkg/types"
)
type ScanOptions struct {
Config string
}
func NewCmdScan() *cobra.Command {
opts := &ScanOptions{
Config: "config.json",
}
cmd := &cobra.Command{
Use: "scan ([\"host\"])",
Short: "Vuls scan your machine information",
RunE: func(_ *cobra.Command, args []string) error {
if err := exec(context.Background(), opts.Config, args); err != nil {
return errors.Wrap(err, "failed to scan")
}
return nil
},
Example: heredoc.Doc(`
$ vuls scan
$ vuls scan host
`),
}
cmd.Flags().StringVarP(&opts.Config, "config", "c", "config.json", "vuls config file path")
return cmd
}
func exec(ctx context.Context, path string, args []string) error {
logger, err := zap.NewProduction()
if err != nil {
return errors.Wrap(err, "create logger")
}
ctx = log.ContextWithLogger(ctx, logger)
c, err := config.Open(path)
if err != nil {
return errors.Wrapf(err, "open %s as config", path)
}
hosts := []types.Host{}
targets := args
if len(args) == 0 {
targets = maps.Keys(c.Hosts)
}
for _, t := range targets {
h, ok := c.Hosts[t]
if !ok {
return errors.Errorf("host %s is not defined in config %s", t, path)
}
hosts = append(hosts, types.Host{
Name: t,
Config: types.Config{
Type: h.Type,
Host: h.Host,
Port: h.Port,
User: h.User,
SSHConfig: h.SSHConfig,
SSHKey: h.SSHKey,
Scan: &h.Scan,
},
})
}
for i := range hosts {
if err := scan.Scan(ctx, &hosts[i]); err != nil {
hosts[i].ScanError = err.Error()
}
}
now := time.Now()
for _, h := range hosts {
if err := func() error {
resultDir := filepath.Join(h.Config.Scan.ResultDir, now.Format("2006-01-02T150405-0700"))
if err := os.MkdirAll(resultDir, os.ModePerm); err != nil {
return errors.Wrapf(err, "mkdir %s", resultDir)
}
f, err := os.Create(filepath.Join(resultDir, fmt.Sprintf("%s.json", h.Name)))
if err != nil {
return errors.Wrapf(err, "create %s", filepath.Join(resultDir, fmt.Sprintf("%s.json", h.Name)))
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(h); err != nil {
return errors.Wrapf(err, "encode %s result", h.Name)
}
return nil
}(); err != nil {
return errors.Wrapf(err, "write %s result", h.Name)
}
}
fmt.Println("Scan Summary")
fmt.Println("============")
for _, h := range hosts {
name := h.Name
if h.Family != "" && h.Release != "" {
name = fmt.Sprintf("%s (%s %s)", h.Name, h.Family, h.Release)
}
if h.ScanError != "" {
fmt.Printf("%s : error msg: %s\n", name, h.ScanError)
continue
}
fmt.Printf("%s: success ospkg: %d, cpe: %d, KB %d installed\n", name, len(h.Packages.OSPkg), len(h.Packages.CPE), len(h.Packages.KB))
}
return nil
}

78
pkg/cmd/server/server.go Normal file
View File

@@ -0,0 +1,78 @@
package server
import (
"context"
"os"
"path/filepath"
"github.com/MakeNowJust/heredoc"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.uber.org/zap"
"github.com/future-architect/vuls/pkg/config"
"github.com/future-architect/vuls/pkg/log"
"github.com/future-architect/vuls/pkg/server"
)
type Serveroptions struct {
Config string
}
func NewCmdServer() *cobra.Command {
opts := &Serveroptions{
Config: "config.json",
}
cmd := &cobra.Command{
Use: "server",
Short: "Vuls start server mode",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
if err := exec(context.Background(), opts.Config); err != nil {
return errors.Wrap(err, "failed to server")
}
return nil
},
Example: heredoc.Doc(`
$ vuls server
`),
}
cmd.Flags().StringVarP(&opts.Config, "config", "c", "config.json", "vuls config file path")
return cmd
}
func exec(ctx context.Context, path string) error {
logger, err := zap.NewProduction()
if err != nil {
return errors.Wrap(err, "create logger")
}
ctx = log.ContextWithLogger(ctx, logger)
c, err := config.Open(path)
if err != nil {
return errors.Wrapf(err, "open %s as config", path)
}
if c.Server == nil {
pwd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "get working directory")
}
c.Server = &config.Server{
Listen: "127.0.0.1:5515",
Path: filepath.Join(pwd, "vuls.db"),
}
}
e := echo.New()
e.POST("/scan", server.Scan())
e.POST("/detect", server.Detect(c.Server.Path))
return e.Start(c.Server.Listen)
}

33
pkg/cmd/tui/tui.go Normal file
View File

@@ -0,0 +1,33 @@
package tui
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
type TUIOptions struct {
Config string
}
func NewCmdTUI() *cobra.Command {
opts := &TUIOptions{
Config: "config.json",
}
cmd := &cobra.Command{
Use: "tui (<result path>)",
Short: "View vulnerabilities detected by TUI",
RunE: func(_ *cobra.Command, _ []string) error {
return nil
},
Example: heredoc.Doc(`
$ vuls tui
$ vuls tui results
$ vuls tui resutls/2022-11-05T01:08:44+09:00/local/localhost.json
`),
}
cmd.Flags().StringVarP(&opts.Config, "config", "c", "config.json", "vuls config file path")
return cmd
}

View File

@@ -0,0 +1,25 @@
package version
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
Version string
Revision string
)
func NewCmdVersion() *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Print the version",
Args: cobra.NoArgs,
Run: func(_ *cobra.Command, _ []string) {
fmt.Fprintf(os.Stdout, "vuls %s %s\n", Version, Revision)
},
}
return cmd
}

59
pkg/config/config.go Normal file
View File

@@ -0,0 +1,59 @@
package config
import (
"encoding/json"
"os"
"os/user"
"path/filepath"
"github.com/pkg/errors"
)
func Open(path string) (Config, error) {
f, err := os.Open(path)
if err != nil {
return Config{}, errors.Wrapf(err, "open %s", path)
}
defer f.Close()
var src Config
if err := json.NewDecoder(f).Decode(&src); err != nil {
return Config{}, errors.Wrap(err, "decode json")
}
u, err := user.Current()
if err != nil {
return Config{}, errors.Wrap(err, "get current user")
}
pwd, err := os.Getwd()
if err != nil {
return Config{}, errors.Wrap(err, "get working directory")
}
config := Config{Server: src.Server, Hosts: map[string]Host{}}
for n, h := range src.Hosts {
c := Host{
Type: h.Type,
Host: h.Host,
Port: h.Port,
User: h.User,
SSHConfig: h.SSHConfig,
SSHKey: h.SSHKey,
Scan: h.Scan,
Detect: h.Detect,
}
if c.User == nil {
c.User = &u.Name
}
if c.Scan.ResultDir == "" {
c.Scan.ResultDir = filepath.Join(pwd, "results")
}
if c.Detect.ResultDir == "" {
c.Detect.ResultDir = filepath.Join(pwd, "results")
}
config.Hosts[n] = c
}
return config, nil
}

41
pkg/config/types.go Normal file
View File

@@ -0,0 +1,41 @@
package config
type Config struct {
Server *Server `json:"server"`
Hosts map[string]Host `json:"hosts"`
}
type Scan struct {
OSPkg *scanOSPkg `json:"ospkg,omitempty"`
CPE []scanCPE `json:"cpe,omitempty"`
ResultDir string `json:"result_dir,omitempty"`
}
type scanOSPkg struct {
Root bool `json:"root"`
}
type scanCPE struct {
CPE string `json:"cpe,omitempty"`
RunningOn string `json:"running_on,omitempty"`
}
type Detect struct {
Path string `json:"path"`
ResultDir string `json:"result_dir"`
}
type Server struct {
Listen string `json:"listen"`
Path string `json:"path"`
}
type Host struct {
Type string `json:"type"`
Host *string `json:"host"`
Port *string `json:"port"`
User *string `json:"user"`
SSHConfig *string `json:"ssh_config"`
SSHKey *string `json:"ssh_key"`
Scan Scan `json:"scan"`
Detect Detect `json:"detect"`
}

490
pkg/db/boltdb/boltdb.go Normal file
View File

@@ -0,0 +1,490 @@
package boltdb
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
bolt "go.etcd.io/bbolt"
"golang.org/x/exp/maps"
"github.com/future-architect/vuls/pkg/db/types"
)
type options struct {
}
type Option interface {
apply(*options)
}
type DB struct {
conn *bolt.DB
}
func Open(dbPath string, debug bool, opts ...Option) (*DB, error) {
db, err := bolt.Open(dbPath, 0666, nil)
if err != nil {
return nil, errors.Wrap(err, "open boltdb")
}
return &DB{conn: db}, nil
}
func (db *DB) Close() error {
if db.conn == nil {
return nil
}
if err := db.conn.Close(); err != nil {
return errors.Wrap(err, "close boltdb")
}
return nil
}
func (db *DB) PutVulnerability(src, key string, value types.Vulnerability) error {
bucket, id, found := strings.Cut(key, ":")
if !found {
return errors.Errorf(`unexpected key. accepts: "vulnerability:<Vulnerability ID>, received: "%s"`, key)
}
if err := db.conn.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(bucket))
if err != nil {
return errors.Wrapf(err, "create %s bucket", bucket)
}
vb, err := b.CreateBucketIfNotExists([]byte(id))
if err != nil {
return errors.Wrapf(err, "create %s/%s bucket", bucket, id)
}
bs, err := json.MarshalIndent(value, "", " ")
if err != nil {
return errors.Wrap(err, "marshal json")
}
if err := vb.Put([]byte(src), bs); err != nil {
return errors.Wrapf(err, "put %%s/%s/%s", bucket, id, src)
}
return nil
}); err != nil {
return errors.Wrap(err, "update db")
}
return nil
}
func (db *DB) PutPackage(src, key string, value map[string]types.Packages) error {
if err := db.conn.Update(func(tx *bolt.Tx) error {
name, version, found := strings.Cut(key, ":")
if !found && name == "" {
return errors.Errorf(`unexpected key. accepts: "<osname>(:<version>)", received: "%s"`, key)
}
bucket := name
b, err := tx.CreateBucketIfNotExists([]byte(name))
if err != nil {
return errors.Wrapf(err, "create %s bucket", name)
}
switch name {
case "arch", "freebsd", "gentoo":
case "redhat":
if version == "" {
return errors.Errorf(`unexpected key. accepts: "<osname>:<version>", received: "%s"`, key)
}
b, err = b.CreateBucketIfNotExists([]byte(version[:1]))
if err != nil {
return errors.Wrapf(err, "create %s/%s bucket", name, version[:1])
}
b, err = b.CreateBucketIfNotExists([]byte(version))
if err != nil {
return errors.Wrapf(err, "create %s/%s/%s bucket", name, version[:1], version)
}
bucket = fmt.Sprintf("%s/%s/%s", name, version[:1], version)
default:
if version == "" {
return errors.Errorf(`unexpected key. accepts: "<osname>:<version>", received: "%s"`, key)
}
b, err = b.CreateBucketIfNotExists([]byte(version))
if err != nil {
return errors.Wrapf(err, "crate %s/%s bucket", name, version)
}
bucket = fmt.Sprintf("%s/%s", name, version)
}
for n, v := range value {
pb, err := b.CreateBucketIfNotExists([]byte(n))
if err != nil {
return errors.Wrapf(err, "create %s/%s bucket", bucket, n)
}
vb, err := pb.CreateBucketIfNotExists([]byte(v.ID))
if err != nil {
return errors.Wrapf(err, "create %s/%s/%s bucket", bucket, n, v.ID)
}
var p map[string]types.Package
bs := vb.Get([]byte(src))
if len(bs) > 0 {
if err := json.Unmarshal(bs, &p); err != nil {
return errors.Wrap(err, "unmarshal json")
}
} else {
p = map[string]types.Package{}
}
maps.Copy(p, v.Package)
bs, err = json.MarshalIndent(p, "", " ")
if err != nil {
return errors.Wrap(err, "marshal json")
}
if err := vb.Put([]byte(src), bs); err != nil {
return errors.Wrapf(err, "put %s", key)
}
}
if name == "windows" {
kbToProduct := map[string][]string{}
for n, ps := range value {
for _, p := range ps.Package {
for _, v := range p.Version {
if _, err := strconv.Atoi(v[0].Version); err != nil {
continue
}
kbToProduct[v[0].Version] = append(kbToProduct[v[0].Version], n)
}
}
}
b, err := tx.CreateBucketIfNotExists([]byte("windows_kb_to_product"))
if err != nil {
return errors.Wrap(err, "create windows_kb_to_product bucket")
}
if version == "" {
return errors.Errorf(`unexpected key. accepts: "<osname>:<version>", received: "%s"`, key)
}
b, err = b.CreateBucketIfNotExists([]byte(version))
if err != nil {
return errors.Wrapf(err, "create %s/%s bucket", name, version)
}
for kb, ps := range kbToProduct {
bs, err := json.Marshal(ps)
if err != nil {
return errors.Wrap(err, "marshal json")
}
b.Put([]byte(kb), bs)
}
}
return nil
}); err != nil {
return errors.Wrap(err, "update db")
}
return nil
}
func (db *DB) PutCPEConfiguration(src, key string, value map[string]types.CPEConfigurations) error {
if err := db.conn.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(key))
if err != nil {
return errors.Wrapf(err, "create %s bucket", key)
}
for pvp, c := range value {
pvpb, err := b.CreateBucketIfNotExists([]byte(pvp))
if err != nil {
return errors.Wrapf(err, "create %s/%s bucket", key, pvp)
}
vb, err := pvpb.CreateBucketIfNotExists([]byte(c.ID))
if err != nil {
return errors.Wrapf(err, "create %s/%s/%s bucket", key, pvp, c.ID)
}
var v map[string][]types.CPEConfiguration
bs := vb.Get([]byte(src))
if len(bs) > 0 {
if err := json.Unmarshal(bs, &v); err != nil {
return errors.Wrap(err, "unmarshal json")
}
} else {
v = map[string][]types.CPEConfiguration{}
}
maps.Copy(v, c.Configuration)
bs, err = json.MarshalIndent(v, "", " ")
if err != nil {
return errors.Wrap(err, "marshal json")
}
if err := vb.Put([]byte(src), bs); err != nil {
return errors.Wrapf(err, "put %s", key)
}
}
return nil
}); err != nil {
return errors.Wrap(err, "update db")
}
return nil
}
func (db *DB) PutRedHatRepoToCPE(src, key string, value types.RepositoryToCPE) error {
if err := db.conn.Update(func(tx *bolt.Tx) error {
name, version, found := strings.Cut(key, ":")
if !found && name == "" {
return errors.Errorf(`unexpected key. accepts: "redhat_cpe:<version>", received: "%s"`, key)
}
b, err := tx.CreateBucketIfNotExists([]byte(name))
if err != nil {
return errors.Wrapf(err, "create %s bucket", name)
}
b, err = b.CreateBucketIfNotExists([]byte(version[:1]))
if err != nil {
return errors.Wrapf(err, "create %s/%s bucket", name, version[:1])
}
b, err = b.CreateBucketIfNotExists([]byte(version))
if err != nil {
return errors.Wrapf(err, "create %s/%s/%s bucket", name, version[:1], version)
}
for repo, cpes := range value {
rb, err := b.CreateBucketIfNotExists([]byte(repo))
if err != nil {
return errors.Wrapf(err, "create %s/%s/%s/%s bucket", name, version[:1], version, repo)
}
bs, err := json.MarshalIndent(cpes, "", " ")
if err != nil {
return errors.Wrap(err, "marshal json")
}
if err := rb.Put([]byte(src), bs); err != nil {
return errors.Wrapf(err, "put %s", key)
}
}
return nil
}); err != nil {
return errors.Wrap(err, "update db")
}
return nil
}
func (db *DB) PutWindowsSupercedence(src, key string, value types.Supercedence) error {
if err := db.conn.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(key))
if err != nil {
return errors.Wrapf(err, "create %s bucket", key)
}
for kb, supercedences := range value {
kbb, err := b.CreateBucketIfNotExists([]byte(kb))
if err != nil {
return errors.Wrapf(err, "create %s/%s bucket", key, kb)
}
bs, err := json.Marshal(supercedences)
if err != nil {
return errors.Wrap(err, "marshal json")
}
if err := kbb.Put([]byte(src), bs); err != nil {
return errors.Wrapf(err, "put %s", key)
}
}
return nil
}); err != nil {
return errors.Wrap(err, "update db")
}
return nil
}
func (db *DB) GetVulnerability(ids []string) (map[string]map[string]types.Vulnerability, error) {
r := map[string]map[string]types.Vulnerability{}
if err := db.conn.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("vulnerability"))
if b == nil {
return nil
}
for _, id := range ids {
vb := b.Bucket([]byte(id))
if vb == nil {
return nil
}
r[string(id)] = map[string]types.Vulnerability{}
if err := vb.ForEach(func(src, bs []byte) error {
var v types.Vulnerability
if err := json.Unmarshal(bs, &v); err != nil {
return errors.Wrapf(err, "decode %s/%s", string(id), string(src))
}
r[string(id)][string(src)] = v
return nil
}); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
return r, nil
}
func (db *DB) GetPackage(family, release string, name string) (map[string]map[string]map[string]types.Package, error) {
r := map[string]map[string]map[string]types.Package{}
if err := db.conn.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(family))
if b == nil {
return nil
}
switch family {
case "debian", "ubuntu", "windows":
b = b.Bucket([]byte(release))
if b == nil {
return nil
}
b = b.Bucket([]byte(name))
if b == nil {
return nil
}
if err := b.ForEach(func(cveid, _ []byte) error {
vb := b.Bucket(cveid)
r[string(cveid)] = map[string]map[string]types.Package{}
if err := vb.ForEach(func(src, bs []byte) error {
var v map[string]types.Package
if err := json.Unmarshal(bs, &v); err != nil {
return errors.Wrapf(err, "decode %s/%s", string(cveid), string(src))
}
r[string(cveid)][string(src)] = v
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return err
}
default:
return errors.New("not implemented")
}
return nil
}); err != nil {
return nil, err
}
return r, nil
}
func (db *DB) GetCPEConfiguration(partvendorproduct string) (map[string]map[string]map[string][]types.CPEConfiguration, error) {
r := map[string]map[string]map[string][]types.CPEConfiguration{}
if err := db.conn.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("cpe"))
if b == nil {
return nil
}
b = b.Bucket([]byte(partvendorproduct))
if b == nil {
return nil
}
if err := b.ForEach(func(cveid, _ []byte) error {
vb := b.Bucket(cveid)
r[string(cveid)] = map[string]map[string][]types.CPEConfiguration{}
if err := vb.ForEach(func(src, bs []byte) error {
var v map[string][]types.CPEConfiguration
if err := json.Unmarshal(bs, &v); err != nil {
return errors.Wrapf(err, "decode cpe/%s/%s/%s", partvendorproduct, string(cveid), string(src))
}
r[string(cveid)][string(src)] = v
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
return r, nil
}
func (db *DB) GetSupercedence(kbs []string) (map[string][]string, error) {
r := map[string][]string{}
if err := db.conn.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("windows_supercedence"))
if b == nil {
return nil
}
for _, kb := range kbs {
kbb := b.Bucket([]byte(kb))
if kbb == nil {
continue
}
if err := kbb.ForEach(func(_, v []byte) error {
var ss []string
if err := json.Unmarshal(v, &ss); err != nil {
return errors.Wrapf(err, "decode windows_supercedence/%s", kb)
}
r[kb] = append(r[kb], ss...)
return nil
}); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
return r, nil
}
func (db *DB) GetKBtoProduct(release string, kbs []string) ([]string, error) {
var r []string
if err := db.conn.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("windows_kb_to_product"))
if b == nil {
return nil
}
b = b.Bucket([]byte(release))
if b == nil {
return nil
}
for _, kb := range kbs {
if bs := b.Get([]byte(kb)); len(bs) > 0 {
var ps []string
if err := json.Unmarshal(bs, &ps); err != nil {
return errors.Wrapf(err, "decode windows_kb_to_product/%s/%s", release, kb)
}
r = append(r, ps...)
}
}
return nil
}); err != nil {
return nil, err
}
return r, nil
}

149
pkg/db/db.go Normal file
View File

@@ -0,0 +1,149 @@
package db
import (
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/db/boltdb"
"github.com/future-architect/vuls/pkg/db/rdb"
"github.com/future-architect/vuls/pkg/db/redis"
"github.com/future-architect/vuls/pkg/db/types"
)
type options struct {
}
type Option interface {
apply(*options)
}
type DB struct {
name string
driver Driver
}
type Driver interface {
Close() error
PutVulnerability(string, string, types.Vulnerability) error
PutPackage(string, string, map[string]types.Packages) error
PutCPEConfiguration(string, string, map[string]types.CPEConfigurations) error
PutRedHatRepoToCPE(string, string, types.RepositoryToCPE) error
PutWindowsSupercedence(string, string, types.Supercedence) error
GetVulnerability([]string) (map[string]map[string]types.Vulnerability, error)
GetPackage(string, string, string) (map[string]map[string]map[string]types.Package, error)
GetCPEConfiguration(string) (map[string]map[string]map[string][]types.CPEConfiguration, error)
GetSupercedence([]string) (map[string][]string, error)
GetKBtoProduct(string, []string) ([]string, error)
}
func (db *DB) Name() string {
return db.name
}
func Open(dbType, dbPath string, debug bool, opts ...Option) (*DB, error) {
switch dbType {
case "boltdb":
d, err := boltdb.Open(dbPath, debug)
if err != nil {
return nil, errors.Wrap(err, "open boltdb")
}
return &DB{name: dbType, driver: d}, nil
case "sqlite3", "mysql", "postgres":
d, err := rdb.Open(dbType, dbPath, debug)
if err != nil {
return nil, errors.Wrap(err, "open rdb")
}
return &DB{name: dbType, driver: d}, nil
case "redis":
d, err := redis.Open(dbPath, debug)
if err != nil {
return nil, errors.Wrap(err, "open rdb")
}
return &DB{name: dbType, driver: d}, nil
default:
return nil, errors.Errorf(`unexpected dbType. accepts: ["boltdb", "sqlite3", "mysql", "postgres", "redis"], received: "%s"`, dbType)
}
}
func (db *DB) Close() error {
if err := db.driver.Close(); err != nil {
return errors.Wrapf(err, "close %s", db.name)
}
return nil
}
func (db *DB) PutVulnerability(src, key string, value types.Vulnerability) error {
if err := db.driver.PutVulnerability(src, key, value); err != nil {
return errors.Wrapf(err, "put vulnerability")
}
return nil
}
func (db *DB) PutPackage(src, key string, value map[string]types.Packages) error {
if err := db.driver.PutPackage(src, key, value); err != nil {
return errors.Wrapf(err, "put package")
}
return nil
}
func (db *DB) PutCPEConfiguration(src, key string, value map[string]types.CPEConfigurations) error {
if err := db.driver.PutCPEConfiguration(src, key, value); err != nil {
return errors.Wrapf(err, "put cpe configuration")
}
return nil
}
func (db *DB) PutRedHatRepoToCPE(src, key string, value types.RepositoryToCPE) error {
if err := db.driver.PutRedHatRepoToCPE(src, key, value); err != nil {
return errors.Wrap(err, "put repository to cpe")
}
return nil
}
func (db *DB) PutWindowsSupercedence(src, key string, value types.Supercedence) error {
if err := db.driver.PutWindowsSupercedence(src, key, value); err != nil {
return errors.Wrap(err, "put supercedence")
}
return nil
}
func (db *DB) GetVulnerability(ids []string) (map[string]map[string]types.Vulnerability, error) {
rs, err := db.driver.GetVulnerability(ids)
if err != nil {
return nil, errors.Wrapf(err, "get vulnerability")
}
return rs, nil
}
func (db *DB) GetPackage(family, release string, name string) (map[string]map[string]map[string]types.Package, error) {
rs, err := db.driver.GetPackage(family, release, name)
if err != nil {
return nil, errors.Wrapf(err, "get package")
}
return rs, nil
}
func (db *DB) GetCPEConfiguration(partvendorproduct string) (map[string]map[string]map[string][]types.CPEConfiguration, error) {
rs, err := db.driver.GetCPEConfiguration(partvendorproduct)
if err != nil {
return nil, errors.Wrapf(err, "get cpe configuration")
}
return rs, nil
}
func (db *DB) GetSupercedence(kb []string) (map[string][]string, error) {
rs, err := db.driver.GetSupercedence(kb)
if err != nil {
return nil, errors.Wrap(err, "get supercedence")
}
return rs, nil
}
func (db *DB) GetKBtoProduct(release string, kb []string) ([]string, error) {
rs, err := db.driver.GetKBtoProduct(release, kb)
if err != nil {
return nil, errors.Wrap(err, "get product from kb")
}
return rs, nil
}

110
pkg/db/rdb/rdb.go Normal file
View File

@@ -0,0 +1,110 @@
package rdb
import (
"database/sql"
"github.com/pkg/errors"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
_ "modernc.org/sqlite"
"github.com/future-architect/vuls/pkg/db/types"
)
type options struct {
}
type Option interface {
apply(*options)
}
type DB struct {
conn *gorm.DB
}
func Open(dbType, dbPath string, debug bool, opts ...Option) (*DB, error) {
switch dbType {
case "sqlite3":
// db, err := gorm.Open(sqlite.Open(dbPath))
db := &gorm.DB{}
conn, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, errors.Wrap(err, "open sqlite3")
}
db.ConnPool = conn
return &DB{conn: db}, nil
case "mysql":
db, err := gorm.Open(mysql.Open(dbPath))
if err != nil {
return nil, errors.Wrap(err, "open mysql")
}
return &DB{conn: db}, nil
case "postgres":
db, err := gorm.Open(postgres.Open(dbPath))
if err != nil {
return nil, errors.Wrap(err, "open postgres")
}
return &DB{conn: db}, nil
default:
return nil, errors.Errorf(`unexpected dbType. accepts: ["sqlite3", "mysql", "postgres"], received: "%s"`, dbType)
}
}
func (db *DB) Close() error {
if db.conn == nil {
return nil
}
var (
sqlDB *sql.DB
err error
)
if sqlDB, err = db.conn.DB(); err != nil {
return errors.Wrap(err, "get *sql.DB")
}
if err := sqlDB.Close(); err != nil {
return errors.Wrap(err, "close *sql.DB")
}
return nil
}
func (db *DB) PutVulnerability(src, key string, value types.Vulnerability) error {
return nil
}
func (db *DB) PutPackage(src, key string, value map[string]types.Packages) error {
return nil
}
func (db *DB) PutCPEConfiguration(src, key string, value map[string]types.CPEConfigurations) error {
return nil
}
func (db *DB) PutRedHatRepoToCPE(src, key string, value types.RepositoryToCPE) error {
return nil
}
func (db *DB) PutWindowsSupercedence(src, key string, value types.Supercedence) error {
return nil
}
func (db *DB) GetVulnerability(ids []string) (map[string]map[string]types.Vulnerability, error) {
return nil, nil
}
func (db *DB) GetPackage(family, release string, name string) (map[string]map[string]map[string]types.Package, error) {
return nil, nil
}
func (db *DB) GetCPEConfiguration(partvendorproduct string) (map[string]map[string]map[string][]types.CPEConfiguration, error) {
return nil, nil
}
func (db *DB) GetSupercedence(kb []string) (map[string][]string, error) {
return nil, nil
}
func (db *DB) GetKBtoProduct(elease string, kb []string) ([]string, error) {
return nil, nil
}

77
pkg/db/redis/redis.go Normal file
View File

@@ -0,0 +1,77 @@
package redis
import (
"github.com/go-redis/redis/v9"
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/db/types"
)
type options struct {
}
type Option interface {
apply(*options)
}
type DB struct {
conn *redis.Client
}
func Open(dbPath string, debug bool, opts ...Option) (*DB, error) {
redisOpts, err := redis.ParseURL(dbPath)
if err != nil {
return nil, errors.Wrap(err, "parse redis URL")
}
return &DB{conn: redis.NewClient(redisOpts)}, nil
}
func (db *DB) Close() error {
if db.conn == nil {
return nil
}
if err := db.conn.Close(); err != nil {
return errors.Wrap(err, "close redis")
}
return nil
}
func (db *DB) PutVulnerability(src, key string, value types.Vulnerability) error {
return nil
}
func (db *DB) PutPackage(src, key string, value map[string]types.Packages) error {
return nil
}
func (db *DB) PutCPEConfiguration(src, key string, value map[string]types.CPEConfigurations) error {
return nil
}
func (db *DB) PutRedHatRepoToCPE(src, key string, value types.RepositoryToCPE) error {
return nil
}
func (db *DB) PutWindowsSupercedence(src, key string, value types.Supercedence) error {
return nil
}
func (db *DB) GetVulnerability(ids []string) (map[string]map[string]types.Vulnerability, error) {
return nil, nil
}
func (db *DB) GetPackage(family, release string, name string) (map[string]map[string]map[string]types.Package, error) {
return nil, nil
}
func (db *DB) GetCPEConfiguration(partvendorproduct string) (map[string]map[string]map[string][]types.CPEConfiguration, error) {
return nil, nil
}
func (db *DB) GetSupercedence(kb []string) (map[string][]string, error) {
return nil, nil
}
func (db *DB) GetKBtoProduct(release string, kb []string) ([]string, error) {
return nil, nil
}

85
pkg/db/types/types.go Normal file
View File

@@ -0,0 +1,85 @@
package types
import "time"
type Vulnerability struct {
ID string `json:"id,omitempty"`
Advisory []string `json:"advisory,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
CVSS []CVSS `json:"cvss,omitempty"`
EPSS *EPSS `json:"epss,omitempty"`
CWE []CWE `json:"cwe,omitempty"`
Metasploit []Metasploit `json:"metasploit,omitempty"`
Exploit []Exploit `json:"exploit,omitempty"`
KEV bool `json:"kev,omitempty"`
Published *time.Time `json:"published,omitempty"`
Modified *time.Time `json:"modified,omitempty"`
Reference []string `json:"reference,omitempty"`
}
type CVSS struct {
Source string `json:"source,omitempty"`
Version string `json:"version,omitempty"`
Vector string `json:"vector,omitempty"`
Score *float64 `json:"score,omitempty"`
Severity string `json:"severity,omitempty"`
}
type EPSS struct {
EPSS *float64 `json:"epss,omitempty"`
Percentile *float64 `json:"percentile,omitempty"`
}
type CWE struct {
Source []string `json:"source,omitempty"`
ID string `json:"id,omitempty"`
}
type Metasploit struct {
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
}
type Exploit struct {
Source []string `json:"source,omitempty"`
URL string `json:"url,omitempty"`
}
type CPEConfigurations struct {
ID string `json:"-,omitempty"`
Configuration map[string][]CPEConfiguration `json:"configuration,omitempty"`
}
type CPEConfiguration struct {
Vulnerable CPE `json:"vulnerable,omitempty"`
RunningOn []CPE `json:"running_on,omitempty"`
}
type CPE struct {
CPEVersion string `json:"cpe_version,omitempty"`
CPE string `json:"cpe,omitempty"`
Version []Version `json:"version,omitempty"`
}
type Packages struct {
ID string `json:"-,omitempty"`
Package map[string]Package `json:"package,omitempty"`
}
type Package struct {
Status string `json:"status,omitempty"`
Version [][]Version `json:"version,omitempty"`
Arch []string `json:"arch,omitempty"`
Repository string `json:"repository,omitempty"`
CPE []string `json:"cpe,omitempty"`
}
type Version struct {
Operator string `json:"operator,omitempty"`
Version string `json:"version,omitempty"`
}
type RepositoryToCPE map[string][]string
type Supercedence map[string][]string

1
pkg/db/util/util.go Normal file
View File

@@ -0,0 +1 @@
package util

179
pkg/detect/cpe/cpe.go Normal file
View File

@@ -0,0 +1,179 @@
package cpe
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/go-version"
"github.com/knqyf263/go-cpe/common"
"github.com/knqyf263/go-cpe/matching"
"github.com/knqyf263/go-cpe/naming"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"github.com/future-architect/vuls/pkg/db"
dbTypes "github.com/future-architect/vuls/pkg/db/types"
"github.com/future-architect/vuls/pkg/types"
"github.com/future-architect/vuls/pkg/util"
)
type Detector struct{}
func (d Detector) Name() string {
return "cpe detector"
}
func (d Detector) Detect(ctx context.Context, host *types.Host) error {
if host.ScannedCves == nil {
host.ScannedCves = map[string]types.VulnInfo{}
}
vulndb, err := db.Open("boltdb", host.Config.Detect.Path, false)
if err != nil {
return errors.Wrapf(err, "open %s", host.Config.Detect.Path)
}
defer vulndb.Close()
for key, cpe := range host.Packages.CPE {
installed, err := naming.UnbindFS(cpe.CPE)
if err != nil {
return errors.Wrapf(err, "unbind %s", cpe.CPE)
}
var runningOn common.WellFormedName
if cpe.RunningOn != "" {
runningOn, err = naming.UnbindFS(cpe.RunningOn)
if err != nil {
return errors.Wrapf(err, "unbind %s", cpe.RunningOn)
}
}
cpes, err := vulndb.GetCPEConfiguration(fmt.Sprintf("%s:%s:%s", installed.GetString(common.AttributePart), installed.GetString(common.AttributeVendor), installed.GetString(common.AttributeProduct)))
if err != nil {
return errors.Wrap(err, "get cpe configuration")
}
for cveid, datasrcs := range cpes {
for datasrc, orcs := range datasrcs {
for id, andcs := range orcs {
for _, c := range andcs {
affected, err := compare(installed, &runningOn, c)
if err != nil {
return errors.Wrap(err, "compare")
}
if affected {
vinfo, ok := host.ScannedCves[cveid]
if !ok {
host.ScannedCves[cveid] = types.VulnInfo{ID: cveid}
}
vinfo.AffectedPackages = append(vinfo.AffectedPackages, types.AffectedPackage{
Name: key,
Source: fmt.Sprintf("%s:%s", datasrc, id),
})
vinfo.AffectedPackages = util.Unique(vinfo.AffectedPackages)
host.ScannedCves[cveid] = vinfo
break
}
}
}
}
}
}
vulns, err := vulndb.GetVulnerability(maps.Keys(host.ScannedCves))
if err != nil {
return errors.Wrap(err, "get vulnerability")
}
for cveid, datasrcs := range vulns {
vinfo := host.ScannedCves[cveid]
vinfo.Content = map[string]dbTypes.Vulnerability{}
for src, v := range datasrcs {
vinfo.Content[src] = v
}
host.ScannedCves[cveid] = vinfo
}
return nil
}
func compare(installedCPE common.WellFormedName, installedRunningOn *common.WellFormedName, target dbTypes.CPEConfiguration) (bool, error) {
var (
wfn common.WellFormedName
err error
)
if target.Vulnerable.CPEVersion == "2.3" {
wfn, err = naming.UnbindFS(target.Vulnerable.CPE)
} else {
wfn, err = naming.UnbindURI(target.Vulnerable.CPE)
}
if err != nil {
return false, errors.Wrapf(err, "unbind %s", target.Vulnerable.CPE)
}
if !matching.IsEqual(installedCPE, wfn) && !matching.IsSubset(installedCPE, wfn) {
return false, nil
}
for _, runningOn := range target.RunningOn {
if runningOn.CPEVersion == "2.3" {
wfn, err = naming.UnbindFS(runningOn.CPE)
} else {
wfn, err = naming.UnbindURI(runningOn.CPE)
}
if err != nil {
return false, errors.Wrapf(err, "unbind %s", runningOn.CPE)
}
if !matching.IsEqual(*installedRunningOn, wfn) && !matching.IsSubset(*installedRunningOn, wfn) {
return false, nil
}
}
if len(target.Vulnerable.Version) == 0 {
return true, nil
}
attrver := installedCPE.GetString(common.AttributeVersion)
switch attrver {
case "ANY":
return true, nil
case "NA":
return false, nil
default:
v, err := version.NewVersion(strings.ReplaceAll(attrver, "\\", ""))
if err != nil {
return false, errors.Wrapf(err, "parse version in %s", installedCPE.GetString(common.AttributeVersion))
}
for _, vconf := range target.Vulnerable.Version {
vconfv, err := version.NewVersion(vconf.Version)
if err != nil {
return false, errors.Wrapf(err, "parse version in %s", vconf.Version)
}
switch vconf.Operator {
case "eq":
if !v.Equal(vconfv) {
return false, nil
}
case "lt":
if !v.LessThan(vconfv) {
return false, nil
}
case "le":
if !v.LessThanOrEqual(vconfv) {
return false, nil
}
case "gt":
if !v.GreaterThan(vconfv) {
return false, nil
}
case "ge":
if !v.GreaterThanOrEqual(vconfv) {
return false, nil
}
default:
return false, errors.New("not supported operator")
}
}
return true, nil
}
}

151
pkg/detect/debian/debian.go Normal file
View File

@@ -0,0 +1,151 @@
package debian
import (
"context"
"fmt"
version "github.com/knqyf263/go-deb-version"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"github.com/future-architect/vuls/pkg/db"
dbTypes "github.com/future-architect/vuls/pkg/db/types"
"github.com/future-architect/vuls/pkg/types"
"github.com/future-architect/vuls/pkg/util"
)
type Detector struct{}
func (d Detector) Name() string {
return "debian detector"
}
func (d Detector) Detect(ctx context.Context, host *types.Host) error {
if host.ScannedCves == nil {
host.ScannedCves = map[string]types.VulnInfo{}
}
vulndb, err := db.Open("boltdb", host.Config.Detect.Path, false)
if err != nil {
return errors.Wrapf(err, "open %s", host.Config.Detect.Path)
}
defer vulndb.Close()
srcpkgs := map[string]string{}
for _, p := range host.Packages.OSPkg {
srcpkgs[p.SrcName] = p.SrcVersion
}
for srcname, srcver := range srcpkgs {
pkgs, err := vulndb.GetPackage(host.Family, host.Release, srcname)
if err != nil {
return errors.Wrap(err, "get package")
}
for cveid, datasrcs := range pkgs {
for datasrc, ps := range datasrcs {
for id, p := range ps {
switch p.Status {
case "fixed":
for _, andVs := range p.Version {
affected := true
for _, v := range andVs {
r, err := compare(v.Operator, srcver, v.Version)
if err != nil {
return errors.Wrap(err, "compare")
}
if !r {
affected = false
break
}
}
if affected {
vinfo, ok := host.ScannedCves[cveid]
if !ok {
host.ScannedCves[cveid] = types.VulnInfo{ID: cveid}
}
vinfo.AffectedPackages = append(vinfo.AffectedPackages, types.AffectedPackage{
Name: srcname,
Source: fmt.Sprintf("%s:%s", datasrc, id),
Status: p.Status,
})
vinfo.AffectedPackages = util.Unique(vinfo.AffectedPackages)
host.ScannedCves[cveid] = vinfo
}
}
case "open":
vinfo, ok := host.ScannedCves[cveid]
if !ok {
host.ScannedCves[cveid] = types.VulnInfo{ID: cveid}
}
vinfo.AffectedPackages = append(vinfo.AffectedPackages, types.AffectedPackage{
Name: srcname,
Source: fmt.Sprintf("%s:%s", datasrc, id),
Status: p.Status,
})
vinfo.AffectedPackages = util.Unique(vinfo.AffectedPackages)
host.ScannedCves[cveid] = vinfo
case "not affected":
}
}
}
}
}
vulns, err := vulndb.GetVulnerability(maps.Keys(host.ScannedCves))
if err != nil {
return errors.Wrap(err, "get vulnerability")
}
for cveid, datasrcs := range vulns {
vinfo := host.ScannedCves[cveid]
vinfo.Content = map[string]dbTypes.Vulnerability{}
for src, v := range datasrcs {
vinfo.Content[src] = v
}
host.ScannedCves[cveid] = vinfo
}
return nil
}
func compare(operator, srcver, ver string) (bool, error) {
v1, err := version.NewVersion(srcver)
if err != nil {
return false, errors.Wrap(err, "parse version")
}
v2, err := version.NewVersion(ver)
if err != nil {
return false, errors.Wrap(err, "parse version")
}
r := v1.Compare(v2)
switch operator {
case "eq":
if r == 0 {
return true, nil
}
return false, nil
case "lt":
if r < 0 {
return true, nil
}
return false, nil
case "le":
if r <= 0 {
return true, nil
}
return false, nil
case "gt":
if r > 0 {
return true, nil
}
return false, nil
case "ge":
if r >= 0 {
return true, nil
}
return false, nil
default:
return false, errors.New("not supported operator")
}
}

63
pkg/detect/detect.go Normal file
View File

@@ -0,0 +1,63 @@
package detect
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/cmd/version"
"github.com/future-architect/vuls/pkg/detect/cpe"
"github.com/future-architect/vuls/pkg/detect/debian"
detectTypes "github.com/future-architect/vuls/pkg/detect/types"
"github.com/future-architect/vuls/pkg/detect/ubuntu"
"github.com/future-architect/vuls/pkg/detect/windows"
"github.com/future-architect/vuls/pkg/types"
)
func Detect(ctx context.Context, host *types.Host) error {
if host.ScanError != "" {
return nil
}
var detectors []detectTypes.Detector
if len(host.Packages.OSPkg) > 0 {
switch host.Family {
case "debian":
detectors = append(detectors, debian.Detector{})
case "ubuntu":
detectors = append(detectors, ubuntu.Detector{})
default:
return errors.New("not implemented")
}
}
if len(host.Packages.KB) > 0 {
detectors = append(detectors, windows.Detector{})
}
if len(host.Packages.CPE) > 0 {
detectors = append(detectors, cpe.Detector{})
}
var err error
for {
if len(detectors) == 0 {
break
}
d := detectors[0]
if err = d.Detect(ctx, host); err != nil {
break
}
detectors = detectors[1:]
}
t := time.Now()
host.DetecteddAt = &t
host.DetectedVersion = version.Version
host.DetectedRevision = version.Revision
if err != nil {
return errors.Wrapf(err, "detect %s", host.Name)
}
return nil
}

12
pkg/detect/types/types.go Normal file
View File

@@ -0,0 +1,12 @@
package types
import (
"context"
"github.com/future-architect/vuls/pkg/types"
)
type Detector interface {
Name() string
Detect(context.Context, *types.Host) error
}

151
pkg/detect/ubuntu/ubuntu.go Normal file
View File

@@ -0,0 +1,151 @@
package ubuntu
import (
"context"
"fmt"
version "github.com/knqyf263/go-deb-version"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"github.com/future-architect/vuls/pkg/db"
dbTypes "github.com/future-architect/vuls/pkg/db/types"
"github.com/future-architect/vuls/pkg/types"
"github.com/future-architect/vuls/pkg/util"
)
type Detector struct{}
func (d Detector) Name() string {
return "ubuntu detector"
}
func (d Detector) Detect(ctx context.Context, host *types.Host) error {
if host.ScannedCves == nil {
host.ScannedCves = map[string]types.VulnInfo{}
}
vulndb, err := db.Open("boltdb", host.Config.Detect.Path, false)
if err != nil {
return errors.Wrapf(err, "open %s", host.Config.Detect.Path)
}
defer vulndb.Close()
srcpkgs := map[string]string{}
for _, p := range host.Packages.OSPkg {
srcpkgs[p.SrcName] = p.SrcVersion
}
for srcname, srcver := range srcpkgs {
pkgs, err := vulndb.GetPackage(host.Family, host.Release, srcname)
if err != nil {
return errors.Wrap(err, "get package")
}
for cveid, datasrcs := range pkgs {
for datasrc, ps := range datasrcs {
for id, p := range ps {
switch p.Status {
case "released":
for _, andVs := range p.Version {
affected := true
for _, v := range andVs {
r, err := compare(v.Operator, srcver, v.Version)
if err != nil {
return errors.Wrap(err, "compare")
}
if !r {
affected = false
break
}
}
if affected {
vinfo, ok := host.ScannedCves[cveid]
if !ok {
host.ScannedCves[cveid] = types.VulnInfo{ID: cveid}
}
vinfo.AffectedPackages = append(vinfo.AffectedPackages, types.AffectedPackage{
Name: srcname,
Source: fmt.Sprintf("%s:%s", datasrc, id),
Status: p.Status,
})
vinfo.AffectedPackages = util.Unique(vinfo.AffectedPackages)
host.ScannedCves[cveid] = vinfo
}
}
case "needed", "deferred", "pending":
vinfo, ok := host.ScannedCves[cveid]
if !ok {
host.ScannedCves[cveid] = types.VulnInfo{ID: cveid}
}
vinfo.AffectedPackages = append(vinfo.AffectedPackages, types.AffectedPackage{
Name: srcname,
Source: fmt.Sprintf("%s:%s", datasrc, id),
Status: p.Status,
})
vinfo.AffectedPackages = util.Unique(vinfo.AffectedPackages)
host.ScannedCves[cveid] = vinfo
case "not-affected", "DNE":
}
}
}
}
}
vulns, err := vulndb.GetVulnerability(maps.Keys(host.ScannedCves))
if err != nil {
return errors.Wrap(err, "get vulnerability")
}
for cveid, datasrcs := range vulns {
vinfo := host.ScannedCves[cveid]
vinfo.Content = map[string]dbTypes.Vulnerability{}
for src, v := range datasrcs {
vinfo.Content[src] = v
}
host.ScannedCves[cveid] = vinfo
}
return nil
}
func compare(operator, srcver, ver string) (bool, error) {
v1, err := version.NewVersion(srcver)
if err != nil {
return false, errors.Wrap(err, "parse version")
}
v2, err := version.NewVersion(ver)
if err != nil {
return false, errors.Wrap(err, "parse version")
}
r := v1.Compare(v2)
switch operator {
case "eq":
if r == 0 {
return true, nil
}
return false, nil
case "lt":
if r < 0 {
return true, nil
}
return false, nil
case "le":
if r <= 0 {
return true, nil
}
return false, nil
case "gt":
if r > 0 {
return true, nil
}
return false, nil
case "ge":
if r >= 0 {
return true, nil
}
return false, nil
default:
return false, errors.New("not supported operator")
}
}

View File

@@ -0,0 +1,120 @@
package windows
import (
"context"
"fmt"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"github.com/future-architect/vuls/pkg/db"
dbTypes "github.com/future-architect/vuls/pkg/db/types"
"github.com/future-architect/vuls/pkg/types"
"github.com/future-architect/vuls/pkg/util"
)
type Detector struct{}
func (d Detector) Name() string {
return "windows detector"
}
func (d Detector) Detect(ctx context.Context, host *types.Host) error {
if host.ScannedCves == nil {
host.ScannedCves = map[string]types.VulnInfo{}
}
vulndb, err := db.Open("boltdb", host.Config.Detect.Path, false)
if err != nil {
return errors.Wrapf(err, "open %s", host.Config.Detect.Path)
}
defer vulndb.Close()
supercedences, err := vulndb.GetSupercedence(host.Packages.KB)
if err != nil {
return errors.Wrap(err, "get supercedence")
}
var unapplied []string
for _, kbs := range supercedences {
var applied bool
for _, kb := range kbs {
if slices.Contains(host.Packages.KB, kb) {
applied = true
break
}
}
if !applied {
unapplied = append(unapplied, kbs...)
}
}
unapplied = util.Unique(unapplied)
products, err := vulndb.GetKBtoProduct(host.Release, append(host.Packages.KB, unapplied...))
if err != nil {
return errors.Wrap(err, "get product from kb")
}
if !slices.Contains(products, host.Release) {
products = append(products, host.Release)
}
for _, product := range util.Unique(products) {
pkgs, err := vulndb.GetPackage(host.Family, host.Release, product)
if err != nil {
return errors.Wrap(err, "get package")
}
for cveid, datasrcs := range pkgs {
for datasrc, ps := range datasrcs {
for id, p := range ps {
switch p.Status {
case "fixed":
for _, v := range p.Version {
if slices.Contains(unapplied, v[0].Version) {
vinfo, ok := host.ScannedCves[cveid]
if !ok {
host.ScannedCves[cveid] = types.VulnInfo{ID: cveid}
}
vinfo.AffectedPackages = append(vinfo.AffectedPackages, types.AffectedPackage{
Name: fmt.Sprintf("%s: KB%s", product, v[0].Version),
Source: fmt.Sprintf("%s:%s", datasrc, id),
Status: p.Status,
})
vinfo.AffectedPackages = util.Unique(vinfo.AffectedPackages)
host.ScannedCves[cveid] = vinfo
}
}
case "unfixed":
vinfo, ok := host.ScannedCves[cveid]
if !ok {
host.ScannedCves[cveid] = types.VulnInfo{ID: cveid}
}
vinfo.AffectedPackages = append(vinfo.AffectedPackages, types.AffectedPackage{
Name: product,
Source: fmt.Sprintf("%s:%s", datasrc, id),
Status: p.Status,
})
vinfo.AffectedPackages = util.Unique(vinfo.AffectedPackages)
host.ScannedCves[cveid] = vinfo
}
}
}
}
}
vulns, err := vulndb.GetVulnerability(maps.Keys(host.ScannedCves))
if err != nil {
return errors.Wrap(err, "get vulnerability")
}
for cveid, datasrcs := range vulns {
vinfo := host.ScannedCves[cveid]
vinfo.Content = map[string]dbTypes.Vulnerability{}
for src, v := range datasrcs {
vinfo.Content[src] = v
}
host.ScannedCves[cveid] = vinfo
}
return nil
}

22
pkg/log/log.go Normal file
View File

@@ -0,0 +1,22 @@
package log
import (
"context"
"go.uber.org/zap"
)
type ctxLogger struct{}
// ContextWithLogger adds logger to context
func ContextWithLogger(ctx context.Context, l *zap.Logger) context.Context {
return context.WithValue(ctx, ctxLogger{}, l)
}
// LoggerFromContext returns logger from context
func LoggerFromContext(ctx context.Context) *zap.Logger {
if l, ok := ctx.Value(ctxLogger{}).(*zap.Logger); ok {
return l
}
return zap.L()
}

44
pkg/scan/cpe/cpe.go Normal file
View File

@@ -0,0 +1,44 @@
package cpe
import (
"context"
"fmt"
"github.com/knqyf263/go-cpe/naming"
"github.com/pkg/errors"
scanTypes "github.com/future-architect/vuls/pkg/scan/types"
"github.com/future-architect/vuls/pkg/types"
)
type Analyzer struct {
}
func (a Analyzer) Name() string {
return "cpe analyzer"
}
func (a Analyzer) Analyze(ctx context.Context, ah *scanTypes.AnalyzerHost) error {
ah.Host.Packages.CPE = map[string]types.CPE{}
for _, c := range ah.Host.Config.Scan.CPE {
if _, err := naming.UnbindFS(c.CPE); err != nil {
return errors.Wrapf(err, "unbind %s", c.CPE)
}
key := c.CPE
if c.RunningOn != "" {
if _, err := naming.UnbindFS(c.RunningOn); err != nil {
return errors.Wrapf(err, "unbind %s", c.RunningOn)
}
key = fmt.Sprintf("%s_on_%s", c.CPE, c.RunningOn)
}
ah.Host.Packages.CPE[key] = types.CPE{
CPE: c.CPE,
RunningOn: c.RunningOn,
}
}
return nil
}

93
pkg/scan/os/os.go Normal file
View File

@@ -0,0 +1,93 @@
package os
import (
"bufio"
"context"
"strings"
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/scan/ospkg/apk"
"github.com/future-architect/vuls/pkg/scan/ospkg/dpkg"
"github.com/future-architect/vuls/pkg/scan/ospkg/rpm"
"github.com/future-architect/vuls/pkg/scan/types"
)
type Analyzer struct {
}
func (a Analyzer) Name() string {
return "os analyzer"
}
func (a Analyzer) Analyze(ctx context.Context, ah *types.AnalyzerHost) error {
status, stdout, stderr, err := ah.Host.Exec(ctx, "cat /etc/os-release", false)
if err != nil {
return errors.Wrap(err, `exec "cat /etc/os-release"`)
}
if stderr != "" {
return errors.New(stderr)
}
if status != 0 {
return errors.Errorf("exit status is %d", status)
}
ah.Host.Family, ah.Host.Release, err = ParseOSRelease(stdout)
if err != nil {
return errors.Wrap(err, "parse /etc/os-release")
}
switch ah.Host.Family {
case "debian", "ubuntu":
ah.Analyzers = append(ah.Analyzers, dpkg.Analyzer{})
case "redhat", "centos", "alma", "rocky", "fedora", "opensuse", "opensuse.tumbleweed", "opensuse.leap", "suse.linux.enterprise.server", "suse.linux.enterprise.desktop":
ah.Analyzers = append(ah.Analyzers, rpm.Analyzer{})
case "alpine":
ah.Analyzers = append(ah.Analyzers, apk.Analyzer{})
case "":
return errors.New("family is unknown")
default:
return errors.New("not supported OS")
}
return nil
}
func ParseOSRelease(stdout string) (string, string, error) {
var family, versionID string
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := scanner.Text()
ss := strings.SplitN(line, "=", 2)
if len(ss) != 2 {
continue
}
key, value := strings.TrimSpace(ss[0]), strings.TrimSpace(ss[1])
switch key {
case "ID":
switch id := strings.Trim(value, `"'`); id {
case "almalinux":
family = "alma"
case "opensuse-leap", "opensuse-tumbleweed":
family = strings.ReplaceAll(id, "-", ".")
case "sles":
family = "suse.linux.enterprise.server"
case "sled":
family = "suse.linux.enterprise.desktop"
default:
family = strings.ToLower(id)
}
case "VERSION_ID":
versionID = strings.Trim(value, `"'`)
default:
continue
}
}
if family == "" {
return "", "", errors.New("family is unknown")
}
return family, versionID, nil
}

71
pkg/scan/ospkg/apk/apk.go Normal file
View File

@@ -0,0 +1,71 @@
package apk
import (
"bufio"
"context"
"strings"
"github.com/pkg/errors"
scanTypes "github.com/future-architect/vuls/pkg/scan/types"
"github.com/future-architect/vuls/pkg/types"
)
type Analyzer struct {
}
func (a Analyzer) Name() string {
return "apk analyzer"
}
func (a Analyzer) Analyze(ctx context.Context, ah *scanTypes.AnalyzerHost) error {
status, stdout, stderr, err := ah.Host.Exec(ctx, "apk info -v", false)
if err != nil {
return errors.Wrap(err, `exec "apk info -v"`)
}
if stderr != "" {
return errors.New(stderr)
}
if status != 0 {
return errors.Errorf("exit status is %d", status)
}
ah.Host.Packages.OSPkg, err = ParseInstalledPackage(stdout)
if err != nil {
return errors.Wrap(err, "parse installed package")
}
return nil
}
func ParseInstalledPackage(stdout string) (map[string]types.Package, error) {
pkgs := map[string]types.Package{}
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
name, version, err := parseApkInfo(scanner.Text())
if err != nil {
return nil, errors.Wrap(err, "parse apk info line")
}
if name == "" || version == "" {
continue
}
pkgs[name] = types.Package{
Name: name,
Version: version,
}
}
return pkgs, nil
}
func parseApkInfo(line string) (string, string, error) {
ss := strings.Split(line, "-")
if len(ss) < 3 {
if strings.Contains(ss[0], "WARNING") {
return "", "", nil
}
return "", "", errors.Errorf(`unexpected package line format. accepts: "<package name>-<version>-<release>", received: "%s"`, line)
}
return strings.Join(ss[:len(ss)-2], "-"), strings.Join(ss[len(ss)-2:], "-"), nil
}

View File

@@ -0,0 +1,95 @@
package dpkg
import (
"bufio"
"context"
"strings"
"github.com/pkg/errors"
scanTypes "github.com/future-architect/vuls/pkg/scan/types"
"github.com/future-architect/vuls/pkg/types"
)
type Analyzer struct {
}
func (a Analyzer) Name() string {
return "dpkg analyzer"
}
func (a Analyzer) Analyze(ctx context.Context, ah *scanTypes.AnalyzerHost) error {
status, stdout, stderr, err := ah.Host.Exec(ctx, `dpkg-query -W -f="\${binary:Package},\${db:Status-Abbrev},\${Version},\${Architecture},\${source:Package},\${source:Version}\n"`, false)
if err != nil {
return errors.Wrap(err, `exec "dpkg-query -W -f="\${binary:Package},\${db:Status-Abbrev},\${Version},\${Architecture},\${source:Package},\${source:Version}\n"`)
}
if stderr != "" {
return errors.New(stderr)
}
if status != 0 {
return errors.Errorf("exit status is %d", status)
}
ah.Host.Packages.OSPkg, err = ParseInstalledPackage(stdout)
if err != nil {
return errors.Wrap(err, "parse installed package")
}
return nil
}
func ParseInstalledPackage(stdout string) (map[string]types.Package, error) {
pkgs := map[string]types.Package{}
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := scanner.Text()
if trimmed := strings.TrimSpace(line); len(trimmed) != 0 {
name, status, version, arch, srcName, srcVersion, err := parseDPKGQueryLine(trimmed)
if err != nil {
return nil, errors.Wrap(err, "parse dpkq query line")
}
packageStatus := status[1]
// Package status:
// n = Not-installed
// c = Config-files
// H = Half-installed
// U = Unpacked
// F = Half-configured
// W = Triggers-awaiting
// t = Triggers-pending
// i = Installed
if packageStatus != 'i' {
continue
}
pkgs[name] = types.Package{
Name: name,
Version: version,
Arch: arch,
SrcName: srcName,
SrcVersion: srcVersion,
}
}
}
return pkgs, nil
}
func parseDPKGQueryLine(line string) (string, string, string, string, string, string, error) {
ss := strings.Split(line, ",")
if len(ss) == 6 {
// remove :amd64, i386...
name, _, _ := strings.Cut(ss[0], ":")
status := strings.TrimSpace(ss[1])
if len(status) < 2 {
return "", "", "", "", "", "", errors.Errorf(`unexpected db:Status-Abbrev format. accepts: "ii", received: "%s"`, status)
}
version := ss[2]
arch := ss[3]
srcName, _, _ := strings.Cut(ss[4], " ")
srcVersion := ss[5]
return name, status, version, arch, srcName, srcVersion, nil
}
return "", "", "", "", "", "", errors.Errorf(`unexpected package line format. accepts: "<bin name>,<status>,<bin version>,<arch>,<src name>,<src version>", received: "%s"`, line)
}

114
pkg/scan/ospkg/rpm/rpm.go Normal file
View File

@@ -0,0 +1,114 @@
package rpm
import (
"bufio"
"context"
"fmt"
"strings"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
scanTypes "github.com/future-architect/vuls/pkg/scan/types"
"github.com/future-architect/vuls/pkg/types"
)
type Analyzer struct {
}
func (a Analyzer) Name() string {
return "rpm analyzer"
}
func (a Analyzer) Analyze(ctx context.Context, ah *scanTypes.AnalyzerHost) error {
status, stdout, stderr, err := ah.Host.Exec(ctx, `rpm --version`, false)
if err != nil {
return errors.Wrap(err, `exec "rpm --version"`)
}
if stderr != "" {
return errors.New(stderr)
}
if status != 0 {
return errors.Errorf("exit status is %d", status)
}
cmd := `rpm -qa --queryformat "%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH} %{VENDOR}\n"`
rpmver, err := version.NewVersion(strings.TrimPrefix(strings.TrimSpace(stdout), "RPM version "))
rpmModukaritylabel, err := version.NewVersion("4.15.0")
if err != nil {
return errors.Wrap(err, "parse rpm version for modularitylabel")
}
rpmEpochNum, err := version.NewVersion("4.8.0")
if err != nil {
return errors.Wrap(err, "parse rpm version for epochnum")
}
if rpmver.GreaterThanOrEqual(rpmModukaritylabel) {
cmd = `rpm -qa --queryformat "%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH} %{VENDOR} %{MODULARITYLABEL}\n"`
} else if rpmver.GreaterThanOrEqual(rpmEpochNum) {
cmd = `rpm -qa --queryformat "%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH} %{VENDOR}\n"`
}
status, stdout, stderr, err = ah.Host.Exec(ctx, cmd, false)
if err != nil {
return errors.Wrapf(err, `exec "%s"`, cmd)
}
if stderr != "" {
return errors.New(stderr)
}
if status != 0 {
return errors.Errorf("exit status is %d", status)
}
ah.Host.Packages.OSPkg, err = ParseInstalledPackage(stdout)
if err != nil {
return errors.Wrap(err, "parse installed package")
}
return nil
}
func ParseInstalledPackage(stdout string) (map[string]types.Package, error) {
pkgs := map[string]types.Package{}
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := scanner.Text()
if trimmed := strings.TrimSpace(line); len(trimmed) != 0 {
name, version, release, arch, vendor, modularitylabel, err := parseRpmQaLine(trimmed)
if err != nil {
return nil, errors.Wrap(err, "parse rpm -qa line")
}
pkgs[name] = types.Package{
Name: name,
Version: version,
Release: release,
Arch: arch,
Vendor: vendor,
ModularityLabel: modularitylabel,
}
}
}
return pkgs, nil
}
func parseRpmQaLine(line string) (string, string, string, string, string, string, error) {
ss := strings.Fields(line)
if len(ss) < 6 {
return "", "", "", "", "", "", errors.Errorf(`unexpected rpm -qa line format. accepts: "<name> <epoch> <version> <release> <arch> <vendor>( <modularitylabel>)", received: "%s"`, line)
}
ver := ss[2]
epoch := ss[1]
if epoch != "0" && epoch != "(none)" {
ver = fmt.Sprintf("%s:%s", epoch, ss[2])
}
var modularitylabel string
if len(ss) == 7 {
modularitylabel = ss[5]
}
return ss[0], ver, ss[3], ss[4], ss[5], modularitylabel, nil
}

54
pkg/scan/scan.go Normal file
View File

@@ -0,0 +1,54 @@
package scan
import (
"context"
"runtime"
"time"
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/cmd/version"
"github.com/future-architect/vuls/pkg/scan/cpe"
"github.com/future-architect/vuls/pkg/scan/os"
"github.com/future-architect/vuls/pkg/scan/systeminfo"
scanTypes "github.com/future-architect/vuls/pkg/scan/types"
"github.com/future-architect/vuls/pkg/types"
)
func Scan(ctx context.Context, host *types.Host) error {
ah := scanTypes.AnalyzerHost{Host: host}
if ah.Host.Config.Scan.OSPkg != nil {
if runtime.GOOS == "windows" {
ah.Analyzers = append(ah.Analyzers, systeminfo.Analyzer{})
} else {
ah.Analyzers = append(ah.Analyzers, os.Analyzer{})
}
}
if len(ah.Host.Config.Scan.CPE) > 0 {
ah.Analyzers = append(ah.Analyzers, cpe.Analyzer{})
}
var (
err error
)
for {
if len(ah.Analyzers) == 0 {
break
}
a := ah.Analyzers[0]
if err = a.Analyze(ctx, &ah); err != nil {
break
}
ah.Analyzers = ah.Analyzers[1:]
}
t := time.Now()
ah.Host.ScannedAt = &t
ah.Host.ScannedVersion = version.Version
ah.Host.ScannedRevision = version.Revision
if err != nil {
return errors.Wrapf(err, "analyze %s", ah.Host.Name)
}
return nil
}

View File

@@ -0,0 +1,480 @@
package systeminfo
import (
"bufio"
"context"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/scan/types"
)
type Analyzer struct {
}
func (a Analyzer) Name() string {
return "systeminfo analyzer"
}
func (a Analyzer) Analyze(ctx context.Context, ah *types.AnalyzerHost) error {
status, stdout, stderr, err := ah.Host.Exec(ctx, "systeminfo", false)
if err != nil {
return errors.Wrap(err, `exec "systeminfo"`)
}
if stderr != "" {
return errors.New(stderr)
}
if status != 0 {
return errors.Errorf("exit status is %d", status)
}
ah.Host.Family, ah.Host.Release, ah.Host.Packages.KB, err = ParseSysteminfo(stdout)
if err != nil {
return errors.Wrap(err, "parse systeminfo")
}
if ah.Host.Family == "" {
return errors.New("family is unknown")
}
if ah.Host.Release == "" {
return errors.New("release is unknown")
}
return nil
}
func ParseSysteminfo(stdout string) (string, string, []string, error) {
var (
o osInfo
kbs []string
)
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "OS Name:"):
o.productName = strings.TrimSpace(strings.TrimPrefix(line, "OS Name:"))
case strings.HasPrefix(line, "OS Version:"):
s := strings.TrimSpace(strings.TrimPrefix(line, "OS Version:"))
lhs, build, _ := strings.Cut(s, " Build ")
vb, sp, _ := strings.Cut(lhs, " ")
o.version = strings.TrimSuffix(vb, fmt.Sprintf(".%s", build))
o.build = build
if sp != "N/A" {
o.servicePack = sp
}
case strings.HasPrefix(line, "System Type:"):
o.arch = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "System Type:"), "PC"))
case strings.HasPrefix(line, "OS Configuration:"):
switch {
case strings.Contains(line, "Server"):
o.installationType = "Server"
case strings.Contains(line, "Workstation"):
o.installationType = "Client"
default:
return "", "", nil, errors.Errorf(`installation type not found from "%s"`, line)
}
case strings.HasPrefix(line, "Hotfix(s):"):
nKB, err := strconv.Atoi(strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "Hotfix(s):"), " Hotfix(s) Installed.")))
if err != nil {
return "", "", nil, errors.Errorf(`number of installed hotfix from "%s"`, line)
}
for i := 0; i < nKB; i++ {
scanner.Scan()
line := scanner.Text()
_, rhs, found := strings.Cut(line, ":")
if !found {
continue
}
s := strings.TrimSpace(rhs)
if strings.HasPrefix(s, "KB") {
kbs = append(kbs, strings.TrimPrefix(s, "KB"))
}
}
default:
}
}
release, err := detectOSName(o)
if err != nil {
return "", "", nil, errors.Wrap(err, "detect os name")
}
return "windows", release, kbs, nil
}
type osInfo struct {
productName string
version string
build string
edition string
servicePack string
arch string
installationType string
}
func detectOSName(osInfo osInfo) (string, error) {
osName, err := detectOSNameFromOSInfo(osInfo)
if err != nil {
return "", errors.Wrapf(err, "detect OS Name from OSInfo: %#v", osInfo)
}
return osName, nil
}
func detectOSNameFromOSInfo(osInfo osInfo) (string, error) {
switch osInfo.version {
case "5.0":
switch osInfo.installationType {
case "Client":
if osInfo.servicePack != "" {
return fmt.Sprintf("Microsoft Windows 2000 %s", osInfo.servicePack), nil
}
return "Microsoft Windows 2000", nil
case "Server":
if osInfo.servicePack != "" {
return fmt.Sprintf("Microsoft Windows 2000 Server %s", osInfo.servicePack), nil
}
return "Microsoft Windows 2000 Server", nil
}
case "5.1":
switch osInfo.installationType {
case "Client":
var n string
switch osInfo.edition {
case "Professional":
n = "Microsoft Windows XP Professional"
case "Media Center":
n = "Microsoft Windows XP Media Center Edition 2005"
case "Tablet PC":
n = "Microsoft Windows XP Tablet PC Edition 2005"
default:
n = "Microsoft Windows XP"
}
switch osInfo.arch {
case "x64":
n = fmt.Sprintf("%s x64 Edition", n)
}
if osInfo.servicePack != "" {
return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil
}
return n, nil
}
case "5.2":
switch osInfo.installationType {
case "Client":
var n string
switch osInfo.edition {
case "Professional":
n = "Microsoft Windows XP Professional"
case "Media Center":
n = "Microsoft Windows XP Media Center Edition 2005"
case "Tablet PC":
n = "Microsoft Windows XP Tablet PC Edition 2005"
default:
n = "Microsoft Windows XP"
}
switch osInfo.arch {
case "x64":
n = fmt.Sprintf("%s x64 Edition", n)
}
if osInfo.servicePack != "" {
return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil
}
return n, nil
case "Server":
n := "Microsoft Windows Server 2003"
if strings.Contains(osInfo.productName, "R2") {
n = "Microsoft Windows Server 2003 R2"
}
switch osInfo.arch {
case "x64":
n = fmt.Sprintf("%s x64 Edition", n)
case "IA64":
if osInfo.edition == "Enterprise" {
n = fmt.Sprintf("%s, Enterprise Edition for Itanium-based Systems", n)
} else {
n = fmt.Sprintf("%s for Itanium-based Systems", n)
}
}
if osInfo.servicePack != "" {
return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil
}
return n, nil
}
case "6.0":
switch osInfo.installationType {
case "Client":
var n string
switch osInfo.arch {
case "x64":
n = "Windows Vista x64 Editions"
default:
n = "Windows Vista"
}
if osInfo.servicePack != "" {
return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil
}
return n, nil
case "Server":
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
if osInfo.servicePack != "" {
return fmt.Sprintf("Windows Server 2008 for %s Systems %s", arch, osInfo.servicePack), nil
}
return fmt.Sprintf("Windows Server 2008 for %s Systems", arch), nil
case "Server Core":
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
if osInfo.servicePack != "" {
return fmt.Sprintf("Windows Server 2008 for %s Systems %s (Server Core installation)", arch, osInfo.servicePack), nil
}
return fmt.Sprintf("Windows Server 2008 for %s Systems (Server Core installation)", arch), nil
}
case "6.1":
switch osInfo.installationType {
case "Client":
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
if osInfo.servicePack != "" {
return fmt.Sprintf("Windows 7 for %s Systems %s", arch, osInfo.servicePack), nil
}
return fmt.Sprintf("Windows 7 for %s Systems", arch), nil
case "Server":
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
if osInfo.servicePack != "" {
return fmt.Sprintf("Windows Server 2008 R2 for %s Systems %s", arch, osInfo.servicePack), nil
}
return fmt.Sprintf("Windows Server 2008 R2 for %s Systems", arch), nil
case "Server Core":
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
if osInfo.servicePack != "" {
return fmt.Sprintf("Windows Server 2008 R2 for %s Systems %s (Server Core installation)", arch, osInfo.servicePack), nil
}
return fmt.Sprintf("Windows Server 2008 R2 for %s Systems (Server Core installation)", arch), nil
}
case "6.2":
switch osInfo.installationType {
case "Client":
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
return fmt.Sprintf("Windows 8 for %s Systems", arch), nil
case "Server":
return "Windows Server 2012", nil
case "Server Core":
return "Windows Server 2012 (Server Core installation)", nil
}
case "6.3":
switch osInfo.installationType {
case "Client":
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
return fmt.Sprintf("Windows 8.1 for %s Systems", arch), nil
case "Server":
return "Windows Server 2012 R2", nil
case "Server Core":
return "Windows Server 2012 R2 (Server Core installation)", nil
}
case "10.0":
switch osInfo.installationType {
case "Client":
if strings.Contains(osInfo.productName, "Windows 10") {
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
name, err := formatNamebyBuild("10", osInfo.build)
if err != nil {
return "", err
}
return fmt.Sprintf("%s for %s Systems", name, arch), nil
}
if strings.Contains(osInfo.productName, "Windows 11") {
arch, err := formatArch(osInfo.arch)
if err != nil {
return "", err
}
name, err := formatNamebyBuild("11", osInfo.build)
if err != nil {
return "", err
}
return fmt.Sprintf("%s for %s Systems", name, arch), nil
}
case "Server":
return formatNamebyBuild("Server", osInfo.build)
case "Server Core":
name, err := formatNamebyBuild("Server", osInfo.build)
if err != nil {
return "", err
}
return fmt.Sprintf("%s (Server Core installation)", name), nil
}
}
return "", errors.New("OS Name not found")
}
func formatArch(arch string) (string, error) {
switch arch {
case "x64-based":
return "x64-based", nil
case "ARM64-based":
return "ARM64-based", nil
case "Itanium-based":
return "Itanium-based", nil
case "X86-based":
return "32-bit", nil
default:
return "", errors.New("CPU Architecture not found")
}
}
type buildNumber struct {
build string
name string
}
var (
winBuilds = map[string][]buildNumber{
"10": {
{
build: "10240",
name: "Windows 10", // not "Windows 10 Version 1507"
},
{
build: "10586",
name: "Windows 10 Version 1511",
},
{
build: "14393",
name: "Windows 10 Version 1607",
},
{
build: "15063",
name: "Windows 10 Version 1703",
},
{
build: "16299",
name: "Windows 10 Version 1709",
},
{
build: "17134",
name: "Windows 10 Version 1803",
},
{
build: "17763",
name: "Windows 10 Version 1809",
},
{
build: "18362",
name: "Windows 10 Version 1903",
},
{
build: "18363",
name: "Windows 10 Version 1909",
},
{
build: "19041",
name: "Windows 10 Version 2004",
},
{
build: "19042",
name: "Windows 10 Version 20H2",
},
{
build: "19043",
name: "Windows 10 Version 21H1",
},
{
build: "19044",
name: "Windows 10 Version 21H2",
},
// It seems that there are cases where the Product Name is Windows 10 even though it is Windows 11
// ref: https://docs.microsoft.com/en-us/answers/questions/586548/in-the-official-version-of-windows-11-why-the-key.html
{
build: "22000",
name: "Windows 11",
},
},
"11": {
{
build: "22000",
name: "Windows 11", // not "Windows 11 Version 21H2"
},
},
"Server": {
{
build: "14393",
name: "Windows Server 2016",
},
{
build: "16299",
name: "Windows Server, Version 1709",
},
{
build: "17134",
name: "Windows Server, Version 1809",
},
{
build: "17763",
name: "Windows Server 2019",
},
{
build: "18362",
name: "Windows Server, Version 1903",
},
{
build: "18363",
name: "Windows Server, Version 1909",
},
{
build: "19041",
name: "Windows Server, Version 2004",
},
{
build: "19042",
name: "Windows Server, Version 20H2",
},
{
build: "20348",
name: "Windows Server 2022",
},
},
}
)
func formatNamebyBuild(osType string, mybuild string) (string, error) {
builds, ok := winBuilds[osType]
if !ok {
return "", errors.New("OS Type not found")
}
v := builds[0].name
for _, b := range builds {
if mybuild == b.build {
return b.name, nil
}
if mybuild < b.build {
break
}
v = b.name
}
return v, nil
}

17
pkg/scan/types/types.go Normal file
View File

@@ -0,0 +1,17 @@
package types
import (
"context"
"github.com/future-architect/vuls/pkg/types"
)
type Analyzer interface {
Name() string
Analyze(context.Context, *AnalyzerHost) error
}
type AnalyzerHost struct {
Host *types.Host
Analyzers []Analyzer
}

108
pkg/server/server.go Normal file
View File

@@ -0,0 +1,108 @@
package server
import (
"context"
"net/http"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/future-architect/vuls/pkg/cmd/version"
"github.com/future-architect/vuls/pkg/config"
"github.com/future-architect/vuls/pkg/detect"
"github.com/future-architect/vuls/pkg/scan/os"
"github.com/future-architect/vuls/pkg/scan/ospkg/apk"
"github.com/future-architect/vuls/pkg/scan/ospkg/dpkg"
"github.com/future-architect/vuls/pkg/scan/ospkg/rpm"
"github.com/future-architect/vuls/pkg/scan/systeminfo"
"github.com/future-architect/vuls/pkg/types"
)
type scanContents struct {
Contents []struct {
ContentType string `json:"type,omitempty"`
Content string `json:"content,omitempty"`
} `json:"contents,omitempty"`
}
func Scan() echo.HandlerFunc {
return func(c echo.Context) error {
s := new(scanContents)
if err := c.Bind(s); err != nil {
return c.JSON(http.StatusBadRequest, "bad request")
}
h := types.Host{Name: uuid.NewString()}
for _, cont := range s.Contents {
switch cont.ContentType {
case "os-release":
family, release, err := os.ParseOSRelease(cont.Content)
if err != nil {
h.ScanError = err.Error()
return c.JSON(http.StatusInternalServerError, h)
}
h.Family = family
h.Release = release
case "systeminfo":
family, release, kbs, err := systeminfo.ParseSysteminfo(cont.Content)
if err != nil {
h.ScanError = err.Error()
return c.JSON(http.StatusInternalServerError, h)
}
h.Family = family
h.Release = release
h.Packages.KB = kbs
case "apk":
pkgs, err := apk.ParseInstalledPackage(cont.Content)
if err != nil {
h.ScanError = err.Error()
return c.JSON(http.StatusInternalServerError, h)
}
h.Packages.OSPkg = pkgs
case "dpkg":
pkgs, err := dpkg.ParseInstalledPackage(cont.Content)
if err != nil {
h.ScanError = err.Error()
return c.JSON(http.StatusInternalServerError, h)
}
h.Packages.OSPkg = pkgs
case "rpm":
pkgs, err := rpm.ParseInstalledPackage(cont.Content)
if err != nil {
h.ScanError = err.Error()
return c.JSON(http.StatusInternalServerError, h)
}
h.Packages.OSPkg = pkgs
}
}
t := time.Now()
h.ScannedAt = &t
h.ScannedVersion = version.Version
h.ScannedRevision = version.Revision
return c.JSON(http.StatusOK, h)
}
}
func Detect(dbpath string) echo.HandlerFunc {
return func(c echo.Context) error {
h := new(types.Host)
if err := c.Bind(h); err != nil {
return c.JSON(http.StatusBadRequest, "bad request")
}
if h.Config.Detect == nil {
h.Config.Detect = &config.Detect{}
}
h.Config.Detect.Path = dbpath
if err := detect.Detect(context.Background(), h); err != nil {
h.DetectError = err.Error()
return c.JSON(http.StatusInternalServerError, h)
}
return c.JSON(http.StatusOK, h)
}
}

182
pkg/types/types.go Normal file
View File

@@ -0,0 +1,182 @@
package types
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"syscall"
"time"
"github.com/pkg/errors"
"github.com/future-architect/vuls/pkg/config"
"github.com/future-architect/vuls/pkg/db/types"
)
type Host struct {
Name string `json:"name,omitempty"`
Family string `json:"family,omitempty"`
Release string `json:"release,omitempty"`
ScannedAt *time.Time `json:"scanned_at,omitempty"`
ScannedVersion string `json:"scanned_version,omitempty"`
ScannedRevision string `json:"scanned_revision,omitempty"`
ScanError string `json:"scan_error,omitempty"`
DetecteddAt *time.Time `json:"detectedd_at,omitempty"`
DetectedVersion string `json:"detected_version,omitempty"`
DetectedRevision string `json:"detected_revision,omitempty"`
DetectError string `json:"detect_error,omitempty"`
ReportedAt *time.Time `json:"reported_at,omitempty"`
ReportedVersion string `json:"reported_version,omitempty"`
ReportedRevision string `json:"reported_revision,omitempty"`
Packages Packages `json:"packages,omitempty"`
ScannedCves map[string]VulnInfo `json:"scanned_cves,omitempty"`
Config Config `json:"config,omitempty"`
}
func (h *Host) Exec(ctx context.Context, cmd string, sudo bool) (int, string, string, error) {
if sudo {
cmd = fmt.Sprintf("sudo -S %s", cmd)
}
switch h.Config.Type {
case "local":
execCmd := exec.CommandContext(ctx, "/bin/sh", "-c", cmd)
if runtime.GOOS == "windows" {
execCmd = exec.CommandContext(ctx, cmd)
}
var stdoutBuf, stderrBuf bytes.Buffer
execCmd.Stdout = &stdoutBuf
execCmd.Stderr = &stderrBuf
if err := execCmd.Run(); err != nil {
if e, ok := err.(*exec.ExitError); ok {
if s, ok := e.Sys().(syscall.WaitStatus); ok {
return s.ExitStatus(), stdoutBuf.String(), stderrBuf.String(), nil
} else {
return 998, stdoutBuf.String(), stderrBuf.String(), nil
}
} else {
return 999, stdoutBuf.String(), stderrBuf.String(), nil
}
} else {
return 0, stdoutBuf.String(), stderrBuf.String(), nil
}
case "remote":
sshBinPath, err := exec.LookPath("ssh")
if err != nil {
return 0, "", "", errors.Wrap(err, "look path to ssh")
}
args := []string{"-tt"}
home, err := os.UserHomeDir()
if err != nil {
return 0, "", "", errors.Wrap(err, "find %s home directory")
}
args = append(args,
"-o", "StrictHostKeyChecking=yes",
"-o", "LogLevel=quiet",
"-o", "ConnectionAttempts=3",
"-o", "ConnectTimeout=10",
"-o", "ControlMaster=auto",
"-o", fmt.Sprintf("ControlPath=%s", filepath.Join(home, ".vuls", fmt.Sprintf("controlmaster-%%r-%s.%%p", h.Name))),
"-o", "Controlpersist=10m",
"-l", *h.Config.User,
)
if h.Config.Port != nil {
args = append(args, "-p", *h.Config.Port)
}
if h.Config.SSHKey != nil {
args = append(args, "-i", *h.Config.SSHKey, "-o", "PasswordAuthentication=no")
}
if runtime.GOOS == "windows" {
args = append(args, *h.Config.Host, cmd)
} else {
args = append(args, *h.Config.Host, fmt.Sprintf("stty cols 1000; %s", cmd))
}
execCmd := exec.CommandContext(ctx, sshBinPath, args...)
var stdoutBuf, stderrBuf bytes.Buffer
execCmd.Stdout = &stdoutBuf
execCmd.Stderr = &stderrBuf
if err := execCmd.Run(); err != nil {
if e, ok := err.(*exec.ExitError); ok {
if s, ok := e.Sys().(syscall.WaitStatus); ok {
return s.ExitStatus(), stdoutBuf.String(), stderrBuf.String(), nil
} else {
return 998, stdoutBuf.String(), stderrBuf.String(), nil
}
} else {
return 999, stdoutBuf.String(), stderrBuf.String(), nil
}
} else {
return 0, stdoutBuf.String(), stderrBuf.String(), nil
}
default:
return 0, "", "", errors.Errorf("%s is not implemented", h.Config.Type)
}
}
type Packages struct {
Kernel Kernel `json:"kernel,omitempty"`
OSPkg map[string]Package `json:"ospkg,omitempty"`
CPE map[string]CPE `json:"cpe,omitempty"`
KB []string `json:"kb,omitempty"`
}
type Kernel struct {
Version string `json:"version,omitempty"`
Release string `json:"release,omitempty"`
RebootRrequired bool `json:"reboot_rrequired,omitempty"`
}
type Package struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Release string `json:"release,omitempty"`
NewVersion string `json:"new_version,omitempty"`
NewRelease string `json:"new_release,omitempty"`
Arch string `json:"arch,omitempty"`
Vendor string `json:"vendor,omitempty"`
Repository string `json:"repository,omitempty"`
ModularityLabel string `json:"modularity_label,omitempty"`
SrcName string `json:"src_name,omitempty"`
SrcVersion string `json:"src_version,omitempty"`
SrcArch string `json:"src_arch,omitempty"`
}
type CPE struct {
CPE string `json:"cpe,omitempty"`
RunningOn string `json:"running_on,omitempty"`
}
type VulnInfo struct {
ID string `json:"id,omitempty"`
Content map[string]types.Vulnerability `json:"content,omitempty"`
AffectedPackages []AffectedPackage `json:"affected_packages,omitempty"`
}
type AffectedPackage struct {
Name string `json:"name,omitempty"`
Source string `json:"source,omitempty"`
Status string `json:"status,omitempty"`
}
type Config struct {
Type string `json:"type,omitempty"`
Host *string `json:"host,omitempty"`
Port *string `json:"port,omitempty"`
User *string `json:"user,omitempty"`
SSHConfig *string `json:"ssh_config,omitempty"`
SSHKey *string `json:"ssh_key,omitempty"`
Scan *config.Scan `json:"scan,omitempty"`
Detect *config.Detect `json:"detect,omitempty"`
}

76
pkg/util/util.go Normal file
View File

@@ -0,0 +1,76 @@
package util
import (
"compress/bzip2"
"compress/gzip"
"io"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/ulikunitz/xz"
"golang.org/x/exp/maps"
)
func CacheDir() string {
cacheDir, err := os.UserCacheDir()
if err != nil {
cacheDir = os.TempDir()
}
dir := filepath.Join(cacheDir, "vuls")
return dir
}
func Unique[T comparable](s []T) []T {
m := map[T]struct{}{}
for _, v := range s {
m[v] = struct{}{}
}
return maps.Keys(m)
}
func Read(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrapf(err, "open %s", path)
}
defer f.Close()
switch filepath.Ext(path) {
case ".gz":
gr, err := gzip.NewReader(f)
if err != nil {
return nil, errors.Wrap(err, "create gzip reader")
}
defer gr.Close()
bs, err := io.ReadAll(gr)
if err != nil {
return nil, errors.Wrap(err, "read data")
}
return bs, nil
case ".bz2":
bs, err := io.ReadAll(bzip2.NewReader(f))
if err != nil {
return nil, errors.Wrap(err, "read data")
}
return bs, nil
case ".xz":
xr, err := xz.NewReader(f)
if err != nil {
return nil, errors.Wrap(err, "create xz reader")
}
bs, err := io.ReadAll(xr)
if err != nil {
return nil, errors.Wrap(err, "read data")
}
return bs, nil
default:
bs, err := io.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read data")
}
return bs, nil
}
}