ONNX
security
malware-detection
File size: 7,871 Bytes
d2507b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package bundle

// signals.go computes the cross-file trust SIGNALS for a bundle independently of
// the heuristic engine. BundleSignals is consumed by BOTH the heuristic bridge
// (pkg/heuristic.BundleCategoryScores) and the next-retrain feature export
// (cmd/train + pkg/types.BundleFeatureVector), so the two stay in sync: the
// boolean/numeric facts live here once, and each consumer projects them.

// BundleSignals holds the named cross-file signals derived from a bundle's files
// and its analyzer findings. The boolean signals correspond 1:1 to the
// cross-file signal catalog; the three numeric signals quantify aggregate risk.
// CorroboratedHighRisk is the precision gate driving bundleEscalates and the
// scorer name-allowlist veto: it is true only when a SevHigh+ finding was
// behaviorally corroborated (or non-structural), never on a bare structural
// signal alone.
type BundleSignals struct {
	DelegatesToBundledScript      bool    `json:"delegates_to_bundled_script"`
	ShipsOpaqueExecutable         bool    `json:"ships_opaque_executable"`
	CompiledWithoutMatchingSource bool    `json:"compiled_without_matching_source"`
	CompiledSourceMismatch        bool    `json:"compiled_source_mismatch"`
	ArchiveContainsExecutable     bool    `json:"archive_contains_executable"`
	ReferencesUnscannedFiletype   bool    `json:"references_unscanned_filetype"`
	DelegatesToData               bool    `json:"delegates_to_data"`
	DelegatesToImage              bool    `json:"delegates_to_image"`
	NLDirectiveActiveTools        bool    `json:"nl_directive_active_tools"`
	SymlinkedExecutableReference  bool    `json:"symlinked_executable_reference"`
	PaddingEvasion                bool    `json:"padding_evasion"`
	HiddenPayloadFiles            int     `json:"hidden_payload_files"`
	PayloadToMarkdownRatio        float64 `json:"payload_to_markdown_ratio"`
	MaxSiblingAnalyzerRisk        float64 `json:"max_sibling_analyzer_risk"`
	CorroboratedHighRisk          bool    `json:"corroborated_high_risk"`
}

// signalAliases maps each boolean BundleSignals field to the stable Finding
// Signal name(s) that set it. Multiple aliases are accepted per field so the
// aggregation stays correct whether an analyzer emits the spec's canonical name
// or a close synonym. The defanged exfil names are intentionally NOT here — they
// drive Severity, which feeds CorroboratedHighRisk / MaxSiblingAnalyzerRisk.
var signalAliases = map[string][]string{
	"DelegatesToBundledScript":      {"delegates-to-bundled-script"},
	"ShipsOpaqueExecutable":         {"ships-opaque-executable"},
	"CompiledWithoutMatchingSource": {"compiled-without-matching-source", "ships-compiled-bytecode"},
	"CompiledSourceMismatch":        {"compiled-source-mismatch"},
	"ArchiveContainsExecutable":     {"archive-contains-executable"},
	"ReferencesUnscannedFiletype":   {"references-unscanned-filetype"},
	"DelegatesToData":               {"delegates-to-data"},
	"DelegatesToImage":              {"delegates-to-image"},
	"NLDirectiveActiveTools":        {"allowed-tools-nl-directive", "nl-directive-active-tools", "nl-directive"},
	"SymlinkedExecutableReference":  {"symlinked-executable-reference"},
	"PaddingEvasion":                {"padding-evasion"},
}

// AggregateSignals folds a bundle's files and analyzer findings into the named
// cross-file signals. It is pure: same inputs produce the same BundleSignals.
func AggregateSignals(b *Bundle, findings []Finding) BundleSignals {
	var s BundleSignals

	present := make(map[string]bool, len(findings))
	for _, f := range findings {
		present[f.Signal] = true
		if w := severityToWeight(f.Severity); w > s.MaxSiblingAnalyzerRisk {
			s.MaxSiblingAnalyzerRisk = w
		}
		// Precision gate: a SevHigh+ finding contributes to high-risk only when
		// it carries a behavioral co-factor (Corroborated) or is non-structural.
		if severityToWeight(f.Severity) >= severityToWeight(SevHigh) &&
			(f.Corroborated || !f.Structural) {
			s.CorroboratedHighRisk = true
		}
	}

	s.DelegatesToBundledScript = anyPresent(present, signalAliases["DelegatesToBundledScript"])
	s.ShipsOpaqueExecutable = anyPresent(present, signalAliases["ShipsOpaqueExecutable"])
	s.CompiledWithoutMatchingSource = anyPresent(present, signalAliases["CompiledWithoutMatchingSource"])
	s.CompiledSourceMismatch = anyPresent(present, signalAliases["CompiledSourceMismatch"])
	s.ArchiveContainsExecutable = anyPresent(present, signalAliases["ArchiveContainsExecutable"])
	s.ReferencesUnscannedFiletype = anyPresent(present, signalAliases["ReferencesUnscannedFiletype"])
	s.DelegatesToData = anyPresent(present, signalAliases["DelegatesToData"])
	s.DelegatesToImage = anyPresent(present, signalAliases["DelegatesToImage"])
	s.NLDirectiveActiveTools = anyPresent(present, signalAliases["NLDirectiveActiveTools"])
	s.SymlinkedExecutableReference = anyPresent(present, signalAliases["SymlinkedExecutableReference"])
	s.PaddingEvasion = anyPresent(present, signalAliases["PaddingEvasion"])

	s.HiddenPayloadFiles = countHiddenPayloadFiles(b, findings)
	s.PayloadToMarkdownRatio = payloadToMarkdownRatio(b)

	return s
}

// anyPresent reports whether any of the given signal names appears in the set.
func anyPresent(present map[string]bool, names []string) bool {
	for _, n := range names {
		if present[n] {
			return true
		}
	}
	return false
}

// countHiddenPayloadFiles counts hidden (dot-prefixed) non-markdown sibling files
// that carry at least one analyzer finding. Scanners that skip hidden files miss
// exactly these, so a hidden file with a finding is a distinct evasion signal.
func countHiddenPayloadFiles(b *Bundle, findings []Finding) int {
	if b == nil {
		return 0
	}
	withFinding := make(map[string]bool, len(findings))
	for _, f := range findings {
		if f.File != "" {
			withFinding[f.File] = true
		}
	}
	n := 0
	for _, f := range b.Files {
		if f == nil || !f.Hidden {
			continue
		}
		if f.Kind == KindMarkdown || f.Kind == KindSkillMd {
			continue
		}
		if withFinding[f.RelPath] {
			n++
		}
	}
	return n
}

// payloadToMarkdownRatio is PayloadBytes / max(SkillMdBytes, 1): a tiny benign
// SKILL.md beside large opaque payloads is suspicious.
func payloadToMarkdownRatio(b *Bundle) float64 {
	if b == nil {
		return 0
	}
	md := b.SkillMdBytes
	if md < 1 {
		md = 1
	}
	return float64(b.PayloadBytes) / float64(md)
}

// Names returns the stable signal names in the same order as ToSlice, so the
// trainer can emit deterministic columns aligned with pkg/types.BundleFeatureNames.
func (s BundleSignals) Names() []string {
	return []string{
		"delegates_to_bundled_script",
		"ships_opaque_executable",
		"compiled_without_matching_source",
		"compiled_source_mismatch",
		"archive_contains_executable",
		"references_unscanned_filetype",
		"delegates_to_data",
		"delegates_to_image",
		"nl_directive_active_tools",
		"symlinked_executable_reference",
		"padding_evasion",
		"hidden_payload_files",
		"payload_to_markdown_ratio",
		"max_sibling_analyzer_risk",
		"corroborated_high_risk",
	}
}

// ToSlice flattens the signals into 15 floats (bools -> 0/1) in Names() order.
// This vector is reserved for the NEXT retrain ONLY and must NEVER be wired into
// the frozen FeatureVector.ToSlice (143 floats) bound to the current ONNX model.
func (s BundleSignals) ToSlice() []float64 {
	return []float64{
		b2f(s.DelegatesToBundledScript),
		b2f(s.ShipsOpaqueExecutable),
		b2f(s.CompiledWithoutMatchingSource),
		b2f(s.CompiledSourceMismatch),
		b2f(s.ArchiveContainsExecutable),
		b2f(s.ReferencesUnscannedFiletype),
		b2f(s.DelegatesToData),
		b2f(s.DelegatesToImage),
		b2f(s.NLDirectiveActiveTools),
		b2f(s.SymlinkedExecutableReference),
		b2f(s.PaddingEvasion),
		float64(s.HiddenPayloadFiles),
		s.PayloadToMarkdownRatio,
		s.MaxSiblingAnalyzerRisk,
		b2f(s.CorroboratedHighRisk),
	}
}

func b2f(v bool) float64 {
	if v {
		return 1
	}
	return 0
}