feat: enhance target handling and improve release/install workflows

- Add file target support to heuristics with automatic TargetSpace derivation and comprehensive unit tests for file path parsing
- Display target space folder location during workflow execution for both module and flow runs
- Fix IP address handling in URL parsing to correctly identify and extract root domain for IP targets
- Add goreleaser --mark-latest flag to manual release workflow for consistent release tagging
- Improve install binary help text and add progress feedback for silent mode installations
- Replace deprecated --list-registry-binaries with --list-registry-direct-fetch and --list-registry-nix-build examples
This commit is contained in:
j3ssie
2026-01-21 17:20:52 +08:00
parent d21acbaf1a
commit 344e6117ca
6 changed files with 170 additions and 11 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
with:
distribution: goreleaser
version: '~> v2'
args: release --clean --skip=validate
args: release --clean --skip=validate --mark-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_CURRENT_TAG: ${{ steps.get-tag.outputs.tag }}
+18
View File
@@ -859,6 +859,15 @@ func (e *Executor) ExecuteModule(ctx context.Context, module *core.Workflow, par
e.printer.WorkflowInfo(module.Name, module.Description, module.Tags, string(module.Runner), len(module.Steps))
}
// Show target space folder location
if !e.dryRun && e.progressBar == nil {
if targetSpace, ok := execCtx.GetVariable("TargetSpace"); ok {
if tsStr, ok := targetSpace.(string); ok && tsStr != "" {
e.printer.Info("Reserving target space folder at: %s", terminal.Cyan(tsStr))
}
}
}
// Execute steps
e.logger.Debug("Starting step execution loop",
zap.Int("total_steps", len(module.Steps)),
@@ -1414,6 +1423,15 @@ func (e *Executor) ExecuteFlow(ctx context.Context, flow *core.Workflow, params
printDryRunHeader(flow.Name, string(core.KindFlow), params["target"], tactic, len(flow.Modules), execCtx)
}
// Show target space folder location
if !e.dryRun && e.progressBar == nil {
if targetSpace, ok := execCtx.GetVariable("TargetSpace"); ok {
if tsStr, ok := targetSpace.(string); ok && tsStr != "" {
e.printer.Info("Reserving target space folder at: %s", terminal.Cyan(tsStr))
}
}
}
// Export flow workflow state (write workflow YAML to output)
if !e.disableWorkflowState && !e.dryRun {
if stateWorkflowFile, ok := execCtx.GetVariable("StateWorkflowFile"); ok {
+31
View File
@@ -3,6 +3,8 @@ package heuristics
import (
"net"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
)
@@ -14,6 +16,7 @@ const (
TargetTypeURL TargetType = "url"
TargetTypeDomain TargetType = "domain"
TargetTypeIP TargetType = "ip"
TargetTypeFile TargetType = "file"
TargetTypeUnknown TargetType = "unknown"
)
@@ -93,6 +96,12 @@ func Analyze(target string, level string) (*TargetInfo, error) {
RootDomain: target,
}
case TargetTypeFile:
info, err = ParseFileTarget(target)
if err != nil {
return nil, err
}
default:
info = &TargetInfo{
Type: TargetTypeUnknown,
@@ -107,6 +116,11 @@ func Analyze(target string, level string) (*TargetInfo, error) {
func DetectType(target string) TargetType {
target = strings.TrimSpace(target)
// Check if it's a file path (exists on disk) - must check before URL
if info, err := os.Stat(target); err == nil && !info.IsDir() {
return TargetTypeFile
}
// Check if it's a URL (has scheme)
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
return TargetTypeURL
@@ -149,3 +163,20 @@ func isValidDomain(s string) bool {
domainRegex := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
return domainRegex.MatchString(s)
}
// ParseFileTarget extracts TargetInfo from a file path
// Derives RootDomain as: basename without extension, _ replaced with -, "-file" suffix
func ParseFileTarget(filePath string) (*TargetInfo, error) {
base := filepath.Base(filePath)
ext := filepath.Ext(base)
nameWithoutExt := strings.TrimSuffix(base, ext)
// Replace _ with - for path friendliness
nameWithoutExt = strings.ReplaceAll(nameWithoutExt, "_", "-")
targetSpace := nameWithoutExt + "-file"
return &TargetInfo{
Type: TargetTypeFile,
Original: filePath,
RootDomain: targetSpace,
}, nil
}
+97
View File
@@ -0,0 +1,97 @@
package heuristics
import (
"os"
"testing"
)
func TestDetectType_File(t *testing.T) {
// Create temp file
f, err := os.CreateTemp("", "test-file*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
f.Close()
got := DetectType(f.Name())
if got != TargetTypeFile {
t.Errorf("DetectType() = %v, want %v", got, TargetTypeFile)
}
}
func TestDetectType_DomainNotFile(t *testing.T) {
got := DetectType("example.com")
if got != TargetTypeDomain {
t.Errorf("DetectType() = %v, want %v", got, TargetTypeDomain)
}
}
func TestDetectType_URL(t *testing.T) {
got := DetectType("https://example.com/path")
if got != TargetTypeURL {
t.Errorf("DetectType() = %v, want %v", got, TargetTypeURL)
}
}
func TestParseFileTarget(t *testing.T) {
tests := []struct {
name string
filePath string
wantRoot string
}{
{
name: "simple filename",
filePath: "/tmp/urls-input.txt",
wantRoot: "urls-input-file",
},
{
name: "underscore replacement",
filePath: "/tmp/my_target_list.txt",
wantRoot: "my-target-list-file",
},
{
name: "multiple underscores",
filePath: "/path/to/some_long_file_name.csv",
wantRoot: "some-long-file-name-file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info, err := ParseFileTarget(tt.filePath)
if err != nil {
t.Errorf("ParseFileTarget() error = %v", err)
return
}
if info.RootDomain != tt.wantRoot {
t.Errorf("ParseFileTarget() RootDomain = %v, want %v", info.RootDomain, tt.wantRoot)
}
if info.Type != TargetTypeFile {
t.Errorf("ParseFileTarget() Type = %v, want %v", info.Type, TargetTypeFile)
}
if info.Original != tt.filePath {
t.Errorf("ParseFileTarget() Original = %v, want %v", info.Original, tt.filePath)
}
})
}
}
func TestAnalyze_FileTarget(t *testing.T) {
// Create temp file
f, err := os.CreateTemp("", "test_analysis*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
f.Close()
info, err := Analyze(f.Name(), "basic")
if err != nil {
t.Errorf("Analyze() error = %v", err)
return
}
if info.Type != TargetTypeFile {
t.Errorf("Analyze() Type = %v, want %v", info.Type, TargetTypeFile)
}
}
+16 -8
View File
@@ -2,6 +2,7 @@ package heuristics
import (
"crypto/tls"
"net"
"net/http"
"net/url"
"path"
@@ -45,15 +46,22 @@ func ParseURL(rawURL string) (*TargetInfo, error) {
info.Port = port
info.Hostname = u.Host // includes port if specified
// Extract root domain using publicsuffix
rootDomain, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
// Fallback: use the last two parts of the domain
rootDomain = extractRootDomainFallback(host)
// Check if host is an IP address
if net.ParseIP(host) != nil {
info.RootDomain = host
info.TLD = ""
info.SLD = ""
} else {
// Extract root domain using publicsuffix
rootDomain, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
// Fallback: use the last two parts of the domain
rootDomain = extractRootDomainFallback(host)
}
info.RootDomain = rootDomain
info.TLD, _ = publicsuffix.PublicSuffix(host)
info.SLD = extractSLD(info.RootDomain, info.TLD)
}
info.RootDomain = rootDomain
info.TLD, _ = publicsuffix.PublicSuffix(host)
info.SLD = extractSLD(info.RootDomain, info.TLD)
// Extract path and file
urlPath := u.Path
+7 -2
View File
@@ -88,7 +88,8 @@ var installBinaryCmd = &cobra.Command{
Short: "Install binary tools from registry",
Long: `Install one or more binary tools from the registry. Skips binaries already available in PATH.`,
Example: ` # List binaries in registry
osmedeus install binary --list-registry-binaries
osmedeus install binary --list-registry-direct-fetch
osmedeus install binary --list-registry-nix-build
# Install specific binaries
osmedeus install binary --name nuclei
@@ -1466,7 +1467,8 @@ func printInstallBinaryHelp(cmd *cobra.Command) {
fmt.Println(terminal.BoldCyan("◆ Examples"))
fmt.Printf(" %s\n", terminal.Green("# List available binaries"))
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary --list-registry-binaries"))
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --list-registry-direct-fetch"))
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary --list-registry-nix-build"))
fmt.Printf(" %s\n", terminal.Green("# Install specific binaries (auto-detects method from registry)"))
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --name nuclei"))
@@ -1743,6 +1745,9 @@ func installBinariesParallel(names []string, registry installer.BinaryRegistry,
go func() {
defer wg.Done()
for name := range workCh {
if silent {
fmt.Printf(" %s Installing: %s\n", terminal.SymbolBullet, terminal.Cyan(name))
}
err := installer.InstallBinary(name, registry, binariesFolder, headers)
statusMu.Lock()
if err != nil {