合并扫描状态与结果备份修复

This commit is contained in:
ZacharyZcR committed 2026-10-01 13:16:05 +08:00
commit 8e78ba557c
14 files changed
+444 -95

No files matched your search

+13 -1
View File
@@ -245,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 {
+2
View File
@@ -315,6 +315,8 @@ config_read_passwords_failed:
other: "Failed to read password file {{.Arg1}}: {{.Arg2}}"
config_read_urls_failed:
other: "Failed to read URL file {{.Arg1}}: {{.Arg2}}"
config_read_ports_failed:
other: "Failed to read port file {{.Arg1}}: {{.Arg2}}"
# ========================= Plugin Scan Messages (with parameters) =========================
scan_plugin_not_found:
+2
View File
@@ -315,6 +315,8 @@ config_read_passwords_failed:
other: "读取密码文件 {{.Arg1}} 失败: {{.Arg2}}"
config_read_urls_failed:
other: "读取URL文件 {{.Arg1}} 失败: {{.Arg2}}"
config_read_ports_failed:
other: "读取端口文件 {{.Arg1}} 失败: {{.Arg2}}"
# ========================= 插件扫描消息 (带参数) =========================
scan_plugin_not_found:
+48
View File
@@ -0,0 +1,48 @@
package output
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestWritersPreserveBackupOnFinalWriteFailure(t *testing.T) {
for _, format := range []Format{FormatTXT, FormatJSON, FormatCSV} {
t.Run(string(format), func(t *testing.T) {
path := filepath.Join(t.TempDir(), "result."+string(format))
manager, err := NewManager(DefaultManagerConfig(path, format))
if err != nil {
t.Fatal(err)
}
defer manager.Close()
if err := manager.SaveResult(&ScanResult{Type: TypeHost, Target: "127.0.0.1"}); err != nil {
t.Fatal(err)
}
backupPath := path + ".realtime.tmp"
before, err := os.ReadFile(backupPath)
if err != nil || len(before) == 0 {
t.Fatalf("backup before Close: %q, %v", before, err)
}
var file *os.File
switch writer := manager.writer.(type) {
case *TXTWriter:
file = writer.file
case *JSONWriter:
file = writer.file
case *CSVWriter:
file = writer.file
}
if err := file.Close(); err != nil {
t.Fatal(err)
}
if err := manager.Close(); err == nil {
t.Fatal("final write failure was not reported")
}
after, err := os.ReadFile(backupPath)
if err != nil || !bytes.Equal(before, after) {
t.Fatalf("recovery backup was lost after final write failed: %q, %v", after, err)
}
})
}
}
+27 -43
View File
@@ -71,6 +71,25 @@ func targetWithPort(target string, port interface{}) string {
return net.JoinHostPort(target, portText)
}
// closeOutputFiles 仅在最终结果完整落盘并关闭后删除恢复备份。
func closeOutputFiles(file, backup *os.File, backupPath string, writeErr error) error {
if writeErr == nil {
writeErr = file.Sync()
}
if err := file.Close(); err != nil && writeErr == nil {
writeErr = err
}
if backup != nil {
if err := backup.Close(); err != nil && writeErr == nil {
writeErr = err
}
if writeErr == nil {
writeErr = os.Remove(backupPath)
}
}
return writeErr
}
// =============================================================================
// TXTWriter - 文本格式写入器
// =============================================================================
@@ -347,23 +366,7 @@ func (w *TXTWriter) Close() error {
w.closed = true
// 关闭并删除实时备份文件(正常结束,不再需要)
if w.realtimeFile != nil {
w.realtimeFile.Close()
os.Remove(w.realtimePath)
}
var firstErr error
if err := w.bufWriter.Flush(); err != nil {
firstErr = err
}
if err := w.file.Sync(); err != nil && firstErr == nil {
firstErr = err
}
if err := w.file.Close(); err != nil && firstErr == nil {
firstErr = err
}
return firstErr
return closeOutputFiles(w.file, w.realtimeFile, w.realtimePath, w.bufWriter.Flush())
}
// writeSection 写入一个分类的所有结果
@@ -565,22 +568,11 @@ func (w *JSONWriter) Close() error {
}
data, err := json.MarshalIndent(output, JSONIndentPrefix, JSONIndentString)
if err != nil {
return err
}
w.closed = true
// 关闭并删除实时备份文件(正常结束,不再需要)
if w.realtimeFile != nil {
w.realtimeFile.Close()
os.Remove(w.realtimePath)
if err == nil {
_, err = w.file.Write(data)
}
if _, err := w.file.Write(data); err != nil {
return err
}
return w.file.Close()
return closeOutputFiles(w.file, w.realtimeFile, w.realtimePath, err)
}
// GetFormat 获取格式类型
@@ -692,20 +684,12 @@ func (w *CSVWriter) Close() error {
w.closed = true
// 关闭并删除实时备份文件(正常结束,不再需要)
if w.realtimeFile != nil {
w.realtimeFile.Close()
os.Remove(w.realtimePath)
}
w.csvWriter.Flush()
if err := w.csvWriter.Error(); err != nil {
return err
err := w.csvWriter.Error()
if err == nil {
err = w.bufWriter.Flush()
}
if err := w.bufWriter.Flush(); err != nil {
return err
}
return w.file.Close()
return closeOutputFiles(w.file, w.realtimeFile, w.realtimePath, err)
}
func (w *CSVWriter) writeSection(title string, headers []string, results []*ScanResult, formatter func(*ScanResult) []string) {
+6 -1
View File
@@ -73,6 +73,11 @@ func (it *HostIterator) Close() error {
return firstErr
}
// IsExcluded 检查单个主机是否匹配当前迭代器的排除规则。
func (it *HostIterator) IsExcluded(host string) bool {
return it.exclude != nil && it.exclude.match(host)
}
func (it *HostIterator) Next() (string, bool, error) {
for {
if it.current == nil {
@@ -94,7 +99,7 @@ func (it *HostIterator) Next() (string, bool, error) {
it.current = nil
continue
}
if it.exclude != nil && it.exclude.match(host) {
if it.IsExcluded(host) {
continue
}
return host, true, nil
+40
View File
@@ -0,0 +1,40 @@
package common
import (
"os"
"path/filepath"
"reflect"
"testing"
"github.com/shadow1ng/fscan/common/config"
"github.com/shadow1ng/fscan/common/parsers"
)
func TestBuildConfigLoadsPortsFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "ports.txt")
if err := os.WriteFile(path, []byte("# ports\n22\n8000-8002\n443,22\n"), 0600); err != nil {
t.Fatal(err)
}
cfg, _, err := BuildConfig(&FlagVars{Ports: config.MainPorts, PortsFile: path}, &HostInfo{Host: "127.0.0.1"})
if err != nil {
t.Fatal(err)
}
if got, want := parsers.ParsePort(cfg.Target.Ports), []int{22, 443, 8000, 8001, 8002}; !reflect.DeepEqual(got, want) {
t.Fatalf("ports = %v, want %v", got, want)
}
}
func TestBuildConfigRejectsUnusablePortsFile(t *testing.T) {
for _, contents := range []string{"", "# no ports\n", "invalid\n0\n65536\n"} {
path := filepath.Join(t.TempDir(), "ports.txt")
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
t.Fatal(err)
}
if _, _, err := BuildConfig(&FlagVars{Ports: "22", PortsFile: path}, &HostInfo{}); err == nil {
t.Errorf("accepted unusable port file %q", contents)
}
}
if _, _, err := BuildConfig(&FlagVars{PortsFile: filepath.Join(t.TempDir(), "missing.txt")}, &HostInfo{}); err == nil {
t.Error("accepted missing port file")
}
}