Refactoring
This commit is contained in:
527
models/cvecontents.go
Normal file
527
models/cvecontents.go
Normal file
@@ -0,0 +1,527 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CveContents has CveContent
|
||||
type CveContents map[CveContentType]CveContent
|
||||
|
||||
// NewCveContents create CveContents
|
||||
func NewCveContents(conts ...CveContent) CveContents {
|
||||
m := map[CveContentType]CveContent{}
|
||||
for _, cont := range conts {
|
||||
m[cont.Type] = cont
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// CveContentStr has CveContentType and Value
|
||||
type CveContentStr struct {
|
||||
Type CveContentType
|
||||
Value string
|
||||
}
|
||||
|
||||
// Except returns CveContents except given keys for enumeration
|
||||
func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) {
|
||||
for ctype, content := range v {
|
||||
found := false
|
||||
for _, exceptCtype := range exceptCtypes {
|
||||
if ctype == exceptCtype {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
values[ctype] = content
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CveContentCvss2 has CveContentType and Cvss2
|
||||
type CveContentCvss2 struct {
|
||||
Type CveContentType
|
||||
Value Cvss2
|
||||
}
|
||||
|
||||
// Cvss2 has CVSS v2
|
||||
type Cvss2 struct {
|
||||
Score float64
|
||||
Vector string
|
||||
Severity string
|
||||
}
|
||||
|
||||
// Format CVSS Score and Vector
|
||||
func (c Cvss2) Format() string {
|
||||
return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector)
|
||||
}
|
||||
|
||||
func cvss2ScoreToSeverity(score float64) string {
|
||||
if 7.0 <= score {
|
||||
return "HIGH"
|
||||
} else if 4.0 <= score {
|
||||
return "MEDIUM"
|
||||
}
|
||||
return "LOW"
|
||||
}
|
||||
|
||||
// Cvss2Scores returns CVSS V2 Scores
|
||||
func (v CveContents) Cvss2Scores() (values []CveContentCvss2) {
|
||||
order := []CveContentType{NVD, RedHat, JVN}
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && 0 < cont.Cvss2Score {
|
||||
// https://nvd.nist.gov/vuln-metrics/cvss
|
||||
sev := cont.Severity
|
||||
if ctype == NVD {
|
||||
sev = cvss2ScoreToSeverity(cont.Cvss2Score)
|
||||
}
|
||||
values = append(values, CveContentCvss2{
|
||||
Type: ctype,
|
||||
Value: Cvss2{
|
||||
Score: cont.Cvss2Score,
|
||||
Vector: cont.Cvss2Vector,
|
||||
Severity: sev,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MaxCvss2Score returns Max CVSS V2 Score
|
||||
func (v CveContents) MaxCvss2Score() CveContentCvss2 {
|
||||
//TODO Severity Ubuntu, Debian...
|
||||
order := []CveContentType{NVD, RedHat, JVN}
|
||||
max := 0.0
|
||||
value := CveContentCvss2{
|
||||
Type: Unknown,
|
||||
Value: Cvss2{},
|
||||
}
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && max < cont.Cvss2Score {
|
||||
// https://nvd.nist.gov/vuln-metrics/cvss
|
||||
sev := cont.Severity
|
||||
if ctype == NVD {
|
||||
sev = cvss2ScoreToSeverity(cont.Cvss2Score)
|
||||
}
|
||||
value = CveContentCvss2{
|
||||
Type: ctype,
|
||||
Value: Cvss2{
|
||||
Score: cont.Cvss2Score,
|
||||
Vector: cont.Cvss2Vector,
|
||||
Severity: sev,
|
||||
},
|
||||
}
|
||||
max = cont.Cvss2Score
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// CveContentCvss3 has CveContentType and Cvss3
|
||||
type CveContentCvss3 struct {
|
||||
Type CveContentType
|
||||
Value Cvss3
|
||||
}
|
||||
|
||||
// Cvss3 has CVSS v3 Score, Vector and Severity
|
||||
type Cvss3 struct {
|
||||
Score float64
|
||||
Vector string
|
||||
Severity string
|
||||
}
|
||||
|
||||
// Format CVSS Score and Vector
|
||||
func (c Cvss3) Format() string {
|
||||
return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector)
|
||||
}
|
||||
|
||||
func cvss3ScoreToSeverity(score float64) string {
|
||||
if 9.0 <= score {
|
||||
return "CRITICAL"
|
||||
} else if 7.0 <= score {
|
||||
return "HIGH"
|
||||
} else if 4.0 <= score {
|
||||
return "MEDIUM"
|
||||
}
|
||||
return "LOW"
|
||||
}
|
||||
|
||||
// Cvss3Scores returns CVSS V3 Score
|
||||
func (v CveContents) Cvss3Scores() (values []CveContentCvss3) {
|
||||
//TODO Severity Ubuntu, Debian...
|
||||
order := []CveContentType{RedHat}
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && 0 < cont.Cvss3Score {
|
||||
// https://nvd.nist.gov/vuln-metrics/cvss
|
||||
sev := cont.Severity
|
||||
if ctype == NVD {
|
||||
sev = cvss3ScoreToSeverity(cont.Cvss2Score)
|
||||
}
|
||||
values = append(values, CveContentCvss3{
|
||||
Type: ctype,
|
||||
Value: Cvss3{
|
||||
Score: cont.Cvss3Score,
|
||||
Vector: cont.Cvss3Vector,
|
||||
Severity: sev,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MaxCvss3Score returns Max CVSS V3 Score
|
||||
func (v CveContents) MaxCvss3Score() CveContentCvss3 {
|
||||
//TODO Severity Ubuntu, Debian...
|
||||
order := []CveContentType{RedHat}
|
||||
max := 0.0
|
||||
value := CveContentCvss3{
|
||||
Type: Unknown,
|
||||
Value: Cvss3{},
|
||||
}
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && max < cont.Cvss3Score {
|
||||
// https://nvd.nist.gov/vuln-metrics/cvss
|
||||
sev := cont.Severity
|
||||
if ctype == NVD {
|
||||
sev = cvss3ScoreToSeverity(cont.Cvss2Score)
|
||||
}
|
||||
value = CveContentCvss3{
|
||||
Type: ctype,
|
||||
Value: Cvss3{
|
||||
Score: cont.Cvss3Score,
|
||||
Vector: cont.Cvss3Vector,
|
||||
Severity: sev,
|
||||
},
|
||||
}
|
||||
max = cont.Cvss3Score
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// FormatMaxCvssScore returns Max CVSS Score
|
||||
func (v CveContents) FormatMaxCvssScore() string {
|
||||
v2Max := v.MaxCvss2Score()
|
||||
v3Max := v.MaxCvss3Score()
|
||||
if v2Max.Value.Score <= v3Max.Value.Score {
|
||||
return fmt.Sprintf("%3.1f %s (%s)",
|
||||
v3Max.Value.Score,
|
||||
strings.ToUpper(v3Max.Value.Severity),
|
||||
v3Max.Type)
|
||||
}
|
||||
return fmt.Sprintf("%3.1f %s (%s)",
|
||||
v2Max.Value.Score,
|
||||
strings.ToUpper(v2Max.Value.Severity),
|
||||
v2Max.Type)
|
||||
}
|
||||
|
||||
// Titles returns tilte (TUI)
|
||||
func (v CveContents) Titles(lang, myFamily string) (values []CveContentStr) {
|
||||
if lang == "ja" {
|
||||
if cont, found := v[JVN]; found && 0 < len(cont.Title) {
|
||||
values = append(values, CveContentStr{JVN, cont.Title})
|
||||
}
|
||||
}
|
||||
|
||||
order := CveContentTypes{NVD, NewCveContentType(myFamily)}
|
||||
order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...)
|
||||
for _, ctype := range order {
|
||||
// Only JVN has meaningful title. so return first 100 char of summary
|
||||
if cont, found := v[ctype]; found && 0 < len(cont.Summary) {
|
||||
summary := strings.Replace(cont.Summary, "\n", " ", -1)
|
||||
index := 75
|
||||
if len(summary) < index {
|
||||
index = len(summary)
|
||||
}
|
||||
values = append(values, CveContentStr{
|
||||
Type: ctype,
|
||||
Value: summary[0:index] + "...",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
values = []CveContentStr{{
|
||||
Type: Unknown,
|
||||
Value: "-",
|
||||
}}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Summaries returns summaries
|
||||
func (v CveContents) Summaries(lang, myFamily string) (values []CveContentStr) {
|
||||
if lang == "ja" {
|
||||
if cont, found := v[JVN]; found && 0 < len(cont.Summary) {
|
||||
summary := cont.Title
|
||||
summary += "\n" + strings.Replace(
|
||||
strings.Replace(cont.Summary, "\n", " ", -1), "\r", " ", -1)
|
||||
values = append(values, CveContentStr{JVN, summary})
|
||||
}
|
||||
}
|
||||
|
||||
order := CveContentTypes{NVD, NewCveContentType(myFamily)}
|
||||
order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...)
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && 0 < len(cont.Summary) {
|
||||
summary := strings.Replace(cont.Summary, "\n", " ", -1)
|
||||
values = append(values, CveContentStr{
|
||||
Type: ctype,
|
||||
Value: summary,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
values = []CveContentStr{{
|
||||
Type: Unknown,
|
||||
Value: "-",
|
||||
}}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SourceLinks returns link of source
|
||||
func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveContentStr) {
|
||||
if lang == "ja" {
|
||||
if cont, found := v[JVN]; found && !cont.Empty() {
|
||||
values = append(values, CveContentStr{JVN, cont.SourceLink})
|
||||
}
|
||||
}
|
||||
|
||||
order := CveContentTypes{NVD, NewCveContentType(myFamily)}
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found {
|
||||
values = append(values, CveContentStr{ctype, cont.SourceLink})
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return []CveContentStr{{
|
||||
Type: NVD,
|
||||
Value: "https://nvd.nist.gov/vuln/detail/" + cveID,
|
||||
}}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// VendorLink returns link of source
|
||||
func (v CveContents) VendorLink(myFamily string) CveContentStr {
|
||||
ctype := NewCveContentType(myFamily)
|
||||
if cont, ok := v[ctype]; ok {
|
||||
return CveContentStr{ctype, cont.SourceLink}
|
||||
}
|
||||
return CveContentStr{ctype, ""}
|
||||
}
|
||||
|
||||
// Severities returns Severities
|
||||
// func (v CveContents) Severities(myFamily string) (values []CveContentValue) {
|
||||
// order := CveContentTypes{NVD, NewCveContentType(myFamily)}
|
||||
// order = append(order, AllCveContetTypes.Except(append(order)...)...)
|
||||
|
||||
// for _, ctype := range order {
|
||||
// if cont, found := v[ctype]; found && 0 < len(cont.Severity) {
|
||||
// values = append(values, CveContentValue{
|
||||
// Type: ctype,
|
||||
// Value: cont.Severity,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
// CveContentCpes has CveContentType and Value
|
||||
type CveContentCpes struct {
|
||||
Type CveContentType
|
||||
Value []Cpe
|
||||
}
|
||||
|
||||
// Cpes returns affected CPEs of this Vulnerability
|
||||
func (v CveContents) Cpes(myFamily string) (values []CveContentCpes) {
|
||||
order := CveContentTypes{NewCveContentType(myFamily)}
|
||||
order = append(order, AllCveContetTypes.Except(append(order)...)...)
|
||||
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && 0 < len(cont.Cpes) {
|
||||
values = append(values, CveContentCpes{
|
||||
Type: ctype,
|
||||
Value: cont.Cpes,
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CveContentRefs has CveContentType and Cpes
|
||||
type CveContentRefs struct {
|
||||
Type CveContentType
|
||||
Value []Reference
|
||||
}
|
||||
|
||||
// References returns References
|
||||
func (v CveContents) References(myFamily string) (values []CveContentRefs) {
|
||||
order := CveContentTypes{NewCveContentType(myFamily)}
|
||||
order = append(order, AllCveContetTypes.Except(append(order)...)...)
|
||||
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && 0 < len(cont.References) {
|
||||
values = append(values, CveContentRefs{
|
||||
Type: ctype,
|
||||
Value: cont.References,
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CweIDs returns related CweIDs of the vulnerability
|
||||
func (v CveContents) CweIDs(myFamily string) (values []CveContentStr) {
|
||||
order := CveContentTypes{NewCveContentType(myFamily)}
|
||||
order = append(order, AllCveContetTypes.Except(append(order)...)...)
|
||||
|
||||
for _, ctype := range order {
|
||||
if cont, found := v[ctype]; found && 0 < len(cont.CweID) {
|
||||
// RedHat's OVAL sometimes contains multiple CWE-IDs separated by spaces
|
||||
for _, cweID := range strings.Fields(cont.CweID) {
|
||||
values = append(values, CveContentStr{
|
||||
Type: ctype,
|
||||
Value: cweID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CveContent has abstraction of various vulnerability information
|
||||
type CveContent struct {
|
||||
Type CveContentType
|
||||
CveID string
|
||||
Title string
|
||||
Summary string
|
||||
Severity string
|
||||
Cvss2Score float64
|
||||
Cvss2Vector string
|
||||
Cvss3Score float64
|
||||
Cvss3Vector string
|
||||
SourceLink string
|
||||
Cpes []Cpe
|
||||
References References
|
||||
CweID string
|
||||
Published time.Time
|
||||
LastModified time.Time
|
||||
}
|
||||
|
||||
// Empty checks the content is empty
|
||||
func (c CveContent) Empty() bool {
|
||||
return c.Summary == ""
|
||||
}
|
||||
|
||||
// CveContentType is a source of CVE information
|
||||
type CveContentType string
|
||||
|
||||
// NewCveContentType create CveContentType
|
||||
func NewCveContentType(name string) CveContentType {
|
||||
switch name {
|
||||
case "nvd":
|
||||
return NVD
|
||||
case "jvn":
|
||||
return JVN
|
||||
case "redhat", "centos":
|
||||
return RedHat
|
||||
case "ubuntu":
|
||||
return Ubuntu
|
||||
case "debian":
|
||||
return Debian
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// NVD is NVD
|
||||
NVD CveContentType = "nvd"
|
||||
|
||||
// JVN is JVN
|
||||
JVN CveContentType = "jvn"
|
||||
|
||||
// RedHat is RedHat
|
||||
RedHat CveContentType = "redhat"
|
||||
|
||||
// Debian is Debian
|
||||
Debian CveContentType = "debian"
|
||||
|
||||
// Ubuntu is Ubuntu
|
||||
Ubuntu CveContentType = "ubuntu"
|
||||
|
||||
// Unknown is Unknown
|
||||
Unknown CveContentType = "unknown"
|
||||
)
|
||||
|
||||
// CveContentTypes has slide of CveContentType
|
||||
type CveContentTypes []CveContentType
|
||||
|
||||
// AllCveContetTypes has all of CveContentTypes
|
||||
var AllCveContetTypes = CveContentTypes{NVD, JVN, RedHat, Debian, Ubuntu}
|
||||
|
||||
// Except returns CveContentTypes except for given args
|
||||
func (c CveContentTypes) Except(excepts ...CveContentType) (excepted CveContentTypes) {
|
||||
for _, ctype := range c {
|
||||
found := false
|
||||
for _, except := range excepts {
|
||||
if ctype == except {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
excepted = append(excepted, ctype)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Cpe is Common Platform Enumeration
|
||||
type Cpe struct {
|
||||
CpeName string
|
||||
}
|
||||
|
||||
// References is a slice of Reference
|
||||
type References []Reference
|
||||
|
||||
// Find elements that matches the function passed in argument
|
||||
func (r References) Find(f func(r Reference) bool) (refs []Reference) {
|
||||
for _, rr := range r {
|
||||
refs = append(refs, rr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Reference has a related link of the CVE
|
||||
type Reference struct {
|
||||
Source string
|
||||
Link string
|
||||
RefID string
|
||||
}
|
||||
17
models/cvecontents_test.go
Normal file
17
models/cvecontents_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package models
|
||||
1012
models/models.go
1012
models/models.go
File diff suppressed because it is too large
Load Diff
@@ -16,45 +16,3 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/k0kubun/pp"
|
||||
)
|
||||
|
||||
func TestMergeNewVersion(t *testing.T) {
|
||||
var test = struct {
|
||||
a Packages
|
||||
b Packages
|
||||
expected Packages
|
||||
}{
|
||||
Packages{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
},
|
||||
},
|
||||
Packages{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
NewVersion: "1.0.0",
|
||||
NewRelease: "release1",
|
||||
},
|
||||
},
|
||||
Packages{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
NewVersion: "1.0.0",
|
||||
NewRelease: "release1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test.a.MergeNewVersion(test.b)
|
||||
if !reflect.DeepEqual(test.a, test.expected) {
|
||||
e := pp.Sprintf("%v", test.a)
|
||||
a := pp.Sprintf("%v", test.expected)
|
||||
t.Errorf("expected %s, actual %s", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
127
models/packages.go
Normal file
127
models/packages.go
Normal file
@@ -0,0 +1,127 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Packages is Map of Package
|
||||
// { "package-name": Package }
|
||||
type Packages map[string]Package
|
||||
|
||||
// NewPackages create Packages
|
||||
func NewPackages(packs ...Package) Packages {
|
||||
m := Packages{}
|
||||
for _, pack := range packs {
|
||||
m[pack.Name] = pack
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// MergeNewVersion merges candidate version information to the receiver struct
|
||||
func (ps Packages) MergeNewVersion(as Packages) {
|
||||
for _, a := range as {
|
||||
if pack, ok := ps[a.Name]; ok {
|
||||
pack.NewVersion = a.NewVersion
|
||||
pack.NewRelease = a.NewRelease
|
||||
ps[a.Name] = pack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge returns merged map (immutable)
|
||||
func (ps Packages) Merge(other Packages) Packages {
|
||||
merged := map[string]Package{}
|
||||
for k, v := range ps {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range other {
|
||||
merged[k] = v
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// FormatVersionsFromTo returns updatable packages
|
||||
func (ps Packages) FormatVersionsFromTo() string {
|
||||
ss := []string{}
|
||||
for _, pack := range ps {
|
||||
ss = append(ss, pack.FormatVersionFromTo())
|
||||
}
|
||||
return strings.Join(ss, "\n")
|
||||
}
|
||||
|
||||
// FormatUpdatablePacksSummary returns a summary of updatable packages
|
||||
func (ps Packages) FormatUpdatablePacksSummary() string {
|
||||
nUpdatable := 0
|
||||
for _, p := range ps {
|
||||
if p.NewVersion != "" {
|
||||
nUpdatable++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%d updatable packages", nUpdatable)
|
||||
}
|
||||
|
||||
// Package has installed packages.
|
||||
type Package struct {
|
||||
Name string
|
||||
Version string
|
||||
Release string
|
||||
NewVersion string
|
||||
NewRelease string
|
||||
Repository string
|
||||
Changelog Changelog
|
||||
NotFixedYet bool // Ubuntu OVAL Only
|
||||
}
|
||||
|
||||
// FormatVer returns package name-version-release
|
||||
func (p Package) FormatVer() string {
|
||||
str := p.Name
|
||||
if 0 < len(p.Version) {
|
||||
str = fmt.Sprintf("%s-%s", str, p.Version)
|
||||
}
|
||||
if 0 < len(p.Release) {
|
||||
str = fmt.Sprintf("%s-%s", str, p.Release)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// FormatNewVer returns package name-version-release
|
||||
func (p Package) FormatNewVer() string {
|
||||
str := p.Name
|
||||
if 0 < len(p.NewVersion) {
|
||||
str = fmt.Sprintf("%s-%s", str, p.NewVersion)
|
||||
}
|
||||
if 0 < len(p.NewRelease) {
|
||||
str = fmt.Sprintf("%s-%s", str, p.NewRelease)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// FormatVersionFromTo formats installed and new package version
|
||||
func (p Package) FormatVersionFromTo() string {
|
||||
return fmt.Sprintf("%s -> %s", p.FormatVer(), p.FormatNewVer())
|
||||
}
|
||||
|
||||
// Changelog has contents of changelog and how to get it.
|
||||
// Method: modesl.detectionMethodStr
|
||||
type Changelog struct {
|
||||
Contents string
|
||||
Method string
|
||||
}
|
||||
59
models/packages_test.go
Normal file
59
models/packages_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/k0kubun/pp"
|
||||
)
|
||||
|
||||
func TestMergeNewVersion(t *testing.T) {
|
||||
var test = struct {
|
||||
a Packages
|
||||
b Packages
|
||||
expected Packages
|
||||
}{
|
||||
Packages{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
},
|
||||
},
|
||||
Packages{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
NewVersion: "1.0.0",
|
||||
NewRelease: "release1",
|
||||
},
|
||||
},
|
||||
Packages{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
NewVersion: "1.0.0",
|
||||
NewRelease: "release1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test.a.MergeNewVersion(test.b)
|
||||
if !reflect.DeepEqual(test.a, test.expected) {
|
||||
e := pp.Sprintf("%v", test.a)
|
||||
a := pp.Sprintf("%v", test.expected)
|
||||
t.Errorf("expected %s, actual %s", e, a)
|
||||
}
|
||||
}
|
||||
297
models/scanresults.go
Normal file
297
models/scanresults.go
Normal file
@@ -0,0 +1,297 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/future-architect/vuls/config"
|
||||
cvedict "github.com/kotakanbe/go-cve-dictionary/models"
|
||||
)
|
||||
|
||||
// ScanResults is slice of ScanResult.
|
||||
type ScanResults []ScanResult
|
||||
|
||||
//TODO
|
||||
// // Len implement Sort Interface
|
||||
// func (s ScanResults) Len() int {
|
||||
// return len(s)
|
||||
// }
|
||||
|
||||
// // Swap implement Sort Interface
|
||||
// func (s ScanResults) Swap(i, j int) {
|
||||
// s[i], s[j] = s[j], s[i]
|
||||
// }
|
||||
|
||||
// // Less implement Sort Interface
|
||||
// func (s ScanResults) Less(i, j int) bool {
|
||||
// if s[i].ServerName == s[j].ServerName {
|
||||
// return s[i].Container.ContainerID < s[i].Container.ContainerID
|
||||
// }
|
||||
// return s[i].ServerName < s[j].ServerName
|
||||
// }
|
||||
|
||||
// ScanResult has the result of scanned CVE information.
|
||||
type ScanResult struct {
|
||||
ScannedAt time.Time
|
||||
JSONVersion string
|
||||
Lang string
|
||||
ServerName string // TOML Section key
|
||||
Family string
|
||||
Release string
|
||||
Container Container
|
||||
Platform Platform
|
||||
|
||||
// Scanned Vulns by SSH scan + CPE + OVAL
|
||||
ScannedCves VulnInfos
|
||||
|
||||
Packages Packages
|
||||
Errors []string
|
||||
Optional [][]interface{}
|
||||
}
|
||||
|
||||
// ConvertNvdToModel convert NVD to CveContent
|
||||
func (r ScanResult) ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent {
|
||||
var cpes []Cpe
|
||||
for _, c := range nvd.Cpes {
|
||||
cpes = append(cpes, Cpe{CpeName: c.CpeName})
|
||||
}
|
||||
|
||||
var refs []Reference
|
||||
for _, r := range nvd.References {
|
||||
refs = append(refs, Reference{
|
||||
Link: r.Link,
|
||||
Source: r.Source,
|
||||
})
|
||||
}
|
||||
|
||||
validVec := true
|
||||
for _, v := range []string{
|
||||
nvd.AccessVector,
|
||||
nvd.AccessComplexity,
|
||||
nvd.Authentication,
|
||||
nvd.ConfidentialityImpact,
|
||||
nvd.IntegrityImpact,
|
||||
nvd.AvailabilityImpact,
|
||||
} {
|
||||
if len(v) == 0 {
|
||||
validVec = false
|
||||
}
|
||||
}
|
||||
|
||||
vector := ""
|
||||
if validVec {
|
||||
vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s",
|
||||
string(nvd.AccessVector[0]),
|
||||
string(nvd.AccessComplexity[0]),
|
||||
string(nvd.Authentication[0]),
|
||||
string(nvd.ConfidentialityImpact[0]),
|
||||
string(nvd.IntegrityImpact[0]),
|
||||
string(nvd.AvailabilityImpact[0]))
|
||||
}
|
||||
|
||||
//TODO CVSSv3
|
||||
return &CveContent{
|
||||
Type: NVD,
|
||||
CveID: cveID,
|
||||
Summary: nvd.Summary,
|
||||
Cvss2Score: nvd.Score,
|
||||
Cvss2Vector: vector,
|
||||
Severity: "", // severity is not contained in NVD
|
||||
SourceLink: "https://nvd.nist.gov/vuln/detail/" + cveID,
|
||||
Cpes: cpes,
|
||||
CweID: nvd.CweID,
|
||||
References: refs,
|
||||
Published: nvd.PublishedDate,
|
||||
LastModified: nvd.LastModifiedDate,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertJvnToModel convert JVN to CveContent
|
||||
func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent {
|
||||
var cpes []Cpe
|
||||
for _, c := range jvn.Cpes {
|
||||
cpes = append(cpes, Cpe{CpeName: c.CpeName})
|
||||
}
|
||||
|
||||
refs := []Reference{}
|
||||
for _, r := range jvn.References {
|
||||
refs = append(refs, Reference{
|
||||
Link: r.Link,
|
||||
Source: r.Source,
|
||||
})
|
||||
}
|
||||
|
||||
vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")")
|
||||
return &CveContent{
|
||||
Type: JVN,
|
||||
CveID: cveID,
|
||||
Title: jvn.Title,
|
||||
Summary: jvn.Summary,
|
||||
Severity: jvn.Severity,
|
||||
Cvss2Score: jvn.Score,
|
||||
Cvss2Vector: vector,
|
||||
SourceLink: jvn.JvnLink,
|
||||
Cpes: cpes,
|
||||
References: refs,
|
||||
Published: jvn.PublishedDate,
|
||||
LastModified: jvn.LastModifiedDate,
|
||||
}
|
||||
}
|
||||
|
||||
// FilterByCvssOver is filter function.
|
||||
func (r ScanResult) FilterByCvssOver(over float64) ScanResult {
|
||||
// TODO: Set correct default value
|
||||
if over == 0 {
|
||||
over = -1.1
|
||||
}
|
||||
|
||||
// TODO: Filter by ignore cves???
|
||||
filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
|
||||
//TODO in the case of only oval, no cvecontents
|
||||
values := v.CveContents.Cvss2Scores()
|
||||
for _, vals := range values {
|
||||
score := vals.Value.Score
|
||||
if over <= score {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
copiedScanResult := r
|
||||
copiedScanResult.ScannedCves = filtered
|
||||
return copiedScanResult
|
||||
}
|
||||
|
||||
// ReportFileName returns the filename on localhost without extention
|
||||
func (r ScanResult) ReportFileName() (name string) {
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
return fmt.Sprintf("%s", r.ServerName)
|
||||
}
|
||||
return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName)
|
||||
}
|
||||
|
||||
// ReportKeyName returns the name of key on S3, Azure-Blob without extention
|
||||
func (r ScanResult) ReportKeyName() (name string) {
|
||||
timestr := r.ScannedAt.Format(time.RFC3339)
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
return fmt.Sprintf("%s/%s", timestr, r.ServerName)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName)
|
||||
}
|
||||
|
||||
// ServerInfo returns server name one line
|
||||
func (r ScanResult) ServerInfo() string {
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
return fmt.Sprintf("%s (%s%s)",
|
||||
r.ServerName, r.Family, r.Release)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s / %s (%s%s) on %s",
|
||||
r.Container.Name,
|
||||
r.Container.ContainerID,
|
||||
r.Family,
|
||||
r.Release,
|
||||
r.ServerName,
|
||||
)
|
||||
}
|
||||
|
||||
// ServerInfoTui returns server infromation for TUI sidebar
|
||||
func (r ScanResult) ServerInfoTui() string {
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
return fmt.Sprintf("%s (%s%s)",
|
||||
r.ServerName, r.Family, r.Release)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"|-- %s (%s%s)",
|
||||
r.Container.Name,
|
||||
r.Family,
|
||||
r.Release,
|
||||
// r.Container.ContainerID,
|
||||
)
|
||||
}
|
||||
|
||||
// FormatServerName returns server and container name
|
||||
func (r ScanResult) FormatServerName() string {
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
return r.ServerName
|
||||
}
|
||||
return fmt.Sprintf("%s@%s",
|
||||
r.Container.Name, r.ServerName)
|
||||
}
|
||||
|
||||
// CveSummary summarize the number of CVEs group by CVSSv2 Severity
|
||||
func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string {
|
||||
var high, medium, low, unknown int
|
||||
for _, vInfo := range r.ScannedCves {
|
||||
score := vInfo.CveContents.MaxCvss2Score().Value.Score
|
||||
if score < 0.1 {
|
||||
score = vInfo.CveContents.MaxCvss3Score().Value.Score
|
||||
}
|
||||
switch {
|
||||
case 7.0 <= score:
|
||||
high++
|
||||
case 4.0 <= score:
|
||||
medium++
|
||||
case 0 < score:
|
||||
low++
|
||||
default:
|
||||
unknown++
|
||||
}
|
||||
}
|
||||
|
||||
if ignoreUnscoreCves {
|
||||
return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)",
|
||||
high+medium+low, high, medium, low)
|
||||
}
|
||||
return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
|
||||
high+medium+low+unknown, high, medium, low, unknown)
|
||||
}
|
||||
|
||||
// FormatTextReportHeadedr returns header of text report
|
||||
func (r ScanResult) FormatTextReportHeadedr() string {
|
||||
serverInfo := r.ServerInfo()
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < len(serverInfo); i++ {
|
||||
buf.WriteString("=")
|
||||
}
|
||||
return fmt.Sprintf("%s\n%s\n%s\t%s\n",
|
||||
r.ServerInfo(),
|
||||
buf.String(),
|
||||
r.CveSummary(config.Conf.IgnoreUnscoredCves),
|
||||
r.Packages.FormatUpdatablePacksSummary(),
|
||||
)
|
||||
}
|
||||
|
||||
// Container has Container information
|
||||
type Container struct {
|
||||
ContainerID string
|
||||
Name string
|
||||
Image string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Platform has platform information
|
||||
type Platform struct {
|
||||
Name string // aws or azure or gcp or other...
|
||||
InstanceID string
|
||||
}
|
||||
17
models/scanresults_test.go
Normal file
17
models/scanresults_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package models
|
||||
150
models/vulninfos.go
Normal file
150
models/vulninfos.go
Normal file
@@ -0,0 +1,150 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// VulnInfos is VulnInfo list, getter/setter, sortable methods.
|
||||
type VulnInfos map[string]VulnInfo
|
||||
|
||||
// Find elements that matches the function passed in argument
|
||||
func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos {
|
||||
filtered := VulnInfos{}
|
||||
for _, vv := range v {
|
||||
if f(vv) {
|
||||
filtered[vv.CveID] = vv
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// FindScoredVulns return socred vulnerabilities
|
||||
func (v VulnInfos) FindScoredVulns() VulnInfos {
|
||||
return v.Find(func(vv VulnInfo) bool {
|
||||
if 0 < vv.CveContents.MaxCvss2Score().Value.Score ||
|
||||
0 < vv.CveContents.MaxCvss3Score().Value.Score {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// VulnInfo holds a vulnerability information and unsecure packages
|
||||
type VulnInfo struct {
|
||||
CveID string
|
||||
Confidence Confidence
|
||||
PackageNames []string
|
||||
DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD
|
||||
CpeNames []string
|
||||
CveContents CveContents
|
||||
}
|
||||
|
||||
// Cvss2CalcURL returns CVSS v2 caluclator's URL
|
||||
func (v VulnInfo) Cvss2CalcURL() string {
|
||||
return "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=" + v.CveID
|
||||
}
|
||||
|
||||
// Cvss3CalcURL returns CVSS v3 caluclator's URL
|
||||
func (v VulnInfo) Cvss3CalcURL() string {
|
||||
return "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=" + v.CveID
|
||||
}
|
||||
|
||||
// TODO
|
||||
// NilToEmpty set nil slice or map fields to empty to avoid null in JSON
|
||||
// func (v *VulnInfo) NilToEmpty() {
|
||||
// if v.CpeNames == nil {
|
||||
// v.CpeNames = []string{}
|
||||
// }
|
||||
// if v.DistroAdvisories == nil {
|
||||
// v.DistroAdvisories = []DistroAdvisory{}
|
||||
// }
|
||||
// if v.PackageNames == nil {
|
||||
// v.PackageNames = []string{}
|
||||
// }
|
||||
// if v.CveContents == nil {
|
||||
// v.CveContents = NewCveContents()
|
||||
// }
|
||||
// }
|
||||
|
||||
// DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information.
|
||||
type DistroAdvisory struct {
|
||||
AdvisoryID string
|
||||
Severity string
|
||||
Issued time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
// Confidence is a ranking how confident the CVE-ID was deteted correctly
|
||||
// Score: 0 - 100
|
||||
type Confidence struct {
|
||||
Score int
|
||||
DetectionMethod string
|
||||
}
|
||||
|
||||
func (c Confidence) String() string {
|
||||
return fmt.Sprintf("%d / %s", c.Score, c.DetectionMethod)
|
||||
}
|
||||
|
||||
const (
|
||||
// CpeNameMatchStr is a String representation of CpeNameMatch
|
||||
CpeNameMatchStr = "CpeNameMatch"
|
||||
|
||||
// YumUpdateSecurityMatchStr is a String representation of YumUpdateSecurityMatch
|
||||
YumUpdateSecurityMatchStr = "YumUpdateSecurityMatch"
|
||||
|
||||
// PkgAuditMatchStr is a String representation of PkgAuditMatch
|
||||
PkgAuditMatchStr = "PkgAuditMatch"
|
||||
|
||||
// OvalMatchStr is a String representation of OvalMatch
|
||||
OvalMatchStr = "OvalMatch"
|
||||
|
||||
// ChangelogExactMatchStr is a String representation of ChangelogExactMatch
|
||||
ChangelogExactMatchStr = "ChangelogExactMatch"
|
||||
|
||||
// ChangelogLenientMatchStr is a String representation of ChangelogLenientMatch
|
||||
ChangelogLenientMatchStr = "ChangelogLenientMatch"
|
||||
|
||||
// FailedToGetChangelog is a String representation of FailedToGetChangelog
|
||||
FailedToGetChangelog = "FailedToGetChangelog"
|
||||
|
||||
// FailedToFindVersionInChangelog is a String representation of FailedToFindVersionInChangelog
|
||||
FailedToFindVersionInChangelog = "FailedToFindVersionInChangelog"
|
||||
)
|
||||
|
||||
var (
|
||||
// CpeNameMatch is a ranking how confident the CVE-ID was deteted correctly
|
||||
CpeNameMatch = Confidence{100, CpeNameMatchStr}
|
||||
|
||||
// YumUpdateSecurityMatch is a ranking how confident the CVE-ID was deteted correctly
|
||||
YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr}
|
||||
|
||||
// PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly
|
||||
PkgAuditMatch = Confidence{100, PkgAuditMatchStr}
|
||||
|
||||
// OvalMatch is a ranking how confident the CVE-ID was deteted correctly
|
||||
OvalMatch = Confidence{100, OvalMatchStr}
|
||||
|
||||
// ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly
|
||||
ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr}
|
||||
|
||||
// ChangelogLenientMatch is a ranking how confident the CVE-ID was deteted correctly
|
||||
ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr}
|
||||
)
|
||||
17
models/vulninfos_test.go
Normal file
17
models/vulninfos_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package models
|
||||
Reference in New Issue
Block a user