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