|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "archive/tar" |
4 | 5 | "archive/zip" |
| 6 | + "bytes" |
| 7 | + "compress/gzip" |
5 | 8 | "os" |
6 | 9 | "path/filepath" |
7 | 10 | "strings" |
@@ -590,6 +593,61 @@ func TestCIWorkflowPathIsContentScanned(t *testing.T) { |
590 | 593 | } |
591 | 594 | } |
592 | 595 |
|
| 596 | +func TestBindingGypPackageLoaderIsContentScanned(t *testing.T) { |
| 597 | + if !textCandidate("package/binding.gyp") { |
| 598 | + t.Fatal("binding.gyp should be treated as text for archive IOC scanning") |
| 599 | + } |
| 600 | + if got := classifyScanFile("repo/node_modules/pkg/binding.gyp", 2048, nil); got != scanContent { |
| 601 | + t.Fatalf("node_modules binding.gyp: classifyScanFile = %v, want scanContent", got) |
| 602 | + } |
| 603 | + if got := classifyScanFile("repo/native-addon/binding.gyp", 2048, nil); got != scanMetadataOnly { |
| 604 | + t.Fatalf("source-tree binding.gyp should remain metadata-only in project profile: got %v", got) |
| 605 | + } |
| 606 | +} |
| 607 | + |
| 608 | +func TestArchiveInspectionFindsPhantomGypBindingGyp(t *testing.T) { |
| 609 | + var archive bytes.Buffer |
| 610 | + gz := gzip.NewWriter(&archive) |
| 611 | + tw := tar.NewWriter(gz) |
| 612 | + payload := []byte(`{ |
| 613 | + "targets": [ |
| 614 | + { |
| 615 | + "target_name": "Setup", |
| 616 | + "type": "none", |
| 617 | + "sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"] |
| 618 | + } |
| 619 | + ] |
| 620 | +}`) |
| 621 | + header := &tar.Header{ |
| 622 | + Name: "package/binding.gyp", |
| 623 | + Mode: 0o644, |
| 624 | + Size: int64(len(payload)), |
| 625 | + } |
| 626 | + if err := tw.WriteHeader(header); err != nil { |
| 627 | + t.Fatal(err) |
| 628 | + } |
| 629 | + if _, err := tw.Write(payload); err != nil { |
| 630 | + t.Fatal(err) |
| 631 | + } |
| 632 | + if err := tw.Close(); err != nil { |
| 633 | + t.Fatal(err) |
| 634 | + } |
| 635 | + if err := gz.Close(); err != nil { |
| 636 | + t.Fatal(err) |
| 637 | + } |
| 638 | + |
| 639 | + detection := NewMiniShaiHuludDetectionWithRemote(phantomGypRemotePack()) |
| 640 | + var findings []Finding |
| 641 | + detection.ScanFile(FileContext{ |
| 642 | + Path: filepath.Join(t.TempDir(), "pkg-1.0.0.tgz"), |
| 643 | + Base: "pkg-1.0.0.tgz", |
| 644 | + Data: archive.Bytes(), |
| 645 | + }, func(finding Finding) { |
| 646 | + findings = append(findings, finding) |
| 647 | + }) |
| 648 | + assertSeverityContains(t, dedupeFindings(findings), "critical", "ioc-string", "Phantom Gyp install-time node execution: 100% match") |
| 649 | +} |
| 650 | + |
593 | 651 | func miasmaRemotePack() *RemoteDetectionPack { |
594 | 652 | return &RemoteDetectionPack{ |
595 | 653 | ID: "miasma-2026-06", |
@@ -628,6 +686,27 @@ func miasmaRemotePack() *RemoteDetectionPack { |
628 | 686 | } |
629 | 687 | } |
630 | 688 |
|
| 689 | +func phantomGypRemotePack() *RemoteDetectionPack { |
| 690 | + return &RemoteDetectionPack{ |
| 691 | + ID: "phantom-gyp-2026-06", |
| 692 | + Campaign: "Miasma Phantom Gyp npm compromise June 2026", |
| 693 | + CompositeIOCs: []RemoteCompositeIOC{ |
| 694 | + { |
| 695 | + Label: "Phantom Gyp install-time node execution", |
| 696 | + Severity: "critical", |
| 697 | + MinMatches: 3, |
| 698 | + Signals: []RemoteIOC{ |
| 699 | + {Label: "gyp targets block", Pattern: `(?i)"targets"\s*:`}, |
| 700 | + {Label: "setup target", Pattern: `(?i)"target_name"\s*:\s*"Setup"`}, |
| 701 | + {Label: "node index.js command substitution", Pattern: `(?i)<!\(\s*node\s+index\.js`}, |
| 702 | + {Label: "silent execution", Pattern: `(?i)>\s*/dev/null\s+2>&1`}, |
| 703 | + {Label: "stub source fallback", Pattern: `(?i)echo\s+stub\.c`}, |
| 704 | + }, |
| 705 | + }, |
| 706 | + }, |
| 707 | + } |
| 708 | +} |
| 709 | + |
631 | 710 | func scanFixture(t *testing.T, fixture string) []Finding { |
632 | 711 | t.Helper() |
633 | 712 |
|
|
0 commit comments