| package bundle |
|
|
| import ( |
| "regexp" |
| "strings" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| type NLDirectiveAnalyzer struct{} |
|
|
| func (NLDirectiveAnalyzer) Name() string { return "nl-directive" } |
|
|
| func (NLDirectiveAnalyzer) Handles(kind FileKind) bool { return kind == KindSkillMd } |
|
|
| |
| |
| var nlExfilPhrasings = []string{ |
| "send your environment", "send the environment", "send all environment", |
| "send environment variables", "post your environment", "upload your environment", |
| "export your environment", "forward your environment", |
| "send your credentials", "send the credentials", "post your credentials", |
| "send your api key", "send the api key", "send your token", "send the token", |
| "send your secrets", "post your secrets", "upload your secrets", |
| "transmit the contents of .env", "share the contents of .env", |
| "read the .env file and send", "read your .env and post", |
| "set the registry to", "point npm at", "point pip at", "point yarn at", |
| "configure the registry to", "use this mirror", "use our mirror", |
| "add this index url", "set the index url to", "install from this url", |
| "pipe it to bash", "pipe it to sh", "pipe to bash", "pipe to sh", |
| "copy this command and run", "run the following in your terminal", |
| } |
|
|
| |
| var nlMirrorURLRe = regexp.MustCompile(`(?i)(registry|index[- ]?url|--index-url|--extra-index-url|--registry)\s*[:=]?\s*(https?://[^\s'")]+)`) |
|
|
| func (NLDirectiveAnalyzer) Analyze(f *File, b *Bundle) ([]Finding, error) { |
| if f == nil || b == nil || b.Skill == nil { |
| return nil, nil |
| } |
|
|
| if !grantsActiveTools(b.AllowedTools) { |
| |
| |
| return nil, nil |
| } |
|
|
| body := b.Skill.Body |
| lower := strings.ToLower(body) |
| bodyLines := strings.Split(body, "\n") |
|
|
| var out []Finding |
|
|
| |
| if exfilHostRe.MatchString(body) { |
| out = append(out, Finding{ |
| Analyzer: "nl-directive", |
| File: f.RelPath, |
| Signal: "exfil-host-reference", |
| Severity: SevCritical, |
| Detail: "SKILL.md prose references known exfiltration host with active tool grant", |
| Line: firstLineMatching(body, exfilHostRe), |
| Corroborated: true, |
| }) |
| } |
|
|
| |
| for i, line := range bodyLines { |
| low := strings.ToLower(line) |
| if matchedAny(low, nlExfilPhrasings) { |
| out = append(out, Finding{ |
| Analyzer: "nl-directive", |
| File: f.RelPath, |
| Signal: "allowed-tools-nl-directive", |
| Severity: SevHigh, |
| Detail: "active tool grant + natural-language install/exfil directive: " + strings.TrimSpace(line), |
| Line: i + 1, |
| Corroborated: true, |
| }) |
| } |
| } |
|
|
| |
| for _, m := range nlMirrorURLRe.FindAllStringSubmatch(body, -1) { |
| if len(m) < 3 { |
| continue |
| } |
| host := extractHost(m[2]) |
| sev := SevHigh |
| detail := "active tool grant + registry/index rewrite to non-default host: " + host |
| corroborated := true |
| if host != "" && isKnownBenignHost(host) { |
| sev = SevLow |
| detail = "registry rewrite to known-benign corporate mirror: " + host |
| corroborated = false |
| } else if host != "" && isInternalRegistryHost(host) { |
| sev = SevLow |
| detail = "registry rewrite to internal/private mirror host: " + host |
| corroborated = false |
| } |
| out = append(out, Finding{ |
| Analyzer: "nl-directive", |
| File: f.RelPath, |
| Signal: "allowed-tools-nl-directive", |
| Severity: sev, |
| Detail: detail, |
| Line: firstLineContaining(body, m[0]), |
| Corroborated: corroborated, |
| }) |
| } |
|
|
| |
| for i, line := range bodyLines { |
| low := strings.ToLower(line) |
| if matchedAny(low, rceTerms) { |
| out = append(out, Finding{ |
| Analyzer: "nl-directive", |
| File: f.RelPath, |
| Signal: "allowed-tools-nl-directive", |
| Severity: SevHigh, |
| Detail: "active tool grant + download-and-execute directive: " + strings.TrimSpace(line), |
| Line: i + 1, |
| Corroborated: true, |
| }) |
| } |
| } |
|
|
| |
| |
| for _, fnd := range sharedIndicatorScan(lower, f.RelPath, "nl-directive") { |
| if fnd.Signal == "exfil-env-to-network" { |
| out = append(out, fnd) |
| } |
| } |
|
|
| return dedupeFindings(out), nil |
| } |
|
|
| |
| |
| func grantsActiveTools(allowedTools []string) bool { |
| for _, t := range allowedTools { |
| switch strings.ToLower(strings.TrimSpace(t)) { |
| case "bash", "shell", "sh", "exec", "run", "terminal", |
| "webfetch", "web_fetch", "fetch", "http", "network", "curl", "wget": |
| return true |
| } |
| |
| lt := strings.ToLower(t) |
| if strings.HasPrefix(lt, "bash") || strings.HasPrefix(lt, "shell") || |
| strings.Contains(lt, "webfetch") || strings.Contains(lt, "fetch") { |
| return true |
| } |
| } |
| return false |
| } |
|
|
| func firstLineContaining(text, substr string) int { |
| if substr == "" { |
| return 0 |
| } |
| for i, l := range strings.Split(text, "\n") { |
| if strings.Contains(l, substr) { |
| return i + 1 |
| } |
| } |
| return 0 |
| } |
|
|