Expand i18n coverage

This commit is contained in:
ZacharyZcR
2026-05-23 15:18:40 +08:00
parent 9ed6cc95b6
commit 73cbe803c4
79 changed files with 2540 additions and 621 deletions
+7 -5
View File
@@ -11,6 +11,8 @@ import (
"sort"
"strings"
"sync"
"github.com/shadow1ng/fscan/common/i18n"
)
//go:embed web_fingerprint_v4.json
@@ -20,10 +22,10 @@ var fingerprintHubData []byte
type EnhancedFingerprint struct {
ID string `json:"id"`
Info struct {
Name string `json:"name"`
Author string `json:"author"`
Tags string `json:"tags"`
Severity string `json:"severity"`
Name string `json:"name"`
Author string `json:"author"`
Tags string `json:"tags"`
Severity string `json:"severity"`
Metadata map[string]interface{} `json:"metadata"`
} `json:"info"`
HTTP []struct {
@@ -58,7 +60,7 @@ var (
func LoadEnhancedFingerprints() error {
var fps []*EnhancedFingerprint
if err := json.Unmarshal(fingerprintHubData, &fps); err != nil {
return fmt.Errorf("解析增强指纹库失败: %w", err)
return fmt.Errorf("%s: %w", i18n.GetText("fingerprint_enhanced_parse_failed"), err)
}
enhancedDB = &EnhancedFingerprintDB{
+24 -23
View File
@@ -13,6 +13,7 @@ import (
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/proxy"
gmtls "github.com/tjfoc/gmsm/gmtls"
"gopkg.in/yaml.v2"
@@ -31,12 +32,12 @@ const (
// 全局HTTP客户端变量
var (
Client *http.Client // 标准HTTP客户端
ClientNoRedirect *http.Client // 不自动跟随重定向的HTTP客户端
ClientGM *http.Client // 国密TLS HTTP客户端
ClientNoRedirectGM *http.Client // 国密TLS 不跟随重定向
dialTimeout = 5 * time.Second // 连接超时时间
keepAlive = 5 * time.Second // 连接保持时间
Client *http.Client // 标准HTTP客户端
ClientNoRedirect *http.Client // 不自动跟随重定向的HTTP客户端
ClientGM *http.Client // 国密TLS HTTP客户端
ClientNoRedirectGM *http.Client // 国密TLS 不跟随重定向
dialTimeout = 5 * time.Second // 连接超时时间
keepAlive = 5 * time.Second // 连接保持时间
)
// Inithttp 初始化HTTP客户端配置
@@ -50,7 +51,7 @@ func Inithttp(cfg *common.Config) error {
// 初始化HTTP客户端
err := InitHTTPClient(pocNum, cfg.Network.HTTPProxy, cfg.Network.WebTimeout, cfg.Network.MaxRedirects, &cfg.Network)
if err != nil {
return fmt.Errorf("HTTP客户端初始化失败: %w", err)
return fmt.Errorf("%s: %w", i18n.GetText("webscan_http_client_init_failed"), err)
}
return nil
}
@@ -85,7 +86,7 @@ func configureHTTPProxy(tr *http.Transport, legacyProxy string, networkConfig *c
proxyManager := proxy.NewProxyManager(proxyConfig)
proxyDialer, err := proxyManager.GetDialer()
if err != nil {
return fmt.Errorf("SOCKS5代理配置失败: %w", err)
return fmt.Errorf("%s: %w", i18n.GetText("webscan_socks5_proxy_config_failed"), err)
}
tr.DialContext = proxyDialer.DialContext
return nil
@@ -110,13 +111,13 @@ func configureHTTPProxy(tr *http.Transport, legacyProxy string, networkConfig *c
// 验证代理类型
if !strings.HasPrefix(httpProxyURL, "socks5://") && !strings.HasPrefix(httpProxyURL, "http://") && !strings.HasPrefix(httpProxyURL, "https://") {
return fmt.Errorf("不支持的代理类型: %s", httpProxyURL)
return fmt.Errorf("%s: %s", i18n.GetText("webscan_unsupported_proxy_type"), httpProxyURL)
}
// 解析代理URL
parsedURL, err := url.Parse(httpProxyURL)
if err != nil {
return fmt.Errorf("代理URL解析失败: %w", err)
return fmt.Errorf("%s: %w", i18n.GetText("webscan_proxy_url_parse_failed"), err)
}
tr.Proxy = http.ProxyURL(parsedURL)
return nil
@@ -137,9 +138,9 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max
// 配置Transport参数
tr := &http.Transport{
DialContext: dialer.DialContext,
MaxConnsPerHost: 100, // 增加到100,避免连接池耗尽
MaxIdleConns: 100, // 保留100个空闲连接
MaxIdleConnsPerHost: 10, // 每主机保留10个空闲连接
MaxConnsPerHost: 100, // 增加到100,避免连接池耗尽
MaxIdleConns: 100, // 保留100个空闲连接
MaxIdleConnsPerHost: 10, // 每主机保留10个空闲连接
IdleConnTimeout: keepAlive,
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true},
TLSHandshakeTimeout: 5 * time.Second,
@@ -266,7 +267,7 @@ func (r *StrMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
key, keyOk := one.Key.(string)
value, valueOk := one.Value.(string)
if !keyOk || !valueOk {
return fmt.Errorf("StrMap解析失败: 键或值不是字符串类型")
return fmt.Errorf("%s", i18n.GetText("webscan_strmap_parse_failed"))
}
*r = append(*r, StrItem{key, value})
}
@@ -297,7 +298,7 @@ func (r *RuleMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
for _, one := range tmp1 {
key, ok := one.Key.(string)
if !ok {
return fmt.Errorf("RuleMap解析失败: 键不是字符串类型")
return fmt.Errorf("%s", i18n.GetText("webscan_rulemap_key_invalid"))
}
value := tmp[key]
*r = append(*r, RuleItem{key, value})
@@ -322,12 +323,12 @@ func (r *ListMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
for _, one := range tmp {
key, keyOk := one.Key.(string)
if !keyOk {
return fmt.Errorf("ListMap解析失败: 键不是字符串类型")
return fmt.Errorf("%s", i18n.GetText("webscan_listmap_key_invalid"))
}
valueSlice, valueOk := one.Value.([]interface{})
if !valueOk {
return fmt.Errorf("ListMap解析失败: 值不是数组类型")
return fmt.Errorf("%s", i18n.GetText("webscan_listmap_value_invalid"))
}
var value []string
@@ -369,7 +370,7 @@ func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc {
if p, err := LoadPoc(f, Pocs); err == nil {
pocs = append(pocs, p)
} else {
common.LogError(fmt.Sprintf("POC加载失败 %s: %v", f, err))
common.LogError(i18n.Tr("webscan_poc_load_one_failed", f, err))
}
}
return pocs
@@ -380,13 +381,13 @@ func parsePocYAML(data []byte, fileName string) (*Poc, error) {
// 使用通用适配器加载POC(自动识别格式)
universalPoc, err := LoadUniversalPoc(fileName, data)
if err != nil {
return nil, fmt.Errorf("POC解析失败 %s: %w", fileName, err)
return nil, fmt.Errorf("%s %s: %w", i18n.GetText("webscan_poc_parse_failed"), fileName, err)
}
// 转换为fscan内部格式
poc, err := universalPoc.ToFscanPoc()
if err != nil {
return nil, fmt.Errorf("POC格式转换失败 %s: %w", fileName, err)
return nil, fmt.Errorf("%s %s: %w", i18n.GetText("webscan_poc_convert_failed"), fileName, err)
}
return poc, nil
@@ -397,7 +398,7 @@ func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
// 读取POC文件内容
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
if err != nil {
return nil, fmt.Errorf("POC文件读取失败 %s: %w", fileName, err)
return nil, fmt.Errorf("%s %s: %w", i18n.GetText("webscan_poc_file_read_failed"), fileName, err)
}
// 解析YAML内容
@@ -408,7 +409,7 @@ func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
func SelectPoc(Pocs embed.FS, pocname string) []string {
entries, err := Pocs.ReadDir("pocs")
if err != nil {
common.LogError(fmt.Sprintf("读取POC目录失败: %v", err))
common.LogError(i18n.Tr("webscan_poc_dir_read_failed", err))
}
var foundFiles []string
@@ -426,7 +427,7 @@ func LoadPocbyPath(fileName string) (*Poc, error) {
// 读取POC文件内容
data, err := os.ReadFile(fileName)
if err != nil {
return nil, fmt.Errorf("POC文件读取失败 %s: %w", fileName, err)
return nil, fmt.Errorf("%s %s: %w", i18n.GetText("webscan_poc_file_read_failed"), fileName, err)
}
// 解析YAML内容
+8 -8
View File
@@ -108,7 +108,7 @@ func GetBaseProgramOptions() []cel.ProgramOption {
func ExtendEnvWithVars(varDecls []*exprpb.Decl) (*cel.Env, error) {
base := GetBaseEnv()
if base == nil {
return nil, fmt.Errorf("基础CEL环境未初始化")
return nil, fmt.Errorf("%s", i18n.GetText("webscan_cel_env_not_initialized"))
}
if len(varDecls) == 0 {
return base, nil
@@ -142,19 +142,19 @@ func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (r
// 编译表达式
ast, issues := env.Compile(expression)
if issues.Err() != nil {
return nil, fmt.Errorf("表达式编译错误: %w", issues.Err())
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_expression_compile_failed"), issues.Err())
}
// 创建程序(使用缓存的程序选项)
program, err := env.Program(ast, GetBaseProgramOptions()...)
if err != nil {
return nil, fmt.Errorf("程序创建错误: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_program_create_failed"), err)
}
// 执行评估
result, _, err := program.Eval(params)
if err != nil {
return nil, fmt.Errorf("表达式评估错误: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_expression_eval_failed"), err)
}
return result, nil
@@ -435,7 +435,7 @@ func DoRequest(req *http.Request, redirect bool) (*Response, error) {
// 检查发包限制
if canSend, reason := common.CanSendPacket(); !canSend {
common.LogError(i18n.Tr("webscan_request_restricted", req.URL.String(), reason))
return nil, fmt.Errorf("发包受限: %s", reason)
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
}
var (
@@ -465,7 +465,7 @@ func DoRequest(req *http.Request, redirect bool) (*Response, error) {
if err != nil {
// HTTP请求失败,计为TCP失败
common.GetGlobalState().IncrementTCPFailedPacketCount()
return nil, fmt.Errorf("请求执行失败: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_request_execute_failed"), err)
}
// HTTP请求成功,计为TCP成功
@@ -512,7 +512,7 @@ func ParseRequest(oReq *http.Request) (*Request, error) {
if oReq.Body != nil && oReq.Body != http.NoBody {
data, err := io.ReadAll(oReq.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_request_body_read_failed"), err)
}
req.Body = data
// 重新设置请求体,允许后续重复读取
@@ -545,7 +545,7 @@ func ParseResponse(oResp *http.Response) (*Response, error) {
// 读取并解析响应体
body, err := getRespBody(oResp)
if err != nil {
return nil, fmt.Errorf("处理响应体失败: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_response_body_process_failed"), err)
}
resp.Body = body
+9 -8
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/shadow1ng/fscan/common/i18n"
"gopkg.in/yaml.v2"
)
@@ -103,7 +104,7 @@ func LoadUniversalPoc(filename string, data []byte) (UniversalPoc, error) {
case FormatAfrog:
return loadAfrogPoc(data)
default:
return nil, fmt.Errorf("未知POC格式: %s", filename)
return nil, fmt.Errorf("%s: %s", i18n.GetText("webscan_unknown_poc_format"), filename)
}
}
@@ -117,7 +118,7 @@ type FscanPocAdapter struct {
func loadFscanPoc(data []byte) (*FscanPocAdapter, error) {
var poc Poc
if err := yaml.Unmarshal(data, &poc); err != nil {
return nil, fmt.Errorf("fscan格式解析失败: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_fscan_format_parse_failed"), err)
}
return &FscanPocAdapter{&poc}, nil
}
@@ -174,7 +175,7 @@ type NucleiPocAdapter struct {
func loadNucleiPoc(data []byte) (*NucleiPocAdapter, error) {
var poc NucleiPoc
if err := yaml.Unmarshal(data, &poc); err != nil {
return nil, fmt.Errorf("nuclei格式解析失败: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_nuclei_format_parse_failed"), err)
}
return &NucleiPocAdapter{&poc}, nil
}
@@ -239,7 +240,7 @@ func (n *NucleiPocAdapter) ToFscanPoc() (*Poc, error) {
}
if len(poc.Rules) == 0 {
return nil, fmt.Errorf("nuclei模板没有有效的HTTP规则")
return nil, fmt.Errorf("%s", i18n.GetText("webscan_nuclei_no_http_rules"))
}
return poc, nil
@@ -348,7 +349,7 @@ type XrayPocAdapter struct {
func loadXrayPoc(data []byte) (*XrayPocAdapter, error) {
var poc XrayPoc
if err := yaml.Unmarshal(data, &poc); err != nil {
return nil, fmt.Errorf("xray格式解析失败: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_xray_format_parse_failed"), err)
}
return &XrayPocAdapter{&poc}, nil
}
@@ -413,7 +414,7 @@ func (x *XrayPocAdapter) ToFscanPoc() (*Poc, error) {
}
if len(poc.Rules) == 0 {
return nil, fmt.Errorf("xray POC没有有效的规则")
return nil, fmt.Errorf("%s", i18n.GetText("webscan_xray_no_rules"))
}
return poc, nil
@@ -447,7 +448,7 @@ type AfrogPocAdapter struct {
func loadAfrogPoc(data []byte) (*AfrogPocAdapter, error) {
var poc AfrogPoc
if err := yaml.Unmarshal(data, &poc); err != nil {
return nil, fmt.Errorf("afrog格式解析失败: %w", err)
return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_afrog_format_parse_failed"), err)
}
return &AfrogPocAdapter{&poc}, nil
}
@@ -519,7 +520,7 @@ func (a *AfrogPocAdapter) ToFscanPoc() (*Poc, error) {
}
if len(poc.Rules) == 0 {
return nil, fmt.Errorf("afrog POC没有有效的规则")
return nil, fmt.Errorf("%s", i18n.GetText("webscan_afrog_no_rules"))
}
return poc, nil
+14 -14
View File
@@ -120,24 +120,24 @@ func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int, pocCtx *POCConte
_ = common.SaveResult(result)
// 构造控制台输出的日志信息
logMsg := fmt.Sprintf("目标: %s\n 漏洞类型: %s\n 漏洞名称: %s\n 详细信息:",
logMsg := i18n.Tr("webscan_vuln_detail_header",
task.Req.URL,
task.Poc.Name,
vulName)
// 添加作者信息到日志
if task.Poc.Detail.Author != "" {
logMsg += "\n\t作者:" + task.Poc.Detail.Author
logMsg += "\n\t" + i18n.Tr("webscan_vuln_author", task.Poc.Detail.Author)
}
// 添加参考链接到日志
if len(task.Poc.Detail.Links) != 0 {
logMsg += "\n\t参考链接:" + strings.Join(task.Poc.Detail.Links, "\n")
logMsg += "\n\t" + i18n.Tr("webscan_vuln_references", strings.Join(task.Poc.Detail.Links, "\n"))
}
// 添加描述信息到日志
if task.Poc.Detail.Description != "" {
logMsg += "\n\t描述:" + task.Poc.Detail.Description
logMsg += "\n\t" + i18n.Tr("webscan_vuln_description", task.Poc.Detail.Description)
}
// 输出成功日志
@@ -191,13 +191,13 @@ func executePoc(oReq *http.Request, p *Poc, pocCtx *POCContext) (bool, string, e
// 从基础环境扩展(复用缓存的基础环境,仅添加变量声明)
env, err := ExtendEnvWithVars(varDecls)
if err != nil {
return false, "", fmt.Errorf("执行环境错误 %s: %w", p.Name, err)
return false, "", fmt.Errorf("%s %s: %w", i18n.GetText("webscan_exec_env_error"), p.Name, err)
}
// 解析请求
req, err := ParseRequest(oReq)
if err != nil {
return false, "", fmt.Errorf("请求解析错误 %s: %w", p.Name, err)
return false, "", fmt.Errorf("%s %s: %w", i18n.GetText("webscan_request_parse_error"), p.Name, err)
}
// 初始化变量映射
@@ -268,7 +268,7 @@ func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{}
strings.NewReader(rule.Body),
)
if err != nil {
return false, fmt.Errorf("请求创建错误: %w", err)
return false, fmt.Errorf("%s: %w", i18n.GetText("webscan_request_create_error"), err)
}
// 设置请求头
@@ -489,9 +489,9 @@ func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{},
payloadExpr = expr
}
output, err := evalset1(env, variableMap, key, expr)
if err != nil {
common.LogError(i18n.Tr("webscan_set_exec_error", key, err))
}
if err != nil {
common.LogError(i18n.Tr("webscan_set_exec_error", key, err))
}
payloads[key] = output
}
@@ -660,9 +660,9 @@ func recordVulnerabilityResult(targetURL string, pocDef *Poc, params StrMap, ski
// 生成日志消息
var logMsg string
if pocDef.Name == "poc-yaml-backup-file" || pocDef.Name == "poc-yaml-sql-file" {
logMsg = fmt.Sprintf("检测到漏洞 %s %s", targetURL, pocDef.Name)
logMsg = i18n.Tr("webscan_vuln_detected", targetURL, pocDef.Name)
} else {
logMsg = fmt.Sprintf("检测到漏洞 %s %s 参数:%v", targetURL, pocDef.Name, params)
logMsg = i18n.Tr("webscan_vuln_detected_params", targetURL, pocDef.Name, params)
}
// 输出成功日志
@@ -773,7 +773,7 @@ func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Re
reqURL := fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.URL.Host, req.URL.Path)
newRequest, err := http.NewRequestWithContext(oReq.Context(), rule.Method, reqURL, strings.NewReader(rule.Body))
if err != nil {
return false, fmt.Errorf("HTTP请求错误: %w", err)
return false, fmt.Errorf("%s: %w", i18n.GetText("webscan_http_request_error"), err)
}
defer func() { newRequest = nil }()
@@ -786,7 +786,7 @@ func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Re
// 发送请求
resp, err := DoRequest(newRequest, rule.FollowRedirects)
if err != nil {
return false, fmt.Errorf("请求发送错误: %w", err)
return false, fmt.Errorf("%s: %w", i18n.GetText("webscan_request_send_error"), err)
}
// 更新响应到变量映射
+4 -4
View File
@@ -31,10 +31,10 @@ const (
// 错误定义
var (
ErrInvalidURL = errors.New("无效的URL格式")
ErrEmptyTarget = errors.New("目标URL为空")
ErrPocNotFound = errors.New("未找到匹配的POC")
ErrPocLoadFailed = errors.New("POC加载失败")
ErrInvalidURL = errors.New(i18n.GetText("webscan_err_invalid_url"))
ErrEmptyTarget = errors.New(i18n.GetText("webscan_err_empty_target"))
ErrPocNotFound = errors.New(i18n.GetText("webscan_err_poc_not_found"))
ErrPocLoadFailed = errors.New(i18n.GetText("webscan_err_poc_load_failed"))
)
//go:embed pocs