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
}
|