cvefeed/diff.go (207 lines of code) (raw):

// 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<<i) != 0 { outputs[i] = bag{ "kind": chunkKind[i], aFeed.name: diffDetails(a, 1<<i), bFeed.name: diffDetails(b, 1<<i), } } } return outputs } // MarshalJSON implements a custom JSON marshaller. func (s *DiffStats) MarshalJSON() ([]byte, error) { var differences []bag for _, entry := range s.entries { differences = append(differences, bag{ "id": entry.id, "chunks": genEntryDiffOutput(&s.diff.a, &s.diff.b, &entry), }) } return json.Marshal(bag{ "differences": differences, }) } func englishDescription(s *schema.NVDCVEFeedJSON10DefCVEItem) string { for _, d := range s.CVE.Description.DescriptionData { if d.Lang == "en" { return d.Value } } return "" } func sameDescription(a, b *schema.NVDCVEFeedJSON10DefCVEItem) bool { return englishDescription(a) == englishDescription(b) } func sameScoreCVSSV2(a, b *schema.NVDCVEFeedJSON10DefCVEItem) bool { var aScore, bScore float64 var aVector, bVector string if a.Impact.BaseMetricV2 != nil && a.Impact.BaseMetricV2.CVSSV2 != nil { aScore = a.Impact.BaseMetricV2.CVSSV2.BaseScore aVector = a.Impact.BaseMetricV2.CVSSV2.VectorString } if b.Impact.BaseMetricV2 != nil && b.Impact.BaseMetricV2.CVSSV2 != nil { bScore = b.Impact.BaseMetricV2.CVSSV2.BaseScore bVector = b.Impact.BaseMetricV2.CVSSV2.VectorString } return aScore == bScore && aVector == bVector } func sameScoreCVSSV3(a, b *schema.NVDCVEFeedJSON10DefCVEItem) bool { var aScore, bScore float64 var aVector, bVector string if a.Impact.BaseMetricV3 != nil && a.Impact.BaseMetricV3.CVSSV3 != nil { aScore = a.Impact.BaseMetricV3.CVSSV3.BaseScore aVector = a.Impact.BaseMetricV3.CVSSV3.VectorString } if b.Impact.BaseMetricV3 != nil && b.Impact.BaseMetricV3.CVSSV3 != nil { bScore = b.Impact.BaseMetricV3.CVSSV3.BaseScore bVector = b.Impact.BaseMetricV3.CVSSV3.VectorString } return aScore == bScore && aVector == bVector } func sameScore(a, b *schema.NVDCVEFeedJSON10DefCVEItem) bool { return sameScoreCVSSV2(a, b) && sameScoreCVSSV3(a, b) } func (d *diff) stats() *DiffStats { stats := DiffStats{ diff: d, numVulnsA: len(d.a.dict), numVulnsB: len(d.b.dict), } // List of vulns that are in a but not in b. for key := range d.a.dict { if _, ok := d.b.dict[key]; !ok { stats.aNotB = append(stats.aNotB, key) continue } // key is in both a and b, let's compare further! a := d.a.dict[key].(*nvd.Vuln).Schema() b := d.b.dict[key].(*nvd.Vuln).Schema() var entry diffEntry if !sameDescription(a, b) { entry.bits |= chunkDescription stats.bitCounts[chunkDescriptionShift]++ } if !sameScore(a, b) { entry.bits |= chunkScore stats.bitCounts[chunkScoreShift]++ } if entry.bits != 0 { entry.id = key stats.entries = append(stats.entries, entry) } } // List of vulns that are in b but not in a. for key := range d.b.dict { if _, ok := d.a.dict[key]; !ok { stats.bNotA = append(stats.bNotA, key) } } return &stats } // Diff performs a diff between two Dictionaries. func Diff(aName string, aDict Dictionary, bName string, bDict Dictionary) *DiffStats { diff := newDiff(diffFeed{aName, aDict}, diffFeed{bName, bDict}) return diff.stats() }