Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion internal/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
100 changes: 100 additions & 0 deletions internal/feature_catalog.go
Original file line number Diff line number Diff line change
@@ -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)
}
111 changes: 111 additions & 0 deletions internal/feature_catalog_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
3 changes: 2 additions & 1 deletion internal/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions internal/logic-cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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 {
Expand All @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion internal/ui-form.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading