-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #168 from vulncheck-oss/offline-cpe
✨ offline CPE support
- Loading branch information
Showing
14 changed files
with
1,474 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package cpe | ||
|
||
import ( | ||
"fmt" | ||
"github.com/spf13/cobra" | ||
"github.com/vulncheck-oss/cli/pkg/cache" | ||
"github.com/vulncheck-oss/cli/pkg/cmd/offline/sync" | ||
"github.com/vulncheck-oss/cli/pkg/config" | ||
"github.com/vulncheck-oss/cli/pkg/cpe/cpeoffline" | ||
"github.com/vulncheck-oss/cli/pkg/cpe/cpeuri" | ||
"github.com/vulncheck-oss/cli/pkg/cpe/cpeutils" | ||
"github.com/vulncheck-oss/cli/pkg/search" | ||
"github.com/vulncheck-oss/cli/pkg/ui" | ||
) | ||
|
||
func Command() *cobra.Command { | ||
|
||
var jsonOutput bool | ||
|
||
var statsOnly bool | ||
|
||
cmd := &cobra.Command{ | ||
Use: "cpe <scheme>", | ||
Short: "Offline CPE lookup", | ||
Long: "Search offline package data via CPE schemes", | ||
Example: "vulncheck offline cpe \"pkg:hackage/aeson@0.3.2.8\"", | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
|
||
cpe, err := cpeuri.ToStruct(args[0]) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
indices, err := cache.Indices() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
indexAvailable, err := sync.EnsureIndexSync(indices, "cpecve", false) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !indexAvailable { | ||
return fmt.Errorf("index cpecve is required to proceed") | ||
} | ||
|
||
query, err := cpeoffline.Query(cpe) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
results, stats, err := search.IndexCPE("cpecve", *cpe, query) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
cves, err := cpeutils.Process(cpe, results) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
if jsonOutput || config.IsCI() { | ||
ui.Json(cves) | ||
return nil | ||
} | ||
|
||
ui.Stat("Results found/filtered", fmt.Sprintf("%d/%d", len(results), len(cves))) | ||
ui.Stat("Files/Lines processed", fmt.Sprintf("%d/%d", stats.TotalFiles, stats.TotalLines)) | ||
ui.Stat("Search duration", fmt.Sprintf("%.2f seconds", stats.Duration.Seconds())) | ||
|
||
if !statsOnly { | ||
ui.Json(cves) | ||
} | ||
|
||
return nil | ||
}, | ||
} | ||
cmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output in JSON format") | ||
cmd.Flags().BoolVarP(&statsOnly, "stats", "s", false, "Output stats only") | ||
return cmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package cpeoffline | ||
|
||
import ( | ||
"fmt" | ||
"github.com/vulncheck-oss/cli/pkg/cpe/cpeutils" | ||
"strings" | ||
) | ||
|
||
func Query(cpe *cpeutils.CPE) (string, error) { | ||
|
||
if cpe.Vendor == "*" && cpe.Product == "*" { | ||
return "", fmt.Errorf("need at least vendor or product specified") | ||
} | ||
|
||
return buildCPEQuery(cpe, !cpeutils.IsParseableVersion(cpeutils.Unquote(cpe.Version))) | ||
} | ||
|
||
func addCondition(field, value string) string { | ||
if value != "*" { | ||
return fmt.Sprintf(`(.%s == %q or .%s == "*")`, field, value, field) | ||
} | ||
return "" | ||
} | ||
|
||
func buildCPEQuery(cpe *cpeutils.CPE, queryVersion bool) (string, error) { | ||
if cpe.Vendor == "*" && cpe.Product == "*" { | ||
return "", fmt.Errorf("need at least vendor or product specified") | ||
} | ||
|
||
var conditions []string | ||
|
||
fields := []struct { | ||
name string | ||
value string | ||
}{ | ||
{"vendor", cpe.Vendor}, | ||
{"product", cpe.Product}, | ||
{"update", cpe.Update}, | ||
{"edition", cpe.Edition}, | ||
{"language", cpe.Language}, | ||
{"sw_edition", cpe.SoftwareEdition}, | ||
{"target_sw", cpe.TargetSoftware}, | ||
{"target_hw", cpe.TargetHardware}, | ||
{"other", cpe.Other}, | ||
} | ||
|
||
// Add version field only if withVersion is true | ||
if queryVersion { | ||
fields = append(fields, struct{ name, value string }{"version", cpeutils.Unquote(cpe.Version)}) | ||
} | ||
|
||
for _, field := range fields { | ||
if condition := addCondition(field.name, field.value); condition != "" { | ||
conditions = append(conditions, condition) | ||
} | ||
} | ||
|
||
if len(conditions) == 0 { | ||
return "true", nil | ||
} | ||
|
||
query := strings.Join(conditions, " and ") | ||
return query, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package cpeoffline | ||
|
||
import ( | ||
"github.com/vulncheck-oss/cli/pkg/cpe/cpeutils" | ||
"testing" | ||
) | ||
|
||
func TestQuery(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
cpe *cpeutils.CPE | ||
want string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "Valid CPE", | ||
cpe: &cpeutils.CPE{ | ||
Vendor: "vendor", | ||
Product: "product", | ||
Version: "1.0", | ||
}, | ||
want: `(.vendor == "vendor" or .vendor == "*") and (.product == "product" or .product == "*") and (.update == "" or .update == "*") and (.edition == "" or .edition == "*") and (.language == "" or .language == "*") and (.sw_edition == "" or .sw_edition == "*") and (.target_sw == "" or .target_sw == "*") and (.target_hw == "" or .target_hw == "*") and (.other == "" or .other == "*")`, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "CPE with all fields", | ||
cpe: &cpeutils.CPE{ | ||
Vendor: "vendor", | ||
Product: "product", | ||
Version: "1.0", | ||
Update: "update", | ||
Edition: "edition", | ||
Language: "language", | ||
SoftwareEdition: "sw_edition", | ||
TargetSoftware: "target_sw", | ||
TargetHardware: "target_hw", | ||
Other: "other", | ||
}, | ||
want: `(.vendor == "vendor" or .vendor == "*") and (.product == "product" or .product == "*") and ` + | ||
`(.update == "update" or .update == "*") and (.edition == "edition" or .edition == "*") and ` + | ||
`(.language == "language" or .language == "*") and (.sw_edition == "sw_edition" or .sw_edition == "*") and ` + | ||
`(.target_sw == "target_sw" or .target_sw == "*") and (.target_hw == "target_hw" or .target_hw == "*") and ` + | ||
`(.other == "other" or .other == "*")`, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "CPE with wildcard version", | ||
cpe: &cpeutils.CPE{ | ||
Vendor: "vendor", | ||
Product: "product", | ||
Version: "x.y", | ||
}, | ||
want: `(.vendor == "vendor" or .vendor == "*") and (.product == "product" or .product == "*") and (.update == "" or .update == "*") and (.edition == "" or .edition == "*") and (.language == "" or .language == "*") and (.sw_edition == "" or .sw_edition == "*") and (.target_sw == "" or .target_sw == "*") and (.target_hw == "" or .target_hw == "*") and (.other == "" or .other == "*") and (.version == "x.y" or .version == "*")`, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "Invalid CPE (both vendor and product are wildcards)", | ||
cpe: &cpeutils.CPE{ | ||
Vendor: "*", | ||
Product: "*", | ||
}, | ||
want: "", | ||
wantErr: true, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := Query(tt.cpe) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("Query() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if got != tt.want { | ||
t.Errorf("Query() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestAddCondition(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
field string | ||
value string | ||
want string | ||
}{ | ||
{ | ||
name: "Non-wildcard value", | ||
field: "vendor", | ||
value: "test", | ||
want: `(.vendor == "test" or .vendor == "*")`, | ||
}, | ||
{ | ||
name: "Wildcard value", | ||
field: "product", | ||
value: "*", | ||
want: "", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := addCondition(tt.field, tt.value); got != tt.want { | ||
t.Errorf("addCondition() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestBuildCPEQuery(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
cpe *cpeutils.CPE | ||
queryVersion bool | ||
want string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "Valid CPE without version", | ||
cpe: &cpeutils.CPE{ | ||
Vendor: "vendor", | ||
Product: "product", | ||
}, | ||
queryVersion: false, | ||
want: `(.vendor == "vendor" or .vendor == "*") and (.product == "product" or .product == "*") and (.update == "" or .update == "*") and (.edition == "" or .edition == "*") and (.language == "" or .language == "*") and (.sw_edition == "" or .sw_edition == "*") and (.target_sw == "" or .target_sw == "*") and (.target_hw == "" or .target_hw == "*") and (.other == "" or .other == "*")`, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "Valid CPE with version", | ||
cpe: &cpeutils.CPE{ | ||
Vendor: "vendor", | ||
Product: "product", | ||
Version: "1.0", | ||
}, | ||
queryVersion: true, | ||
want: `(.vendor == "vendor" or .vendor == "*") and (.product == "product" or .product == "*") and (.update == "" or .update == "*") and (.edition == "" or .edition == "*") and (.language == "" or .language == "*") and (.sw_edition == "" or .sw_edition == "*") and (.target_sw == "" or .target_sw == "*") and (.target_hw == "" or .target_hw == "*") and (.other == "" or .other == "*") and (.version == "1.0" or .version == "*")`, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "Invalid CPE (both vendor and product are wildcards)", | ||
cpe: &cpeutils.CPE{ | ||
Vendor: "*", | ||
Product: "*", | ||
}, | ||
queryVersion: false, | ||
want: "", | ||
wantErr: true, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := buildCPEQuery(tt.cpe, tt.queryVersion) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("buildCPEQuery() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if got != tt.want { | ||
t.Errorf("buildCPEQuery() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.