mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 12:41:53 +08:00
fix csv web title output (#575)
This commit is contained in:
+24
-1
@@ -59,7 +59,8 @@ func (b *ResultBuffer) Add(result *ScanResult) {
|
|||||||
b.seenServices[key] = len(b.ServiceResults)
|
b.seenServices[key] = len(b.ServiceResults)
|
||||||
b.ServiceResults = append(b.ServiceResults, result)
|
b.ServiceResults = append(b.ServiceResults, result)
|
||||||
} else {
|
} else {
|
||||||
// 保留信息更完整的记录
|
b.mergeDetails(b.ServiceResults[idx], result)
|
||||||
|
// 保留信息更完整的记录,同时保留另一条记录补充的字段
|
||||||
if b.isMoreComplete(result, b.ServiceResults[idx]) {
|
if b.isMoreComplete(result, b.ServiceResults[idx]) {
|
||||||
b.ServiceResults[idx] = result
|
b.ServiceResults[idx] = result
|
||||||
}
|
}
|
||||||
@@ -72,6 +73,28 @@ func (b *ResultBuffer) Add(result *ScanResult) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) {
|
||||||
|
if oldResult == nil || newResult == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if oldResult.Details == nil {
|
||||||
|
oldResult.Details = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
if newResult.Details == nil {
|
||||||
|
newResult.Details = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
for k, v := range oldResult.Details {
|
||||||
|
if _, exists := newResult.Details[k]; !exists {
|
||||||
|
newResult.Details[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range newResult.Details {
|
||||||
|
if _, exists := oldResult.Details[k]; !exists {
|
||||||
|
oldResult.Details[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// generateKey 生成结果的唯一键(用于去重)
|
// generateKey 生成结果的唯一键(用于去重)
|
||||||
func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
||||||
switch result.Type {
|
switch result.Type {
|
||||||
|
|||||||
@@ -226,6 +226,40 @@ func TestResultBuffer_ServiceUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResultBuffer_ServiceUpdateMergesDetails(t *testing.T) {
|
||||||
|
buf := NewResultBuffer()
|
||||||
|
|
||||||
|
buf.Add(&ScanResult{
|
||||||
|
Type: TypeService,
|
||||||
|
Target: "192.168.1.1:80",
|
||||||
|
Status: "identified",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"service": "http",
|
||||||
|
"banner": "HTTP/1.1 200 OK",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
buf.Add(&ScanResult{
|
||||||
|
Type: TypeService,
|
||||||
|
Target: "192.168.1.1:80",
|
||||||
|
Status: "web",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"title": "Home",
|
||||||
|
"status": 200,
|
||||||
|
"server": "nginx",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(buf.ServiceResults) != 1 {
|
||||||
|
t.Fatalf("期望1条服务记录,实际 %d", len(buf.ServiceResults))
|
||||||
|
}
|
||||||
|
details := buf.ServiceResults[0].Details
|
||||||
|
for _, key := range []string{"service", "banner", "title", "status", "server"} {
|
||||||
|
if _, ok := details[key]; !ok {
|
||||||
|
t.Errorf("合并后的服务记录缺少字段 %q: %#v", key, details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
|
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
|
||||||
//
|
//
|
||||||
// 当新记录不如旧记录完整时,不应替换
|
// 当新记录不如旧记录完整时,不应替换
|
||||||
|
|||||||
+53
-10
@@ -13,13 +13,26 @@ import (
|
|||||||
|
|
||||||
// escapeControlChars 转义控制字符
|
// escapeControlChars 转义控制字符
|
||||||
func escapeControlChars(s string) string {
|
func escapeControlChars(s string) string {
|
||||||
replacer := strings.NewReplacer(
|
s = strings.ToValidUTF8(s, "?")
|
||||||
"\r\n", "\\r\\n",
|
|
||||||
"\n", "\\n",
|
var b strings.Builder
|
||||||
"\r", "\\r",
|
for _, r := range s {
|
||||||
"\t", "\\t",
|
switch r {
|
||||||
)
|
case '\n':
|
||||||
return replacer.Replace(s)
|
b.WriteString("\\n")
|
||||||
|
case '\r':
|
||||||
|
b.WriteString("\\r")
|
||||||
|
case '\t':
|
||||||
|
b.WriteString("\\t")
|
||||||
|
default:
|
||||||
|
if r < 0x20 || r == 0x7f {
|
||||||
|
fmt.Fprintf(&b, "\\x%02x", r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -647,7 +660,7 @@ func (w *CSVWriter) Close() error {
|
|||||||
// 写入各分类
|
// 写入各分类
|
||||||
w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord)
|
w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord)
|
||||||
w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord)
|
w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord)
|
||||||
w.writeSection("# Services", []string{"Target", "Service", "Version", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord)
|
w.writeSection("# Services", []string{"Target", "Service", "Version", "Title", "Status", "Server", "Fingerprints", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord)
|
||||||
w.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord)
|
w.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord)
|
||||||
|
|
||||||
w.closed = true
|
w.closed = true
|
||||||
@@ -697,7 +710,7 @@ func (w *CSVWriter) formatPortRecord(result *ScanResult) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
||||||
service, version, banner := "", "", ""
|
service, version, title, status, server, fingerprints, banner := "", "", "", "", "", "", ""
|
||||||
if result.Details != nil {
|
if result.Details != nil {
|
||||||
if s, ok := result.Details["service"].(string); ok {
|
if s, ok := result.Details["service"].(string); ok {
|
||||||
service = s
|
service = s
|
||||||
@@ -705,9 +718,22 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
|||||||
if s, ok := result.Details["name"].(string); ok && service == "" {
|
if s, ok := result.Details["name"].(string); ok && service == "" {
|
||||||
service = s
|
service = s
|
||||||
}
|
}
|
||||||
|
if s, ok := result.Details["plugin"].(string); ok && service == "" {
|
||||||
|
service = s
|
||||||
|
}
|
||||||
if v, ok := result.Details["version"].(string); ok {
|
if v, ok := result.Details["version"].(string); ok {
|
||||||
version = v
|
version = v
|
||||||
}
|
}
|
||||||
|
if t, ok := result.Details["title"].(string); ok {
|
||||||
|
title = escapeControlChars(t)
|
||||||
|
}
|
||||||
|
if s, ok := result.Details["status"]; ok && s != nil && s != 0 {
|
||||||
|
status = fmt.Sprintf("%v", s)
|
||||||
|
}
|
||||||
|
if s, ok := result.Details["server"].(string); ok {
|
||||||
|
server = escapeControlChars(s)
|
||||||
|
}
|
||||||
|
fingerprints = formatFingerprints(result.Details["fingerprints"])
|
||||||
if b, ok := result.Details["banner"].(string); ok {
|
if b, ok := result.Details["banner"].(string); ok {
|
||||||
banner = escapeControlChars(b)
|
banner = escapeControlChars(b)
|
||||||
if len(banner) > 100 {
|
if len(banner) > 100 {
|
||||||
@@ -721,7 +747,24 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
|||||||
target = fmt.Sprintf("%s:%v", target, p)
|
target = fmt.Sprintf("%s:%v", target, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{target, service, version, banner}
|
return []string{target, service, version, title, status, server, fingerprints, banner}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatFingerprints(value interface{}) string {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []string:
|
||||||
|
return strings.Join(v, ",")
|
||||||
|
case []interface{}:
|
||||||
|
parts := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
if s, ok := item.(string); ok && s != "" {
|
||||||
|
parts = append(parts, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *CSVWriter) formatVulnRecord(result *ScanResult) []string {
|
func (w *CSVWriter) formatVulnRecord(result *ScanResult) []string {
|
||||||
|
|||||||
@@ -1139,7 +1139,7 @@ func TestCSVWriter_ErrorHandling(t *testing.T) {
|
|||||||
// TestCSVWriter_DetailsFormatting 测试CSV的Details字段格式化
|
// TestCSVWriter_DetailsFormatting 测试CSV的Details字段格式化
|
||||||
//
|
//
|
||||||
// CSVWriter 对不同类型有不同的格式:
|
// CSVWriter 对不同类型有不同的格式:
|
||||||
// - Service类型:Target, Service, Version, Banner
|
// - Service类型:Target, Service, Version, Title, Status, Server, Fingerprints, Banner
|
||||||
func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
||||||
dir := createTestDir(t)
|
dir := createTestDir(t)
|
||||||
filePath := filepath.Join(dir, "test.csv")
|
filePath := filepath.Join(dir, "test.csv")
|
||||||
@@ -1188,6 +1188,48 @@ func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
|||||||
t.Logf("✓ CSV Details格式化测试通过")
|
t.Logf("✓ CSV Details格式化测试通过")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCSVWriter_WebServiceFields(t *testing.T) {
|
||||||
|
dir := createTestDir(t)
|
||||||
|
filePath := filepath.Join(dir, "test.csv")
|
||||||
|
|
||||||
|
writer, _ := NewCSVWriter(filePath)
|
||||||
|
defer func() { _ = writer.Close() }()
|
||||||
|
|
||||||
|
_ = writer.WriteHeader()
|
||||||
|
result := createTestResult(
|
||||||
|
TypeService,
|
||||||
|
"192.168.1.1:80",
|
||||||
|
"web",
|
||||||
|
map[string]interface{}{
|
||||||
|
"plugin": "webtitle",
|
||||||
|
"is_web": true,
|
||||||
|
"port": 80,
|
||||||
|
"title": "Home",
|
||||||
|
"status": 200,
|
||||||
|
"server": "nginx",
|
||||||
|
"fingerprints": []string{"nginx", "php"},
|
||||||
|
"banner": "HTTP/1.1 200 OK\x00\nServer: nginx",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_ = writer.Write(result)
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
content := readFileContent(t, filePath)
|
||||||
|
for _, want := range []string{
|
||||||
|
"Target,Service,Version,Title,Status,Server,Fingerprints,Banner",
|
||||||
|
"webtitle",
|
||||||
|
"Home",
|
||||||
|
"200",
|
||||||
|
"nginx",
|
||||||
|
"nginx,php",
|
||||||
|
"\\x00\\nServer: nginx",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(content, want) {
|
||||||
|
t.Errorf("CSV文件缺少 %q,内容:\n%s", want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestJSONWriter_FlushAndFormat 测试JSON的Flush和GetFormat
|
// TestJSONWriter_FlushAndFormat 测试JSON的Flush和GetFormat
|
||||||
func TestJSONWriter_FlushAndFormat(t *testing.T) {
|
func TestJSONWriter_FlushAndFormat(t *testing.T) {
|
||||||
dir := createTestDir(t)
|
dir := createTestDir(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user