diff --git a/README.md b/README.md index fc83360..af01ebd 100644 --- a/README.md +++ b/README.md @@ -111,12 +111,13 @@ Examples: ### features - rete features list — list available features - - rete features seed — seed the feature catalog from a CSV file + - rete features list — list available features + - rete features seed — import a custom feature catalog from a CSV file (admin use) Examples: - rete features list - rete features list --category 'Reconnaissance & Discovery' - - rete features seed --file table.csv + - rete features seed --file custom.csv ### jobs diff --git a/internal/database.go b/internal/database.go index b7e02f7..3e9e00d 100644 --- a/internal/database.go +++ b/internal/database.go @@ -70,7 +70,7 @@ type FeatureCategory struct { Features []Feature `gorm:"foreignKey:CategoryID"` } -// Feature is a single capability entry sourced from table.csv. +// Feature is a single capability entry in the feature catalog. type Feature struct { ID uint `gorm:"primaryKey;autoIncrement"` CategoryID uint `gorm:"not null;index"` diff --git a/internal/feature_catalog.go b/internal/feature_catalog.go new file mode 100644 index 0000000..acd8368 --- /dev/null +++ b/internal/feature_catalog.go @@ -0,0 +1,100 @@ +/* +Copyright 2026 Joseph Anthony Abbott III + +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 internal + +// CatalogEntry represents a single feature in the compiled-in default catalog. +type CatalogEntry struct { + Category string + Feature string + CobraCommand string + ShortDescription string +} + +// DefaultFeatureCatalog is the canonical feature catalog compiled into the binary. +// It mirrors the data previously distributed as table.csv. +var DefaultFeatureCatalog = []CatalogEntry{ + // Reconnaissance & Discovery + {"Reconnaissance & Discovery", "Ping Sweep", "recon ping-sweep", "Discover live hosts by sending ICMP echo requests"}, + {"Reconnaissance & Discovery", "Port Scan", "recon port-scan", "Scan TCP/UDP ports on a target host"}, + {"Reconnaissance & Discovery", "OS Fingerprinting", "recon os-finger", "Identify the remote host operating system"}, + {"Reconnaissance & Discovery", "Service Version Enumeration", "recon service-enum", "Detect running services and their versions"}, + {"Reconnaissance & Discovery", "DNS Lookup", "recon dns", "Perform forward and reverse DNS resolution"}, + {"Reconnaissance & Discovery", "WHOIS Lookup", "recon whois", "Query WHOIS data for a domain or IP address"}, + {"Reconnaissance & Discovery", "Subdomain Enumeration", "recon subdomain", "Enumerate subdomains via DNS brute-force"}, + // Network Diagnostics + {"Network Diagnostics", "Traceroute", "diag traceroute", "Trace the path packets take to a destination"}, + {"Network Diagnostics", "Bandwidth Test", "diag bandwidth", "Measure available network bandwidth to a host"}, + {"Network Diagnostics", "Latency Monitor", "diag latency", "Monitor round-trip latency over time"}, + {"Network Diagnostics", "Packet Loss Check", "diag packet-loss", "Measure packet loss percentage to a remote host"}, + {"Network Diagnostics", "MTU Discovery", "diag mtu", "Discover the maximum transmission unit on a path"}, + {"Network Diagnostics", "ARP Scan", "diag arp-scan", "Discover hosts on the local network via ARP"}, + // Security & Penetration Testing + {"Security & Penetration Testing", "Packet Sniffer", "sec sniff", "Capture and inspect packets on a network interface"}, + {"Security & Penetration Testing", "Packet Forger", "sec forge", "Craft and inject custom network packets"}, + {"Security & Penetration Testing", "ARP Spoofer", "sec arp-spoof", "Poison ARP caches to perform MITM interception"}, + {"Security & Penetration Testing", "Vulnerability Scanner", "sec vuln-scan", "Scan for common vulnerabilities on a target"}, + {"Security & Penetration Testing", "Brute Force", "sec brute", "Attempt credential brute-force against a service"}, + {"Security & Penetration Testing", "SSL/TLS Audit", "sec tls-audit", "Audit SSL/TLS configuration on a remote host"}, + // Payload Delivery + {"Payload Delivery", "Reverse Shell Helper", "payload rev-shell", "Generate reverse shell payload templates"}, + {"Payload Delivery", "File Transfer", "payload file-xfer", "Transfer files over raw TCP or HTTP channels"}, + {"Payload Delivery", "Bind Shell", "payload bind-shell", "Set up a bind shell listener on a port"}, + {"Payload Delivery", "Encoded Payload", "payload encode", "Encode a payload to evade simple pattern filters"}, +} + +// SeedDefaultFeatures upserts every entry from DefaultFeatureCatalog into the +// database. It is safe to call multiple times; existing records are not +// duplicated. +func SeedDefaultFeatures(db *Database) error { + catCache := make(map[string]*FeatureCategory) + + for _, entry := range DefaultFeatureCatalog { + cat, ok := catCache[entry.Category] + if !ok { + var err error + cat, err = db.UpsertCategory(entry.Category) + if err != nil { + return err + } + catCache[entry.Category] = cat + } + + f := &Feature{ + CategoryID: cat.ID, + Name: entry.Feature, + CobraCommand: entry.CobraCommand, + ShortDescription: entry.ShortDescription, + } + if err := db.UpsertFeature(f); err != nil { + return err + } + } + return nil +} + +// EnsureDefaultFeatures seeds the default catalog if no features exist yet. +// This is intended to be called once at startup. +func EnsureDefaultFeatures(db *Database) error { + features, err := db.ListFeatures("") + if err != nil { + return err + } + if len(features) > 0 { + return nil + } + return SeedDefaultFeatures(db) +} diff --git a/internal/feature_catalog_test.go b/internal/feature_catalog_test.go new file mode 100644 index 0000000..d16303c --- /dev/null +++ b/internal/feature_catalog_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2026 Joseph Anthony Abbott III + +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 internal + +import ( + "testing" +) + +func TestSeedDefaultFeatures(t *testing.T) { + db := newTestDB(t) + + if err := SeedDefaultFeatures(db); err != nil { + t.Fatalf("SeedDefaultFeatures: %v", err) + } + + features, err := db.ListFeatures("") + if err != nil { + t.Fatalf("ListFeatures: %v", err) + } + if len(features) != len(DefaultFeatureCatalog) { + t.Errorf("expected %d features, got %d", len(DefaultFeatureCatalog), len(features)) + } +} + +func TestSeedDefaultFeaturesIdempotent(t *testing.T) { + db := newTestDB(t) + + if err := SeedDefaultFeatures(db); err != nil { + t.Fatalf("first SeedDefaultFeatures: %v", err) + } + if err := SeedDefaultFeatures(db); err != nil { + t.Fatalf("second SeedDefaultFeatures: %v", err) + } + + features, err := db.ListFeatures("") + if err != nil { + t.Fatalf("ListFeatures: %v", err) + } + if len(features) != len(DefaultFeatureCatalog) { + t.Errorf("expected %d features after double seed, got %d", len(DefaultFeatureCatalog), len(features)) + } +} + +func TestEnsureDefaultFeaturesSeedsWhenEmpty(t *testing.T) { + db := newTestDB(t) + + if err := EnsureDefaultFeatures(db); err != nil { + t.Fatalf("EnsureDefaultFeatures: %v", err) + } + + features, err := db.ListFeatures("") + if err != nil { + t.Fatalf("ListFeatures: %v", err) + } + if len(features) != len(DefaultFeatureCatalog) { + t.Errorf("expected %d features after EnsureDefaultFeatures, got %d", len(DefaultFeatureCatalog), len(features)) + } +} + +func TestEnsureDefaultFeaturesSkipsWhenPopulated(t *testing.T) { + db := newTestDB(t) + + // Seed once so catalog is not empty. + if err := SeedDefaultFeatures(db); err != nil { + t.Fatalf("SeedDefaultFeatures: %v", err) + } + + // EnsureDefaultFeatures should be a no-op; no duplicates should appear. + if err := EnsureDefaultFeatures(db); err != nil { + t.Fatalf("EnsureDefaultFeatures: %v", err) + } + + features, err := db.ListFeatures("") + if err != nil { + t.Fatalf("ListFeatures: %v", err) + } + if len(features) != len(DefaultFeatureCatalog) { + t.Errorf("expected %d features, got %d", len(DefaultFeatureCatalog), len(features)) + } +} + +func TestDefaultCatalogCategories(t *testing.T) { + db := newTestDB(t) + + if err := SeedDefaultFeatures(db); err != nil { + t.Fatalf("SeedDefaultFeatures: %v", err) + } + + cats, err := db.ListCategories() + if err != nil { + t.Fatalf("ListCategories: %v", err) + } + // Verify at least 4 categories are present. + if len(cats) < 4 { + t.Errorf("expected at least 4 categories, got %d", len(cats)) + } +} diff --git a/internal/features_test.go b/internal/features_test.go index 32254fc..c1b4407 100644 --- a/internal/features_test.go +++ b/internal/features_test.go @@ -98,7 +98,8 @@ func TestSeedFeaturesMissingColumn(t *testing.T) { func TestSeedFeaturesFromCSVFile(t *testing.T) { db := newTestDB(t) - // Use the real table.csv in the repo root (two directories up from internal/). + // Use the real table.csv in the repo root (two directories up from internal/) + // to validate that the CSV admin-import path continues to work. count, err := SeedFeaturesFromCSV(db, "../table.csv") if err != nil { t.Fatalf("SeedFeaturesFromCSV: %v", err) diff --git a/internal/logic-cli.go b/internal/logic-cli.go index 1f5d69f..6df9685 100644 --- a/internal/logic-cli.go +++ b/internal/logic-cli.go @@ -741,7 +741,7 @@ func newFeaturesListCmd(db *Database) *cobra.Command { return err } if len(features) == 0 { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "No features found. Run: rete features seed --file table.csv") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "No features found.") return nil } @@ -767,8 +767,8 @@ func newFeaturesSeedCmd(db *Database) *cobra.Command { var csvFile string cmd := &cobra.Command{ Use: "seed", - Short: "Seed the feature catalog from a CSV file", - Example: " rete features seed --file table.csv", + Short: "Seed the feature catalog from a CSV file (admin import)", + Example: " rete features seed --file custom.csv", RunE: func(cmd *cobra.Command, args []string) error { count, err := SeedFeaturesFromCSV(db, csvFile) if err != nil { @@ -778,7 +778,8 @@ func newFeaturesSeedCmd(db *Database) *cobra.Command { return nil }, } - cmd.Flags().StringVarP(&csvFile, "file", "f", "table.csv", "Path to the CSV feature catalog") + cmd.Flags().StringVarP(&csvFile, "file", "f", "", "Path to a CSV feature catalog to import") + _ = cmd.MarkFlagRequired("file") return cmd } diff --git a/internal/ui-form.go b/internal/ui-form.go index 943a128..eff79d7 100644 --- a/internal/ui-form.go +++ b/internal/ui-form.go @@ -115,7 +115,7 @@ func (m *DashboardModel) View() string { // Feature catalog if len(m.categories) == 0 { - sb.WriteString(" No features loaded. Run: rete features seed --file table.csv\n\n") + sb.WriteString(" No features loaded.\n\n") } else { sb.WriteString(" Features\n") sb.WriteString(" " + strings.Repeat("─", 40) + "\n") diff --git a/main.go b/main.go index 171c34d..e20651c 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,10 @@ func main() { log.Fatalf("failed to initialize database: %v", err) } + if err := internal.EnsureDefaultFeatures(db); err != nil { + log.Fatalf("failed to seed default features: %v", err) + } + rootCmd := internal.NewRootCmd(db) if err := rootCmd.Execute(); err != nil { os.Exit(1)