ONNX
security
malware-detection
File size: 10,988 Bytes
d2507b5
 
 
 
 
 
a2a3348
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
package bundle

import (
	"path/filepath"
	"strings"

	"huggingface.co/turenlabs/Vigil/source/pkg/types"
)

// resolveReferences marks each File.Referenced by searching the SKILL.md for any
// form of its name. The indirection layer (context-loader / simple-formatter)
// is not defeated by verb omission: we match the basename, the bundle-relative
// path, ./name, markdown-link forms ([x](./payload.sh)), and code-span forms
// (`./payload.sh`), recording which surface named the file in RefSources.
//
// Symlinked siblings are matched on their OWN basename/relpath too, so a
// directive like "run ./helper" where helper -> /bin/sh resolves to the File
// entry for helper.
func resolveReferences(b *Bundle) {
	for _, f := range b.Files {
		referenced, sources := fileIsReferenced(b.Skill, b.AllowedTools, f)
		f.Referenced = referenced
		f.RefSources = sources
	}
}

// fileIsReferenced reports whether the SKILL.md (body/raw/description/triggers)
// or the allowed-tools frontmatter names f, and via which surfaces. A nil skill
// yields no references (e.g. when SKILL.md failed to parse).
func fileIsReferenced(skill *types.SkillFile, allowedTools []string, f *File) (bool, []string) {
	forms := referenceForms(f.RelPath)
	if len(forms) == 0 {
		return false, nil
	}

	var sources []string
	seen := map[string]bool{}
	addSource := func(s string) {
		if !seen[s] {
			seen[s] = true
			sources = append(sources, s)
		}
	}

	// allowed-tools frontmatter list (e.g. Bash(./setup.sh)).
	for _, tool := range allowedTools {
		if containsAnyForm(tool, forms) {
			addSource("allowed-tools")
			break
		}
	}

	if skill != nil {
		// Body, raw content, and description are scanned for plain, code-span, and
		// markdown-link forms. RawContent is a superset of Body+frontmatter, so it
		// is the primary surface; Body is checked separately so a body-only mention
		// is attributed to "body" rather than the broader "frontmatter".
		bodyHit := containsAnyForm(skill.Body, forms)
		if bodyHit {
			addSource("body")
			if hasMarkdownLink(skill.Body, forms) {
				addSource("markdown-link")
			}
			if hasCodeSpan(skill.Body, forms) {
				addSource("code-span")
			}
		}

		// Frontmatter-region mention: present in RawContent but not in Body and not
		// already attributed to allowed-tools.
		if !bodyHit && containsAnyForm(skill.RawContent, forms) {
			addSource("frontmatter")
			if hasMarkdownLink(skill.RawContent, forms) {
				addSource("markdown-link")
			}
			if hasCodeSpan(skill.RawContent, forms) {
				addSource("code-span")
			}
		}

		if containsAnyForm(skill.Description, forms) {
			addSource("body")
		}
		for _, t := range skill.Triggers {
			if containsAnyForm(t, forms) {
				addSource("frontmatter")
				break
			}
		}
	}

	return len(sources) > 0, sources
}

// referenceForms returns the distinct textual surfaces under which relPath might
// be named in SKILL.md: the basename, the bundle-relative path (slash form), and
// the ./-prefixed relative path. Empty/degenerate names yield no forms so a
// stray "." cannot match everything.
func referenceForms(relPath string) []string {
	rel := filepath.ToSlash(strings.TrimSpace(relPath))
	if rel == "" || rel == "." || rel == ".." {
		return nil
	}
	base := pathBase(rel)

	forms := []string{}
	add := func(s string) {
		s = strings.TrimSpace(s)
		if s == "" || s == "." {
			return
		}
		for _, existing := range forms {
			if existing == s {
				return
			}
		}
		forms = append(forms, s)
	}

	add(base)
	add(rel)
	add("./" + rel)
	if base != rel {
		add("./" + base)
	}
	return forms
}

// pathBase returns the final slash-separated segment of a slash path.
func pathBase(slashPath string) string {
	if i := strings.LastIndex(slashPath, "/"); i >= 0 {
		return slashPath[i+1:]
	}
	return slashPath
}

// containsAnyForm reports whether haystack contains any reference form as a
// whole-token match. A bare basename must not match as a substring of an
// unrelated longer word (e.g. "go" inside "category"), so we require the match
// to be bounded by a non-identifier character (or string edge) on each side.
func containsAnyForm(haystack string, forms []string) bool {
	if haystack == "" {
		return false
	}
	for _, form := range forms {
		if tokenMatch(haystack, form) {
			return true
		}
	}
	return false
}

// tokenMatch reports whether needle occurs in haystack bounded by non-filename
// characters. Filename characters are letters, digits, '_', '-', '.', '/'.
// A form that itself starts with "./" or contains "/" is already specific
// enough that a plain substring check is safe and is used directly.
func tokenMatch(haystack, needle string) bool {
	if needle == "" {
		return false
	}
	// Path-ish forms are specific; substring is sufficient and avoids missing a
	// match adjacent to quotes/parens.
	if strings.ContainsAny(needle, "/") || strings.HasPrefix(needle, "./") {
		return strings.Contains(haystack, needle)
	}

	from := 0
	for {
		idx := strings.Index(haystack[from:], needle)
		if idx < 0 {
			return false
		}
		start := from + idx
		end := start + len(needle)

		leftOK := start == 0 || !isFilenameByte(haystack[start-1])
		rightOK := end == len(haystack) || !isFilenameByte(haystack[end])
		if leftOK && rightOK {
			return true
		}
		from = start + 1
		if from >= len(haystack) {
			return false
		}
	}
}

// isFilenameByte reports whether b can appear inside an unquoted filename token.
func isFilenameByte(b byte) bool {
	switch {
	case b >= 'a' && b <= 'z':
		return true
	case b >= 'A' && b <= 'Z':
		return true
	case b >= '0' && b <= '9':
		return true
	case b == '_' || b == '-' || b == '.' || b == '/':
		return true
	}
	return false
}

// hasMarkdownLink reports whether any form appears inside a markdown link target
// "](...form...)". This is a heuristic surface attribution, not a strict parse.
func hasMarkdownLink(haystack string, forms []string) bool {
	for _, form := range forms {
		// Look for the closing "](" of a link whose target contains the form.
		from := 0
		for {
			idx := strings.Index(haystack[from:], "](")
			if idx < 0 {
				break
			}
			start := from + idx + 2
			closeIdx := strings.Index(haystack[start:], ")")
			if closeIdx < 0 {
				break
			}
			target := haystack[start : start+closeIdx]
			if strings.Contains(target, form) {
				return true
			}
			from = start + closeIdx + 1
			if from >= len(haystack) {
				break
			}
		}
	}
	return false
}

// hasCodeSpan reports whether any form appears inside a backtick code span.
func hasCodeSpan(haystack string, forms []string) bool {
	for _, form := range forms {
		from := 0
		for {
			open := strings.Index(haystack[from:], "`")
			if open < 0 {
				break
			}
			openAbs := from + open + 1
			close := strings.Index(haystack[openAbs:], "`")
			if close < 0 {
				break
			}
			span := haystack[openAbs : openAbs+close]
			if strings.Contains(span, form) {
				return true
			}
			from = openAbs + close + 1
			if from >= len(haystack) {
				break
			}
		}
	}
	return false
}

// parseAllowedTools extracts the allowed-tools list from raw frontmatter. The
// existing parser does not capture this field. Supported forms:
//
//	allowed-tools: [Bash, Read, Write]      # inline flow list
//	allowed-tools: Bash, Read               # inline comma list
//	allowed-tools:                          # block list
//	  - Bash(./setup.sh)
//	  - Read
//
// Only the frontmatter region (the first --- ... --- block) is considered.
func parseAllowedTools(rawContent string) []string {
	fm := frontmatterRegion(rawContent)
	if fm == "" {
		return nil
	}

	lines := strings.Split(fm, "\n")
	var tools []string
	inBlock := false

	for _, line := range lines {
		trimmedRight := strings.TrimRight(line, " \t\r")
		trimmed := strings.TrimSpace(trimmedRight)

		if inBlock {
			if strings.HasPrefix(trimmed, "- ") {
				tools = append(tools, splitToolList(trimmed[2:])...)
				continue
			}
			// A new top-level key ends the block.
			if isTopLevelKey(trimmedRight) {
				inBlock = false
				// fall through to key handling below
			} else if trimmed == "" {
				continue
			} else {
				inBlock = false
			}
		}

		key, val, ok := splitKey(trimmedRight)
		if !ok {
			continue
		}
		if !isAllowedToolsKey(key) {
			continue
		}

		val = strings.TrimSpace(val)
		switch {
		case val == "":
			// Block list follows on subsequent "- " lines.
			inBlock = true
		case strings.HasPrefix(val, "[") && strings.HasSuffix(val, "]"):
			inner := strings.TrimSuffix(strings.TrimPrefix(val, "["), "]")
			tools = append(tools, splitToolList(inner)...)
		default:
			tools = append(tools, splitToolList(val)...)
		}
	}

	return dedupeNonEmpty(tools)
}

// frontmatterRegion returns the inner text of the leading --- ... --- block, or
// "" if there is no frontmatter.
func frontmatterRegion(rawContent string) string {
	trimmed := strings.TrimLeft(rawContent, " \t\r\n")
	if !strings.HasPrefix(trimmed, "---") {
		return ""
	}
	rest := trimmed[3:]
	// Skip to end of the opening delimiter line.
	if nl := strings.IndexByte(rest, '\n'); nl >= 0 {
		rest = rest[nl+1:]
	} else {
		return ""
	}
	idx := strings.Index(rest, "\n---")
	if idx < 0 {
		// Closing delimiter might be the very first line of rest.
		if strings.HasPrefix(rest, "---") {
			return ""
		}
		return ""
	}
	return rest[:idx]
}

// isAllowedToolsKey reports whether a frontmatter key is the allowed-tools list
// under any of its common spellings.
func isAllowedToolsKey(key string) bool {
	k := strings.ToLower(strings.TrimSpace(key))
	return k == "allowed-tools" || k == "allowed_tools" || k == "allowedtools" || k == "tools"
}

// isTopLevelKey reports whether a raw line is an unindented "key:" pair.
func isTopLevelKey(rawLine string) bool {
	if rawLine == "" {
		return false
	}
	if rawLine[0] == ' ' || rawLine[0] == '\t' {
		return false
	}
	_, _, ok := splitKey(rawLine)
	return ok
}

// splitKey splits a "key: value" line. ok is false when there is no colon-led
// key. Returns the trimmed key and the raw remainder.
func splitKey(line string) (key, val string, ok bool) {
	colon := strings.Index(line, ":")
	if colon <= 0 {
		return "", "", false
	}
	return strings.TrimSpace(line[:colon]), line[colon+1:], true
}

// splitToolList splits a comma/space-separated tool list, trimming quotes and
// whitespace. "Bash(./setup.sh)" is preserved whole so the argument path inside
// can be matched by reference resolution.
func splitToolList(s string) []string {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil
	}
	var out []string
	for _, part := range strings.Split(s, ",") {
		p := strings.TrimSpace(part)
		p = strings.Trim(p, "'\"")
		if p != "" {
			out = append(out, p)
		}
	}
	return out
}

// dedupeNonEmpty removes empties and duplicates, preserving order.
func dedupeNonEmpty(in []string) []string {
	if len(in) == 0 {
		return nil
	}
	seen := map[string]bool{}
	var out []string
	for _, s := range in {
		s = strings.TrimSpace(s)
		if s == "" || seen[s] {
			continue
		}
		seen[s] = true
		out = append(out, s)
	}
	return out
}