mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-26 16:00:06 +02:00
232 lines
6.4 KiB
Go
232 lines
6.4 KiB
Go
package project
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"cyberstrike-ai/internal/database"
|
|
)
|
|
|
|
var factIndexEdgeTypeOrder = []string{
|
|
"discovered_on", "leads_to", "enables", "depends_on", "exploits", "contains", "part_of", "supports",
|
|
}
|
|
|
|
func filterIndexEdges(edges []*database.ProjectFactEdge) []*database.ProjectFactEdge {
|
|
if len(edges) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]*database.ProjectFactEdge, 0, len(edges))
|
|
for _, e := range edges {
|
|
if e == nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(e.Confidence), "deprecated") {
|
|
continue
|
|
}
|
|
edgeType := strings.ToLower(strings.TrimSpace(e.EdgeType))
|
|
if _, ok := database.ValidProjectFactEdgeTypes[edgeType]; !ok {
|
|
continue
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func edgeConfidenceSuffix(confidence string) string {
|
|
c := strings.ToLower(strings.TrimSpace(confidence))
|
|
if c == "" || c == "confirmed" {
|
|
return ""
|
|
}
|
|
return " (" + c + ")"
|
|
}
|
|
|
|
func formatRelationHintPart(e *database.ProjectFactEdge) string {
|
|
return fmt.Sprintf("%s←%s%s", e.EdgeType, e.SourceFactKey, edgeConfidenceSuffix(e.Confidence))
|
|
}
|
|
|
|
func formatOutgoingHintPart(e *database.ProjectFactEdge) string {
|
|
return fmt.Sprintf("%s→%s%s", e.EdgeType, e.TargetFactKey, edgeConfidenceSuffix(e.Confidence))
|
|
}
|
|
|
|
func formatIncomingHintPart(e *database.ProjectFactEdge) string {
|
|
return formatRelationHintPart(e)
|
|
}
|
|
|
|
func joinEdgeHintParts(edges []*database.ProjectFactEdge, formatter func(*database.ProjectFactEdge) string) string {
|
|
parts := make([]string, 0, len(edges))
|
|
for _, e := range edges {
|
|
parts = append(parts, formatter(e))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
// FormatOutgoingLinksHint 黑板索引用出边摘要(全部有效边类型,不截断)。
|
|
func FormatOutgoingLinksHint(edges []*database.ProjectFactEdge) string {
|
|
edges = filterIndexEdges(edges)
|
|
if len(edges) == 0 {
|
|
return ""
|
|
}
|
|
return " {出边: " + joinEdgeHintParts(edges, formatOutgoingHintPart) + "}"
|
|
}
|
|
|
|
// FormatIncomingLinksHint 黑板索引用入边摘要(全部有效边类型,不截断)。
|
|
func FormatIncomingLinksHint(edges []*database.ProjectFactEdge) string {
|
|
edges = filterIndexEdges(edges)
|
|
if len(edges) == 0 {
|
|
return ""
|
|
}
|
|
return " {入边: " + joinEdgeHintParts(edges, formatIncomingHintPart) + "}"
|
|
}
|
|
|
|
// FormatFactIndexLinksHint 黑板索引行内关系边(from → 当前 fact,与 upsert links 一致)。
|
|
func FormatFactIndexLinksHint(_ string, incoming []*database.ProjectFactEdge) string {
|
|
in := filterIndexEdges(incoming)
|
|
if len(in) == 0 {
|
|
return ""
|
|
}
|
|
return " {关系边: " + joinEdgeHintParts(in, formatRelationHintPart) + "}"
|
|
}
|
|
|
|
func indexEdgeGroupMaps(edges []*database.ProjectFactEdge) (outgoing, incoming map[string][]*database.ProjectFactEdge) {
|
|
outgoing = map[string][]*database.ProjectFactEdge{}
|
|
incoming = map[string][]*database.ProjectFactEdge{}
|
|
for _, e := range filterIndexEdges(edges) {
|
|
outgoing[e.SourceFactKey] = append(outgoing[e.SourceFactKey], e)
|
|
incoming[e.TargetFactKey] = append(incoming[e.TargetFactKey], e)
|
|
}
|
|
return outgoing, incoming
|
|
}
|
|
|
|
func relationOverviewLine(e *database.ProjectFactEdge) string {
|
|
return fmt.Sprintf("- %s → %s%s · %s", e.SourceFactKey, e.TargetFactKey, edgeConfidenceSuffix(e.Confidence), e.EdgeType)
|
|
}
|
|
|
|
func indexEdgeSortKey(e *database.ProjectFactEdge) (int, int, string) {
|
|
confRank := 0
|
|
if strings.EqualFold(strings.TrimSpace(e.Confidence), "tentative") {
|
|
confRank = 1
|
|
}
|
|
typeRank := len(factIndexEdgeTypeOrder) + 1
|
|
for i, t := range factIndexEdgeTypeOrder {
|
|
if strings.EqualFold(e.EdgeType, t) {
|
|
typeRank = i
|
|
break
|
|
}
|
|
}
|
|
return confRank, typeRank, e.SourceFactKey + ">" + e.TargetFactKey + ">" + e.EdgeType
|
|
}
|
|
|
|
func sortIndexOverviewEdges(edges []*database.ProjectFactEdge) {
|
|
sort.SliceStable(edges, func(i, j int) bool {
|
|
ci, ti, ki := indexEdgeSortKey(edges[i])
|
|
cj, tj, kj := indexEdgeSortKey(edges[j])
|
|
if ci != cj {
|
|
return ci < cj
|
|
}
|
|
if ti != tj {
|
|
return ti < tj
|
|
}
|
|
return ki < kj
|
|
})
|
|
}
|
|
|
|
// BuildFactPathOverviewSection 生成事实关系速览(全部有效边类型,不含 body)。
|
|
func BuildFactPathOverviewSection(edges []*database.ProjectFactEdge, indexedKeys map[string]struct{}, maxRunes int) string {
|
|
if maxRunes <= 0 {
|
|
return ""
|
|
}
|
|
candidates := filterIndexEdges(edges)
|
|
if len(candidates) == 0 {
|
|
return ""
|
|
}
|
|
filtered := make([]*database.ProjectFactEdge, 0, len(candidates))
|
|
for _, e := range candidates {
|
|
if len(indexedKeys) > 0 {
|
|
if _, ok := indexedKeys[e.SourceFactKey]; !ok {
|
|
continue
|
|
}
|
|
if _, ok := indexedKeys[e.TargetFactKey]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, e)
|
|
}
|
|
if len(filtered) == 0 {
|
|
return ""
|
|
}
|
|
sortIndexOverviewEdges(filtered)
|
|
|
|
header := "### 攻击路径(事实关系)\n"
|
|
header += "source → target · type(与攻击路径图/库中方向一致;写入时在目标 fact 的 links 用 from 声明来源)\n"
|
|
var b strings.Builder
|
|
b.WriteString(header)
|
|
used := len([]rune(header))
|
|
omitted := 0
|
|
|
|
for _, e := range filtered {
|
|
line := relationOverviewLine(e) + "\n"
|
|
lineRunes := len([]rune(line))
|
|
if used+lineRunes > maxRunes {
|
|
omitted++
|
|
continue
|
|
}
|
|
b.WriteString(line)
|
|
used += lineRunes
|
|
}
|
|
if omitted > 0 {
|
|
extra := fmt.Sprintf("(另有 %d 条关系边未列入,请 get_project_fact 查看完整关系。)\n", omitted)
|
|
if used+len([]rune(extra)) <= maxRunes {
|
|
b.WriteString(extra)
|
|
}
|
|
}
|
|
if used <= len([]rune(header)) {
|
|
return ""
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func factIndexSortPriority(f *database.ProjectFact) int {
|
|
if f == nil {
|
|
return 0
|
|
}
|
|
score := 0
|
|
if f.Pinned {
|
|
score += 1000
|
|
}
|
|
c := strings.ToLower(strings.TrimSpace(f.Category))
|
|
switch c {
|
|
case FactCategoryTarget:
|
|
score += 400
|
|
case FactCategoryFinding, FactCategoryChain:
|
|
score += 300
|
|
case FactCategoryExploit, FactCategoryPOC:
|
|
score += 250
|
|
case "auth", "infra", "business":
|
|
score += 200
|
|
case "note":
|
|
score += 50
|
|
default:
|
|
key := strings.ToLower(strings.TrimSpace(f.FactKey))
|
|
if strings.HasPrefix(key, "target/") {
|
|
score += 400
|
|
} else if strings.HasPrefix(key, "finding/") || strings.HasPrefix(key, "chain/") {
|
|
score += 300
|
|
}
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(f.Confidence), "confirmed") {
|
|
score += 80
|
|
}
|
|
return score
|
|
}
|
|
|
|
func sortFactsForIndex(facts []*database.ProjectFact) {
|
|
sort.SliceStable(facts, func(i, j int) bool {
|
|
pi, pj := factIndexSortPriority(facts[i]), factIndexSortPriority(facts[j])
|
|
if pi != pj {
|
|
return pi > pj
|
|
}
|
|
return facts[i].UpdatedAt.After(facts[j].UpdatedAt)
|
|
})
|
|
}
|