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
71 changes: 38 additions & 33 deletions .github/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,56 @@
# 发版流程

正式版由 `main` 的 push 触发:读取 `common/globals.go` 的版本号、创建对应 tag,并在同一次工作流中构建和发布 GitHub Release。

**合并到 `main` 就会开始发布。** 合并前必须更新版本号、准备 Release Notes,并验证包含全部待发布改动的候选提交。测试构建工作流与发布工作流独立,发布不会等待合并后的测试结果。

## 预检查

```bash
# 1. 确认 CI 通过
gh run list --branch dev --limit 3
# 使用已包含全部待发布改动的分支:dev 或 release/*
RELEASE_REF=release/v2.2.2

# 1. 确认该分支最新提交的测试 CI 通过
gh run list --branch "$RELEASE_REF" --workflow test-build.yml --limit 3

# 2. 全平台 dry-run;必须指定 ref,避免默认构建 main
gh workflow run release.yml --ref "$RELEASE_REF" -f snapshot=true
gh run list --branch "$RELEASE_REF" --workflow release.yml --limit 3

# 2. 全平台 dry-run(手动触发 snapshot 模式)
gh workflow run release.yml -f snapshot=true
# 3. 在候选分支确认版本号一致,且 Release Notes 已就绪
rg 'version = ' common/globals.go
rg '^\*\*版本\*\*:' README.md
rg '^\*\*Version\*\*:' README_EN.md
cat .github/release-notes/v2.2.2.md

# 3. 确认版本号一致
grep "version" common/globals.go
grep "版本" README.md
grep "Version" README_EN.md
# 4. 新版本 tag 应不存在;已有 tag 不可移动或覆盖
git ls-remote --tags origin refs/tags/v2.2.2
```

## 发版
等待 snapshot 工作流成功,并核对该次运行的 SHA 与待合并提交一致。产物包括标准版、无本地插件版、Web 版和 SHA256 校验文件;snapshot 不创建 tag 或 GitHub Release。

## 正式版发布

```bash
# 1. 确认 release notes 已就绪
cat .github/release-notes/v<VERSION>.md
# 从已验证的候选分支创建 PR
gh pr create --base main --head "$RELEASE_REF"

# 2. 打 tag(RC 手动打;正式版合并到 main 后由 CI 自动打 tag)
git tag v<VERSION>
git push origin v<VERSION>
# 审核通过后合并 PR;此次 main push 会自动打 tag 并发布
# 无需再手动执行 git tag / git push tag

# CI 自动执行:
# - goreleaser 全平台构建 + UPX 压缩
# - 创建 GitHub Release(RC 自动标记 pre-release)
# - 用 .github/release-notes/ 下的文件覆盖 release body
# 跟踪发布任务,确认 Release 与附件完整
gh run list --branch main --workflow release.yml --limit 3
gh release view v2.2.2
```

若工作流提示 tag 已存在且不指向当前提交,说明版本号未递增或该版本已经发布;先核对提交和版本,不能移动已有 tag。若构建失败,在对应 Actions 运行中查看失败步骤。

发布完成后将 `main` 同步回 `dev`,保留已发布提交的历史。手动重跑正式版构建必须指定已有 tag;`draft=true` 仅控制本次手动运行创建草稿,不改变 `main` 合并即发布的行为。

## 预发布

在候选分支准备 RC 版本号与同名 Release Notes,验证后手动推送 `vX.Y.Z-rc.N` tag。tag push 会触发构建,GoReleaser 自动将 RC 标记为 prerelease。

## 版本号规范

| 场景 | 格式 | 分支 | 示例 |
Expand All @@ -44,18 +64,3 @@ git push origin v<VERSION>
放在 `.github/release-notes/<tag>.md`,格式参考 `v2.2.0-rc.md`。

如果文件不存在,goreleaser 会自动生成基于 commit 的 changelog。

## 正式版发布(RC → 正式)

```bash
# 1. 在 dev 分支准备正式版内容
# common/globals.go, README.md, README_EN.md
# .github/release-notes/v2.2.0.md

# 2. 创建 dev -> main PR
gh pr create --base main --head dev

# 3. 合并 PR
# main push 会自动读取 common/globals.go 中的版本号,创建 v<VERSION> tag
# tag push 会触发 GoReleaser 构建并创建 GitHub Release
```
44 changes: 44 additions & 0 deletions .github/release-notes/v2.2.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# fscan v2.2.2

v2.2.2 是 v2.2 系列的修复版本,重点解决扫描参数未生效、目标遗漏、SMTP 扫描挂起、POC 表达式兼容性和结果备份丢失问题。

## 扫描参数与目标调度

- 修复只指定 `-pwda` 或 `-usera` 时默认字典被覆盖的问题;追加参数保留基础字典
- 新增 `-list-plugins`,列出当前构建实际可用的模块及注册端口
- 指定固定端口服务模块且未设置 `-p`、`-pf` 时,使用所选模块的注册端口集合;NetBIOS 跳过不适用端口
- 修复 `-pf` 端口文件未读取的问题,支持逐行端口、范围和逗号列表;不可读或没有有效端口的文件返回错误
- 修复 IPv4/IPv6 `host:port` 在解析后被误判为缺少扫描目标的问题
- 服务扫描中的缓存目标遵守 `-eh`、`-ehf`、`-ep`,UDP 插件调度也遵守 `-ep`
- 服务扫描策略使用当前 session 的服务识别缓存

## 协议与 POC 兼容性

- 为 SMTP 匿名访问和开放中继检查设置连接读写期限,避免目标接受连接后不发送 greeting 导致扫描一直等待
- 修复 Base64、JWT 等编码字符串被误当作 CEL 表达式编译的问题,覆盖 POC 的 `set` 和 `sets`
- 补充 `timestamp_second()`,修正 POC 中 `randomLowercase` 的调用

## Web、SDK 与结果保存

- Web 扫描使用完整配置解析,使自定义账号密码和 `host:port` 生效;项目缓存与显式目标合并
- SDK 普通扫描的 `OnProgress` 回调关联实际扫描状态
- TXT、JSON、CSV 仅在最终结果写入、同步并关闭成功后删除 `.realtime.tmp`;失败时保留恢复备份并关闭文件资源
- 增加对应回归测试,并将 Web 接口测试纳入 CI

## 相关 Issue 与 PR

- #605:POC CEL 表达式兼容性
- #609、#610、#611:凭据追加、模块端口选择与模块列表
- #608:SMTP 静默服务端导致扫描挂起
- #612:端口文件、目标调度、Web/SDK 状态及结果备份

## 升级说明

- 从 v2.2.1 可直接升级
- 使用 `-pwda`、`-usera` 追加凭据时会保留基础字典;只测试指定凭据时请使用 `-user`、`-pwd` 或相应字典文件参数
- 如需指定完整扫描端口集合,请显式使用 `-p` 或 `-pf`
- 结果保存失败时可检查输出文件旁的 `.realtime.tmp` 恢复备份

完整变更记录:

https://github.com/shadow1ng/fscan/compare/v2.2.1...v2.2.2
3 changes: 3 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ jobs:
PKGS=$(go list ./... | grep -v '/libs/grdp/')
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS

- name: Web 接口回归测试
run: go test -vet=off -race -tags web ./web/api

- name: 上传覆盖率
uses: actions/upload-artifact@v4
with:
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

内网综合扫描工具,一键自动化漏扫。

**版本**: 2.2.1
**版本**: 2.2.2

## 功能特性

Expand Down 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
7 changes: 6 additions & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Comprehensive intranet scanning tool for automated vulnerability assessment.

**Version**: 2.2.1
**Version**: 2.2.2

## Features

Expand Down 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
25 changes: 22 additions & 3 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 Expand Up @@ -238,8 +245,20 @@ func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
// =============================================================================

func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error {
// 检查是否为 host:port 格式
ports := fv.Ports
if fv.PortsFile != "" {
lines, err := parsers.ReadLinesFromFile(fv.PortsFile)
if err != nil {
return fmt.Errorf("%s", i18n.Tr("config_read_ports_failed", fv.PortsFile, err))
}
ports = strings.Join(lines, ",")
if len(parsers.ParsePort(ports)) == 0 {
return fmt.Errorf("%s", i18n.Tr("invalid_port", ports))
}
cfg.Target.Ports = ports
}

// 检查是否为 host:port 格式
if info.Host != "" && strings.Contains(info.Host, ":") {
if _, portStr, err := net.SplitHostPort(info.Host); err == nil {
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
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
Loading
Loading