Skip to content

Commit

Permalink
Merge pull request #181 from vulncheck-oss/update-cpe-parse
Browse files Browse the repository at this point in the history
🚧 add new cpe parsing func
  • Loading branch information
acidjazz authored Dec 12, 2024
2 parents e4c193b + 75a3b27 commit 4566bd2
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 125 deletions.
94 changes: 73 additions & 21 deletions pkg/cpe/cpeuri/cpeuri.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func ToStruct(s string) (*cpeutils.CPE, error) {
return nil, fmt.Errorf("invalid CPE string: %v", err)
}

if cpe.Vendor == "*" || cpe.Product == "*" {
return nil, fmt.Errorf("CPE Vendor or Product cannot be *")
if cpe.Vendor == "*" && cpe.Product == "*" {
return nil, fmt.Errorf("CPE Vendor and Product cannot be *")
}

return &cpe, nil
Expand Down Expand Up @@ -105,46 +105,98 @@ func isAllASCII(s string) bool {

// UnbindCPEFormattedString attempts to unbind a cpe 2.3 formatted string to
// a CPE struct

func UnbindCPEFormattedString(str string) (cpeutils.CPE, error) {
str = strings.ToLower(str)
if !isAllASCII(str) {
return cpeutils.CPE{}, fmt.Errorf("cpe string contains non-ASCII chars")
}

cpe := cpeutils.CPE{}
fields := []string{"Part", "Vendor", "Product", "Version", "Update", "Edition", "Language", "SoftwareEdition", "TargetSoftware", "TargetHardware", "Other"}

components := strings.Split(str, ":")
if len(components) < 13 {
return cpeutils.CPE{}, fmt.Errorf("invalid CPE formatted string")
}

for i, fieldName := range fields {
v, err := getAndUnbindComponent(components[i+2])
for a := 2; a <= 12; a++ {
v := getCompFS(str, a)
v, err := unbindValueFS(v)
if err != nil {
return cpeutils.CPE{}, err
}

field := reflect.ValueOf(&cpe).Elem().FieldByName(fieldName)
if field.IsValid() && field.CanSet() {
field.SetString(v)
switch a {
case 2:
cpe.Part = v

case 3:
cpe.Vendor = v

case 4:
cpe.Product = v

case 5:
cpe.Version = v

case 6:
cpe.Update = v

case 7:
cpe.Edition = v

case 8:
cpe.Language = v

case 9:
cpe.SoftwareEdition = v

case 10:
cpe.TargetSoftware = v

case 11:
cpe.TargetHardware = v

case 12:
cpe.Other = v
}
}

return cpe, nil
}

func getAndUnbindComponent(str string) (string, error) {
// Unescape colons
str = strings.ReplaceAll(str, "\\:", ":")
func getCompFS(str string, i int) string {
fcount := 0
sidx := 0

if i < 0 || i > 12 {
return ""
}

for idx, v := range str {
if v == ':' && (idx == 0 || str[idx-1] != '\\') {
if i == fcount {
return str[sidx:idx]
}
fcount++
sidx = idx + 1
}

}

if fcount == i {
return str[sidx:]
}

return ""
}

func unbindValueFS(str string) (string, error) {
switch str {
case "", "*":
case "*":
return "*", nil

case "":
return "*", nil

case "-":
return "-", nil
default:
return addQuoting(str)

}
return addQuoting(str)
}

func addQuoting(str string) (string, error) {
Expand Down
46 changes: 26 additions & 20 deletions pkg/cpe/cpeuri/cpeuri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,26 @@ func TestToStruct(t *testing.T) {
wantErr bool
}{
{
name: "Valid CPE URI",
input: "cpe:/a:vendor:product:version:update",
name: "Valid CPE 2.3",
input: "cpe:2.3:a:vendor:product:version:update:edition:lang:sw_edition:target_sw:target_hw:other",
want: &cpeutils.CPE{
Part: "a",
Vendor: "vendor",
Product: "product",
Version: "version",
Update: "update",
Edition: "*",
Language: "*",
SoftwareEdition: "*",
TargetSoftware: "*",
TargetHardware: "*",
Other: "*",
Edition: "edition",
Language: "lang",
SoftwareEdition: "sw_edition",
TargetSoftware: "target_sw",
TargetHardware: "target_hw",
Other: "other",
},
wantErr: false,
},
{
name: "Invalid CPE string",
input: "invalid:cpe:string",
want: nil,
wantErr: true,
},
{
name: "CPE with * as vendor",
input: "cpe:/a:*:product:1.0",
name: "Invalid CPE 2.3 (not enough components)",
input: "cpe:2.3:a:vendor:product",
want: nil,
wantErr: true,
},
Expand Down Expand Up @@ -127,10 +121,22 @@ func TestUnbindCPEFormattedString(t *testing.T) {
wantErr: false,
},
{
name: "Invalid CPE 2.3 (not enough components)",
input: "cpe:2.3:a:vendor:product",
want: cpeutils.CPE{},
wantErr: true,
name: "Invalid CPE 2.3 (not enough components)",
input: "cpe:2.3:a:vendor:product",
want: cpeutils.CPE{
Part: "a",
Vendor: "vendor",
Product: "product",
Version: "*",
Update: "*",
Edition: "*",
Language: "*",
SoftwareEdition: "*",
TargetSoftware: "*",
TargetHardware: "*",
Other: "*",
},
wantErr: false,
},
}

Expand Down
51 changes: 15 additions & 36 deletions pkg/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,6 @@ func IndexCPE(indexName string, cpe cpeutils.CPE, query string) ([]cpeutils.CPEV
stats.TotalFiles = 1
stats.Query = query

// Compile the jq query
jq, err := gojq.Parse(query)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse query: %w", err)
}
code, err := gojq.Compile(jq)
if err != nil {
return nil, nil, fmt.Errorf("failed to compile query: %w", err)
}

file, err := os.Open(filePath)
if err != nil {
return nil, nil, fmt.Errorf("failed to open file %s: %w", filePath, err)
Expand All @@ -110,29 +100,11 @@ func IndexCPE(indexName string, cpe cpeutils.CPE, query string) ([]cpeutils.CPEV

for scanner.Scan() {
stats.TotalLines++
line := scanner.Bytes()

// Quick filter using gjson
if !quickFilterCPE(line, cpe) {
continue
}
line := scanner.Text()

// Full processing with gojq
var input interface{}
if err := json.Unmarshal(line, &input); err != nil {
continue
}

iter := code.Run(input)
v, ok := iter.Next()
if !ok || v == nil {
continue
}

// If the query returned true, we have a match
if v == true {
if matchesCPE(line, cpe) {
var entry cpeutils.CPEVulnerabilities
if err := json.Unmarshal(line, &entry); err != nil {
if err := json.Unmarshal([]byte(line), &entry); err != nil {
continue
}
results = append(results, entry)
Expand All @@ -148,13 +120,20 @@ func IndexCPE(indexName string, cpe cpeutils.CPE, query string) ([]cpeutils.CPEV
return results, &stats, nil
}

func quickFilterCPE(line []byte, cpe cpeutils.CPE) bool {
if cpe.Vendor != "" && gjson.GetBytes(line, "vendor").String() != cpe.Vendor {
return false
func matchesCPE(line string, cpe cpeutils.CPE) bool {

if cpe.Vendor != "" {
if !strings.Contains(line, strings.ToLower(cpe.Vendor)) {
return false
}
}
if cpe.Product != "" && gjson.GetBytes(line, "product").String() != cpe.Product {
return false

if cpe.Product != "" {
if !strings.Contains(line, strings.ToLower(cpe.Product)) {
return false
}
}

return true
}

Expand Down
48 changes: 0 additions & 48 deletions pkg/search/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"github.com/itchyny/gojq"
"github.com/package-url/packageurl-go"
"github.com/vulncheck-oss/cli/pkg/cpe/cpeutils"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -286,50 +285,3 @@ func TestProcessFile(t *testing.T) {
assert.Equal(t, int64(3), stats.TotalLines, "Unexpected total lines count")
assert.Equal(t, int64(2), stats.MatchedLines, "Unexpected matched lines count")
}

func TestQuickFilterCPE(t *testing.T) {
testCases := []struct {
name string
json string
cpe cpeutils.CPE
expected bool
}{
{
name: "Match vendor",
json: `{"vendor": "microsoft", "product": "windows"}`,
cpe: cpeutils.CPE{Vendor: "microsoft"},
expected: true,
},
{
name: "Match product",
json: `{"vendor": "apache", "product": "tomcat"}`,
cpe: cpeutils.CPE{Product: "tomcat"},
expected: true,
},
{
name: "Match both",
json: `{"vendor": "oracle", "product": "java"}`,
cpe: cpeutils.CPE{Vendor: "oracle", Product: "java"},
expected: true,
},
{
name: "No match",
json: `{"vendor": "google", "product": "chrome"}`,
cpe: cpeutils.CPE{Vendor: "mozilla", Product: "firefox"},
expected: false,
},
{
name: "Empty CPE",
json: `{"vendor": "apple", "product": "macos"}`,
cpe: cpeutils.CPE{},
expected: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := quickFilterCPE([]byte(tc.json), tc.cpe)
assert.Equal(t, tc.expected, result, "Unexpected result for case: %s", tc.name)
})
}
}

0 comments on commit 4566bd2

Please sign in to comment.