mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00

主要改进: - 将output和parsers包中的硬编码错误消息迁移到i18n国际化系统 - 修复constants.go中未引用常量的使用问题 - 消除所有硬编码字符串、魔术数字和协议标识符 - 清理死代码常量MinIPv4OctetValue 具体变更: * output包: 删除19个硬编码错误消息,新增13个i18n消息键 * parsers包: 删除24个硬编码错误模板,新增22个i18n消息键 * 修复8个未引用的Default常量在对应默认选项函数中的使用 * 替换11处网络协议相关的硬编码值为常量引用 * 更新6个错误类型常量和验证逻辑的硬编码使用 * 修复2处IPv4八位组计数的硬编码为常量引用 提升效果: - 支持中英文错误消息国际化切换 - 统一常量管理,提高代码可维护性 - 消除代码重复,符合DRY原则 - 清理死代码,优化代码质量 涉及文件: 11个文件,新增210行,删除103行
459 lines
8.8 KiB
Go
459 lines
8.8 KiB
Go
package output
|
||
|
||
import (
|
||
"encoding/csv"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
)
|
||
|
||
// TXTWriter 文本格式写入器
|
||
type TXTWriter struct {
|
||
file *os.File
|
||
mu sync.Mutex
|
||
closed bool
|
||
}
|
||
|
||
// NewTXTWriter 创建文本写入器
|
||
func NewTXTWriter(filePath string) (*TXTWriter, error) {
|
||
file, err := os.OpenFile(filePath, DefaultFileFlags, DefaultFilePermissions)
|
||
if err != nil {
|
||
return nil, fmt.Errorf(i18n.GetText("output_create_file_failed"), "文本", err)
|
||
}
|
||
|
||
return &TXTWriter{
|
||
file: file,
|
||
}, nil
|
||
}
|
||
|
||
// WriteHeader 写入头部(文本格式无需头部)
|
||
func (w *TXTWriter) WriteHeader() error {
|
||
return nil
|
||
}
|
||
|
||
// Write 写入扫描结果
|
||
func (w *TXTWriter) Write(result *ScanResult) error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return fmt.Errorf(i18n.GetText("output_writer_closed"))
|
||
}
|
||
|
||
// 格式化 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(TxtKeyValueFormat, k, v))
|
||
}
|
||
details = strings.Join(pairs, TxtDetailsSeparator)
|
||
}
|
||
|
||
// 使用类似原有格式的文本输出
|
||
txt := fmt.Sprintf(TxtOutputTemplate,
|
||
result.Time.Format(TxtTimeFormat),
|
||
result.Type,
|
||
result.Target,
|
||
result.Status,
|
||
)
|
||
if details != "" {
|
||
txt += fmt.Sprintf(TxtDetailsFormat, details)
|
||
}
|
||
txt += TxtNewline
|
||
|
||
_, err := w.file.WriteString(txt)
|
||
return err
|
||
}
|
||
|
||
// Flush 刷新缓冲区
|
||
func (w *TXTWriter) Flush() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return nil
|
||
}
|
||
|
||
return w.file.Sync()
|
||
}
|
||
|
||
// Close 关闭写入器
|
||
func (w *TXTWriter) Close() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return nil
|
||
}
|
||
|
||
w.closed = true
|
||
return w.file.Close()
|
||
}
|
||
|
||
// GetFormat 获取格式类型
|
||
func (w *TXTWriter) GetFormat() OutputFormat {
|
||
return FormatTXT
|
||
}
|
||
|
||
// JSONWriter JSON格式写入器
|
||
type JSONWriter struct {
|
||
file *os.File
|
||
encoder *json.Encoder
|
||
mu sync.Mutex
|
||
closed bool
|
||
}
|
||
|
||
// NewJSONWriter 创建JSON写入器
|
||
func NewJSONWriter(filePath string) (*JSONWriter, error) {
|
||
file, err := os.OpenFile(filePath, DefaultFileFlags, DefaultFilePermissions)
|
||
if err != nil {
|
||
return nil, fmt.Errorf(i18n.GetText("output_create_file_failed"), "JSON", err)
|
||
}
|
||
|
||
encoder := json.NewEncoder(file)
|
||
encoder.SetIndent(JSONIndentPrefix, JSONIndentString)
|
||
|
||
return &JSONWriter{
|
||
file: file,
|
||
encoder: encoder,
|
||
}, nil
|
||
}
|
||
|
||
// WriteHeader 写入头部(JSON格式无需头部)
|
||
func (w *JSONWriter) WriteHeader() error {
|
||
return nil
|
||
}
|
||
|
||
// Write 写入扫描结果
|
||
func (w *JSONWriter) Write(result *ScanResult) error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return fmt.Errorf(i18n.GetText("output_writer_closed"))
|
||
}
|
||
|
||
return w.encoder.Encode(result)
|
||
}
|
||
|
||
// Flush 刷新缓冲区
|
||
func (w *JSONWriter) Flush() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return nil
|
||
}
|
||
|
||
return w.file.Sync()
|
||
}
|
||
|
||
// Close 关闭写入器
|
||
func (w *JSONWriter) Close() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return nil
|
||
}
|
||
|
||
w.closed = true
|
||
return w.file.Close()
|
||
}
|
||
|
||
// GetFormat 获取格式类型
|
||
func (w *JSONWriter) GetFormat() OutputFormat {
|
||
return FormatJSON
|
||
}
|
||
|
||
// CSVWriter CSV格式写入器
|
||
type CSVWriter struct {
|
||
file *os.File
|
||
csvWriter *csv.Writer
|
||
mu sync.Mutex
|
||
closed bool
|
||
headerWritten bool
|
||
}
|
||
|
||
// NewCSVWriter 创建CSV写入器
|
||
func NewCSVWriter(filePath string) (*CSVWriter, error) {
|
||
file, err := os.OpenFile(filePath, DefaultFileFlags, DefaultFilePermissions)
|
||
if err != nil {
|
||
return nil, fmt.Errorf(i18n.GetText("output_create_file_failed"), "CSV", err)
|
||
}
|
||
|
||
csvWriter := csv.NewWriter(file)
|
||
|
||
return &CSVWriter{
|
||
file: file,
|
||
csvWriter: csvWriter,
|
||
}, nil
|
||
}
|
||
|
||
// WriteHeader 写入CSV头部
|
||
func (w *CSVWriter) WriteHeader() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.headerWritten {
|
||
return nil
|
||
}
|
||
|
||
headers := CSVHeaders
|
||
err := w.csvWriter.Write(headers)
|
||
if err != nil {
|
||
return fmt.Errorf(i18n.GetText("output_write_header_failed"), err)
|
||
}
|
||
|
||
w.csvWriter.Flush()
|
||
w.headerWritten = true
|
||
return w.csvWriter.Error()
|
||
}
|
||
|
||
// Write 写入扫描结果
|
||
func (w *CSVWriter) Write(result *ScanResult) error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return fmt.Errorf(i18n.GetText("output_writer_closed"))
|
||
}
|
||
|
||
// 确保头部已写入
|
||
if !w.headerWritten {
|
||
if err := w.writeHeaderUnsafe(); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 序列化Details为JSON字符串
|
||
details, err := json.Marshal(result.Details)
|
||
if err != nil {
|
||
details = []byte(EmptyJSONObject)
|
||
}
|
||
|
||
record := []string{
|
||
result.Time.Format(DefaultTimeFormat),
|
||
string(result.Type),
|
||
result.Target,
|
||
result.Status,
|
||
string(details),
|
||
}
|
||
|
||
err = w.csvWriter.Write(record)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
w.csvWriter.Flush()
|
||
return w.csvWriter.Error()
|
||
}
|
||
|
||
// writeHeaderUnsafe 不安全的写入头部(内部使用,无锁)
|
||
func (w *CSVWriter) writeHeaderUnsafe() error {
|
||
if w.headerWritten {
|
||
return nil
|
||
}
|
||
|
||
headers := CSVHeaders
|
||
err := w.csvWriter.Write(headers)
|
||
if err != nil {
|
||
return fmt.Errorf(i18n.GetText("output_write_header_failed"), err)
|
||
}
|
||
|
||
w.csvWriter.Flush()
|
||
w.headerWritten = true
|
||
return w.csvWriter.Error()
|
||
}
|
||
|
||
// Flush 刷新缓冲区
|
||
func (w *CSVWriter) Flush() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return nil
|
||
}
|
||
|
||
w.csvWriter.Flush()
|
||
return w.csvWriter.Error()
|
||
}
|
||
|
||
// Close 关闭写入器
|
||
func (w *CSVWriter) Close() error {
|
||
w.mu.Lock()
|
||
defer w.mu.Unlock()
|
||
|
||
if w.closed {
|
||
return nil
|
||
}
|
||
|
||
w.csvWriter.Flush()
|
||
err := w.csvWriter.Error()
|
||
w.closed = true
|
||
|
||
if fileErr := w.file.Close(); fileErr != nil && err == nil {
|
||
err = fileErr
|
||
}
|
||
|
||
return err
|
||
}
|
||
|
||
// GetFormat 获取格式类型
|
||
func (w *CSVWriter) GetFormat() OutputFormat {
|
||
return FormatCSV
|
||
}
|
||
|
||
// CSVReader CSV格式读取器
|
||
type CSVReader struct {
|
||
filePath string
|
||
mu sync.Mutex
|
||
}
|
||
|
||
// NewCSVReader 创建CSV读取器
|
||
func NewCSVReader(filePath string) *CSVReader {
|
||
return &CSVReader{
|
||
filePath: filePath,
|
||
}
|
||
}
|
||
|
||
// Read 读取所有结果
|
||
func (r *CSVReader) Read() ([]*ScanResult, error) {
|
||
return r.ReadWithFilter(nil)
|
||
}
|
||
|
||
// ReadWithFilter 带过滤条件读取结果
|
||
func (r *CSVReader) ReadWithFilter(filter *ResultFilter) ([]*ScanResult, error) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
file, err := os.Open(r.filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf(i18n.GetText("output_open_file_failed"), err)
|
||
}
|
||
defer file.Close()
|
||
|
||
reader := csv.NewReader(file)
|
||
records, err := reader.ReadAll()
|
||
if err != nil {
|
||
return nil, fmt.Errorf(i18n.GetText("output_read_file_failed"), err)
|
||
}
|
||
|
||
var results []*ScanResult
|
||
for i, row := range records {
|
||
// 跳过CSV头部
|
||
if i == CSVHeaderRowIndex {
|
||
continue
|
||
}
|
||
if len(row) < CSVMinColumns {
|
||
continue // 数据不完整
|
||
}
|
||
|
||
result, err := r.parseCSVRow(row)
|
||
if err != nil {
|
||
continue // 跳过解析失败的行
|
||
}
|
||
|
||
// 应用过滤器
|
||
if filter != nil && !r.matchFilter(result, filter) {
|
||
continue
|
||
}
|
||
|
||
results = append(results, result)
|
||
|
||
// 应用限制
|
||
if filter != nil && filter.Limit > 0 && len(results) >= filter.Limit {
|
||
break
|
||
}
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// parseCSVRow 解析CSV行
|
||
func (r *CSVReader) parseCSVRow(row []string) (*ScanResult, error) {
|
||
// 解析时间
|
||
t, err := parseTime(row[CSVTimeColumnIndex])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 解析Details
|
||
var details map[string]interface{}
|
||
if err := json.Unmarshal([]byte(row[CSVDetailsColumnIndex]), &details); err != nil {
|
||
details = make(map[string]interface{})
|
||
}
|
||
|
||
return &ScanResult{
|
||
Time: t,
|
||
Type: ResultType(row[CSVTypeColumnIndex]),
|
||
Target: row[CSVTargetColumnIndex],
|
||
Status: row[CSVStatusColumnIndex],
|
||
Details: details,
|
||
}, nil
|
||
}
|
||
|
||
// matchFilter 检查结果是否匹配过滤器
|
||
func (r *CSVReader) matchFilter(result *ScanResult, filter *ResultFilter) bool {
|
||
// 检查类型过滤
|
||
if len(filter.Types) > 0 {
|
||
found := false
|
||
for _, t := range filter.Types {
|
||
if result.Type == t {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查目标过滤
|
||
if len(filter.Targets) > 0 {
|
||
found := false
|
||
for _, target := range filter.Targets {
|
||
if strings.Contains(result.Target, target) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查时间范围过滤
|
||
if filter.TimeRange != nil {
|
||
if result.Time.Before(filter.TimeRange.Start) || result.Time.After(filter.TimeRange.End) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// Close 关闭读取器
|
||
func (r *CSVReader) Close() error {
|
||
return nil // CSV读取器无需特殊关闭操作
|
||
}
|
||
|
||
// parseTime 解析时间字符串
|
||
func parseTime(timeStr string) (time.Time, error) {
|
||
// 尝试多种时间格式
|
||
formats := GetSupportedTimeFormats()
|
||
|
||
for _, format := range formats {
|
||
if t, err := time.Parse(format, timeStr); err == nil {
|
||
return t, nil
|
||
}
|
||
}
|
||
|
||
return time.Time{}, fmt.Errorf(i18n.GetText("output_parse_time_failed"), timeStr)
|
||
} |