AID-H-022.001: Sandboxed Dependency Installation
Pillars: infra
Lifecycle: building
Scope boundary
Owns no-network ephemeral installation of approved dependency bytes from promoted offline caches, deterministic layer export, installer execution denial, resource bounds, and evidence that the application consumed the exact produced layer.
AID-H-022.002: AID-H-022.001 contains installation of immutable approved bytes; AID-H-022.002 reviews the proposed dependency change before merge. A reviewed change still requires hostile-install containment.
AID-H-003.001: AID-H-022.001 proves how approved packages were installed and which layer was consumed; AID-H-003.001 decides whether each resolved package byte is admissible. The sandbox must consume an admission result rather than deciding package trust from successful installation.
AID-H-023: AID-H-022.001 contains package installation; AID-H-023 verifies publisher and CI workflow provenance. Installation isolation does not establish trusted publication origin.
Implementation guidance
- AID-H-022.001-G001: Build, verify, and promote dependency layers only from signed offline inputs inside ephemeral no-network containers.
- AID-H-022.001-G002: Fail the dependency build when a resolver would change the reviewed lockfile, select an unpinned artifact, or consume bytes outside the signed cache manifest.
- AID-H-022.001-G003: Deny dependency lifecycle scripts and unreviewed source builds during installation, and treat any attempted execution as a failed build.
- AID-H-022.001-G004: Populate dependency caches only through a separate quarantine-and-promotion workflow, then expose the promoted cache read-only to the sandboxed installer.
AID-H-022.001-G001
Control unit and prerequisites
Run each dependency installation as one evidence-producing, ephemeral build job. The job verifies a signed build policy and signed offline-cache manifest; uses digest-pinned images; mounts source, lockfiles, npm cache, and Python wheelhouse read-only; gives writes only to disposable tmpfs plus an evidence directory; disables network and install scripts/source builds; inventories packages inside the same container that performed the install; and independently proves the runtime boundary before promotion.
Step 1: Verify policy and offline inputs
# File: scripts/run_sandboxed_dependency_install.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077
: "${BUILD_POLICY:?set BUILD_POLICY to the reviewed JSON policy}"
: "${BUILD_POLICY_SIG:?set BUILD_POLICY_SIG}"
: "${BUILD_POLICY_KEY:?set BUILD_POLICY_KEY}"
: "${CACHE_ROOT:?set CACHE_ROOT to the promoted immutable cache}"
: "${CACHE_MANIFEST_SIG:?set CACHE_MANIFEST_SIG}"
: "${CACHE_MANIFEST_KEY:?set CACHE_MANIFEST_KEY}"
SOURCE_ROOT=$(pwd -P)
CI_PIPELINE_VALUE="$(printenv CI_PIPELINE_ID || true)"
[[ -n "$CI_PIPELINE_VALUE" ]] || CI_PIPELINE_VALUE=local
[[ "$CI_PIPELINE_VALUE" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]] || { echo "invalid CI_PIPELINE_ID" >&2; exit 2; }
: "${DEPENDENCY_EVIDENCE_ROOT:?set DEPENDENCY_EVIDENCE_ROOT to a pre-created authorized output root outside the source tree}"
[[ -d "$DEPENDENCY_EVIDENCE_ROOT" && ! -L "$DEPENDENCY_EVIDENCE_ROOT" ]] || { echo "invalid dependency evidence root" >&2; exit 2; }
EVIDENCE_PARENT=$(cd -- "$DEPENDENCY_EVIDENCE_ROOT" && pwd -P)
case "$EVIDENCE_PARENT/" in "$SOURCE_ROOT/"*) echo "evidence root must be outside source tree" >&2; exit 2 ;; esac
BUILD_ID="aidefend-deps-$CI_PIPELINE_VALUE"
EVIDENCE_ROOT="$EVIDENCE_PARENT/$BUILD_ID"
if [[ -e "$EVIDENCE_ROOT" ]]; then echo "refusing to reuse an existing evidence directory" >&2; exit 2; fi
install -d -m 0700 "$EVIDENCE_ROOT"
install -m 0600 -- "$BUILD_POLICY" "$EVIDENCE_ROOT/build-policy.json"
install -m 0600 -- "$BUILD_POLICY_SIG" "$EVIDENCE_ROOT/build-policy.sig"
install -m 0600 -- "$CACHE_ROOT/cache.manifest.sha256" "$EVIDENCE_ROOT/cache.manifest.sha256"
install -m 0600 -- "$CACHE_MANIFEST_SIG" "$EVIDENCE_ROOT/cache.manifest.sig"
BUILD_POLICY="$EVIDENCE_ROOT/build-policy.json"
CACHE_MANIFEST_SNAPSHOT="$EVIDENCE_ROOT/cache.manifest.sha256"
cosign verify-blob --key "$BUILD_POLICY_KEY" --bundle "$EVIDENCE_ROOT/build-policy.sig" "$BUILD_POLICY" >/dev/null
cosign verify-blob --key "$CACHE_MANIFEST_KEY" --bundle "$EVIDENCE_ROOT/cache.manifest.sig" "$CACHE_MANIFEST_SNAPSHOT" >/dev/null
(cd "$CACHE_ROOT" && sha256sum --check --strict "$CACHE_MANIFEST_SNAPSHOT")
POLICY_VERSION=$(jq -er '.policy_version' "$BUILD_POLICY")
NODE_IMAGE=$(jq -er '.node_image' "$BUILD_POLICY")
PYTHON_IMAGE=$(jq -er '.python_image' "$BUILD_POLICY")
PIDS_LIMIT=$(jq -er '.container_limits.pids' "$BUILD_POLICY")
MEMORY_BYTES=$(jq -er '.container_limits.memory_bytes' "$BUILD_POLICY")
CPU_PERIOD=$(jq -er '.container_limits.cpu_period' "$BUILD_POLICY")
CPU_QUOTA=$(jq -er '.container_limits.cpu_quota' "$BUILD_POLICY")
WORK_TMPFS_BYTES=$(jq -er '.container_limits.work_tmpfs_bytes' "$BUILD_POLICY")
TMP_TMPFS_BYTES=$(jq -er '.container_limits.tmp_tmpfs_bytes' "$BUILD_POLICY")
SOURCE_DATE_EPOCH=$(jq -er '.source_date_epoch' "$BUILD_POLICY")
NETWORK_PROBE_TIMEOUT_SECONDS=$(jq -er '.network_probe_timeout_seconds | select(type == "number" and . > 0) | tostring' "$BUILD_POLICY")
for image in "$NODE_IMAGE" "$PYTHON_IMAGE"; do
[[ "$image" =~ ^[^[:space:]]+@sha256:[0-9a-f]{64}$ ]] || {
echo "builder image is not digest pinned" >&2
exit 2
}
done
[[ "$PIDS_LIMIT" =~ ^[1-9][0-9]*$ ]] && (( PIDS_LIMIT <= 4096 )) || exit 2
[[ "$MEMORY_BYTES" =~ ^[1-9][0-9]*$ ]] || exit 2
[[ "$CPU_PERIOD" =~ ^[1-9][0-9]*$ ]] || exit 2
[[ "$CPU_QUOTA" =~ ^[1-9][0-9]*$ ]] || exit 2
[[ "$WORK_TMPFS_BYTES" =~ ^[1-9][0-9]*$ ]] || exit 2
[[ "$TMP_TMPFS_BYTES" =~ ^[1-9][0-9]*$ ]] || exit 2
[[ "$SOURCE_DATE_EPOCH" =~ ^[1-9][0-9]*$ ]] || exit 2
[[ "$NETWORK_PROBE_TIMEOUT_SECONDS" =~ ^(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$ ]] || exit 2
trap 'docker rm -f "$BUILD_ID-node" "$BUILD_ID-python" >/dev/null 2>&1 || true' EXITStep 2: Install Node dependencies with a read-only source and cache
set +e
docker run --pull=never --name "$BUILD_ID-node" --network=none --read-only --cap-drop=ALL --security-opt=no-new-privileges --pids-limit="$PIDS_LIMIT" --memory="$MEMORY_BYTES" --cpu-period="$CPU_PERIOD" --cpu-quota="$CPU_QUOTA" --user "$(id -u):$(id -g)" --mount "type=bind,src=$SOURCE_ROOT,dst=/src,readonly" --mount "type=bind,src=$CACHE_ROOT/npm,dst=/cache/npm,readonly" --mount "type=bind,src=$EVIDENCE_ROOT,dst=/evidence" --tmpfs "/work:rw,noexec,nosuid,nodev,size=$WORK_TMPFS_BYTES" --tmpfs "/tmp:rw,noexec,nosuid,nodev,size=$TMP_TMPFS_BYTES" --env "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" --env "NETWORK_PROBE_TIMEOUT_SECONDS=$NETWORK_PROBE_TIMEOUT_SECONDS" "$NODE_IMAGE" sh -ceu '
cp /src/package.json /src/package-lock.json /work/
if find /cache/npm -xdev ! \( -type d -o -type f \) -print -quit | grep -q .; then echo "unsupported npm cache object" >&2; exit 2; fi
cp -a /cache/npm /work/npm-cache
chmod -R u+w /work/npm-cache
export HOME=/work/home npm_config_logs_dir=/tmp/npm-logs
mkdir -p "$HOME" "$npm_config_logs_dir"
cd /work
npm ci --ignore-scripts --offline --no-audit --no-fund --no-update-notifier --cache /work/npm-cache
npm ls --all --json >/evidence/node-installed.json
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner --format=posix -C /work -cf /evidence/node-deps.tar node_modules
node - <<"NODE"
const fs = require("node:fs");
const net = require("node:net");
const timeoutSeconds = Number(process.env.NETWORK_PROBE_TIMEOUT_SECONDS);
const timeoutMilliseconds = timeoutSeconds * 1000;
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0 ||
!Number.isSafeInteger(timeoutMilliseconds) || timeoutMilliseconds < 1 ||
timeoutMilliseconds > 2147483647) {
throw new Error("signed network-probe timeout is outside the Node timer domain");
}
const socket = net.createConnection({
host: "192.0.2.1", port: 443, timeout: timeoutMilliseconds
});
let finished = false;
function pass(reason) {
if (finished) return;
finished = true;
fs.writeFileSync("/evidence/node-network-probe.json",
JSON.stringify({outcome: "PASS", reason, timeout_seconds: timeoutSeconds}) + "\n",
{flag: "wx", mode: 0o600});
socket.destroy();
}
socket.on("connect", () => process.exit(41));
socket.on("error", error => pass(error.code || "network_denied"));
socket.on("timeout", () => pass("timeout_without_connection"));
socket.on("close", () => { if (!finished) pass("closed_without_connection"); });
NODE
'
NODE_RC=$?
set -e
docker inspect "$BUILD_ID-node" --format '{{json .HostConfig}}' >"$EVIDENCE_ROOT/node-host-config.json"
docker inspect "$BUILD_ID-node" --format '{{.Image}}' >"$EVIDENCE_ROOT/node-runtime-image-id.txt"
docker rm "$BUILD_ID-node" >/dev/null
[[ "$NODE_RC" -eq 0 ]] || exit "$NODE_RC"Step 3: Install Python wheels offline and inventory that exact target
set +e
docker run --pull=never --name "$BUILD_ID-python" --network=none --read-only --cap-drop=ALL --security-opt=no-new-privileges --pids-limit="$PIDS_LIMIT" --memory="$MEMORY_BYTES" --cpu-period="$CPU_PERIOD" --cpu-quota="$CPU_QUOTA" --user "$(id -u):$(id -g)" --mount "type=bind,src=$SOURCE_ROOT,dst=/src,readonly" --mount "type=bind,src=$CACHE_ROOT/wheelhouse,dst=/wheelhouse,readonly" --mount "type=bind,src=$EVIDENCE_ROOT,dst=/evidence" --tmpfs "/work:rw,noexec,nosuid,nodev,size=$WORK_TMPFS_BYTES" --tmpfs "/tmp:rw,noexec,nosuid,nodev,size=$TMP_TMPFS_BYTES" --env "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" --env "NETWORK_PROBE_TIMEOUT_SECONDS=$NETWORK_PROBE_TIMEOUT_SECONDS" "$PYTHON_IMAGE" sh -ceu '
python -m pip install --disable-pip-version-check --no-input --no-cache-dir --no-index --find-links /wheelhouse --require-hashes --only-binary=:all: --requirement /src/requirements.lock --target /work/site-packages
PYTHONPATH=/work/site-packages python - <<"PY"
import importlib.metadata
import math
import os
import json
import socket
from pathlib import Path
try:
network_probe_timeout_seconds = float(os.environ["NETWORK_PROBE_TIMEOUT_SECONDS"])
except (KeyError, ValueError) as exc:
raise RuntimeError("signed network-probe timeout is absent or non-numeric") from exc
if not math.isfinite(network_probe_timeout_seconds) or network_probe_timeout_seconds <= 0:
raise RuntimeError("signed network-probe timeout must be finite and positive")
inventory = sorted(
(
{"name": item.metadata["Name"], "version": item.version}
for item in importlib.metadata.distributions(path=["/work/site-packages"])
),
key=lambda item: (item["name"].lower(), item["version"]),
)
Path("/evidence/python-installed.json").write_text(
json.dumps(inventory, sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
try:
socket.create_connection(("192.0.2.1", 443), timeout=network_probe_timeout_seconds)
except OSError as exc:
Path("/evidence/python-network-probe.json").write_text(
json.dumps({"outcome": "PASS", "reason": type(exc).__name__, "timeout_seconds": network_probe_timeout_seconds}) + "\n",
encoding="utf-8",
)
else:
raise SystemExit("network unexpectedly reachable")
PY
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner --format=posix -C /work -cf /evidence/python-deps.tar site-packages
'
PYTHON_RC=$?
set -e
docker inspect "$BUILD_ID-python" --format '{{json .HostConfig}}' >"$EVIDENCE_ROOT/python-host-config.json"
docker inspect "$BUILD_ID-python" --format '{{.Image}}' >"$EVIDENCE_ROOT/python-runtime-image-id.txt"
docker rm "$BUILD_ID-python" >/dev/null
[[ "$PYTHON_RC" -eq 0 ]] || exit "$PYTHON_RC"Step 4: Seal and independently verify the evidence
test -s "$EVIDENCE_ROOT/build-policy.json"
test -s "$EVIDENCE_ROOT/build-policy.sig"
test -s "$EVIDENCE_ROOT/cache.manifest.sha256"
test -s "$EVIDENCE_ROOT/cache.manifest.sig"
printf '%s\n' "$BUILD_POLICY_KEY" >"$EVIDENCE_ROOT/build-policy-key-locator.txt"
printf '%s\n' "$CACHE_MANIFEST_KEY" >"$EVIDENCE_ROOT/cache-key-locator.txt"
sha256sum package-lock.json requirements.lock "$CACHE_MANIFEST_SNAPSHOT" >"$EVIDENCE_ROOT/input-digests.txt"
(
cd "$EVIDENCE_ROOT"
find . -maxdepth 1 -type f ! -name evidence.manifest.sha256 -print0 | sort -z | xargs -0 sha256sum >evidence.manifest.sha256
)
cosign sign-blob --yes --key "env://DEPENDENCY_EVIDENCE_SIGNING_KEY" --bundle "$EVIDENCE_ROOT/evidence.manifest.sig" "$EVIDENCE_ROOT/evidence.manifest.sha256"Step 5: Independently verify and mint the only promotion receipt
# File: scripts/verify_dependency_layers.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077
: "${EVIDENCE_ROOT:?point to one immutable dependency-install evidence set}"
: "${BUILD_POLICY_VERIFY_KEY:?trusted verifier key, not supplied by the build job}"
: "${CACHE_MANIFEST_VERIFY_KEY:?trusted cache-promotion verifier key}"
: "${DEPENDENCY_EVIDENCE_VERIFY_KEY:?trusted build-evidence verifier key}"
: "${DEPENDENCY_PROMOTION_SIGNING_KEY:?separate release-verifier signing key}"
: "${DEPENDENCY_PROMOTION_VERIFY_KEY:?separate release-verifier public key for readback}"
SNAPSHOT_DIR=$(mktemp -d)
trap 'rm -rf -- "$SNAPSHOT_DIR"' EXIT
POLICY_SNAPSHOT="$SNAPSHOT_DIR/build-policy.json"
CACHE_MANIFEST_SNAPSHOT="$SNAPSHOT_DIR/cache.manifest.sha256"
EVIDENCE_MANIFEST_SNAPSHOT="$SNAPSHOT_DIR/evidence.manifest.sha256"
install -m 0600 -- "$EVIDENCE_ROOT/build-policy.json" "$POLICY_SNAPSHOT"
install -m 0600 -- "$EVIDENCE_ROOT/build-policy.sig" "$SNAPSHOT_DIR/build-policy.sig"
install -m 0600 -- "$EVIDENCE_ROOT/cache.manifest.sha256" "$CACHE_MANIFEST_SNAPSHOT"
install -m 0600 -- "$EVIDENCE_ROOT/cache.manifest.sig" "$SNAPSHOT_DIR/cache.manifest.sig"
install -m 0600 -- "$EVIDENCE_ROOT/evidence.manifest.sha256" "$EVIDENCE_MANIFEST_SNAPSHOT"
install -m 0600 -- "$EVIDENCE_ROOT/evidence.manifest.sig" "$SNAPSHOT_DIR/evidence.manifest.sig"
cosign verify-blob --key "$BUILD_POLICY_VERIFY_KEY" --bundle "$SNAPSHOT_DIR/build-policy.sig" "$POLICY_SNAPSHOT" >/dev/null
cosign verify-blob --key "$CACHE_MANIFEST_VERIFY_KEY" --bundle "$SNAPSHOT_DIR/cache.manifest.sig" "$CACHE_MANIFEST_SNAPSHOT" >/dev/null
cosign verify-blob --key "$DEPENDENCY_EVIDENCE_VERIFY_KEY" --bundle "$SNAPSHOT_DIR/evidence.manifest.sig" "$EVIDENCE_MANIFEST_SNAPSHOT" >/dev/null
(cd "$EVIDENCE_ROOT" && sha256sum --check --strict "$EVIDENCE_MANIFEST_SNAPSHOT")
PIDS=$(jq -er '.container_limits.pids' "$POLICY_SNAPSHOT")
MEMORY=$(jq -er '.container_limits.memory_bytes' "$POLICY_SNAPSHOT")
PERIOD=$(jq -er '.container_limits.cpu_period' "$POLICY_SNAPSHOT")
QUOTA=$(jq -er '.container_limits.cpu_quota' "$POLICY_SNAPSHOT")
WORK_BYTES=$(jq -er '.container_limits.work_tmpfs_bytes' "$POLICY_SNAPSHOT")
TMP_BYTES=$(jq -er '.container_limits.tmp_tmpfs_bytes' "$POLICY_SNAPSHOT")
PROBE_TIMEOUT=$(jq -er '.network_probe_timeout_seconds | select(type == "number" and . > 0)' "$POLICY_SNAPSHOT")
for ecosystem in node python; do
jq -e --argjson pids "$PIDS" --argjson memory "$MEMORY" --argjson period "$PERIOD" --argjson quota "$QUOTA" --arg work "rw,noexec,nosuid,nodev,size=$WORK_BYTES" --arg tmp "rw,noexec,nosuid,nodev,size=$TMP_BYTES" '.NetworkMode == "none" and .ReadonlyRootfs == true and (.CapDrop | index("ALL")) != null and (.SecurityOpt | index("no-new-privileges")) != null and .PidsLimit == $pids and .Memory == $memory and .CpuPeriod == $period and .CpuQuota == $quota and .Tmpfs["/work"] == $work and .Tmpfs["/tmp"] == $tmp' "$EVIDENCE_ROOT/$ecosystem-host-config.json" >/dev/null
jq -e --argjson timeout "$PROBE_TIMEOUT" '.outcome == "PASS" and .timeout_seconds == $timeout' "$EVIDENCE_ROOT/$ecosystem-network-probe.json" >/dev/null
test -s "$EVIDENCE_ROOT/$ecosystem-runtime-image-id.txt"
done
jq -e 'type == "object"' "$EVIDENCE_ROOT/node-installed.json" >/dev/null
jq -e 'type == "array"' "$EVIDENCE_ROOT/python-installed.json" >/dev/null
for layer in node-deps.tar python-deps.tar; do
test -s "$EVIDENCE_ROOT/$layer"
tar -tvf "$EVIDENCE_ROOT/$layer" | awk 'substr($1,1,1) !~ /^[-d]$/ { bad=1 } END { exit bad }'
tar -tf "$EVIDENCE_ROOT/$layer" | awk '/^\// || /(^|\/)\.\.($|\/)/ { bad=1 } END { exit bad }'
done
NODE_SHA=$(sha256sum "$EVIDENCE_ROOT/node-deps.tar" | cut -d' ' -f1)
PYTHON_SHA=$(sha256sum "$EVIDENCE_ROOT/python-deps.tar" | cut -d' ' -f1)
EVIDENCE_SHA=$(sha256sum "$EVIDENCE_MANIFEST_SNAPSHOT" | cut -d' ' -f1)
POLICY_SHA=$(sha256sum "$POLICY_SNAPSHOT" | cut -d' ' -f1)
jq -cn --arg node "$NODE_SHA" --arg python "$PYTHON_SHA" --arg evidence "$EVIDENCE_SHA" --arg policy "$POLICY_SHA" --arg policyVersion "$(jq -er '.policy_version' "$POLICY_SNAPSHOT")" '{schema_version:"aidefend.dependency-promotion.v1",control_id:"AID-H-022.001",outcome:"PASS",policy_version:$policyVersion,policy_sha256:$policy,evidence_manifest_sha256:$evidence,node_layer_sha256:$node,python_layer_sha256:$python}' >"$EVIDENCE_ROOT/promotion-receipt.json"
cosign sign-blob --yes --key "$DEPENDENCY_PROMOTION_SIGNING_KEY" --bundle "$EVIDENCE_ROOT/promotion-receipt.sig" "$EVIDENCE_ROOT/promotion-receipt.json"
cosign verify-blob --key "$DEPENDENCY_PROMOTION_VERIFY_KEY" --bundle "$EVIDENCE_ROOT/promotion-receipt.sig" "$EVIDENCE_ROOT/promotion-receipt.json" >/dev/nullEvidence: retain signed policy, cache-manifest signature and key locator, cache/evidence manifests, lockfile and cache digests, immutable image references and observed IDs, exact container host configuration, in-container inventories, deterministic dependency layers, network probes, CI job/release IDs, source revision, verifier build digest, signed promotion receipt, downstream extraction receipt, and complete-population reconciliation. Populate caches only in a separate reviewed quarantine-and-promotion zone; never make a writable cache or source tree available to this install job.
AID-H-022.001-G002
Install from the reviewed lock state
Use the ecosystem's frozen mode, such as npm ci --ignore-scripts, pnpm install --frozen-lockfile --ignore-scripts, or uv sync --frozen --no-install-project. Pin the package-manager version through Corepack or the immutable builder image. Before installation, verify the signed cache manifest and reconcile every lockfile artifact name, version, digest, and platform marker with exactly one offline cache object.
Prove that resolution did not drift
After installation, re-hash the lockfile, export the resolved package inventory, and compare it with the lock-derived expected inventory. A changed lockfile, implicit network resolution, missing digest, duplicate candidate, mutable version selector, or unexpected installed package is FAIL. Preserve the before/after lock digest, expected and observed inventories, cache-manifest digest, package-manager build, and independent replay receipt.
AID-H-022.001-G003
Make installation data-only
Run npm-family installers with lifecycle scripts disabled and Python installers against prebuilt wheels only, for example pip install --no-index --only-binary=:all: --require-hashes. Mount source trees, lockfiles, and promoted caches read-only; provide no compiler, shell startup hooks, package-manager plugin discovery path, cloud credentials, Docker socket, or writable host path inside the install container.
Exercise the negative boundary
Seed fixtures with preinstall/postinstall hooks, native addons, sdists, build-backend downloads, path dependencies, symlinks, and package-manager plugins. The job must deny them without executing payloads or making network calls. Retain process-execution telemetry, attempted package identity, network-denial evidence, container image digest, filesystem mounts, and an independently verified negative-test result.
AID-H-022.001-G004
Separate acquisition from trusted installation
An egress-enabled fetcher downloads candidates into quarantine and records source URL, registry identity, package coordinates, byte digest, signature or provenance result, and scanner outputs. A separate promotion identity verifies the complete candidate population against signed policy, rejects extra or missing files, and publishes a new immutable cache manifest plus content-addressed objects. The install job receives only that promoted snapshot and its trust-root-verifiable manifest.
Verify the handoff
Attempt cache substitution, manifest rollback, mixed promotion generations, writable-cache mutation, and inclusion of an unlisted object. The installer must fail before package parsing. Re-hash the consumed cache after the build and have the release verifier compare it with the promotion receipt. Acquisition or vetting decisions are inputs; this guidance passes only when the exact promoted snapshot is the read-only population actually consumed.
Open-source tools
- Docker Engine
- Podman
- npm
- pnpm
- Yarn
- Corepack
- pip
- pip-tools
- uv
- PyPA build
- twine
- Cosign
- jq
- Verdaccio (npm package staging/cache only; immutable promotion, offline export, signature verification, and sandbox enforcement remain external)
- devpi (Python package staging/cache only; immutable promotion, offline export, signature verification, and sandbox enforcement remain external)
Commercial / hosted tools
- JFrog Artifactory
- JFrog Xray
- Sonatype Repository Firewall
Threat mappings
MITRE ATLAS
- AML.T0011 User Execution
- AML.T0011.001 User Execution: Malicious Package
- AML.T0072 Cyber Communication Channel (no-network ephemeral install containers deny install-time callback channels)
- AML.T0119 Exploit Automated Artifact Processing Pipeline (dependency artifacts are installed only in a no-network ephemeral worker with install scripts and source builds denied)
- AML.T0025 Exfiltration via Cyber Means
- AML.T0010 AI Supply Chain Compromise
- AML.T0010.001 AI Supply Chain Compromise: AI Software
MAESTRO
- Lateral Movement (Cross-Layer)
- Supply Chain Attacks (Cross-Layer)
OWASP LLM Top 10 2026
- LLM04:2026 Supply Chain
OWASP ML Top 10 2023
- ML06:2023 AI Supply Chain Attacks
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities
- ASI05:2026 Unexpected Code Execution (RCE) (sandboxing contains malicious install script execution)
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-9.3.1 Malicious Package / Tool Injection
- AISubtech-9.3.3 Dependency Replacement / Rug Pull
- AITech-9.3 Dependency / Plugin Compromise
Google Secure AI Framework 2.0 - Risks
- N/A
Databricks AI Security Framework 3.0
- Algorithms 5.4: Malicious libraries
- Model 7.3: ML Supply chain vulnerabilities
- Agents - Core 13.11: Unexpected RCE and Code Attacks