1package commands
2
3import (
4	"context"
5	"flag"
6	"os"
7	"strconv"
8	"time"
9
10	"github.com/google/subcommands"
11	"github.com/kotakanbe/go-cve-dictionary/config"
12	c "github.com/kotakanbe/go-cve-dictionary/config"
13	db "github.com/kotakanbe/go-cve-dictionary/db"
14	jvn "github.com/kotakanbe/go-cve-dictionary/fetcher/jvn/xml"
15	log "github.com/kotakanbe/go-cve-dictionary/log"
16	"github.com/kotakanbe/go-cve-dictionary/models"
17	util "github.com/kotakanbe/go-cve-dictionary/util"
18)
19
20// FetchJvnCmd is Subcommand for fetch JVN information.
21type FetchJvnCmd struct {
22	debug     bool
23	debugSQL  bool
24	quiet     bool
25	logDir    string
26	logJSON   bool
27	dbpath    string
28	dbtype    string
29	dumpPath  string
30	latest    bool
31	last2Y    bool
32	years     bool
33	httpProxy string
34	light     bool
35	force     bool
36}
37
38// Name return subcommand name
39func (*FetchJvnCmd) Name() string { return "fetchjvn" }
40
41// Synopsis return synopsis
42func (*FetchJvnCmd) Synopsis() string { return "Fetch Vulnerability dictionary from JVN" }
43
44// Usage return usage
45func (*FetchJvnCmd) Usage() string {
46	return `fetchjvn:
47	fetchjvn
48		[-latest]
49		[-last2y]
50		[-years] 1998 1999 ...
51		[-dbpath=$PWD/cve.sqlite3 or connection string]
52		[-dbtype=mysql|postgres|sqlite3|redis]
53		[-http-proxy=http://192.168.0.1:8080]
54		[-debug]
55		[-debug-sql]
56		[-quiet]
57		[-log-dir=/path/to/log]
58		[-log-json]
59		[-light]
60		[-force]
61
62`
63}
64
65// SetFlags set flag
66func (p *FetchJvnCmd) SetFlags(f *flag.FlagSet) {
67	f.BoolVar(&p.debug, "debug", false, "debug mode")
68	f.BoolVar(&p.debugSQL, "debug-sql", false, "SQL debug mode")
69	f.BoolVar(&p.quiet, "quiet", false, "quiet mode (no output)")
70
71	defaultLogDir := util.GetDefaultLogDir()
72	f.StringVar(&p.logDir, "log-dir", defaultLogDir, "/path/to/log")
73	f.BoolVar(&p.logJSON, "log-json", false, "output log as JSON")
74
75	pwd := os.Getenv("PWD")
76	f.StringVar(&p.dbpath, "dbpath", pwd+"/cve.sqlite3",
77		"/path/to/sqlite3 or SQL connection string")
78
79	f.StringVar(&p.dbtype, "dbtype", "sqlite3",
80		"Database type to store data in (sqlite3,  mysql, postgres or redis supported)")
81
82	f.BoolVar(&p.latest, "latest", false,
83		"Refresh JVN data for latest.")
84
85	f.BoolVar(&p.last2Y, "last2y", false,
86		"Refresh JVN data in the last two years.")
87
88	f.BoolVar(&p.years, "years", false,
89		"Refresh JVN data of specific years.")
90
91	f.StringVar(
92		&p.httpProxy,
93		"http-proxy",
94		"",
95		"http://proxy-url:port (default: empty)",
96	)
97
98	f.BoolVar(&p.light, "light", false, "Don't collect *HEAVY* CPE relate data")
99	f.BoolVar(&p.force, "force", false, "Force update")
100}
101
102// Execute execute
103func (p *FetchJvnCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
104	c.Conf.Debug = p.debug
105	c.Conf.Quiet = p.quiet
106	c.Conf.DebugSQL = p.debugSQL
107	c.Conf.DBPath = p.dbpath
108	c.Conf.DBType = p.dbtype
109	//  c.Conf.DumpPath = p.dumpPath
110	c.Conf.HTTPProxy = p.httpProxy
111	c.Conf.Light = p.light
112	c.Conf.Force = p.force
113
114	log.SetLogger(p.logDir, c.Conf.Quiet, c.Conf.Debug, p.logJSON)
115	if !c.Conf.Validate() {
116		return subcommands.ExitUsageError
117	}
118
119	years := []int{}
120	thisYear := time.Now().Year()
121
122	switch {
123	case p.latest:
124		years = append(years, c.Latest)
125	case p.last2Y:
126		for i := 0; i < 2; i++ {
127			years = append(years, thisYear-i)
128		}
129		years = append(years, c.Latest)
130	case p.years:
131		if len(f.Args()) == 0 {
132			log.Errorf("Specify years to fetch (from 1998 to %d)", thisYear)
133			return subcommands.ExitUsageError
134		}
135		for _, arg := range f.Args() {
136			year, err := strconv.Atoi(arg)
137			if err != nil || year < 1998 || time.Now().Year() < year {
138				log.Errorf("Specify years to fetch (from 1998 to %d), arg: %s", thisYear, arg)
139				return subcommands.ExitUsageError
140			}
141			found := false
142			for _, y := range years {
143				if y == year {
144					found = true
145					break
146				}
147			}
148			if !found {
149				years = append(years, year)
150			}
151		}
152		years = append(years, c.Latest)
153	default:
154		log.Errorf("specify -latest, -last2y or -years")
155		return subcommands.ExitUsageError
156	}
157
158	driver, locked, err := db.NewDB(c.Conf.DBType, c.Conf.DBPath, c.Conf.DebugSQL)
159	if err != nil {
160		if locked {
161			log.Errorf("Failed to Open DB. Close DB connection before fetching: %s", err)
162			return subcommands.ExitFailure
163		}
164		log.Errorf("%s", err)
165		return subcommands.ExitFailure
166	}
167	defer func() {
168		_ = driver.CloseDB()
169	}()
170
171	metas, err := jvn.FetchLatestFeedMeta(driver, years)
172	if err != nil {
173		log.Errorf("%s", err)
174		return subcommands.ExitFailure
175	}
176
177	if len(metas) == 0 {
178		log.Errorf("No meta files fetched")
179		return subcommands.ExitFailure
180	}
181
182	//TODO use meta.Status()
183	needUpdates := []models.FeedMeta{}
184	if config.Conf.Force {
185		needUpdates = metas
186	} else {
187		for _, m := range metas {
188			if m.Newly() {
189				needUpdates = append(needUpdates, m)
190				log.Infof("Newly     : %s", m.URL)
191			} else if m.OutDated() {
192				needUpdates = append(needUpdates, m)
193				log.Infof("Outdated  : %s", m.URL)
194			} else {
195				log.Infof("Up to date: %s", m.URL)
196			}
197		}
198	}
199
200	if len(needUpdates) == 0 {
201		log.Infof("Already up to date")
202		return subcommands.ExitSuccess
203	}
204
205	log.Infof("Fetching CVE information from JVN.")
206	cves, err := jvn.FetchConvert(needUpdates)
207	if err != nil {
208		log.Errorf("Failed to fetch JVN: %s", err)
209		return subcommands.ExitFailure
210	}
211	log.Infof("Fetched %d CVEs", len(cves))
212
213	log.Infof("Inserting JVN into DB (%s).", driver.Name())
214	if err := driver.InsertJvn(cves); err != nil {
215		log.Fatalf("Failed to insert. dbpath: %s, err: %s", c.Conf.DBPath, err)
216		return subcommands.ExitFailure
217	}
218
219	if err := jvn.UpdateMeta(driver, needUpdates); err != nil {
220		log.Fatalf("Failed to Update meta. dbpath: %s, err: %s", c.Conf.DBPath, err)
221		return subcommands.ExitFailure
222	}
223	return subcommands.ExitSuccess
224}
225