File size: 3,067 Bytes
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 | package bundle
import "strings"
// ScriptOtherAnalyzer covers the executable-text formats the threat model says
// scanners ignore: .mjs/.cjs/.ts/.rb/.ps1/.bat/.lua/.pl and by-name
// Makefile/Dockerfile/justfile (KindScriptOther). It reuses the shared
// language-agnostic source/sink vocabulary plus a per-language family pass so
// no executable text format stays sub-threshold.
type ScriptOtherAnalyzer struct{}
func (ScriptOtherAnalyzer) Name() string { return "script-other" }
func (ScriptOtherAnalyzer) Handles(kind FileKind) bool { return kind == KindScriptOther }
// scriptOtherIndicatorFamilies maps a language/format to the network-sink and
// exec primitives that, when paired with an env/secret read, indicate exfil.
// These augment the shared vocabulary with idioms specific to each runtime.
var scriptOtherIndicatorFamilies = map[string][]string{
"node": {"child_process", "require('child_process')", "exec(", "execsync(", "spawn(", "https.request", "http.request", "net.connect"},
"ruby": {"net::http", "open-uri", "open(\"|", "system(", "`", "%x{", "exec(", "io.popen"},
"powershell": {"invoke-webrequest", "invoke-restmethod", "iwr ", "irm ", "start-process", "downloadstring", "system.net.webclient", "invoke-expression", "iex "},
"batch": {"powershell", "bitsadmin", "certutil -urlcache", "start /b", "reg add"},
"lua": {"os.execute", "io.popen", "socket.http", "require(\"socket"},
"perl": {"lwp::useragent", "system(", "exec(", "`", "io::socket", "use net::"},
"make": {"curl ", "wget ", "$(shell", "&& sh", "| sh", "| bash"},
"docker": {"run curl", "run wget", "run pip", "run npm", "add http", "&& sh", "| sh"},
}
func (ScriptOtherAnalyzer) Analyze(f *File, b *Bundle) ([]Finding, error) {
if f == nil {
return nil, nil
}
text := string(f.Sniff)
out := sharedIndicatorScan(text, f.RelPath, "script-other")
// Per-language: a runtime exec/network primitive co-occurring with an
// env/secret source is SevHigh+Corroborated even when the shared vocab
// missed the exact sink token.
lang := strings.ToLower(f.ScriptLang)
fam := scriptOtherIndicatorFamilies[lang]
if len(fam) > 0 {
lines := strings.Split(text, "\n")
lower := make([]string, len(lines))
for i, l := range lines {
lower[i] = strings.ToLower(l)
}
for i := range lower {
window := lower[i]
if i+1 < len(lower) {
window += "\n" + lower[i+1]
}
if i+2 < len(lower) {
window += "\n" + lower[i+2]
}
hasPrimitive := matchedAny(window, fam)
hasSource := matchedAny(window, envSourceTerms)
if hasPrimitive && hasSource {
out = append(out, Finding{
Analyzer: "script-other",
File: f.RelPath,
Signal: "exfil-env-to-network",
Severity: SevHigh,
Detail: lang + " runtime exec/network primitive co-occurs with env/credential read",
Line: i + 1,
Corroborated: true,
})
}
}
}
if pe, ok := paddingEvasionFinding(f, "script-other"); ok {
out = append(out, pe)
}
return dedupeFindings(out), nil
}
|