Skip to content

Commit 2dbac62

Browse files
feat: add Nuclei scanner auto-installation and store scan config/results metadata
1 parent fd282ff commit 2dbac62

File tree

4 files changed

+308
-24
lines changed

4 files changed

+308
-24
lines changed

cmd/self.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package cmd
33

44
import (
55
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
69
"time"
710

811
"github.com/CodeMonkeyCybersecurity/shells/internal/config"
@@ -141,6 +144,28 @@ func runUpdate(cmd *cobra.Command, args []string) error {
141144
fmt.Println(" Database migrations completed successfully!")
142145
}
143146

147+
// Install/update Nuclei scanner
148+
logger.Infow("Checking Nuclei installation",
149+
"component", "self_update",
150+
)
151+
fmt.Println()
152+
fmt.Println(" Checking Nuclei scanner...")
153+
154+
if err := ensureNucleiInstalled(logger); err != nil {
155+
logger.Warnw("Nuclei installation/update failed",
156+
"component", "self_update",
157+
"error", err,
158+
)
159+
fmt.Printf("⚠️ Warning: Nuclei installation failed: %v\n", err)
160+
fmt.Printf(" Nuclei scanning will be disabled until installed\n")
161+
fmt.Printf(" You can install manually with: %s/scripts/install-nuclei.sh\n", updateSourceDir)
162+
} else {
163+
logger.Infow("Nuclei scanner ready",
164+
"component", "self_update",
165+
)
166+
fmt.Println(" Nuclei scanner ready!")
167+
}
168+
144169
fmt.Println()
145170
log.Info(" Shells updated successfully!", "component", "self")
146171
fmt.Printf(" Duration: %s\n", duration.Round(time.Second))
@@ -149,3 +174,87 @@ func runUpdate(cmd *cobra.Command, args []string) error {
149174

150175
return nil
151176
}
177+
178+
// ensureNucleiInstalled checks if Nuclei is installed and installs/updates it if needed
179+
func ensureNucleiInstalled(logger *logger.Logger) error {
180+
// Check if nuclei is already in PATH
181+
nucleiPath, err := exec.LookPath("nuclei")
182+
if err == nil {
183+
// Nuclei found, check version and update templates
184+
logger.Infow("Nuclei already installed",
185+
"path", nucleiPath,
186+
"component", "nuclei_setup",
187+
)
188+
189+
// Update templates
190+
cmd := exec.Command("nuclei", "-update-templates", "-silent")
191+
if err := cmd.Run(); err != nil {
192+
logger.Warnw("Failed to update Nuclei templates",
193+
"error", err,
194+
"component", "nuclei_setup",
195+
)
196+
// Don't fail if template update fails
197+
} else {
198+
logger.Infow("Nuclei templates updated",
199+
"component", "nuclei_setup",
200+
)
201+
}
202+
203+
return nil
204+
}
205+
206+
// Nuclei not found, need to install
207+
logger.Infow("Nuclei not found, installing from GitHub",
208+
"component", "nuclei_setup",
209+
)
210+
fmt.Println(" Installing Nuclei scanner...")
211+
212+
// Install using go install
213+
installCmd := exec.Command("go", "install", "-v", "github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest")
214+
installCmd.Stdout = nil // Suppress output
215+
installCmd.Stderr = nil
216+
217+
if err := installCmd.Run(); err != nil {
218+
return fmt.Errorf("failed to install nuclei: %w", err)
219+
}
220+
221+
// Verify installation
222+
nucleiPath, err = exec.LookPath("nuclei")
223+
if err != nil {
224+
// Try in Go bin directory
225+
goPath := os.Getenv("GOPATH")
226+
if goPath == "" {
227+
homeDir, _ := os.UserHomeDir()
228+
goPath = filepath.Join(homeDir, "go")
229+
}
230+
nucleiPath = filepath.Join(goPath, "bin", "nuclei")
231+
232+
if _, err := os.Stat(nucleiPath); err != nil {
233+
return fmt.Errorf("nuclei installed but not found in PATH. Please add %s/bin to your PATH", goPath)
234+
}
235+
}
236+
237+
logger.Infow("Nuclei installed successfully",
238+
"path", nucleiPath,
239+
"component", "nuclei_setup",
240+
)
241+
242+
// Update templates
243+
fmt.Println(" Updating Nuclei templates...")
244+
templatesCmd := exec.Command(nucleiPath, "-update-templates", "-silent")
245+
if err := templatesCmd.Run(); err != nil {
246+
logger.Warnw("Failed to update Nuclei templates",
247+
"error", err,
248+
"component", "nuclei_setup",
249+
)
250+
// Don't fail - templates will update on first run
251+
} else {
252+
logger.Infow("Nuclei templates updated",
253+
"component", "nuclei_setup",
254+
)
255+
}
256+
257+
fmt.Println(" Nuclei scanner installed successfully!")
258+
259+
return nil
260+
}

internal/orchestrator/bounty_engine.go

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package orchestrator
44
import (
55
"context"
66
"fmt"
7+
"os/exec"
78
"sort"
89
"strings"
910
"sync"
@@ -219,20 +220,34 @@ func NewBugBountyEngine(
219220
logger.Infow("Nmap scanner initialized", "component", "orchestrator")
220221
}
221222

222-
// Initialize Nuclei scanner (if enabled)
223+
// Initialize Nuclei scanner (if enabled and binary exists)
223224
var nucleiScanner core.Scanner
224225
if config.EnableNucleiScan {
225-
nucleiConfig := nuclei.NucleiConfig{
226-
BinaryPath: "nuclei",
227-
TemplatesPath: "", // Use default
228-
Timeout: config.ScanTimeout,
229-
RateLimit: int(config.RateLimitPerSecond),
230-
BulkSize: 25,
231-
Concurrency: 25,
232-
Retries: 2,
226+
// Check if nuclei binary is available
227+
nucleiBinaryPath := "nuclei"
228+
if _, err := exec.LookPath(nucleiBinaryPath); err != nil {
229+
logger.Warnw("Nuclei scanner disabled - binary not found in PATH",
230+
"error", err,
231+
"binary", nucleiBinaryPath,
232+
"install_instructions", "Run: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest",
233+
"component", "orchestrator",
234+
)
235+
} else {
236+
nucleiConfig := nuclei.NucleiConfig{
237+
BinaryPath: nucleiBinaryPath,
238+
TemplatesPath: "", // Use default
239+
Timeout: config.ScanTimeout,
240+
RateLimit: int(config.RateLimitPerSecond),
241+
BulkSize: 25,
242+
Concurrency: 25,
243+
Retries: 2,
244+
}
245+
nucleiScanner = nuclei.NewScanner(nucleiConfig, samlLogger)
246+
logger.Infow("Nuclei scanner initialized",
247+
"binary_path", nucleiBinaryPath,
248+
"component", "orchestrator",
249+
)
233250
}
234-
nucleiScanner = nuclei.NewScanner(nucleiConfig, samlLogger)
235-
logger.Infow("Nuclei scanner initialized", "component", "orchestrator")
236251
}
237252

238253
// Initialize GraphQL scanner (if enabled)
@@ -2309,7 +2324,67 @@ func extractHost(urlStr string) string {
23092324

23102325
// storeResults saves scan results to the database
23112326
func (e *BugBountyEngine) storeResults(ctx context.Context, scanID string, result *BugBountyResult) error {
2312-
// Save scan metadata
2327+
// Prepare scan configuration for storage
2328+
configJSON := map[string]interface{}{
2329+
"discovery_timeout": e.config.DiscoveryTimeout.String(),
2330+
"scan_timeout": e.config.ScanTimeout.String(),
2331+
"total_timeout": e.config.TotalTimeout.String(),
2332+
"max_assets": e.config.MaxAssets,
2333+
"max_depth": e.config.MaxDepth,
2334+
"enable_port_scan": e.config.EnablePortScan,
2335+
"enable_web_crawl": e.config.EnableWebCrawl,
2336+
"enable_dns": e.config.EnableDNS,
2337+
"enable_subdomain_enum": e.config.EnableSubdomainEnum,
2338+
"enable_cert_transparency": e.config.EnableCertTransparency,
2339+
"enable_whois_analysis": e.config.EnableWHOISAnalysis,
2340+
"enable_related_domain_disc": e.config.EnableRelatedDomainDisc,
2341+
"enable_auth_testing": e.config.EnableAuthTesting,
2342+
"enable_api_testing": e.config.EnableAPITesting,
2343+
"enable_scim_testing": e.config.EnableSCIMTesting,
2344+
"enable_graphql_testing": e.config.EnableGraphQLTesting,
2345+
"enable_nuclei_scan": e.config.EnableNucleiScan,
2346+
"enable_service_fingerprint": e.config.EnableServiceFingerprint,
2347+
}
2348+
2349+
// Prepare results summary for storage
2350+
resultJSON := map[string]interface{}{
2351+
"scan_id": result.ScanID,
2352+
"target": result.Target,
2353+
"start_time": result.StartTime,
2354+
"end_time": result.EndTime,
2355+
"duration": result.Duration.String(),
2356+
"status": result.Status,
2357+
"discovered_at": result.DiscoveredAt,
2358+
"tested_assets": result.TestedAssets,
2359+
"total_findings": result.TotalFindings,
2360+
"phase_results": result.PhaseResults,
2361+
"findings_by_severity": map[string]int{
2362+
"critical": 0,
2363+
"high": 0,
2364+
"medium": 0,
2365+
"low": 0,
2366+
"info": 0,
2367+
},
2368+
}
2369+
2370+
// Count findings by severity
2371+
severityCounts := resultJSON["findings_by_severity"].(map[string]int)
2372+
for _, finding := range result.Findings {
2373+
switch strings.ToUpper(string(finding.Severity)) {
2374+
case "CRITICAL":
2375+
severityCounts["critical"]++
2376+
case "HIGH":
2377+
severityCounts["high"]++
2378+
case "MEDIUM":
2379+
severityCounts["medium"]++
2380+
case "LOW":
2381+
severityCounts["low"]++
2382+
default:
2383+
severityCounts["info"]++
2384+
}
2385+
}
2386+
2387+
// Save scan metadata with config and results
23132388
startedAt := result.StartTime
23142389
completedAt := result.EndTime
23152390
scan := &types.ScanRequest{
@@ -2320,6 +2395,8 @@ func (e *BugBountyEngine) storeResults(ctx context.Context, scanID string, resul
23202395
CreatedAt: result.StartTime,
23212396
StartedAt: &startedAt, // Set started time for duration calculation
23222397
CompletedAt: &completedAt,
2398+
Config: configJSON, // Store scan configuration
2399+
Result: resultJSON, // Store scan results summary
23232400
}
23242401

23252402
if err := e.store.SaveScan(ctx, scan); err != nil {

pkg/types/types.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,21 @@ type Finding struct {
5858
}
5959

6060
type ScanRequest struct {
61-
ID string `json:"id" db:"id"`
62-
Target string `json:"target" db:"target"`
63-
Type ScanType `json:"type" db:"type"`
64-
Profile string `json:"profile,omitempty" db:"profile"`
65-
Options map[string]string `json:"options,omitempty"`
66-
ScheduledAt *time.Time `json:"scheduled_at,omitempty" db:"scheduled_at"`
67-
CreatedAt time.Time `json:"created_at" db:"created_at"`
68-
StartedAt *time.Time `json:"started_at,omitempty" db:"started_at"`
69-
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
70-
Status ScanStatus `json:"status" db:"status"`
71-
ErrorMessage string `json:"error_message,omitempty" db:"error_message"`
72-
WorkerID string `json:"worker_id,omitempty" db:"worker_id"`
61+
ID string `json:"id" db:"id"`
62+
Target string `json:"target" db:"target"`
63+
Type ScanType `json:"type" db:"type"`
64+
Profile string `json:"profile,omitempty" db:"profile"`
65+
Options map[string]string `json:"options,omitempty"`
66+
ScheduledAt *time.Time `json:"scheduled_at,omitempty" db:"scheduled_at"`
67+
CreatedAt time.Time `json:"created_at" db:"created_at"`
68+
StartedAt *time.Time `json:"started_at,omitempty" db:"started_at"`
69+
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
70+
Status ScanStatus `json:"status" db:"status"`
71+
ErrorMessage string `json:"error_message,omitempty" db:"error_message"`
72+
WorkerID string `json:"worker_id,omitempty" db:"worker_id"`
73+
Config map[string]interface{} `json:"config,omitempty" db:"config"` // Scan configuration (timeouts, enabled scanners)
74+
Result map[string]interface{} `json:"result,omitempty" db:"result"` // Scan results summary (assets, phases, findings count)
75+
Checkpoint map[string]interface{} `json:"checkpoint,omitempty" db:"checkpoint"` // Checkpoint data for resumable scans
7376
}
7477

7578
type ScanResult struct {

scripts/install-nuclei.sh

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/bin/bash
2+
# install-nuclei.sh - Install and configure Nuclei for shells
3+
# This script is run automatically during shells setup
4+
5+
set -e
6+
7+
echo "===== Installing Nuclei for shells ====="
8+
9+
# Check if Go is installed
10+
if ! command -v go &> /dev/null; then
11+
echo "ERROR: Go is not installed. Please install Go first."
12+
exit 1
13+
fi
14+
15+
# Get Go bin directory
16+
GOBIN=$(go env GOPATH)/bin
17+
mkdir -p "$GOBIN"
18+
19+
echo "Installing Nuclei from GitHub..."
20+
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
21+
22+
# Verify installation
23+
if [ ! -f "$GOBIN/nuclei" ]; then
24+
echo "ERROR: Nuclei installation failed"
25+
exit 1
26+
fi
27+
28+
echo "Nuclei installed successfully at: $GOBIN/nuclei"
29+
30+
# Add to PATH if not already there
31+
if [[ ":$PATH:" != *":$GOBIN:"* ]]; then
32+
echo "Adding $GOBIN to PATH..."
33+
34+
# Add to current session
35+
export PATH=$PATH:$GOBIN
36+
37+
# Add to shell profile
38+
if [ -f "$HOME/.bashrc" ]; then
39+
if ! grep -q "$GOBIN" "$HOME/.bashrc"; then
40+
echo "export PATH=\$PATH:$GOBIN" >> "$HOME/.bashrc"
41+
echo "Added to ~/.bashrc"
42+
fi
43+
fi
44+
45+
if [ -f "$HOME/.zshrc" ]; then
46+
if ! grep -q "$GOBIN" "$HOME/.zshrc"; then
47+
echo "export PATH=\$PATH:$GOBIN" >> "$HOME/.zshrc"
48+
echo "Added to ~/.zshrc"
49+
fi
50+
fi
51+
fi
52+
53+
# Update Nuclei templates
54+
echo "Updating Nuclei templates..."
55+
"$GOBIN/nuclei" -update-templates -silent
56+
57+
# Create nuclei config directory
58+
NUCLEI_CONFIG_DIR="$HOME/.config/nuclei"
59+
mkdir -p "$NUCLEI_CONFIG_DIR"
60+
61+
# Create minimal config file
62+
cat > "$NUCLEI_CONFIG_DIR/config.yaml" <<EOF
63+
# Nuclei configuration for shells
64+
rate-limit: 150
65+
bulk-size: 25
66+
concurrency: 25
67+
retries: 2
68+
timeout: 900
69+
silent: true
70+
stats: true
71+
EOF
72+
73+
echo "Nuclei config created at: $NUCLEI_CONFIG_DIR/config.yaml"
74+
75+
# Verify installation
76+
echo "Verifying Nuclei installation..."
77+
NUCLEI_VERSION=$("$GOBIN/nuclei" -version 2>&1 | head -1)
78+
echo "Nuclei version: $NUCLEI_VERSION"
79+
80+
# Test basic functionality
81+
echo "Testing Nuclei..."
82+
if "$GOBIN/nuclei" -u https://scanme.sh -tags dns -silent > /dev/null 2>&1; then
83+
echo "✓ Nuclei test successful"
84+
else
85+
echo "⚠ Nuclei test failed, but installation complete"
86+
fi
87+
88+
echo ""
89+
echo "===== Nuclei Installation Complete ====="
90+
echo "Binary location: $GOBIN/nuclei"
91+
echo "Templates location: $HOME/nuclei-templates/"
92+
echo "Config location: $NUCLEI_CONFIG_DIR/config.yaml"
93+
echo ""
94+
echo "To use immediately, run: export PATH=\$PATH:$GOBIN"
95+
echo "Or restart your shell to load from profile"

0 commit comments

Comments
 (0)