Skip to content

Commit 338820f

Browse files
committed
feat(cmd): add list and search commands
1 parent 7a43138 commit 338820f

File tree

7 files changed

+493
-2
lines changed

7 files changed

+493
-2
lines changed

cmd/list.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2025 Valentin Lobstein (Chocapikk) <[email protected]>
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
// this software and associated documentation files (the "Software"), to deal in
5+
// the Software without restriction, including without limitation the rights to
6+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
// the Software, and to permit persons to whom the Software is furnished to do so,
8+
// subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
20+
package cmd
21+
22+
import (
23+
"github.com/Chocapikk/wpprobe/internal/list"
24+
"github.com/spf13/cobra"
25+
)
26+
27+
var listCmd = &cobra.Command{
28+
Use: "list",
29+
Short: "Show how many CVEs wpprobe can detect, split by severity",
30+
RunE: func(cmd *cobra.Command, args []string) error { return list.RunList() },
31+
}
32+
33+
func init() {
34+
rootCmd.AddCommand(listCmd)
35+
}

cmd/root.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ func Execute() {
4646
}
4747

4848
func init() {
49-
rootCmd.CompletionOptions.DisableDefaultCmd = true
50-
5149
rootCmd.AddCommand(scanCmd)
5250
rootCmd.AddCommand(updateCmd)
5351
rootCmd.AddCommand(updateDbCmd)

cmd/search.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) 2025 Valentin Lobstein (Chocapikk) <[email protected]>
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
// this software and associated documentation files (the "Software"), to deal in
5+
// the Software without restriction, including without limitation the rights to
6+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
// the Software, and to permit persons to whom the Software is furnished to do so,
8+
// subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
20+
package cmd
21+
22+
import (
23+
"fmt"
24+
25+
"github.com/Chocapikk/wpprobe/internal/scanner"
26+
"github.com/Chocapikk/wpprobe/internal/search"
27+
"github.com/Chocapikk/wpprobe/internal/wordfence"
28+
"github.com/charmbracelet/lipgloss/tree"
29+
"github.com/spf13/cobra"
30+
)
31+
32+
var (
33+
flagCVE string
34+
flagPlugin string
35+
flagTitle string
36+
flagSeverity string
37+
flagAuth string
38+
showDetails bool
39+
)
40+
41+
var searchCmd = &cobra.Command{
42+
Use: "search",
43+
Short: "Search vulnerabilities by various filters",
44+
RunE: runSearch,
45+
}
46+
47+
func init() {
48+
rootCmd.AddCommand(searchCmd)
49+
searchCmd.Flags().StringVar(&flagCVE, "cve", "", "Filter by CVE ID substring")
50+
searchCmd.Flags().StringVar(&flagPlugin, "plugin", "", "Filter by plugin slug substring")
51+
searchCmd.Flags().StringVar(&flagTitle, "title", "", "Filter by keyword in title")
52+
searchCmd.Flags().
53+
StringVar(&flagSeverity, "severity", "", "Filter by severity (critical, high, medium, low)")
54+
searchCmd.Flags().
55+
StringVar(&flagAuth, "auth", "", "Filter by auth type (Unauth, Auth, Privileged)")
56+
searchCmd.Flags().
57+
BoolVarP(&showDetails, "details", "d", false, "Show detailed CVE entries per plugin")
58+
}
59+
60+
func runSearch(cmd *cobra.Command, args []string) error {
61+
if !search.AnyFilterSet(flagCVE, flagPlugin, flagTitle, flagSeverity, flagAuth) {
62+
return fmt.Errorf(
63+
"please specify at least one filter: --cve, --plugin, --title, --severity, or --auth",
64+
)
65+
}
66+
67+
vulns, err := wordfence.LoadVulnerabilities("wordfence_vulnerabilities.json")
68+
if err != nil {
69+
return err
70+
}
71+
72+
filtered := search.FilterAll(vulns, flagCVE, flagPlugin, flagTitle, flagSeverity, flagAuth)
73+
if len(filtered) == 0 {
74+
fmt.Println(scanner.UnknownStyle.Render("No vulnerabilities match filters."))
75+
return nil
76+
}
77+
byPlugin := search.GroupByPlugin(filtered)
78+
79+
root := tree.Root(
80+
scanner.TitleStyle.Render(fmt.Sprintf("🔍 %d vulnerabilities found", len(filtered))),
81+
)
82+
search.BuildTree(root, byPlugin, showDetails)
83+
84+
fmt.Println(scanner.SeparatorStyle.Render(root.String()))
85+
return nil
86+
}

internal/list/list.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) 2025 Valentin Lobstein (Chocapikk) <[email protected]>
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
// this software and associated documentation files (the "Software"), to deal in
5+
// the Software without restriction, including without limitation the rights to
6+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
// the Software, and to permit persons to whom the Software is furnished to do so,
8+
// subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all
11+
// copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
20+
package list
21+
22+
import (
23+
"fmt"
24+
"strings"
25+
26+
"github.com/Chocapikk/wpprobe/internal/scanner"
27+
"github.com/Chocapikk/wpprobe/internal/wordfence"
28+
"github.com/charmbracelet/lipgloss/tree"
29+
)
30+
31+
func RunList() error {
32+
vulns, err := wordfence.LoadVulnerabilities("wordfence_vulnerabilities.json")
33+
if err != nil {
34+
return err
35+
}
36+
37+
count := map[string]int{
38+
"critical": 0,
39+
"high": 0,
40+
"medium": 0,
41+
"low": 0,
42+
"unknown": 0,
43+
}
44+
for _, v := range vulns {
45+
sev := strings.ToLower(v.Severity)
46+
if _, ok := count[sev]; !ok {
47+
sev = "unknown"
48+
}
49+
count[sev]++
50+
}
51+
52+
total := 0
53+
for _, c := range count {
54+
total += c
55+
}
56+
57+
root := tree.Root(scanner.TitleStyle.Render("📊 CVE coverage database"))
58+
59+
type line struct {
60+
key string
61+
style func(string) string
62+
}
63+
64+
lines := []line{
65+
{"critical", func(s string) string { return scanner.CriticalStyle.Render(s) }},
66+
{"high", func(s string) string { return scanner.HighStyle.Render(s) }},
67+
{"medium", func(s string) string { return scanner.MediumStyle.Render(s) }},
68+
{"low", func(s string) string { return scanner.LowStyle.Render(s) }},
69+
{"unknown", func(s string) string { return scanner.UnknownStyle.Render(s) }},
70+
}
71+
72+
for _, l := range lines {
73+
if count[l.key] == 0 {
74+
continue
75+
}
76+
capKey := strings.ToUpper(l.key[:1]) + l.key[1:]
77+
label := l.style(fmt.Sprintf("%-8s", capKey))
78+
root.Child(tree.Root(fmt.Sprintf("%s: %d", label, count[l.key])))
79+
}
80+
81+
root.Child(tree.Root(scanner.URLStyle.Render(
82+
fmt.Sprintf("Total detectable CVEs: %d", total),
83+
)))
84+
85+
fmt.Println(scanner.SeparatorStyle.Render(root.String()))
86+
return nil
87+
}

internal/scanner/display.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ var (
5454
unknownStyle = bold.Foreground(lipgloss.Color("#FFA500"))
5555
)
5656

57+
var (
58+
TitleStyle = titleStyle
59+
URLStyle = urlStyle
60+
SeparatorStyle = separatorStyle
61+
CriticalStyle = criticalStyle
62+
HighStyle = highStyle
63+
MediumStyle = mediumStyle
64+
LowStyle = lowStyle
65+
UnknownStyle = unknownStyle
66+
67+
UnauthStyle = unauthStyle
68+
AuthStyle = authStyle
69+
PrivilegedStyle = privilegedStyle
70+
)
71+
5772
type VulnCategories struct {
5873
Critical []string
5974
High []string

0 commit comments

Comments
 (0)