pip-install attestor
Captures the Python package environment that exists after a build step by shelling out to pip, then statically analyses installed code, setup.py, pyproject.toml, and pickle files, and queries PyPI for PEP 740 provenance bundles.
| Name | pip-install |
|---|---|
| Predicate type | https://aflock.ai/attestations/pip-install/v0.1 |
| Lifecycle | postproduct |
| Default binary? | No |
| Category | dependency-resolve (primary) |
| Recommended trace | full — benefits from full eBPF trace |
| Auto-attaches when |
|
The facts in this box are generated from the cilock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.
What it captures
The top-level predicate has eight JSON fields:
pipVersion— output ofpip --version, or"unknown"if the binary is missing.pythonVersion— output ofpython3 --version.packages— array ofPackageInfo(see below), one per row ofpip list --format=json.setupPyAnalysis— array ofSetupPyAnalysisrecords found by walking the working directory and/tmp/pip-{download,install,build}.totalInstalled—len(packages).installedFileAnalysis— singleInstalledFileAnalysisfrom walkingsite.getsitepackages()[0].pyprojectAnalysis— array of build-backend records parsed from anypyproject.tomlunder/tmp/pip-downloador/tmp/pip-build.pep740Verification— array ofPEP740Status, one per installed package (excludingpip,setuptools,wheel), each fetched frompypi.org.
Each PackageInfo carries name, version, installType, location, requires, requiredBy, homePage, author, license, hasSetupPy, hasCmdClass, and installer. The descriptive fields (location, requires, requiredBy, homePage, author, license) are parsed line-by-line from pip show <name>. The provenance fields come from the installed package's on-disk layout, not from pip show: installer is read verbatim from <dist-info>/INSTALLER (e.g. pip); installType is editable (an .egg-link/.egg-info marker), wheel, or sdist (resolved from <dist-info>/direct_url.json, falling back to whether a build-time setup.py was executed); hasSetupPy/hasCmdClass are set when a setup.py correlated to this package (by canonical name + project directory) was found in the build cache and, for hasCmdClass, referenced cmdclass.
InstalledFileAnalysis records suspiciousImports (sys.meta_path, atexit.register, codecs.register references inside any __init__.py), subprocessInInit, networkInInit, every .pkl/.pickle/.pt file under site-packages (pickleFiles), a pickleAnalysis disassembly (capped at the first 50 files) produced by an embedded python3 -c "import pickletools..." script that flags REDUCE, GLOBAL, STACK_GLOBAL, INST opcodes, all .pth files, and a totalPyFiles count.
Each installed package is also emitted as an in-toto subject pip://<name>@<version> digested over the literal bytes <name>==<version> (SHA-256). That digest is not a hash of the wheel — it is only an identifier.
When to use
After a Python build or install step, to record what actually ended up on disk and to surface a few classes of supply-chain red flag in the same predicate. Pair with lockfiles (pre-material pin) and sbom (resolved tree) for full coverage; this attestor is the only one that observes the post-install state of site-packages.
Flags
None. The package exposes a WithTargetPackage Option internally, but it is not wired to any CLI flag.
Output shape
{
"pipVersion": "pip 24.0 from /usr/lib/python3.12/site-packages/pip (python 3.12)",
"pythonVersion": "Python 3.12.3",
"totalInstalled": 1,
"packages": [
{
"name": "requests",
"version": "2.32.3",
"location": "/usr/lib/python3.12/site-packages",
"requires": ["charset-normalizer", "idna", "urllib3", "certifi"],
"homePage": "https://requests.readthedocs.io",
"author": "Kenneth Reitz",
"license": "Apache-2.0"
}
],
"pep740Verification": [
{
"package": "requests",
"version": "2.32.3",
"hasAttestation": true,
"attestationUrl": "https://pypi.org/integrity/requests/2.32.3/requests-2.32.3-py3-none-any.whl/provenance",
"publisherKind": "GitHub",
"repository": "psf/requests",
"workflow": "publish.yml"
}
]
}
Gotchas
- Requires
pipandpython3onPATH. Ifpip listfails the attestor logs and returnsnil— you get an attestation with emptypackagesand zerototalInstalled, not a failure. - Does not run
pip installitself, does not read arequirements.txtorpip freezeoutput, and does not scan declared products. It snapshots whatever is installed in the active Python environment at the timeAttestruns, so virtualenv activation must happen before the step. setupPyAnalysisandpyprojectAnalysisonly look under the working directory and three hard-coded/tmp/pip-*paths; if pip's build cache lives elsewhere (e.g. inside a container withTMPDIRoverride) nothing is found.- The pickle scanner shells out to
python3 -c '...pickletools...'per file and is capped at 50 pickles per attestation.IsSafeis a heuristic — absence ofREDUCE/GLOBAL/INSTdoes not prove safety. - PEP 740 lookup hits
pypi.org/pypi/<pkg>/<ver>/jsonandpypi.org/integrity/.../provenanceover a 5 s HTTP client per package, serially. For an environment with hundreds of packages this dominates wall time, and offline runs silently returnhasAttestation: falsefor every package. - Bundled but not default: even though
pip-installregisters correctly and appears incilock attestors list, it is absent fromoptions.DefaultAttestors, so you must pass-a pip-installexplicitly or it will never run.
CLI example
Real pip install against PyPI inside a virtualenv. The attestor reads pip list --format=json, walks site-packages, and checks PEP 740 attestations for every installed package.
python3 -m venv .venv && source .venv/bin/activate
cilock run --step pip-install-real \
--signer-file-key-path key.pem --outfile attestation.json \
--attestations pip-install,environment,git \
-- pip install --quiet requests httpx
Validated against real PyPI: 10 packages, 9 had valid PEP 740 attestations from GitHub-issued Sigstore certs. See the full real-data example at https://github.com/aflock-ai/attestor-compliance-examples/tree/main/11-pip-install.
See also
- Catalog row
lockfiles,sbom- Upstream: witness/pip-install.md