---
name: detect-dependency-confusion
description: "Detection, hunting, and hardening methodology for dependency-confusion and namespace-takeover attacks, where an adversary publishes a public package matching an internal package name or scope so a misconfigured resolver pulls the attacker's higher version (MITRE ATT&CK T1195.002, TA0001, T1059). Works from artifact-repository logs (Artifactory, Nexus, GitHub Packages, Azure Artifacts), CI/CD logs, lock-file diffs, build-agent egress proxy/DNS, and endpoint telemetry. Covers namespace takeover and resolver-order flip across npm, PyPI, NuGet, Maven/Gradle, Go, RubyGems, and Docker, with a hunt, registry pinning, and public rule sources. Use when investigating a suspicious package install, detecting dependency confusion or namespace squatting, checking if internal package names were claimed publicly, hunting resolver-order flips in CI, triaging odd outbound callbacks from a build agent, or hardening npm/pip/NuGet/Maven/Go registry config."
---

# Dependency Confusion and Namespace Takeover - Defensive Methodology

> **Tier 2 - methodology skill.** No bespoke UMBRASEC detection rule ships with this skill yet. It gives the defender's method, the log sources and signals, and the hardening, and points to public rule sources. Promotion to Tier 1 (a sourced, tuned UMBRASEC rule) is tracked in [COVERAGE.md](https://github.com/umbrasec-labs/umbra-blue/blob/main/COVERAGE.md). Do not treat the content below as citing a UMBRASEC rule; there is none for this threat yet.

## TL;DR

Dependency confusion exploits a resolver that can reach both a private registry and a public one. When the private registry does not have a package, or when the public copy carries a higher version, the build silently pulls the public artifact. An attacker who learns an internal package name registers it publicly and waits for a build to install it. Two signals carry the whole detection and neither needs you to decrypt anything: **namespace takeover** (a name in your internal package inventory suddenly exists on the public registry, often brand new, published by an unknown author, with an absurdly high version) and **resolver-order flip** (a package that resolved from your private registry in the last build now resolves from the public upstream, visible as a lock-file `resolved` URL change or a private-registry miss immediately followed by an upstream fetch). Install-time scripts then beacon out, so an unexpected child process or DNS lookup from a build agent is the execution tripwire. Monitor your own namespace on public registries, diff resolved sources between builds, and pin every internal scope to the private registry so the fallback path never exists.

## What it looks like, defender view

In the private artifact repository's request log you see a request for an internal package name return a miss, and moments later the same build agent fetches a package of that name from `registry.npmjs.org`, `pypi.org`, `nuget.org`, Maven Central, `proxy.golang.org`, or `rubygems.org`. In a lock file under version control the `resolved` field for one dependency changes from your internal registry host to a public one, or a new transitive entry appears pointing upstream, often with a version far ahead of anything you publish (attackers use `9.9.9` or `99.0.0` to win resolution). On the build agent, package installation spawns a child process it should not: `npm`/`node` running a shell, `pip`/`python` running `curl` or `powershell`, `dotnet` or `gradle` reaching the network outside artifact downloads. Those install hooks (npm `preinstall`/`postinstall`, a Python build backend or `setup.py`, a Gradle init script) commonly fire a DNS or HTTP callback to a canary domain, so the first external evidence is frequently a single odd DNS query from a host that normally only talks to your registry and your VCS.

## Log sources and signals

- **Private artifact-repository access logs.** JFrog Artifactory `artifactory-request.log` and Sonatype Nexus `request.log` record every artifact request with the requested path, the repository or routing rule that served it, the response code, and the remote address. The signal is an internal name served from a **remote/proxy (upstream) repository** rather than the hosted local one, or a 404 on the local repo followed by a 200 from the public upstream. GitHub Packages and Azure Artifacts expose equivalent audit and feed-upstream logs.
- **CI/CD build logs.** npm with `--loglevel verbose`/`npm config get registry`, `pip install -v` (which prints the index each candidate came from), NuGet restore detailed verbosity, and Gradle `--info` all show the resolved source per package. The signal is any internal-namespace package whose printed source is a public registry.
- **Lock-file resolved-source diffs.** `package-lock.json`/`yarn.lock`/`pnpm-lock.yaml` (`resolved`), `Pipfile.lock`/`poetry.lock` (`source`/`url`), `packages.lock.json` for NuGet, `go.sum`/`go.mod`, `Gemfile.lock`. These are version-controlled, so a diff per build (or a pull-request check) reveals a source host that flipped from private to public, or a brand-new upstream entry. This is the highest-fidelity resolver-order-flip signal.
- **Public-registry inventory checks.** Querying `https://registry.npmjs.org/<name>`, `https://pypi.org/pypi/<name>/json`, `https://www.nuget.org/packages/<name>`, or `https://rubygems.org/api/v1/gems/<name>.json` for each name in your internal inventory tells you whether an internal name is now claimed on the public side (200 where you expect 404). This is a defensive inventory poll against your own package names, not reconnaissance of anyone else.
- **Egress proxy, firewall, NetFlow, and DNS logs from build agents.** A build host reaching a public package registry it is not supposed to use, or any DNS query to an interact-style canary or newly registered domain, is the install-time callback. Build agents have a narrow, predictable egress baseline, which makes this unusually clean.
- **Endpoint telemetry on build agents.** Windows **Security Event ID 4688** and **Sysmon Event ID 1** (process creation with parent image and command line) catch an installer spawning an unexpected child; **Sysmon Event ID 3** (network connect) ties the callback to the installer process; **Sysmon Event ID 22** and the **Microsoft-Windows-DNS-Client/Operational** log (**Event ID 3008**) give per-process DNS. On Linux build agents, **auditd `execve`** records the same parent/child chain. Normal looks like `node`, `python`, `dotnet`, `java`, and `gem` downloading archives and writing files; it does not look like them spawning `curl`, `wget`, `bash -c`, `powershell -enc`, or a reverse-shell one-liner.

## Detection approach

Alert on the two spine signals and on the execution tripwire; score, do not rely on one field.

- **Namespace takeover.** Maintain an inventory of every internal package name and scope (`@yourco/*` npm scopes, internal PyPI and NuGet names, Go module paths, gem names, private image repositories) from your lock files and private registry. On a schedule, poll the matching public registry endpoint for each name and alert when one that should be 404 returns 200, especially when the publish date is recent, the maintainer is unknown to you, or the public version exceeds your internal one. A public claim on a name you own internally is high-confidence evidence of a staged confusion attack even before any build pulls it.
- **Resolver-order flip.** Diff lock-file `resolved`/`source` fields between the previous and current build, or as a pull-request gate, and alert on any dependency whose source host moved from the private registry to a public one, or any new upstream entry for a name inside an internal namespace. Cross-check against the private repository access log for a local miss followed by an upstream fetch of that name.
- **Install-time execution.** Alert when a package-manager process on a build agent spawns a shell, interpreter, or network tool as a child (4688/Sysmon 1 parent-child chains) or makes an unexpected outbound connection or DNS query (Sysmon 3/22, DNS-Client 3008, auditd). This catches the payload even if the two resolution signals were missed.

Point to public, maintained rules and tools rather than inventing one. **github.com/DataDog/guarddog** scans npm and PyPI packages for malicious indicators including suspicious install scripts and exfiltration. **github.com/visma-prodsec/confused** checks whether the dependencies in a manifest are claimable on the public registry, which is a direct namespace-takeover inventory check. **github.com/ossf/package-analysis** (OpenSSF) runs packages in a sandbox and reports their network and file behavior, and **osv.dev** tracks known-malicious packages. For the endpoint tripwire, the SigmaHQ `rules/windows/process_creation` and `rules/linux/process_creation` directories at **github.com/SigmaHQ/sigma** carry process-creation analytics you can adapt to parent images `node.exe`, `python.exe`, `dotnet.exe`, `java.exe`, and `ruby.exe` on build agents. A tuned UMBRASEC rule is not yet shipped for this threat.

## Threat hunt

Hypothesis: an internal package name has been claimed publicly, or a build has already flipped a dependency to the public upstream.

1. Build the internal inventory. Enumerate every internal scope, package, module path, and private image reference from your lock files and the hosted repositories on the private registry. This is the asset list the whole hunt runs against.
2. Poll public registries for each internal name and record 200 responses, publish date, maintainer, and highest public version. Any hit on a name you believed private is a lead; a recent publish by an unknown author winning on version is a strong one. `confused` automates this step from a manifest.
3. Pull a window of the private repository access log and look for internal names served from a remote/proxy repository, or local 404s immediately followed by an upstream 200 for the same path. List the build agents and timestamps involved.
4. For those agents and times, pull 4688/Sysmon 1 and auditd `execve`, filtering to children of package-manager processes, and pull Sysmon 3/22 and DNS-Client 3008 for outbound connections and DNS to uncategorized or newly registered domains. A callback from a build agent co-located in time with an upstream fetch of an internal name is a confirmed chain.
5. Diff the lock files across the same window in VCS history and confirm whether any `resolved`/`source` host flipped from private to public. Rank findings by the privilege and reach of the affected build pipeline.

## Hardening

Shrink the attack surface so the fallback-to-public path does not exist. The controls below map to the control codes used in the UMBRASEC [governance and maturity guide](https://umbrasec.dev/guide/governance-and-maturity.html) and [foundation guide](https://umbrasec.dev/guide/foundation.html); a cell is left blank where no single control maps cleanly.

| Control | What it does | Essential Eight | ISO 27001:2022 | NIST CSF 2.0 |
|---|---|---|---|---|
| Pin internal namespaces to the private registry | Map every internal npm scope to the private registry in `.npmrc`, use a single pip `--index-url` (never `--extra-index-url`, which makes pip choose the highest version across all indexes), enable NuGet Package Source Mapping, set Maven `mirrorOf` and Gradle repository content filtering (`includeGroup`), set `GOPRIVATE`/`GONOSUMDB`, and use fully qualified image references. The resolver can no longer fall back to public for an internal name. | - (configuration) | A.8.19 Software on systems | PR.PS Platform security |
| Reserve internal names on public registries and verify integrity | Pre-claim your internal scopes and names on the public registries so no attacker can, and pin artifacts by hash (npm lockfile integrity, pip `--require-hashes`, `go.sum`). | - (configuration) | A.8.19 Software on systems | PR.PS Platform security |
| Govern the software supply chain | Vet and inventory upstream feeds, maintain an SBOM, and review the registry and CI configuration as a supplier-risk item with named ownership. | (Beyond E8) | A.5.19-A.5.22 Supplier relationships | GV.SC Supply chain |
| Track and patch vulnerable or unneeded dependencies | Run SCA across builds, remove dependencies you do not need, and keep the dependency graph current so a confused package stands out. | Patch applications | A.8.8 Technical vulnerabilities | ID.RA; PR.PS Platform security |

## Honest limitations

The namespace poll only covers names you know about, so an internal package that never appears in a lock file or inventory stays invisible to it; keep the inventory complete. A patient attacker who takes a name and ships a benign-looking package, delaying any callback, defeats the install-time execution tripwire, which is why the resolver-order and namespace signals matter even with no outbound activity. If a build agent already proxies all package traffic through one registry that transparently caches upstream, the private-versus-public distinction can blur in logs, so configure the proxy to label the serving repository. Lock-file diffing assumes lock files are committed and respected; a pipeline that regenerates them unpinned, or installs without a lock file, removes the signal. None of these is decisive alone, so stack the namespace poll, the resolver-source diff, and the endpoint tripwire, and close the gap with registry pinning so detection is the backstop and not the only line.

## ATT&CK and D3FEND

- MITRE ATT&CK: [T1195.002 Supply Chain Compromise: Compromise Software Supply Chain](https://attack.mitre.org/techniques/T1195/002/) (tactic TA0001 Initial Access); install-time code execution via [T1059 Command and Scripting Interpreter](https://attack.mitre.org/techniques/T1059/).
- MITRE D3FEND: [D3-EAL Executable Allowlisting](https://d3fend.mitre.org/technique/d3f:ExecutableAllowlisting/) - allow only approved packages and binaries to run, which denies an unvetted confused package execution even if it is pulled.
- Primary source for the threat: Alex Birsan, "Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies" (2021).
- Public rule and tool sources: Microsoft, "3 Ways to Mitigate Risk When Using Private Package Feeds" (Feb 2021); github.com/DataDog/guarddog; github.com/visma-prodsec/confused; github.com/ossf/package-analysis; osv.dev; SigmaHQ process-creation rules at github.com/SigmaHQ/sigma.

**Scope note.** This is a defensive skill. It explains dependency confusion only to the depth needed to detect, hunt, and mitigate it, and ships no package-publishing, reconnaissance, or callback tooling; UMBRASEC publishes defense, not offense.
