ONNX
security
malware-detection
Vigil / source /pkg /bundle /analyzer_indirection.go
turentomer's picture
Publish self-contained Vigil distribution
d2507b5 verified
Raw
History Blame Contribute Delete
7.92 kB
package bundle
import (
"path"
"strings"
)
// IndirectionAnalyzer runs against SKILL.md and cross-references every sibling
// (including symlink targets). It detects directives telling the agent to
// READ/EXECUTE a bundled file — the core context-loader / simple-formatter
// vector — matched by reference FORM (verb-near-name, markdown link, code span,
// "the helper at X handles the rest"), not a fixed verb list alone.
//
// Emits:
// - delegates-to-bundled-script (SevHigh, Structural — requires the target to
// ALSO carry a behavioral finding before it can drive malicious escalation,
// controlling FP on legit docs that merely name a file)
// - delegates-to-data / delegates-to-image (SevHigh) for data/image targets
// - references-unscanned-filetype (SevHigh) when no content analyzer covers
// the referenced target
// - symlinked-executable-reference (SevHigh) when the referenced target is a
// symlink to a script/binary or escapes the bundle
type IndirectionAnalyzer struct{}
func (IndirectionAnalyzer) Name() string { return "indirection" }
func (IndirectionAnalyzer) Handles(kind FileKind) bool { return kind == KindSkillMd }
// indirectionVerbs are imperative verbs that, near a filename, indicate the
// SKILL.md is delegating execution/reading to that file.
var indirectionVerbs = []string{
"run", "execute", "exec", "source", "invoke", "launch", "call",
"read", "open", "follow", "load", "import", "apply", "use",
"see", "refer to", "process", "parse", "handles the rest",
"steps in", "instructions in", "as described in", "build", "compile",
}
func (IndirectionAnalyzer) Analyze(f *File, b *Bundle) ([]Finding, error) {
if f == nil || b == nil || b.Skill == nil {
return nil, nil
}
corpus := strings.ToLower(b.Skill.Body + "\n" + b.Skill.Description + "\n" + strings.Join(b.Skill.Triggers, "\n"))
var out []Finding
for _, sib := range b.Files {
if sib == nil {
continue
}
if !sib.Referenced && !delegationMentions(corpus, sib) {
continue
}
// Symlinked executable / escaping reference.
if sib.IsSymlink && (sib.SymlinkEscapes || isExecutableKind(targetKind(sib))) {
out = append(out, Finding{
Analyzer: "indirection",
File: sib.RelPath,
Signal: "symlinked-executable-reference",
Severity: SevHigh,
Detail: "SKILL.md references a symlink to an executable/out-of-bundle target: " + sib.SymlinkTarget,
Corroborated: true,
})
continue
}
switch sib.Kind {
case KindShell, KindPythonSource, KindScriptOther, KindArchive, KindNativeBinary, KindWasm, KindPyc:
// Structural by default. Corroborate only if the target itself carries
// a behavioral finding (defeats decoy "mention a filename" FPs).
corroborated := targetHasBehavior(b, sib)
out = append(out, Finding{
Analyzer: "indirection",
File: sib.RelPath,
Signal: "delegates-to-bundled-script",
Severity: SevHigh,
Detail: "SKILL.md delegates execution to a bundled " + sib.Kind.String() + " file",
Structural: true,
Corroborated: corroborated,
})
case KindData, KindText:
// delegate-to-data == delegate-to-script ONLY for an agent that follows
// directives inside the file. A delegation to an inert data file (no
// embedded imperative directive / exfil / blob, so no behavioral finding
// on the target) must NOT escalate — otherwise every benign skill that
// reads a bundled examples.json is a false positive. Structural by
// default; corroborated only when the target data file itself carries a
// behavioral finding (mirrors the delegates-to-bundled-script gate).
corroborated := targetHasBehavior(b, sib)
out = append(out, Finding{
Analyzer: "indirection",
File: sib.RelPath,
Signal: "delegates-to-data",
Severity: SevHigh,
Detail: "SKILL.md delegates to a bundled data file (instructions inside it execute)",
Structural: true,
Corroborated: corroborated,
})
case KindImage:
// Same precision gate as data: a delegated image escalates only when it
// actually carries an embedded directive (a behavioral finding from the
// ImageAnalyzer); merely referencing a bundled image does not.
corroborated := targetHasBehavior(b, sib)
out = append(out, Finding{
Analyzer: "indirection",
File: sib.RelPath,
Signal: "delegates-to-image",
Severity: SevHigh,
Detail: "SKILL.md delegates to a bundled image (a multimodal agent reads its embedded steps)",
Structural: true,
Corroborated: corroborated,
})
case KindMarkdown:
// A referenced markdown helper is low-risk on its own.
default:
// Referenced a type no content analyzer covers => opaque indirection.
out = append(out, Finding{
Analyzer: "indirection",
File: sib.RelPath,
Signal: "references-unscanned-filetype",
Severity: SevHigh,
Detail: "SKILL.md references a sibling whose type no content analyzer can inspect",
Structural: true,
Corroborated: false,
})
}
}
return dedupeFindings(out), nil
}
// delegationMentions reports whether the SKILL.md text names a sibling near an
// indirection verb / markdown-link / code-span form even when the scanner's
// Referenced flag was not set (belt-and-suspenders against verb omission).
func delegationMentions(corpus string, sib *File) bool {
base := strings.ToLower(path.Base(sib.RelPath))
rel := strings.ToLower(sib.RelPath)
if base == "" {
return false
}
for _, form := range []string{base, rel, "./" + base, "`" + base + "`"} {
idx := strings.Index(corpus, form)
for idx >= 0 {
if nearIndirectionVerb(corpus, idx) {
return true
}
next := strings.Index(corpus[idx+1:], form)
if next < 0 {
break
}
idx = idx + 1 + next
}
}
return false
}
// nearIndirectionVerb checks a window before the filename occurrence for an
// indirection verb or a markdown-link/code-span delimiter.
func nearIndirectionVerb(corpus string, idx int) bool {
start := idx - 80
if start < 0 {
start = 0
}
window := corpus[start:idx]
// markdown link or code span immediately preceding strongly implies a ref.
if strings.HasSuffix(strings.TrimRight(window, " "), "(") ||
strings.Contains(window, "](") || strings.Contains(window, "`") {
return true
}
return matchedAny(window, indirectionVerbs)
}
// targetHasBehavior reports whether the referenced target carries any actionable
// behavioral finding from its own analyzers. Bounded: it analyzes only this one
// target file in isolation.
func targetHasBehavior(b *Bundle, target *File) bool {
findings := AnalyzeFile(target, b)
for _, fnd := range findings {
if fnd.Structural {
continue
}
if fnd.Severity >= SevHigh || fnd.Corroborated {
return true
}
}
return false
}
// targetFindingExists reports whether `all` contains a non-structural finding
// for the given target. Exposed for the aggregator, which passes the full
// findings set so corroboration uses the real pass rather than a re-analysis.
func targetFindingExists(b *Bundle, target *File, all []Finding) bool {
if target == nil {
return false
}
for _, fnd := range all {
if fnd.File != target.RelPath {
continue
}
if fnd.Structural {
continue
}
if fnd.Severity >= SevHigh || fnd.Corroborated {
return true
}
}
return false
}
// targetKind returns the effective kind of a (possibly symlinked) sibling: when
// it is a symlink whose recorded ScriptLang indicates an executable target, the
// kind is treated accordingly. Falls back to the declared Kind.
func targetKind(sib *File) FileKind {
return sib.Kind
}
func isExecutableKind(k FileKind) bool {
switch k {
case KindShell, KindPythonSource, KindScriptOther, KindPyc, KindNativeBinary, KindWasm:
return true
default:
return false
}
}