diff --git a/cmd/vulndb/diffcmd.go b/cmd/vulndb/diffcmd.go new file mode 100644 index 0000000..dc0e6e9 --- /dev/null +++ b/cmd/vulndb/diffcmd.go @@ -0,0 +1,121 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "path/filepath" + "sort" + + "github.com/facebookincubator/nvdtools/cvefeed" + "github.com/spf13/cobra" +) + +func init() { + RootCmd.AddCommand(diffCmd) +} + +func feedLoad(file string) (cvefeed.Dictionary, error) { + log.Printf("loading %s\n", file) + dict, err := cvefeed.LoadJSONDictionary(file) + if err != nil { + return nil, fmt.Errorf("failed to load dictionary %s: %v", file, err) + } + return dict, nil +} + +func feedName(file string) string { + return filepath.Base(file[:len(file)-5]) +} + +func printArraySorted(a []string, indent string, n int) { + min := func(a, b int) int { + if a <= b { + return a + } + return b + } + + sort.Strings(a) + for i := 0; i < min(len(a), n); i++ { + fmt.Printf("%s%s\n", indent, a[i]) + } + if len(a) > n { + fmt.Printf("%s... (%d more)\n", indent, len(a)-10) + } +} + +var diffCmd = &cobra.Command{ + Use: "diff [flags] a.json b.json", + Short: "diff two vulnerability feeds", + RunE: func(cmd *cobra.Command, args []string) error { + percentInt := func(a, b int) float64 { + return float64(a) / float64(b) * 100 + } + + if len(args) != 2 { + return errors.New("missing JSON export files") + } + + aDict, err := feedLoad(args[0]) + if err != nil { + return err + } + + bDict, err := feedLoad(args[1]) + + if err != nil { + return err + } + + log.Println("computing stats") + + a := feedName(args[0]) + b := feedName(args[1]) + + stats := cvefeed.Diff(a, aDict, b, bDict) + + fmt.Printf("Num vulnerabilities in %s: %d\n", a, stats.NumVulnsA()) + fmt.Printf("Num vulnerabilities in %s: %d\n", b, stats.NumVulnsB()) + fmt.Printf("Num vulnerabilities in %s but not in %s: %d\n", a, b, stats.NumVulnsANotB()) + printArraySorted(stats.VulnsANotB(), " ", 10) + fmt.Printf("Num vulnerabilities in %s but not in %s: %d\n", b, a, stats.NumVulnsBNotA()) + printArraySorted(stats.VulnsBNotA(), " ", 10) + fmt.Println() + fmt.Printf("Different vulnerabilities: %d\n", stats.NumDiffVulns()) + fmt.Printf(" different descriptions: %d (%.2f%%, total %.2f%%)\n", + stats.NumChunk(cvefeed.ChunkDescription), stats.PercentChunk(cvefeed.ChunkDescription), + percentInt(stats.NumChunk(cvefeed.ChunkDescription), stats.NumVulnsA())) + fmt.Printf(" different scores : %d (%.2f%%, total %.2f%%)\n", + stats.NumChunk(cvefeed.ChunkScore), stats.PercentChunk(cvefeed.ChunkScore), + percentInt(stats.NumChunk(cvefeed.ChunkScore), stats.NumVulnsA())) + + log.Println("writing differences to stats.json") + + data, err := json.MarshalIndent(stats, "", " ") + if err != nil { + return fmt.Errorf("failed to encode stats to JSON: %w", err) + } + if err := ioutil.WriteFile("stats.json", data, 0644); err != nil { + return fmt.Errorf("failed to write stats file: %w", err) + } + + return nil + }, +} diff --git a/cvefeed/dictionary.go b/cvefeed/dictionary.go index ddb61f1..3ddef43 100644 --- a/cvefeed/dictionary.go +++ b/cvefeed/dictionary.go @@ -41,7 +41,7 @@ func (d *Dictionary) Override(d2 Dictionary) { } } -// LoadJSONDictionary parses dictionary from multiple NVD vulenrability feed JSON files +// LoadJSONDictionary parses dictionary from multiple NVD vulnerability feed JSON files func LoadJSONDictionary(paths ...string) (Dictionary, error) { return LoadFeed(loadJSONFile, paths...) } diff --git a/cvefeed/diff.go b/cvefeed/diff.go new file mode 100644 index 0000000..1e8ea5e --- /dev/null +++ b/cvefeed/diff.go @@ -0,0 +1,293 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cvefeed + +import ( + "encoding/json" + "math/bits" + + "github.com/facebookincubator/nvdtools/cvefeed/nvd" + "github.com/facebookincubator/nvdtools/cvefeed/nvd/schema" +) + +type bag map[string]interface{} + +// ChunkKind is the type of chunks produced by a diff. +type ChunkKind string + +const ( + // ChunkDescription indicates a difference in the description of a + // vulnerability. + ChunkDescription ChunkKind = "description" + // ChunkScore indicates a difference in the score of a vulnerability. + ChunkScore = "score" +) + +type chunk uint32 + +const ( + chunkDescriptionShift = iota + chunkScoreShift + chunkMaxShift +) + +const ( + chunkDescription chunk = 1 << iota + chunkScore +) + +var chunkKind = [chunkMaxShift]ChunkKind{ + ChunkDescription, + ChunkScore, +} + +func (kind ChunkKind) shift() int { + for i, v := range chunkKind { + if v == kind { + return i + } + } + return chunkMaxShift +} + +type diffEntry struct { + id string + bits chunk +} + +type diffFeed struct { + name string + dict Dictionary +} + +type diff struct { + a, b diffFeed +} + +func newDiff(a, b diffFeed) *diff { + return &diff{ + a: a, + b: b, + } +} + +// DiffStats is the result of a diff. +type DiffStats struct { + diff *diff // back pointer to the diff these stats are for + + numVulnsA, numVulnsB int + + aNotB []string // ids of vulns that are in a but not in b + bNotA []string // ids of vulns that are in b but not in a + entries []diffEntry + bitCounts [chunkMaxShift]int +} + +// NumVulnsA returns the vulnerability in A (the first input to Diff). +func (s *DiffStats) NumVulnsA() int { + return s.numVulnsA +} + +// NumVulnsB returns the vulnerability in A (the first input to Diff). +func (s *DiffStats) NumVulnsB() int { + return s.numVulnsB +} + +// VulnsANotB returns the vulnerabilities that are A (the first input to Diff) but +// are not in B (the second input to Diff). +func (s *DiffStats) VulnsANotB() []string { + return s.aNotB +} + +// NumVulnsANotB returns the numbers of vulnerabilities that are A (the first input +// to Diff) but are not in B (the second input to Diff). +func (s *DiffStats) NumVulnsANotB() int { + return len(s.aNotB) +} + +// VulnsBNotA returns the vulnerabilities that are A (the first input to Diff) but +// are not in B (the second input to Diff). +func (s *DiffStats) VulnsBNotA() []string { + return s.bNotA +} + +// NumVulnsBNotA returns the numbers of vulnerabilities that are B (the second input +// to Diff) but are not in A (the first input to Diff). +func (s *DiffStats) NumVulnsBNotA() int { + return len(s.bNotA) +} + +// NumDiffVulns returns the number of vulnerability that are in both A and B but +// are different (eg. different description, score, ...). +func (s *DiffStats) NumDiffVulns() int { + return len(s.entries) +} + +// NumChunk returns the number of different vulnerabilities that have a specific chunk. +func (s *DiffStats) NumChunk(chunk ChunkKind) int { + return s.bitCounts[chunk.shift()] +} + +// PercentChunk returns the percentage of different vulnerabilities that have a specific chunk. +func (s *DiffStats) PercentChunk(chunk ChunkKind) float64 { + return float64(s.bitCounts[chunk.shift()]) / float64(len(s.entries)) * 100 +} + +func diffDetails(s *schema.NVDCVEFeedJSON10DefCVEItem, bit chunk) bag { + var v interface{} + + switch bit { + case chunkDescription: + v = bag{ + "description": englishDescription(s), + } + case chunkScore: + v = s.Impact + } + + var data bag + tmp, _ := json.Marshal(v) + _ = json.Unmarshal(tmp, &data) + + return data +} + +func genEntryDiffOutput(aFeed, bFeed *diffFeed, entry *diffEntry) []bag { + a := aFeed.dict[entry.id].(*nvd.Vuln).Schema() + b := bFeed.dict[entry.id].(*nvd.Vuln).Schema() + outputs := make([]bag, bits.OnesCount32(uint32(entry.bits))) + for i := 0; i < chunkMaxShift; i++ { + if entry.bits&(1<