ONNX
security
malware-detection
Vigil / source /pkg /bundle /signals.go
turentomer's picture
Publish self-contained Vigil distribution
d2507b5 verified
Raw
History Blame Contribute Delete
7.87 kB
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
}