| package bundle |
|
|
| import ( |
| "path/filepath" |
| "strings" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| type PycAnalyzer struct{} |
|
|
| func (PycAnalyzer) Name() string { return "pyc" } |
|
|
| func (PycAnalyzer) Handles(kind FileKind) bool { return kind == KindPyc } |
|
|
| func (PycAnalyzer) Analyze(f *File, b *Bundle) ([]Finding, error) { |
| if f == nil { |
| return nil, nil |
| } |
| out := []Finding{ |
| { |
| Analyzer: "pyc", |
| File: f.RelPath, |
| Signal: "ships-compiled-bytecode", |
| Severity: SevHigh, |
| Detail: "ships compiled Python bytecode (shipped artifact may differ from source)", |
| Structural: true, |
| }, |
| } |
|
|
| |
| |
| |
| |
| |
| symbols, ok := recoverPycStrings(f.Sniff) |
| if !ok { |
| out = append(out, Finding{ |
| Analyzer: "pyc", |
| File: f.RelPath, |
| Signal: "opaque-bytecode", |
| Severity: SevHigh, |
| Detail: "compiled bytecode could not be decoded (opaque artifact)", |
| Opaque: true, |
| Structural: true, |
| }) |
| return out, nil |
| } |
|
|
| |
| joined := strings.Join(symbols, "\n") |
| out = append(out, sharedIndicatorScan(joined, f.RelPath, "pyc")...) |
|
|
| |
| |
| |
| if src, found := siblingSourceFor(b, f); found { |
| srcText := strings.ToLower(string(src.Sniff)) |
| var missing []string |
| for _, sym := range symbols { |
| if !isInterestingSymbol(sym) { |
| continue |
| } |
| if !strings.Contains(srcText, strings.ToLower(sym)) { |
| missing = append(missing, sym) |
| if len(missing) >= 8 { |
| break |
| } |
| } |
| } |
| if len(missing) > 0 { |
| out = append(out, Finding{ |
| Analyzer: "pyc", |
| File: f.RelPath, |
| Signal: "compiled-source-mismatch", |
| Severity: SevHigh, |
| Detail: "bytecode references symbols absent from sibling source: " + strings.Join(missing, ", "), |
| Corroborated: true, |
| }) |
| } |
| } else { |
| out = append(out, Finding{ |
| Analyzer: "pyc", |
| File: f.RelPath, |
| Signal: "compiled-without-matching-source", |
| Severity: SevHigh, |
| Detail: "compiled bytecode ships with no same-stem source file (uninspectable, xz pattern)", |
| |
| |
| |
| |
| |
| Corroborated: true, |
| }) |
| } |
|
|
| return dedupeFindings(out), nil |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func recoverPycStrings(data []byte) (symbols []string, ok bool) { |
| defer func() { |
| if recover() != nil { |
| symbols = nil |
| ok = false |
| } |
| }() |
|
|
| |
| |
| if len(data) < 16 { |
| return nil, false |
| } |
| if data[2] != 0x0d || data[3] != 0x0a { |
| return nil, false |
| } |
|
|
| body := data[16:] |
| syms := walkMarshalStrings(body) |
| |
| return syms, true |
| } |
|
|
| |
| const ( |
| marshalString = 's' |
| marshalUnicode = 'u' |
| marshalInterned = 't' |
| marshalShortASCII = 'z' |
| marshalShortInt = 'Z' |
| marshalASCII = 'a' |
| marshalASCIIInt = 'A' |
| ) |
|
|
| |
| |
| |
| |
| |
| func walkMarshalStrings(body []byte) []string { |
| var out []string |
| n := len(body) |
| i := 0 |
| const maxStrings = 4096 |
| for i < n && len(out) < maxStrings { |
| c := body[i] & 0x7f |
| switch c { |
| case marshalString, marshalUnicode, marshalInterned, marshalASCII, marshalASCIIInt: |
| if i+5 > n { |
| i++ |
| continue |
| } |
| length := int(body[i+1]) | int(body[i+2])<<8 | int(body[i+3])<<16 | int(body[i+4])<<24 |
| if length < 0 || length > 1<<16 || i+5+length > n { |
| i++ |
| continue |
| } |
| s := string(body[i+5 : i+5+length]) |
| if isPrintableRun(s) { |
| out = append(out, s) |
| i += 5 + length |
| continue |
| } |
| i++ |
| case marshalShortASCII, marshalShortInt: |
| if i+2 > n { |
| i++ |
| continue |
| } |
| length := int(body[i+1]) |
| if i+2+length > n { |
| i++ |
| continue |
| } |
| s := string(body[i+2 : i+2+length]) |
| if isPrintableRun(s) { |
| out = append(out, s) |
| i += 2 + length |
| continue |
| } |
| i++ |
| default: |
| i++ |
| } |
| } |
| return dedupeStrings(out) |
| } |
|
|
| |
| |
| func siblingSourceFor(b *Bundle, pyc *File) (*File, bool) { |
| if b == nil { |
| return nil, false |
| } |
| stem := pycStem(filepath.Base(pyc.RelPath)) |
| dir := filepath.Dir(pyc.RelPath) |
| want := stem + ".py" |
| for _, f := range b.Files { |
| if f == nil || f == pyc { |
| continue |
| } |
| if f.Kind != KindPythonSource { |
| continue |
| } |
| if filepath.Dir(f.RelPath) != dir { |
| continue |
| } |
| if filepath.Base(f.RelPath) == want { |
| return f, true |
| } |
| } |
| return nil, false |
| } |
|
|
| |
| func pycStem(base string) string { |
| base = strings.TrimSuffix(base, ".pyc") |
| if idx := strings.Index(base, ".cpython-"); idx >= 0 { |
| base = base[:idx] |
| } |
| if idx := strings.Index(base, ".opt-"); idx >= 0 { |
| base = base[:idx] |
| } |
| return base |
| } |
|
|
| |
| |
| func isInterestingSymbol(s string) bool { |
| s = strings.TrimSpace(s) |
| if len(s) < 3 || len(s) > 200 { |
| return false |
| } |
| if strings.ContainsAny(s, " \t") && !strings.Contains(s, "://") { |
| return false |
| } |
| return true |
| } |
|
|
| func dedupeStrings(in []string) []string { |
| seen := map[string]bool{} |
| var out []string |
| for _, s := range in { |
| if s == "" || seen[s] { |
| continue |
| } |
| seen[s] = true |
| out = append(out, s) |
| } |
| return out |
| } |
|
|
| |
| |
| func isPrintableRun(s string) bool { |
| if s == "" { |
| return false |
| } |
| runes := []rune(s) |
| printable := 0 |
| for _, r := range runes { |
| if r >= 0x20 && r < 0x7f { |
| printable++ |
| } |
| } |
| return float64(printable)/float64(len(runes)) >= 0.85 |
| } |
|
|