Localize user-facing UI, agent output, and backend messages to Simplified Chinese
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
docs / links (push) Waiting to run
web / web (push) Waiting to run

This commit is contained in:
dela
2026-10-09 14:08:31 +08:00
parent 8af63417c8
commit 97668dab7e
148 changed files with 1671 additions and 1672 deletions
+30 -30
View File
@@ -30,7 +30,7 @@ func sortFindingsForExport(fs []*db.DBFinding) {
// findingTitle 은 취약점의 사람이 읽을 수 있는 제목을 고른다: 이름 → 유형 → 「미분류」.
func findingTitle(f *db.DBFinding) string {
return nz(f.Name, nz(f.VulnClass, "미분류"))
return nz(f.Name, nz(f.VulnClass, "未分类"))
}
// FindingsMarkdown 은 여러 건의 findings 를 하나의 요약 보고서로 통합한다(요약 + 심각도별
@@ -40,48 +40,48 @@ func FindingsMarkdown(fs []*db.DBFinding, generatedAt time.Time) string {
sortFindingsForExport(items)
var b strings.Builder
b.WriteString("# 취약점 요약 보고서\n\n")
fmt.Fprintf(&b, "- **생성 시각**: %s\n", generatedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(&b, "- **취약점 총계**: %d건\n\n", len(items))
b.WriteString("# 漏洞摘要报告\n\n")
fmt.Fprintf(&b, "- **生成时间**: %s\n", generatedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(&b, "- **漏洞总数**: %d 个\n\n", len(items))
// 요약: 심각도별 집계.
counts := map[string]int{}
for _, f := range items {
counts[f.Severity]++
}
b.WriteString("## 요약\n\n")
b.WriteString("| 심각도 | 개수 |\n| --- | --- |\n")
b.WriteString("## 摘要\n\n")
b.WriteString("| 严重程度 | 数量 |\n| --- | --- |\n")
for _, s := range []struct{ key, label string }{
{"critical", "심각"}, {"high", "높음"}, {"medium", "중간"}, {"low", "낮음"},
{"critical", "严重"}, {"high", "高危"}, {"medium", "中危"}, {"low", "低危"},
} {
fmt.Fprintf(&b, "| %s | %d |\n", s.label, counts[s.key])
}
b.WriteString("\n")
if len(items) == 0 {
b.WriteString("_일치하는 취약점이 없습니다._\n")
b.WriteString("_没有匹配的漏洞。_\n")
return b.String()
}
b.WriteString("## 취약점 상세\n\n")
b.WriteString("## 漏洞详情\n\n")
for i, f := range items {
fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), findingTitle(f))
if f.VulnClass != "" {
fmt.Fprintf(&b, "- **유형**: %s\n", f.VulnClass)
fmt.Fprintf(&b, "- **类型**: %s\n", f.VulnClass)
}
fmt.Fprintf(&b, "- **상태**: %s\n", nz(f.Status, "pending"))
fmt.Fprintf(&b, "- **状态**: %s\n", nz(f.Status, "pending"))
if desc := strings.TrimSpace(f.TaskDescription); desc != "" {
fmt.Fprintf(&b, "- **소속 작업**: %s\n", desc)
fmt.Fprintf(&b, "- **所属任务**: %s\n", desc)
}
fmt.Fprintf(&b, "- **발견 시각**: %s\n\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(&b, "- **发现时间**: %s\n\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
if s := strings.TrimSpace(f.Summary); s != "" {
fmt.Fprintf(&b, "%s\n\n", s)
}
if e := strings.TrimSpace(f.Evidence); e != "" {
fmt.Fprintf(&b, "**증거:**\n\n```\n%s\n```\n\n", e)
fmt.Fprintf(&b, "**证据:**\n\n```\n%s\n```\n\n", e)
}
if rep := strings.TrimSpace(f.Report); rep != "" {
b.WriteString("**상세 보고서:**\n\n")
b.WriteString("**详细报告:**\n\n")
b.WriteString(rep)
b.WriteString("\n\n")
}
@@ -96,23 +96,23 @@ func SingleFindingMarkdown(f *db.DBFinding, generatedAt time.Time) string {
var b strings.Builder
fmt.Fprintf(&b, "# [%s] %s\n\n", strings.ToUpper(nz(f.Severity, "info")), findingTitle(f))
if f.VulnClass != "" {
fmt.Fprintf(&b, "- **유형**: %s\n", f.VulnClass)
fmt.Fprintf(&b, "- **类型**: %s\n", f.VulnClass)
}
fmt.Fprintf(&b, "- **심각도**: %s\n", nz(f.Severity, "info"))
fmt.Fprintf(&b, "- **상태**: %s\n", nz(f.Status, "pending"))
fmt.Fprintf(&b, "- **严重程度**: %s\n", nz(f.Severity, "info"))
fmt.Fprintf(&b, "- **状态**: %s\n", nz(f.Status, "pending"))
if desc := strings.TrimSpace(f.TaskDescription); desc != "" {
fmt.Fprintf(&b, "- **소속 작업**: %s\n", desc)
fmt.Fprintf(&b, "- **所属任务**: %s\n", desc)
}
fmt.Fprintf(&b, "- **발견 시각**: %s\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(&b, "- **생성 시각**: %s\n\n", generatedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(&b, "- **发现时间**: %s\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(&b, "- **生成时间**: %s\n\n", generatedAt.Format("2006-01-02 15:04:05"))
if s := strings.TrimSpace(f.Summary); s != "" {
fmt.Fprintf(&b, "## 개요\n\n%s\n\n", s)
fmt.Fprintf(&b, "## 概述\n\n%s\n\n", s)
}
if e := strings.TrimSpace(f.Evidence); e != "" {
fmt.Fprintf(&b, "## 증거\n\n```\n%s\n```\n\n", e)
fmt.Fprintf(&b, "## 证据\n\n```\n%s\n```\n\n", e)
}
if rep := strings.TrimSpace(f.Report); rep != "" {
b.WriteString("## 상세 보고서\n\n")
b.WriteString("## 详细报告\n\n")
b.WriteString(rep)
b.WriteString("\n")
}
@@ -151,7 +151,7 @@ func FindingsCSV(fs []*db.DBFinding) []byte {
var buf bytes.Buffer
buf.WriteString("\xEF\xBB\xBF") // UTF-8 BOM
w := csv.NewWriter(&buf)
_ = w.Write([]string{"ID", "이름", "유형", "심각도", "상태", "소속 작업", "발견 시각", "개요", "트래픽 증거 개수", "트래픽 증거 ID"})
_ = w.Write([]string{"ID", "名称", "类型", "严重程度", "状态", "所属任务", "发现时间", "概述", "流量证据数量", "流量证据 ID"})
for _, f := range items {
_ = w.Write([]string{
fmt.Sprintf("%d", f.ID),
@@ -183,18 +183,18 @@ func findingTrafficMarkdown(f *db.DBFinding, attachments bool) string {
return ""
}
var out strings.Builder
out.WriteString("\n## 관련 트래픽 증거\n\n")
fmt.Fprintf(&out, "증거 버전: %d, 바인딩 개수: %d.\n\n", f.EvidenceVersion, len(f.TrafficBindings))
out.WriteString("\n## 相关流量证据\n\n")
fmt.Fprintf(&out, "证据版本: %d, 绑定数量: %d.\n\n", f.EvidenceVersion, len(f.TrafficBindings))
if stale {
out.WriteString("증거가 변경되어 상세 보고서를 갱신해야 합니다.\n\n")
out.WriteString("证据已变更,需要更新详细报告。\n\n")
}
for i, b := range f.TrafficBindings {
fmt.Fprintf(&out, "%d. **증거 #%d · %s**: `%s %s`, 상태 코드 %d\n", i+1, b.ID, b.Role, b.Snapshot.Method, strings.ReplaceAll(b.Snapshot.URL, "`", "%60"), b.Snapshot.Status)
fmt.Fprintf(&out, "%d. **证据 #%d · %s**: `%s %s`, 状态码 %d\n", i+1, b.ID, b.Role, b.Snapshot.Method, strings.ReplaceAll(b.Snapshot.URL, "`", "%60"), b.Snapshot.Status)
if b.Note != "" {
fmt.Fprintf(&out, " %s\n", strings.ReplaceAll(b.Note, "\n", "\n "))
}
if attachments {
fmt.Fprintf(&out, " [요청 메시지](evidence/%d/%d/request.http) · [응답 메시지](evidence/%d/%d/response.http)\n", f.ID, b.ID, f.ID, b.ID)
fmt.Fprintf(&out, " [请求消息](evidence/%d/%d/request.http) · [响应消息](evidence/%d/%d/response.http)\n", f.ID, b.ID, f.ID, b.ID)
}
}
out.WriteString("\n")
+10 -10
View File
@@ -48,14 +48,14 @@ var sevRank = map[string]int{"critical": 0, "high": 1, "medium": 2, "low": 3, ""
// Markdown renders the report.
func Markdown(in Input) string {
var b strings.Builder
fmt.Fprintf(&b, "# 침투 테스트 보고서: %s\n\n", nz(in.Title, "이름 없는 작업"))
fmt.Fprintf(&b, "- **작업 목표**: %s\n", nz(in.Goal, "(지정되지 않음)"))
fmt.Fprintf(&b, "- **생성 시각**: %s\n\n", in.GeneratedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(&b, "# 渗透测试报告: %s\n\n", nz(in.Title, "未命名任务"))
fmt.Fprintf(&b, "- **任务目标**: %s\n", nz(in.Goal, "(未指定)"))
fmt.Fprintf(&b, "- **生成时间**: %s\n\n", in.GeneratedAt.Format("2006-01-02 15:04:05"))
// summary
fmt.Fprintf(&b, "## 요약\n\n")
fmt.Fprintf(&b, "- 확인된 취약점: **%d**건\n", len(in.Findings))
fmt.Fprintf(&b, "- 자산: ")
fmt.Fprintf(&b, "## 摘要\n\n")
fmt.Fprintf(&b, "- 已确认漏洞: **%d** 个\n", len(in.Findings))
fmt.Fprintf(&b, "- 资产: ")
var types []string
for t := range in.AssetCounts {
types = append(types, t)
@@ -70,9 +70,9 @@ func Markdown(in Input) string {
b.WriteString("\n\n")
// findings
fmt.Fprintf(&b, "## 취약점\n\n")
fmt.Fprintf(&b, "## 漏洞\n\n")
if len(in.Findings) == 0 {
b.WriteString("_이번에 확인된 취약점이 없습니다._\n\n")
b.WriteString("_本次未确认到任何漏洞。_\n\n")
} else {
fs := make([]findingView, 0, len(in.Findings))
for _, n := range in.Findings {
@@ -80,10 +80,10 @@ func Markdown(in Input) string {
}
sort.SliceStable(fs, func(i, j int) bool { return sevRank[fs[i].Severity] < sevRank[fs[j].Severity] })
for i, f := range fs {
fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), nz(f.Name, nz(f.VulnClass, "미분류")))
fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), nz(f.Name, nz(f.VulnClass, "未分类")))
fmt.Fprintf(&b, "%s\n\n", nz(f.Summary, ""))
if f.PoC != "" {
fmt.Fprintf(&b, "**PoC / 증거:**\n\n```\n%s\n```\n\n", f.PoC)
fmt.Fprintf(&b, "**PoC / 证据:**\n\n```\n%s\n```\n\n", f.PoC)
}
}
}
+68 -56
View File
@@ -21,13 +21,35 @@ func containsHan(s string) (rune, bool) {
return 0, false
}
// containsHangul 은 문자열에 한글 음절이 남아 있으면 true 를 돌려준다.
// 중국어로 바뀐 뒤 한글이 되돌아오는 회귀를 잡는다.
func containsHangul(s string) (rune, bool) {
for _, r := range s {
if unicode.Is(unicode.Hangul, r) {
return r, true
}
}
return 0, false
}
// assertChinese 는 s 에 한자가 있고 한글이 없음을 단언한다.
func assertChinese(t *testing.T, label, s string) {
t.Helper()
if r, ok := containsHangul(s); ok {
t.Fatalf("%s: 한글이 남아 있습니다 %q:\n%s", label, string(r), s)
}
if _, ok := containsHan(s); !ok {
t.Fatalf("%s: 한자가 없습니다:\n%s", label, s)
}
}
func sampleFindingNode(t *testing.T) *db.Node {
t.Helper()
payload, err := json.Marshal(map[string]any{
"vulnclass": "SQL Injection",
"name": "로그인 폼 SQL 주입",
"name": "登录表单 SQL 注入",
"severity": "high",
"summary": "로그인 파라미터에서 오류 기반 SQL 주입이 확인되었습니다.",
"summary": "在登录参数中确认了基于错误的 SQL 注入。",
"evidence": map[string]string{"poc": "' OR '1'='1"},
})
if err != nil {
@@ -40,20 +62,20 @@ func sampleDBFinding() *db.DBFinding {
return &db.DBFinding{
ID: 123,
VulnClass: "SQL Injection",
Name: "로그인 폼 SQL 주입",
Name: "登录表单 SQL 注入",
Severity: "high",
Summary: "로그인 파라미터에서 오류 기반 SQL 주입이 확인되었습니다.",
Summary: "在登录参数中确认了基于错误的 SQL 注入。",
Evidence: "' OR '1'='1",
Status: "confirmed",
Report: "상세 분석 본문.",
Report: "详细分析正文。",
CreatedAt: time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC),
EvidenceVersion: 2,
TaskDescription: "데모 대상 침투 테스트",
TaskDescription: "演示目标渗透测试",
TrafficBindings: []db.FindingTrafficBinding{
{
ID: 9,
Role: "request",
Note: "주입 페이로드 전송 지점",
Note: "注入载荷发送点",
Snapshot: db.TrafficEvidenceSnapshot{
Method: "POST",
URL: "https://sandbox.local/login",
@@ -68,23 +90,21 @@ func sampleDBFinding() *db.DBFinding {
// 중국어 한자가 전혀 없음을 검증한다.
func TestReportMarkdownLocalized(t *testing.T) {
out := Markdown(Input{
Title: "데모 작업",
Goal: "샌드박스 전수 점검",
Title: "演示任务",
Goal: "沙箱全面检查",
GeneratedAt: time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC),
AssetCounts: map[string]int{"host": 2, "url": 5},
Findings: []*db.Node{sampleFindingNode(t)},
})
if r, ok := containsHan(out); ok {
t.Fatalf("보고서 골격에 중국어 한자 %q 가 남아 있다:\n%s", string(r), out)
}
assertChinese(t, "보고서", out)
for _, want := range []string{
"# 침투 테스트 보고서: 데모 작업",
"- **작업 목표**: 샌드박스 전수 점검",
"- **생성 시각**:",
"## 요약",
"- 확인된 취약점: **1**건",
"## 취약점",
"**PoC / 증거:**",
"# 渗透测试报告: 演示任务",
"- **任务目标**: 沙箱全面检查",
"- **生成时间**:",
"## 摘要",
"- 已确认漏洞: **1** 个",
"## 漏洞",
"**PoC / 证据:**",
} {
if !strings.Contains(out, want) {
t.Errorf("보고서 골격에 %q 가 없다", want)
@@ -94,12 +114,10 @@ func TestReportMarkdownLocalized(t *testing.T) {
// TestReportMarkdownEmptyLocalized 는 취약점이 없을 때의 안내 문구가 한국어임을 검증한다.
func TestReportMarkdownEmptyLocalized(t *testing.T) {
out := Markdown(Input{Title: "빈 작업", GeneratedAt: time.Now()})
if r, ok := containsHan(out); ok {
t.Fatalf("빈 보고서에 중국어 한자 %q 가 남아 있다:\n%s", string(r), out)
}
if !strings.Contains(out, "_이번에 확인된 취약점이 없습니다._") {
t.Errorf("빈 보고서 안내 문구가 한국어가 아니다:\n%s", out)
out := Markdown(Input{Title: "空任务", GeneratedAt: time.Now()})
assertChinese(t, "빈 보고서", out)
if !strings.Contains(out, "_本次未确认到任何漏洞。_") {
t.Errorf("빈 보고서 안내 문구가 중국어가 아니다:\n%s", out)
}
}
@@ -108,25 +126,23 @@ func TestReportMarkdownEmptyLocalized(t *testing.T) {
func TestFindingsMarkdownLocalized(t *testing.T) {
f := sampleDBFinding()
out := FindingsMarkdown([]*db.DBFinding{f}, time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC))
if r, ok := containsHan(out); ok {
t.Fatalf("취약점 요약 보고서에 중국어 한자 %q 가 남아 있다:\n%s", string(r), out)
}
assertChinese(t, "취약점 요약 보고서", out)
for _, want := range []string{
"# 취약점 요약 보고서",
"- **생성 시각**:",
"- **취약점 총계**: 1건",
"## 요약",
"| 심각도 | 개수 |",
"| 심각 |", "| 높음 |", "| 중간 |", "| 낮음 |", // 심각도 라벨(UI status.severity 와 동일)
"## 취약점 상세",
"- **유형**: SQL Injection",
"- **상태**: confirmed",
"- **소속 작업**: 데모 대상 침투 테스트",
"- **발견 시각**:",
"**증거:**",
"**상세 보고서:**",
"## 관련 트래픽 증거",
"증거 버전: 2, 바인딩 개수: 1.",
"# 漏洞摘要报告",
"- **生成时间**:",
"- **漏洞总数**: 1 个",
"## 摘要",
"| 严重程度 | 数量 |",
"| 严重 |", "| 高危 |", "| 中危 |", "| 低危 |", // 심각도 라벨(UI status.severity 와 동일)
"## 漏洞详情",
"- **类型**: SQL Injection",
"- **状态**: confirmed",
"- **所属任务**: 演示目标渗透测试",
"- **发现时间**:",
"**证据:**",
"**详细报告:**",
"## 相关流量证据",
"证据版本: 2, 绑定数量: 1.",
} {
if !strings.Contains(out, want) {
t.Errorf("요약 보고서에 %q 가 없다", want)
@@ -134,16 +150,14 @@ func TestFindingsMarkdownLocalized(t *testing.T) {
}
single := SingleFindingMarkdown(f, time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC))
if r, ok := containsHan(single); ok {
t.Fatalf("단건 보고서에 중국어 한자 %q 가 남아 있다:\n%s", string(r), single)
}
assertChinese(t, "단건 보고서", single)
for _, want := range []string{
"- **심각도**: high",
"## 개요",
"## 증거",
"## 상세 보고서",
"[요청 메시지](evidence/123/9/request.http)",
"[응답 메시지](evidence/123/9/response.http)",
"- **严重程度**: high",
"## 概述",
"## 证据",
"## 详细报告",
"[请求消息](evidence/123/9/request.http)",
"[响应消息](evidence/123/9/response.http)",
} {
if !strings.Contains(single, want) {
t.Errorf("단건 보고서에 %q 가 없다", want)
@@ -157,10 +171,8 @@ func TestFindingsCSVLocalized(t *testing.T) {
raw := FindingsCSV([]*db.DBFinding{sampleDBFinding()})
out := strings.TrimPrefix(string(raw), "\xEF\xBB\xBF")
header := strings.SplitN(out, "\n", 2)[0]
if r, ok := containsHan(header); ok {
t.Fatalf("CSV 헤더에 중국어 한자 %q 가 남아 있다: %s", string(r), header)
}
for _, col := range []string{"ID", "이름", "유형", "심각도", "상태", "소속 작업", "발견 시각", "개요", "트래픽 증거 개수", "트래픽 증거 ID"} {
assertChinese(t, "CSV 헤더", header)
for _, col := range []string{"ID", "名称", "类型", "严重程度", "状态", "所属任务", "发现时间", "概述", "流量证据数量", "流量证据 ID"} {
if !strings.Contains(header, col) {
t.Errorf("CSV 헤더에 %q 열이 없다: %s", col, header)
}