| package bundle |
|
|
| import ( |
| "encoding/json" |
| "regexp" |
| "strings" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| type DataAnalyzer struct{} |
|
|
| func (DataAnalyzer) Name() string { return "data" } |
|
|
| func (DataAnalyzer) Handles(kind FileKind) bool { |
| return kind == KindData || kind == KindText |
| } |
|
|
| |
| |
| var imperativeDirectiveRe = regexp.MustCompile(`(?i)\b(run|execute|exec|source|eval|invoke|fetch|download|curl|wget|pipe to (?:sh|bash)|chmod \+x|install (?:and run)?|launch|spawn)\b`) |
|
|
| func (DataAnalyzer) Analyze(f *File, b *Bundle) ([]Finding, error) { |
| if f == nil { |
| return nil, nil |
| } |
| text := string(f.Sniff) |
| var out []Finding |
|
|
| |
| if exfilHostRe.MatchString(text) { |
| line := firstLineMatching(text, exfilHostRe) |
| out = append(out, Finding{ |
| Analyzer: "data", |
| File: f.RelPath, |
| Signal: "exfil-host-reference", |
| Severity: SevCritical, |
| Detail: "data file references known exfiltration host", |
| Line: line, |
| Corroborated: true, |
| }) |
| } |
|
|
| |
| if strings.HasSuffix(strings.ToLower(f.RelPath), ".ipynb") { |
| cells := scanNotebookCells(f.Sniff) |
| if len(cells) == 0 && looksLikeJSON(text) { |
| out = append(out, Finding{ |
| Analyzer: "data", |
| File: f.RelPath, |
| Signal: "opaque-notebook", |
| Severity: SevMedium, |
| Detail: "could not parse notebook cells (malformed JSON)", |
| Opaque: true, |
| }) |
| } |
| for _, src := range cells { |
| cellFindings := sharedIndicatorScan(src, f.RelPath, "data") |
| out = append(out, cellFindings...) |
| } |
| } |
|
|
| |
| for _, dir := range scanImperativeDirectives(text) { |
| out = append(out, Finding{ |
| Analyzer: "data", |
| File: f.RelPath, |
| Signal: "data-embedded-directive", |
| Severity: SevMedium, |
| Detail: "data file embeds an imperative agent directive: " + dir, |
| }) |
| } |
|
|
| |
| lines := strings.Split(text, "\n") |
| for i, l := range lines { |
| if base64BlobRe.MatchString(l) || longHexRe.MatchString(l) { |
| out = append(out, Finding{ |
| Analyzer: "data", |
| File: f.RelPath, |
| Signal: "embedded-encoded-blob", |
| Severity: SevMedium, |
| Detail: "long base64/hex blob inside data file", |
| Line: i + 1, |
| }) |
| } |
| } |
|
|
| |
| |
| |
| out = append(out, decodeAndRescan(text, f.RelPath, "data", 0)...) |
|
|
| if pe, ok := paddingEvasionFinding(f, "data"); ok { |
| out = append(out, pe) |
| } |
| return dedupeFindings(out), nil |
| } |
|
|
| |
| |
| func scanImperativeDirectives(text string) []string { |
| seen := map[string]bool{} |
| var out []string |
| for _, l := range strings.Split(text, "\n") { |
| if !imperativeDirectiveRe.MatchString(l) { |
| continue |
| } |
| |
| |
| low := strings.ToLower(l) |
| if !urlRe.MatchString(l) && |
| !strings.Contains(low, "./") && |
| !strings.Contains(low, ".sh") && |
| !strings.Contains(low, ".py") && |
| !matchedAny(low, networkSinkTerms) && |
| !matchedAny(low, rceTerms) { |
| continue |
| } |
| snippet := strings.TrimSpace(l) |
| if len(snippet) > 120 { |
| snippet = snippet[:120] |
| } |
| if !seen[snippet] { |
| seen[snippet] = true |
| out = append(out, snippet) |
| } |
| if len(out) >= 16 { |
| break |
| } |
| } |
| return out |
| } |
|
|
| |
| |
| func scanNotebookCells(jsonBytes []byte) []string { |
| var nb struct { |
| Cells []struct { |
| CellType string `json:"cell_type"` |
| Source json.RawMessage `json:"source"` |
| } `json:"cells"` |
| } |
| if err := json.Unmarshal(jsonBytes, &nb); err != nil { |
| return nil |
| } |
| var out []string |
| for _, c := range nb.Cells { |
| if c.CellType != "code" { |
| continue |
| } |
| out = append(out, joinSource(c.Source)) |
| } |
| return out |
| } |
|
|
| |
| |
| func joinSource(raw json.RawMessage) string { |
| if len(raw) == 0 { |
| return "" |
| } |
| var asString string |
| if err := json.Unmarshal(raw, &asString); err == nil { |
| return asString |
| } |
| var asArray []string |
| if err := json.Unmarshal(raw, &asArray); err == nil { |
| return strings.Join(asArray, "") |
| } |
| return "" |
| } |
|
|
| func looksLikeJSON(text string) bool { |
| t := strings.TrimSpace(text) |
| return strings.HasPrefix(t, "{") || strings.HasPrefix(t, "[") |
| } |
|
|
| func firstLineMatching(text string, re *regexp.Regexp) int { |
| for i, l := range strings.Split(text, "\n") { |
| if re.MatchString(l) { |
| return i + 1 |
| } |
| } |
| return 0 |
| } |
|
|