# Package Provenance Attestation (`kingii98/package-provenance-attestation-and-transparency-log`) Actor

Prove that a published npm or PyPI artifact carries a signed provenance attestation, that the attestation subject digest equals the registry artifact digest, and that the public transparency log holds the entry.

- **URL**: https://apify.com/kingii98/package-provenance-attestation-and-transparency-log.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 run\_starteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Package Provenance Attestation and Transparency-Log Verification Gate

Give this Actor the package addresses of a lockfile change. It reads the
public npm and PyPI registries, and returns one row for each package: whether
a signed provenance attestation exists, the digest the registry publishes for
the artifact, the digest the attestation signs, whether the two are the same,
the signer identity out of the signing certificate, the source repository and
commit, and the public transparency-log entry that holds the record.

A package manifest flag that says `provenance: true` is a statement about
metadata. This Actor answers a different question: **is the artifact in the
registry today the artifact that the named build produced?**

### Who this is for

A platform engineer or an application-security engineer who promotes
third-party packages into a build, and who must show an auditor that each
dependency comes from a named source repository and a named workflow. Run it
on each lockfile change, and on a schedule, because a transparency-log entry
or a registry dist record can change after the first install.

### What the Actor does

1. Reads 1 to 500 package addresses, each written as `ecosystem:name@version`.
2. Reads the registry record of the published artifact, and takes the digest
   the registry publishes for it.
3. Calls the registry attestation endpoint for the same artifact.
4. Decodes the in-toto statement inside the attestation bundle, and takes the
   subject name and the subject digest.
5. Compares the two digests. This is the gate.
6. Reads the Sigstore (Fulcio) signing certificate in the bundle, and takes
   the OIDC issuer, the workflow path, the workflow ref, the runner
   environment, the source repository and the source commit.
7. Resolves the transparency-log index of the bundle against the public Rekor
   log, and records the entry identifier and the log timestamp.
8. Applies the gate rules, and writes one dataset row for each package address
   and one run-summary record.

The Actor makes HTTP GET calls to three fixed public hosts. It uses no
account, no cookie, no key, no browser, no proxy, no language model and no
paid API.

### Endpoints

| Host | Purpose |
| --- | --- |
| `registry.npmjs.org` | `"/{name}/{version}"` for the dist record, and `/-/npm/v1/attestations/{name}@{version}` for the attestation bundle. |
| `pypi.org` | `/pypi/{name}/{version}/json` for the file record, and `/integrity/{name}/{version}/{filename}/provenance` for the attestation bundle. |
| `rekor.sigstore.dev` | `/api/v1/log/entries?logIndex={index}` to resolve the transparency-log entry. |

No buyer-supplied URL is fetched. A package address is split and validated
before it reaches a URL, so a package string cannot steer a request at another
host.

### Input

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `packages` | array of strings | four-package sample set | 1 to 500 addresses, each `ecosystem:name@version`. |
| `requireAttestation` | boolean | `true` | A package with no attestation counts as a gate failure. |
| `allowedSourceRepos` | array of strings | `[]` | Optional `owner/repo` allow list. An attestation from a repository outside a non-empty list is a gate failure. |
| `pypiArtifact` | string | `sdist` | Which PyPI release file to verify: `sdist` or `wheel`. |

Example:

```json
{
  "packages": [
    "npm:sigstore@2.3.1",
    "npm:@sigstore/bundle@2.3.2",
    "pypi:sigstore@3.6.5"
  ],
  "requireAttestation": true,
  "allowedSourceRepos": ["sigstore/sigstore-js", "sigstore/sigstore-python"],
  "pypiArtifact": "sdist"
}
```

#### Package addresses

- npm: `npm:sigstore@2.3.1`, and a scoped name as `npm:@sigstore/bundle@2.3.2`.
- PyPI: `pypi:sigstore@3.6.5`. The project name is normalized as PEP 503
  states, so `pypi:Zope.Interface@5.5.2` reads the same project as
  `pypi:zope-interface@5.5.2`.
- `node`, `nodejs`, `py`, `pip` and `python` are accepted as ecosystem names.
- A malformed address is **not** an error that stops the run. It becomes a row
  with the verdict `error` and a reason.
- Two input rows that name the same package share one lookup, and share one
  `package_verified` charge. Both rows still appear in the dataset.

#### The PyPI file

PyPI attests each release file on its own, and a release can hold many wheels.
The run verifies one file, and the row names it in `artifactFilename`. The
`pypiArtifact` field chooses the kind; the other kind answers when the
preferred kind is absent.

### Output

One dataset row for each package address, plus one run-summary record.

| Field | Meaning |
| --- | --- |
| `recordType` | `package` or `summary`. |
| `inputSpec`, `ecosystem`, `package`, `version` | The address as given, and its parts. |
| `artifactFilename`, `artifactUrl` | The published file this row checked. |
| `attestationPresent` | Whether the registry returned an attestation bundle. |
| `attestationDeclaredByRegistry` | Whether the registry record itself claims an attestation. A `true` here with `attestationPresent: false` is the metadata-flag gap. The value is `null` for a PyPI file, because the PyPI file record does not always carry the provenance link even when a bundle exists. |
| `predicateType` | The attestation predicate, for example `https://slsa.dev/provenance/v1`. |
| `registryDigestAlgorithm`, `registryArtifactDigest` | The digest the registry publishes (npm: `sha512` from the dist integrity; PyPI: `sha256` from the file record). |
| `attestationSubjectName`, `attestationSubjectDigest` | The subject the attestation signs. |
| `digestMatch` | `pass`, `fail` or `not_checked`. |
| `signerIdentity`, `signerOidcIssuer`, `signerWorkflowPath`, `signerWorkflowRef`, `runnerEnvironment` | Read out of the Fulcio signing certificate. |
| `builderId`, `publisher` | The builder the statement names, and the publisher PyPI names. |
| `sourceRepo`, `sourceRepoUrl`, `sourceCommitSha` | The build source. |
| `repoAllowed` | `true`, `false`, or `null` when no allow list is set. |
| `transparencyLogEntryId`, `transparencyLogIndex`, `transparencyLogTimestamp`, `transparencyLogUrl`, `transparencyLogStatus` | The public log record. |
| `verdict` | `pass`, `fail`, `not_attested` or `error`. |
| `failureReason` | Why the row is not a pass. Empty on a pass. |
| `gateFailure` | Whether this row breaks the gate under the run's settings. |

The summary record carries `gateVerdict` (`PASS` or `FAIL`), `checkedAt`, and
the counts: `packageCount`, `uniquePackageCount`, `verifiedCount`,
`passCount`, `failCount`, `notAttestedCount`, `errorCount`, `invalidCount`,
`gateFailureCount`, `digestMismatchCount`, `repoRejectedCount`,
`logEntriesResolved` and `registryCalls`.

### Verdict rules

| Verdict | When |
| --- | --- |
| `pass` | An attestation exists, its subject digest equals the registry artifact digest, its subject names this artifact, the source repository is allowed, and the public log holds the entry. |
| `fail` | The digest differs, the two digests cannot be compared, the subject names another artifact, the source repository is outside the allow list, or the public log holds no entry at the index the bundle states. |
| `not_attested` | The registry publishes no attestation for the artifact. |
| `error` | The address is malformed, or the registry record could not be read. |

`gateFailure` applies the run's policy to the verdict: a `fail` and an `error`
always break the gate; a `not_attested` breaks the gate only when
`requireAttestation` is true. A transparency-log lookup that fails on the
network (`transparencyLogStatus: ERROR`) is recorded, and does not change the
verdict; a log that answers and holds no entry does.

**A failed gate is a result, not a malfunction.** The run always ends with the
status SUCCEEDED, and the verdict is in the dataset and in the run status
message. Reserve a failed run for a real malfunction.

### What this Actor does not do

- It does not check the signature arithmetic, and it does not walk the
  certificate chain to a trust root. It checks the binding the buyer cannot
  see from the registry page: attestation exists, subject digest equals the
  published digest, the named signer, and the public log entry.
- It does not download the artifact, so it does not recompute the digest from
  the file bytes. It compares the digest the registry publishes with the
  digest the attestation signs.
- It reads no private registry, and takes no credential.

### Pricing (pay per event)

| Event | Unit | Price | Counted |
| --- | --- | --- | --- |
| `run_started` | one run | $0.015 | Once, after the input is accepted. |
| `package_verified` | one package | $0.008 | Once for each unique package address the Actor sent to a registry. A malformed address is never charged, and a duplicate address is charged once. |
| `transparency_log_entry_resolved` | one attestation | $0.004 | Once for each package whose attestation carried a log index that the public log answered. A package with no attestation never reaches a log lookup. |

A 500-package lockfile in which every package is attested costs
`0.015 + 500 × 0.008 + 500 × 0.004 = $6.02`.

### Bounds

| Bound | Value |
| --- | --- |
| Packages for each run | 500 |
| Package address length | 300 characters |
| Allowed repositories | 200 |
| HTTP requests in flight | 6 |
| Request timeout | 25 s |
| Response body | 8 MB |
| Certificate body | 64 KB |

### Local use

```bash
uv sync
uv run pytest
uv run ruff check .
apify run --input-file .actor/default_input.json
```

### Default input

`.actor/default_input.json` holds four stable public packages: two that carry
a provenance attestation, and two that do not. The default run therefore ends
with the gate verdict `FAIL` and the status SUCCEEDED, which is what a gate
with `requireAttestation: true` is supposed to report for an unattested
dependency. The fixture completes well inside five minutes.

# Actor input Schema

## `packages` (type: `array`):

1 to 500 package addresses, each written as `ecosystem:name@version`. The ecosystem is `npm` or `pypi`, for example `npm:sigstore@2.3.1`, `npm:@sigstore/bundle@2.3.2` or `pypi:sigstore@3.6.5`. A malformed address becomes a row with a reason, and does not stop the run.

## `requireAttestation` (type: `boolean`):

Count a package that carries no provenance attestation as a gate failure. Turn this off to inventory the attestation state of a lockfile before the gate is enforced.

## `allowedSourceRepos` (type: `array`):

Optional list of `owner/repo` values, for example `sigstore/sigstore-js`. When the list holds one or more entries, an attestation whose source repository is outside the list is a gate failure. An empty list accepts any source repository.

## `pypiArtifact` (type: `string`):

PyPI attests each release file on its own, so the gate names the file it checked. `sdist` verifies the source distribution, `wheel` verifies the first wheel by file name. The other kind answers when the preferred kind is absent.

## Actor input object example

```json
{
  "packages": [
    "npm:sigstore@2.3.1",
    "npm:left-pad@1.3.0",
    "pypi:sigstore@3.6.5",
    "pypi:packaging@24.2"
  ],
  "requireAttestation": true,
  "allowedSourceRepos": [],
  "pypiArtifact": "sdist"
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row for each package address, plus one run-summary record that carries the gate verdict and the counts.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "packages": [
        "npm:sigstore@2.3.1",
        "npm:left-pad@1.3.0",
        "pypi:sigstore@3.6.5",
        "pypi:packaging@24.2"
    ],
    "requireAttestation": true,
    "allowedSourceRepos": [],
    "pypiArtifact": "sdist"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/package-provenance-attestation-and-transparency-log").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "packages": [
        "npm:sigstore@2.3.1",
        "npm:left-pad@1.3.0",
        "pypi:sigstore@3.6.5",
        "pypi:packaging@24.2",
    ],
    "requireAttestation": True,
    "allowedSourceRepos": [],
    "pypiArtifact": "sdist",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/package-provenance-attestation-and-transparency-log").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "packages": [
    "npm:sigstore@2.3.1",
    "npm:left-pad@1.3.0",
    "pypi:sigstore@3.6.5",
    "pypi:packaging@24.2"
  ],
  "requireAttestation": true,
  "allowedSourceRepos": [],
  "pypiArtifact": "sdist"
}' |
apify call kingii98/package-provenance-attestation-and-transparency-log --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/package-provenance-attestation-and-transparency-log"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/XasD58UFC6dj0it5y/builds/lxPCMMCZbx30Xw0p3/openapi.json
