修复凭证测试器计数器、消除panic、补齐i18n

- credential_tester: testCredentialWithRetry返回ErrorType,修复网络错误计数器永久不递增的bug
- scanner: os.Exit(1)改为return,defer Cleanup可正常执行
- probe_parser: 5处panic改为error返回,调用链透传到init()
- common库: parsers/initialize/network/session共17处硬编码中文改用i18n
- services插件: 18个文件115处硬编码中文改用i18n
- locale: 补齐service/parser/network相关~25个中英文键
This commit is contained in:
ZacharyZcR
2026-05-18 04:55:20 +08:00
parent c266912dcb
commit 77827bef66
33 changed files with 221 additions and 100 deletions
+3 -3
View File
@@ -218,7 +218,7 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
return &ScanResult{
Success: false,
Service: "activemq",
Error: fmt.Errorf("无法发送STOMP请求: %w", writeErr),
Error: fmt.Errorf("STOMP request send failed: %w", writeErr),
}
}
@@ -229,7 +229,7 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
return &ScanResult{
Success: false,
Service: "activemq",
Error: fmt.Errorf("无法读取响应: %w", err),
Error: fmt.Errorf("Failed to read response: %w", err),
}
}
if n == 0 {
@@ -267,7 +267,7 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
return &ScanResult{
Success: false,
Service: "activemq",
Error: fmt.Errorf("无法识别为ActiveMQ STOMP服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "ActiveMQ STOMP")),
}
}
+1 -1
View File
@@ -43,7 +43,7 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, sessi
return &ScanResult{
Success: false,
Service: "cassandra",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
+12 -11
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
)
@@ -147,7 +148,7 @@ func TestCredentialsConcurrently(
return &ScanResult{
Success: false,
Service: serviceName,
Error: fmt.Errorf("无凭据可测试"),
Error: fmt.Errorf(i18n.GetText("service_no_test_creds")),
}
}
@@ -159,7 +160,7 @@ func TestCredentialsConcurrently(
return &ScanResult{
Success: false,
Service: serviceName,
Error: fmt.Errorf("目标不可达: %w", err),
Error: fmt.Errorf(i18n.Tr("service_target_unreachable", "%w"), err),
}
}
_ = preConn.Close()
@@ -222,7 +223,7 @@ func TestCredentialsConcurrently(
Type: plugins.ResultTypeCredential, // 标记这是凭据测试结果
Success: false,
Service: serviceName,
Error: fmt.Errorf("未发现弱密码"),
Error: fmt.Errorf(i18n.GetText("service_no_weak_pass")),
}
}
@@ -255,14 +256,14 @@ func workerTestCredentials(
}
// 带重试的凭据测试
result := testCredentialWithRetry(ctx, cred, authFn, serviceName, testConfig)
result, errType := testCredentialWithRetry(ctx, cred, authFn, serviceName, testConfig)
if result != nil && result.Success {
resultChan <- result
return
}
// 跟踪连续网络错误
if result != nil && result.Error != nil {
if errType == ErrorTypeNetwork {
consecutiveNetErrors++
} else {
consecutiveNetErrors = 0
@@ -277,12 +278,12 @@ func testCredentialWithRetry(
authFn AuthFunc,
serviceName string,
testConfig ConcurrentTestConfig,
) *ScanResult {
) (*ScanResult, ErrorType) {
for attempt := 0; attempt < testConfig.MaxRetries; attempt++ {
// 检查是否应该停止
select {
case <-ctx.Done():
return nil
return nil, ErrorTypeUnknown
default:
}
@@ -298,14 +299,14 @@ func testCredentialWithRetry(
Service: serviceName,
Username: cred.Username,
Password: cred.Password,
}
}, ErrorTypeUnknown
}
// 根据错误类型决定是否重试
switch result.ErrorType {
case ErrorTypeAuth:
// 认证错误(密码错误),不重试
return nil
return nil, result.ErrorType
case ErrorTypeNetwork, ErrorTypeUnknown:
// 网络错误或未知错误,可以重试(可能是服务端限流等临时问题)
if attempt < testConfig.MaxRetries-1 {
@@ -313,13 +314,13 @@ func testCredentialWithRetry(
select {
case <-ctx.Done():
timer.Stop()
return nil
return nil, result.ErrorType
case <-timer.C:
}
}
}
}
return nil
return nil, ErrorTypeNetwork
}
// =============================================================================
+3 -3
View File
@@ -51,7 +51,7 @@ func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, s
return &ScanResult{
Success: false,
Service: "elasticsearch",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -71,7 +71,7 @@ func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, s
return &ScanResult{
Success: false,
Service: "elasticsearch",
Error: fmt.Errorf("未发现弱密码"),
Error: fmt.Errorf(i18n.GetText("service_no_weak_pass")),
}
}
@@ -137,7 +137,7 @@ func (p *ElasticsearchPlugin) identifyService(ctx context.Context, info *common.
return &ScanResult{
Success: false,
Service: "elasticsearch",
Error: fmt.Errorf("无法识别为Elasticsearch服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "Elasticsearch")),
}
}
+3 -2
View File
@@ -15,6 +15,7 @@ import (
"unicode"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
)
@@ -45,7 +46,7 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, session
return &ScanResult{
Success: false,
Service: "findnet",
Error: fmt.Errorf("FindNet插件仅支持RPC端口135"),
Error: fmt.Errorf(i18n.Tr("service_port_restriction", "FindNet", "135")),
}
}
@@ -54,7 +55,7 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, session
return &ScanResult{
Success: false,
Service: "findnet",
Error: fmt.Errorf("连接RPC端口失败: %w", err),
Error: fmt.Errorf(i18n.Tr("service_conn_port_failed", "%w"), err),
}
}
defer func() { _ = conn.Close() }()
+1 -1
View File
@@ -43,7 +43,7 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co
return &ScanResult{
Success: false,
Service: "ftp",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
+2 -2
View File
@@ -38,7 +38,7 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, session *
return &ScanResult{
Success: false,
Service: "kafka",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -195,7 +195,7 @@ func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo
return &ScanResult{
Success: false,
Service: "kafka",
Error: fmt.Errorf("无法识别为Kafka服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "Kafka")),
}
}
state.IncrementTCPSuccessPacketCount()
+1 -1
View File
@@ -44,7 +44,7 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c
return &ScanResult{
Success: false,
Service: "ldap",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
+2 -2
View File
@@ -59,7 +59,7 @@ func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *comm
Type: plugins.ResultTypeVuln,
Success: true,
Service: "memcached",
Banner: "未授权访问",
Banner: i18n.GetText("service_unauthorized"),
}
}
@@ -140,7 +140,7 @@ func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.Host
return &ScanResult{
Success: false,
Service: "memcached",
Error: fmt.Errorf("无法识别为Memcached服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "Memcached")),
}
}
+3 -3
View File
@@ -63,7 +63,7 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session
return &ScanResult{
Success: false,
Service: "mongodb",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -237,14 +237,14 @@ func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo
return false, nil
}
return false, fmt.Errorf("无法识别为MongoDB服务")
return false, fmt.Errorf(i18n.Tr("service_not_identified", "MongoDB"))
}
// checkMongoAuth 检查MongoDB认证状态
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, session *common.ScanSession) (string, error) {
conn, err := session.DialTCP(ctx, "tcp", address, session.Config.Timeout)
if err != nil {
return "", fmt.Errorf("连接失败: %w", err)
return "", fmt.Errorf(i18n.Tr("service_connection_failed", "%w"), err)
}
defer func() { _ = conn.Close() }()
+2 -2
View File
@@ -39,7 +39,7 @@ func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *
return &ScanResult{
Success: false,
Service: "mssql",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -188,7 +188,7 @@ func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
return &ScanResult{
Success: false,
Service: "mssql",
Error: fmt.Errorf("无法识别为MSSQL服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "MSSQL")),
}
}
+2 -2
View File
@@ -48,7 +48,7 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *
return &ScanResult{
Success: false,
Service: "mysql",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -165,7 +165,7 @@ func (p *MySQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
return &ScanResult{
Success: false,
Service: "mysql",
Error: fmt.Errorf("无法识别为MySQL服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "MySQL")),
}
}
+4 -4
View File
@@ -45,7 +45,7 @@ func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, session *
return &ScanResult{
Success: false,
Service: "neo4j",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -112,7 +112,7 @@ func (p *Neo4jPlugin) doNeo4jAuth(ctx context.Context, info *common.HostInfo, cr
return &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
Error: fmt.Errorf("认证失败,状态码: %d", resp.StatusCode),
Error: fmt.Errorf(i18n.GetText("service_auth_failed")+": %d", resp.StatusCode),
}
}
@@ -170,7 +170,7 @@ func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
Type: plugins.ResultTypeVuln,
Success: true,
Service: "neo4j",
Banner: "未授权访问",
Banner: i18n.GetText("service_unauthorized"),
}
}
@@ -220,7 +220,7 @@ func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo
return &ScanResult{
Success: false,
Service: "neo4j",
Error: fmt.Errorf("无法识别为Neo4j服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "Neo4j")),
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, session
return &ScanResult{
Success: false,
Service: "oracle",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
+2 -2
View File
@@ -45,7 +45,7 @@ func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, sess
return &ScanResult{
Success: false,
Service: "postgresql",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -243,7 +243,7 @@ func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.Hos
return &ScanResult{
Success: false,
Service: "postgresql",
Error: fmt.Errorf("无法识别为PostgreSQL服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "PostgreSQL")),
}
}
} else {
+4 -4
View File
@@ -46,7 +46,7 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, sessio
return &ScanResult{
Success: false,
Service: "rabbitmq",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -121,7 +121,7 @@ func (p *RabbitMQPlugin) doRabbitMQAuth(ctx context.Context, info *common.HostIn
return &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
Error: fmt.Errorf("认证失败,状态码: %d", resp.StatusCode),
Error: fmt.Errorf(i18n.GetText("service_auth_failed")+": %d", resp.StatusCode),
}
}
@@ -184,7 +184,7 @@ func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *commo
Type: plugins.ResultTypeVuln,
Success: true,
Service: "rabbitmq",
Banner: "未授权访问",
Banner: i18n.GetText("service_unauthorized"),
}
}
}
@@ -307,7 +307,7 @@ func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *comm
return &ScanResult{
Success: false,
Service: "rabbitmq",
Error: fmt.Errorf("无法识别为RabbitMQ服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "RabbitMQ")),
}
}
+1 -1
View File
@@ -153,7 +153,7 @@ func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co
return &ScanResult{
Success: false,
Service: "rdp",
Error: fmt.Errorf("RDP认证失败"),
Error: fmt.Errorf(i18n.GetText("service_auth_failed")),
}
}
+1 -1
View File
@@ -135,7 +135,7 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr
return &AuthResult{
Success: false,
ErrorType: errType,
Error: fmt.Errorf("redis认证失败: %s", strings.TrimSpace(responseStr)),
Error: fmt.Errorf(i18n.GetText("service_auth_failed")+": %s", strings.TrimSpace(responseStr)),
}
}
}
+2 -2
View File
@@ -58,7 +58,7 @@ func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, session *
return &ScanResult{
Success: false,
Service: "rsync",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -374,7 +374,7 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
return &ScanResult{
Success: false,
Service: "rsync",
Error: fmt.Errorf("无法识别为Rsync服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "Rsync")),
}
}
+6 -5
View File
@@ -16,6 +16,7 @@ import (
"github.com/hirochachacha/go-smb2"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/stacktitan/smb/smb"
)
@@ -206,7 +207,7 @@ func probeTarget(ctx context.Context, host string, port int, timeout time.Durati
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
return nil, fmt.Errorf("连接失败: %w", err)
return nil, fmt.Errorf(i18n.Tr("service_connection_failed", "%w"), err)
}
defer func() { _ = conn.Close() }()
@@ -291,7 +292,7 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget
func probeSMBv2(ctx context.Context, target string, timeout time.Duration, session *common.ScanSession) (*SMBTarget, error) {
conn2, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
return nil, fmt.Errorf("SMBv2连接失败: %w", err)
return nil, fmt.Errorf(i18n.Tr("service_connection_failed", "%w"), err)
}
defer func() { _ = conn2.Close() }()
@@ -436,7 +437,7 @@ func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port
resultChan <- &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
Error: fmt.Errorf("认证失败:用户名或密码错误"),
Error: fmt.Errorf(i18n.GetText("service_auth_failed")),
}
}
}()
@@ -507,7 +508,7 @@ func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port
return &AuthResult{
Success: false,
ErrorType: classifySMBError(err),
Error: fmt.Errorf("SMB2认证失败: %w", err),
Error: fmt.Errorf(i18n.Tr("service_connection_failed", "%w"), err),
}, nil
}
@@ -662,7 +663,7 @@ func classifySMBError(err error) ErrorType {
"smb: wrong password",
"smb: login failed",
"smb: unauthorized",
"smb2认证失败",
i18n.GetText("service_auth_failed"),
"ntlm authentication failed",
"ntlm auth failed",
// NT Status codes
+1 -1
View File
@@ -45,7 +45,7 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c
return &ScanResult{
Success: false,
Service: "smtp",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
+1 -1
View File
@@ -248,7 +248,7 @@ func (p *SSHPlugin) identifyService(ctx context.Context, info *common.HostInfo,
return &ScanResult{
Success: false,
Service: "ssh",
Error: fmt.Errorf("无法识别为SSH服务"),
Error: fmt.Errorf(i18n.Tr("service_not_identified", "SSH")),
}
}
+2 -2
View File
@@ -75,7 +75,7 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, session
return &ScanResult{
Success: false,
Service: "telnet",
Error: fmt.Errorf("没有可用的测试凭据"),
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
@@ -145,7 +145,7 @@ func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo,
resultChan <- &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
Error: fmt.Errorf("认证失败"),
Error: fmt.Errorf(i18n.GetText("service_auth_failed")),
}
}
}()
+1 -1
View File
@@ -182,7 +182,7 @@ func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo,
Type: plugins.ResultTypeVuln,
Success: true,
Service: "vnc",
Banner: "未授权访问",
Banner: i18n.GetText("service_unauthorized"),
}
}