2025-01-14 23:38:58 +08:00
|
|
|
package Common
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/csv"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// 全局输出管理器
|
|
|
|
var ResultOutput *OutputManager
|
|
|
|
|
|
|
|
// OutputManager 输出管理器结构体
|
|
|
|
type OutputManager struct {
|
|
|
|
mu sync.Mutex
|
|
|
|
outputPath string
|
|
|
|
outputFormat string
|
|
|
|
file *os.File
|
|
|
|
csvWriter *csv.Writer
|
|
|
|
jsonEncoder *json.Encoder
|
|
|
|
isInitialized bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResultType 定义结果类型
|
|
|
|
type ResultType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
HOST ResultType = "HOST" // 主机存活
|
|
|
|
PORT ResultType = "PORT" // 端口开放
|
|
|
|
SERVICE ResultType = "SERVICE" // 服务识别
|
|
|
|
VULN ResultType = "VULN" // 漏洞发现
|
|
|
|
)
|
|
|
|
|
|
|
|
// ScanResult 扫描结果结构
|
|
|
|
type ScanResult struct {
|
|
|
|
Time time.Time `json:"time"` // 发现时间
|
|
|
|
Type ResultType `json:"type"` // 结果类型
|
|
|
|
Target string `json:"target"` // 目标(IP/域名/URL)
|
|
|
|
Status string `json:"status"` // 状态描述
|
|
|
|
Details map[string]interface{} `json:"details"` // 详细信息
|
|
|
|
}
|
|
|
|
|
|
|
|
// InitOutput 初始化输出系统
|
|
|
|
func InitOutput() error {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_init_start"))
|
2025-01-14 23:38:58 +08:00
|
|
|
|
|
|
|
// 验证输出格式
|
|
|
|
switch OutputFormat {
|
|
|
|
case "txt", "json", "csv":
|
|
|
|
// 有效的格式
|
|
|
|
default:
|
2025-02-07 13:10:06 +08:00
|
|
|
return fmt.Errorf(GetText("output_format_invalid"), OutputFormat)
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 验证输出路径
|
|
|
|
if Outputfile == "" {
|
2025-02-07 13:10:06 +08:00
|
|
|
return fmt.Errorf(GetText("output_path_empty"))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
dir := filepath.Dir(Outputfile)
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_create_dir_failed", err))
|
|
|
|
return fmt.Errorf(GetText("output_create_dir_failed", err))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
2025-04-22 12:36:10 +08:00
|
|
|
if ApiAddr != "" {
|
|
|
|
OutputFormat = "csv"
|
|
|
|
Outputfile = filepath.Join(dir, "fscanapi.csv")
|
|
|
|
}
|
|
|
|
|
2025-01-14 23:38:58 +08:00
|
|
|
manager := &OutputManager{
|
|
|
|
outputPath: Outputfile,
|
|
|
|
outputFormat: OutputFormat,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := manager.initialize(); err != nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_init_failed", err))
|
|
|
|
return fmt.Errorf(GetText("output_init_failed", err))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
ResultOutput = manager
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_init_success"))
|
2025-01-14 23:38:58 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (om *OutputManager) initialize() error {
|
|
|
|
om.mu.Lock()
|
|
|
|
defer om.mu.Unlock()
|
|
|
|
|
|
|
|
if om.isInitialized {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_already_init"))
|
2025-01-14 23:38:58 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_opening_file", om.outputPath))
|
2025-01-14 23:38:58 +08:00
|
|
|
file, err := os.OpenFile(om.outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
|
|
if err != nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_open_file_failed", err))
|
|
|
|
return fmt.Errorf(GetText("output_open_file_failed", err))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
om.file = file
|
|
|
|
|
|
|
|
switch om.outputFormat {
|
|
|
|
case "csv":
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_init_csv"))
|
2025-01-14 23:38:58 +08:00
|
|
|
om.csvWriter = csv.NewWriter(file)
|
|
|
|
headers := []string{"Time", "Type", "Target", "Status", "Details"}
|
|
|
|
if err := om.csvWriter.Write(headers); err != nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_write_csv_header_failed", err))
|
2025-01-14 23:38:58 +08:00
|
|
|
file.Close()
|
2025-02-07 13:10:06 +08:00
|
|
|
return fmt.Errorf(GetText("output_write_csv_header_failed", err))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
om.csvWriter.Flush()
|
|
|
|
case "json":
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_init_json"))
|
2025-01-14 23:38:58 +08:00
|
|
|
om.jsonEncoder = json.NewEncoder(file)
|
|
|
|
om.jsonEncoder.SetIndent("", " ")
|
|
|
|
case "txt":
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_init_txt"))
|
2025-01-14 23:38:58 +08:00
|
|
|
default:
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_format_invalid", om.outputFormat))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
om.isInitialized = true
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_init_complete"))
|
2025-01-14 23:38:58 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SaveResult 保存扫描结果
|
|
|
|
func SaveResult(result *ScanResult) error {
|
|
|
|
if ResultOutput == nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_not_init"))
|
|
|
|
return fmt.Errorf(GetText("output_not_init"))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_saving_result", result.Type, result.Target))
|
2025-01-14 23:38:58 +08:00
|
|
|
return ResultOutput.saveResult(result)
|
|
|
|
}
|
2025-04-22 12:36:10 +08:00
|
|
|
func GetResults() ([]*ScanResult, error) {
|
|
|
|
if ResultOutput == nil {
|
|
|
|
return nil, fmt.Errorf(GetText("output_not_init"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if ResultOutput.outputFormat == "csv" {
|
|
|
|
return ResultOutput.getResult()
|
|
|
|
}
|
|
|
|
// 其他格式尚未实现读取支持
|
|
|
|
return nil, fmt.Errorf(GetText("output_format_read_not_supported"))
|
|
|
|
}
|
2025-01-14 23:38:58 +08:00
|
|
|
|
|
|
|
func (om *OutputManager) saveResult(result *ScanResult) error {
|
|
|
|
om.mu.Lock()
|
|
|
|
defer om.mu.Unlock()
|
|
|
|
|
|
|
|
if !om.isInitialized {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_not_init"))
|
|
|
|
return fmt.Errorf(GetText("output_not_init"))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
switch om.outputFormat {
|
|
|
|
case "txt":
|
|
|
|
err = om.writeTxt(result)
|
|
|
|
case "json":
|
|
|
|
err = om.writeJson(result)
|
|
|
|
case "csv":
|
|
|
|
err = om.writeCsv(result)
|
|
|
|
default:
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_format_invalid", om.outputFormat))
|
|
|
|
return fmt.Errorf(GetText("output_format_invalid", om.outputFormat))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_save_failed", err))
|
2025-01-14 23:38:58 +08:00
|
|
|
} else {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_save_success", result.Type, result.Target))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
2025-04-22 12:36:10 +08:00
|
|
|
func (om *OutputManager) getResult() ([]*ScanResult, error) {
|
|
|
|
om.mu.Lock()
|
|
|
|
defer om.mu.Unlock()
|
|
|
|
|
|
|
|
if !om.isInitialized {
|
|
|
|
LogDebug(GetText("output_not_init"))
|
|
|
|
return nil, fmt.Errorf(GetText("output_not_init"))
|
|
|
|
}
|
|
|
|
|
|
|
|
file, err := os.Open(om.outputPath)
|
|
|
|
if err != nil {
|
|
|
|
LogDebug(GetText("output_open_file_failed", err))
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
reader := csv.NewReader(file)
|
|
|
|
records, err := reader.ReadAll()
|
|
|
|
if err != nil {
|
|
|
|
LogDebug(GetText("output_read_csv_failed", err))
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var results []*ScanResult
|
|
|
|
for i, row := range records {
|
|
|
|
// 跳过 CSV 头部
|
|
|
|
if i == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(row) < 5 {
|
|
|
|
continue // 数据不完整
|
|
|
|
}
|
|
|
|
|
|
|
|
t, err := time.Parse("2006-01-02 15:04:05", row[0])
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var details map[string]interface{}
|
|
|
|
if err := json.Unmarshal([]byte(row[4]), &details); err != nil {
|
|
|
|
details = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
|
|
|
result := &ScanResult{
|
|
|
|
Time: t,
|
|
|
|
Type: ResultType(row[1]),
|
|
|
|
Target: row[2],
|
|
|
|
Status: row[3],
|
|
|
|
Details: details,
|
|
|
|
}
|
|
|
|
results = append(results, result)
|
|
|
|
}
|
|
|
|
|
|
|
|
LogDebug(GetText("output_read_csv_success", len(results)))
|
|
|
|
return results, nil
|
|
|
|
}
|
2025-01-14 23:38:58 +08:00
|
|
|
|
|
|
|
func (om *OutputManager) writeTxt(result *ScanResult) error {
|
|
|
|
// 格式化 Details 为键值对字符串
|
|
|
|
var details string
|
|
|
|
if len(result.Details) > 0 {
|
|
|
|
pairs := make([]string, 0, len(result.Details))
|
|
|
|
for k, v := range result.Details {
|
|
|
|
pairs = append(pairs, fmt.Sprintf("%s=%v", k, v))
|
|
|
|
}
|
|
|
|
details = strings.Join(pairs, ", ")
|
|
|
|
}
|
|
|
|
|
2025-02-07 13:10:06 +08:00
|
|
|
txt := GetText("output_txt_format",
|
2025-01-14 23:38:58 +08:00
|
|
|
result.Time.Format("2006-01-02 15:04:05"),
|
|
|
|
result.Type,
|
|
|
|
result.Target,
|
|
|
|
result.Status,
|
|
|
|
details,
|
2025-02-07 13:10:06 +08:00
|
|
|
) + "\n"
|
2025-01-14 23:38:58 +08:00
|
|
|
_, err := om.file.WriteString(txt)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (om *OutputManager) writeJson(result *ScanResult) error {
|
|
|
|
return om.jsonEncoder.Encode(result)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (om *OutputManager) writeCsv(result *ScanResult) error {
|
|
|
|
details, err := json.Marshal(result.Details)
|
|
|
|
if err != nil {
|
|
|
|
details = []byte("{}")
|
|
|
|
}
|
|
|
|
|
|
|
|
record := []string{
|
|
|
|
result.Time.Format("2006-01-02 15:04:05"),
|
|
|
|
string(result.Type),
|
|
|
|
result.Target,
|
|
|
|
result.Status,
|
|
|
|
string(details),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := om.csvWriter.Write(record); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
om.csvWriter.Flush()
|
|
|
|
return om.csvWriter.Error()
|
|
|
|
}
|
|
|
|
|
|
|
|
// CloseOutput 关闭输出系统
|
|
|
|
func CloseOutput() error {
|
|
|
|
if ResultOutput == nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_no_need_close"))
|
2025-01-14 23:38:58 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_closing"))
|
2025-01-14 23:38:58 +08:00
|
|
|
ResultOutput.mu.Lock()
|
|
|
|
defer ResultOutput.mu.Unlock()
|
|
|
|
|
|
|
|
if !ResultOutput.isInitialized {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_no_need_close"))
|
2025-01-14 23:38:58 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if ResultOutput.csvWriter != nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_flush_csv"))
|
2025-01-14 23:38:58 +08:00
|
|
|
ResultOutput.csvWriter.Flush()
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := ResultOutput.file.Close(); err != nil {
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_close_failed", err))
|
|
|
|
return fmt.Errorf(GetText("output_close_failed", err))
|
2025-01-14 23:38:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
ResultOutput.isInitialized = false
|
2025-02-07 13:10:06 +08:00
|
|
|
LogDebug(GetText("output_closed"))
|
2025-01-14 23:38:58 +08:00
|
|
|
return nil
|
|
|
|
}
|