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: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ yay -S fscan-git

## 运行截图

使用 `fscan -list-plugins` 查看当前构建可用的模块名称、类型、默认端口和调用方式。
通过 `-m` 指定有固定端口的模块且未传 `-p/-pf` 时,扫描范围使用所选模块默认端口的并集;
例如 `fscan -hf ip.txt -m netbios` 使用 `137,139`。指定 `-p` 可扫描非标准端口,Web 模块仍使用通用端口范围。
`-pwda/-usera` 追加到默认字典;与 `-pwd/-user` 或 `-pwdf/-userf` 同用时,追加到指定字典。

`fscan.exe -h 192.168.x.x`
![](image/1.png)

Expand Down
5 changes: 5 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ yay -S fscan-git

## Screenshots

Use `fscan -list-plugins` to list the plugins available in the current build, their types, default ports and usage.
When `-m` selects plugins with fixed ports and neither `-p` nor `-pf` is supplied, scanning uses the union of their default ports;
for example, `fscan -hf ip.txt -m netbios` uses `137,139`. Use `-p` for nonstandard ports. Web plugins retain the common port range.
`-pwda/-usera` append to the default dictionaries, or to the supplied dictionaries when used with `-pwd/-user` or `-pwdf/-userf`.

`fscan.exe -h 192.168.x.x`
![](image/1.png)

Expand Down
11 changes: 9 additions & 2 deletions common/config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ func parseCredentials(fv *FlagVars, cfg *Config) error {
return err
}
if len(usernames) > 0 {
for serviceName := range cfg.Credentials.Userdict {
cfg.Credentials.Userdict[serviceName] = usernames
for serviceName, defaults := range cfg.Credentials.Userdict {
if fv.Username == "" && fv.UsersFile == "" {
cfg.Credentials.Userdict[serviceName] = removeDuplicate(append(defaults, usernames...))
} else {
cfg.Credentials.Userdict[serviceName] = usernames
}
}
}

Expand All @@ -66,6 +70,9 @@ func parseCredentials(fv *FlagVars, cfg *Config) error {
return err
}
if len(passwords) > 0 {
if fv.Password == "" && fv.PasswordsFile == "" {
passwords = removeDuplicate(append(cfg.Credentials.Passwords, passwords...))
}
cfg.Credentials.Passwords = passwords
}

Expand Down
100 changes: 100 additions & 0 deletions common/config_builder_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,113 @@
package common

import (
"os"
"path/filepath"
"reflect"
"testing"
"time"

fscanconfig "github.com/shadow1ng/fscan/common/config"
)

func TestBuildConfigAdditionalCredentials(t *testing.T) {
original := NewConfig().Credentials
dir := t.TempDir()
usersFile := filepath.Join(dir, "users.txt")
passwordsFile := filepath.Join(dir, "passwords.txt")
if err := os.WriteFile(usersFile, []byte("file-user\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(passwordsFile, []byte("file-password\n"), 0600); err != nil {
t.Fatal(err)
}

tests := []struct {
name string
flags FlagVars
users []string
passwords []string
appendUsers bool
}{
{name: "defaults"},
{
name: "append to defaults",
flags: FlagVars{
AddUsers: "extra-user,extra-user",
AddPasswords: "extra-password extra-password",
},
users: []string{"extra-user"},
passwords: append(append([]string(nil), fscanconfig.DefaultPasswords...), "extra-password"),
appendUsers: true,
},
{
name: "default password is not duplicated",
flags: FlagVars{AddPasswords: "123456,123456"},
},
{
name: "append to command line overrides",
flags: FlagVars{
Username: "custom-user", Password: "primary password",
AddUsers: "extra-user,custom-user", AddPasswords: "extra-password extra-password",
},
users: []string{"custom-user", "extra-user"},
passwords: []string{"primary password", "extra-password"},
},
{
name: "append to file overrides",
flags: FlagVars{
UsersFile: usersFile, PasswordsFile: passwordsFile,
AddUsers: "extra-user,file-user", AddPasswords: "extra-password file-password",
},
users: []string{"file-user", "extra-user"},
passwords: []string{"file-password", "extra-password"},
},
{
name: "command line and files still replace defaults",
flags: FlagVars{
Username: "custom-user", Password: "primary password",
UsersFile: usersFile, PasswordsFile: passwordsFile,
AddUsers: "extra-user", AddPasswords: "extra-password",
},
users: []string{"custom-user", "file-user", "extra-user"},
passwords: []string{"primary password", "file-password", "extra-password"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, _, err := BuildConfig(&tt.flags, &HostInfo{})
if err != nil {
t.Fatal(err)
}
wantPasswords := tt.passwords
if wantPasswords == nil {
wantPasswords = append([]string(nil), fscanconfig.DefaultPasswords...)
}
if !reflect.DeepEqual(cfg.Credentials.Passwords, wantPasswords) {
t.Errorf("passwords = %v, want %v", cfg.Credentials.Passwords, wantPasswords)
}
for service, defaults := range fscanconfig.DefaultUserDict {
wantUsers := tt.users
if wantUsers == nil || tt.appendUsers {
wantUsers = append(append([]string{}, defaults...), wantUsers...)
}
if !reflect.DeepEqual(cfg.Credentials.Userdict[service], wantUsers) {
t.Errorf("%s users = %v, want %v", service, cfg.Credentials.Userdict[service], wantUsers)
}
}
})
}
defaults, _, err := BuildConfig(&FlagVars{}, &HostInfo{})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(defaults.Credentials.Passwords, original.Passwords) ||
!reflect.DeepEqual(defaults.Credentials.Userdict, original.Userdict) {
t.Fatal("additional credentials leaked into a later config")
}
}

func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
fv := &FlagVars{
Password: "root admin,pass0",
Expand Down
7 changes: 7 additions & 0 deletions common/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func Flag(Info *HostInfo) error {

// 帮助参数
flag.BoolVar(&fv.ShowHelp, "help", false, i18n.GetText("flag_help"))
flag.BoolVar(&fv.ListPlugins, "list-plugins", false, i18n.GetText("flag_list_plugins"))

// 解析命令行参数
if err := parseCommandLineArgs(); err != nil {
Expand All @@ -218,6 +219,8 @@ func Flag(Info *HostInfo) error {
// 检测用户是否显式指定了 -t
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "p":
fv.PortsExplicit = true
case "t":
fv.ThreadNumExplicit = true
case "time":
Expand Down Expand Up @@ -348,6 +351,10 @@ func preProcessLanguage() {

// shouldShowHelp 检查是否应该显示帮助信息
func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
if fv.ListPlugins {
return false
}

// Web模式不需要目标参数
if WebMode {
return false
Expand Down
4 changes: 3 additions & 1 deletion common/flag_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type FlagVars struct {
ExcludeHosts string
ExcludeHostsFile string
Ports string
PortsExplicit bool
ExcludePorts string
HostsFile string
PortsFile string
Expand Down Expand Up @@ -121,7 +122,8 @@ type FlagVars struct {
DownloadSavePath string

// 帮助
ShowHelp bool
ShowHelp bool
ListPlugins bool
}

// =============================================================================
Expand Down
8 changes: 6 additions & 2 deletions common/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ flag_exclude_hosts:
flag_exclude_hosts_file:
other: "Exclude hosts file"
flag_ports:
other: "Ports: default 1000 common ports"
other: "Ports: defaults to selected plugins' ports, otherwise common ports"
flag_exclude_ports:
other: "Exclude ports"
flag_hosts_file:
other: "Hosts file"
flag_ports_file:
other: "Ports file"
flag_scan_mode:
other: "Scan mode: all(all plugins), icmp(alive detection), or specific plugin names"
other: "Scan mode: all(all plugins), icmp(alive detection), or specific plugin names (see -list-plugins)"
flag_list_plugins:
other: "List available plugins, types, default ports and usage, then exit"
plugin_list_header:
other: "Plugin\tType\tDefault ports\tUsage"
flag_thread_num:
other: "Port scan thread count"
flag_timeout:
Expand Down
8 changes: 6 additions & 2 deletions common/i18n/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ flag_exclude_hosts:
flag_exclude_hosts_file:
other: "排除主机文件"
flag_ports:
other: "端口: 默认1000个常用端口"
other: "端口: 指定模块时默认使用模块端口,否则使用常用端口"
flag_exclude_ports:
other: "排除端口"
flag_hosts_file:
other: "主机文件"
flag_ports_file:
other: "端口文件"
flag_scan_mode:
other: "扫描模式: all(全部), icmp(存活探测), 或指定插件名称"
other: "扫描模式: all(全部), icmp(存活探测), 或指定插件名称(-list-plugins 查看)"
flag_list_plugins:
other: "列出当前构建可用的模块、类型、默认端口和调用方式后退出"
plugin_list_header:
other: "模块\t类型\t默认端口\t调用方式"
flag_thread_num:
other: "端口扫描线程数"
flag_timeout:
Expand Down
10 changes: 10 additions & 0 deletions main_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,21 @@ func run() int {
return 1
}

flags := common.GetFlagVars()
if flags.ListPlugins {
if err := printPluginList(os.Stdout); err != nil {
common.LogError(i18n.Tr("error_generic", err))
return 1
}
return 0
}

// 检查参数互斥性
if err := common.ValidateExclusiveParams(&info); err != nil {
common.LogError(i18n.Tr("error_generic", err))
return 1
}
setPluginDefaultPorts(flags)

// 统一初始化:解析 → 配置 → 输出
result, err := common.Initialize(&info)
Expand Down
74 changes: 74 additions & 0 deletions main_cli_plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//go:build !web

package main

import (
"fmt"
"io"
"slices"
"strconv"
"strings"
"text/tabwriter"

"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
)

func setPluginDefaultPorts(flags *common.FlagVars) {
if flags.PortsExplicit || flags.PortsFile != "" || flags.AliveOnly || flags.LocalPlugin != "" {
return
}

var ports []int
for _, name := range strings.Split(flags.ScanMode, ",") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
defaults := plugins.GetPluginPorts(name)
// Web、本地插件和特殊模式没有固定端口,保留通用端口范围。
if len(defaults) == 0 {
return
}
ports = append(ports, defaults...)
}
if len(ports) > 0 {
flags.Ports = formatPluginPorts(ports)
}
}

func formatPluginPorts(ports []int) string {
ports = slices.Clone(ports)
slices.Sort(ports)
var values []string
for _, port := range slices.Compact(ports) {
values = append(values, strconv.Itoa(port))
}
return strings.Join(values, ",")
}

func printPluginList(w io.Writer) error {
names := plugins.All()
slices.Sort(names)
table := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
_, _ = fmt.Fprintln(table, i18n.GetText("plugin_list_header"))
for _, name := range names {
var types []string
for _, kind := range []string{plugins.PluginTypeService, plugins.PluginTypeUDP, plugins.PluginTypeWeb, plugins.PluginTypeLocal} {
if plugins.HasType(name, kind) {
types = append(types, kind)
}
}
ports := formatPluginPorts(plugins.GetPluginPorts(name))
if ports == "" {
ports = "-"
}
option := "-m"
if plugins.HasType(name, plugins.PluginTypeLocal) {
option = "-local"
}
_, _ = fmt.Fprintf(table, "%s\t%s\t%s\t%s %s\n", name, strings.Join(types, ","), ports, option, name)
}
return table.Flush()
}
Loading
Loading