# SBOM Preservation Receipt (`l3digital/sbom-preservation-receipt`) Actor

Experimental comparison of an SPDX 2.3 original and CycloneDX 1.6 conversion: supported facts, direct edges, changes and uncertainty. No conversion or acceptance certification.

- **URL**: https://apify.com/l3digital/sbom-preservation-receipt.md
- **Developed by:** [L3Digital](https://apify.com/l3digital) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.05 / useful sbom preservation receipt

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

## SBOM Preservation Receipt

Identify covered declarations that changed or disappeared during an SPDX 2.3 to CycloneDX 1.6 conversion. Supply the original and already-converted JSON artifacts; the receipt compares covered facts and dependency edges, localizes differences and unresolved correspondence, and gives a supplier-intake agent repairs or questions to review before handoff.

This experimental Actor requires the caller to acquire both artifacts. It does not convert or fetch files, scan vulnerabilities, log into a recipient system, or import an SBOM.

### Input

Supply `original` and `converted` as inline JSON objects. The following is a **minimal structural example**, not a complete standards-valid SBOM:

```json
{
  "original": {
    "spdxVersion": "SPDX-2.3",
    "packages": [
      {
        "SPDXID": "SPDXRef-alpha",
        "name": "alpha",
        "versionInfo": "1.0",
        "externalRefs": [
          {
            "referenceCategory": "PACKAGE-MANAGER",
            "referenceType": "purl",
            "referenceLocator": "pkg:generic/alpha@1.0"
          }
        ]
      }
    ]
  },
  "converted": {
    "bomFormat": "CycloneDX",
    "specVersion": "1.6",
    "components": [
      {
        "bom-ref": "alpha",
        "name": "alpha",
        "version": "1.0",
        "purl": "pkg:generic/alpha@1.0"
      }
    ]
  }
}
```

Under the contract, this pair matches by its unique exact PURL and compares three declared facts. SHA256 is `not_declared`; its absence is not preservation. There are no declared dependency edges in this example.

Eligible records have a unique nonempty source `SPDXID` or target `bom-ref`. Automatic correspondence requires eligible IDs and one exact PURL occurring once on each side. Source references must use exactly `PACKAGE-MANAGER` and `purl`. No name/version, CPE, substring or normalized-PURL fallback is used. A target `metadata.component` is included alongside top-level `components`.

When a PURL changes or is absent, you may add an explicit correspondence assertion:

```json
"mapping": [{"sourceId": "SPDXRef-alpha", "targetRef": "alpha"}]
```

This is an optional field excerpt to add to the request, not a standalone request. Each endpoint must have a unique nonempty ID in its inventory; mappings must be one-to-one. An invalid mapping refuses the request. An explicit mapping records your assertion of correspondence, not independently verified software identity. Missing or duplicate IDs and ambiguous PURLs remain visible as records that were not compared.

Use `{"demo":true}` for a fixed synthetic example without a customer event charge. Demo forbids the keys `original`, `converted` and `mapping`, even with null or empty values. A customer pair requires both document objects. URLs, archives, filesystem paths and executable inputs are not alternatives.

### What the receipt covers

For matched records, the receipt compares exact declared name, version and PURL strings, plus a single selected SHA256 digest on each side. Hexadecimal digest case is ignored; name/version/PURL case and whitespace are retained. Multiple selected PURLs or SHA256 entries are ambiguous, even if identical. Missing values are distinguished from empty values; malformed supported data is refused.

It compares source `DEPENDS_ON` and inverse `DEPENDENCY_OF` with explicit target `dependencies[].ref -> dependsOn[]` edges. Both endpoints must be eligible and mapped. It does not infer transitive dependencies. Missing target entries, absent `dependsOn` and explicit empty arrays remain distinct: a missing graph declaration does not establish that the software has no dependencies. Duplicate target entries with the same `ref` are ambiguous.

Fact and edge findings use `preserved`, `changed`, `removed`, `added`, `not_declared` or `not_compared` as applicable. A removed declaration describes the supplied pair; it does not prove removal of actual software or a software dependency. Unmatched records do not prove software addition or removal.

Nested components, services, `provides`, specialized relationships, external-document references, non-package elements, other hashes, licenses, CPEs, group, properties/extensions and other uncovered fields are inventoried as exclusions. Excluded subtrees are wholly unexamined. A target-only edge associated with a specialized source relationship is not compared rather than flattened into an addition. Exclusions identify paths and keys without copying their values.

### Read the result

A receipt uses coverage version `sbom-pair-1` and these typed fields:

| Field | How to use it |
| --- | --- |
| `sourceDigest`, `targetDigest` | Bind the full parsed inputs to this comparison. |
| `status`, `useful`, `hasUncertainty` | Read the outcome, whether any declared fact/edge was actually compared, and independent uncertainty. |
| `correspondence` | Inspect source/target IDs and paths, match status, `basis` (`exact_purl` or `explicit_mapping`), reason and any owner action. |
| `fieldFindings` | Inspect `field`, `status`, IDs, basis, `sourcePaths`/`targetPaths`, declared `sourceValue`/`targetValue`, reason and any owner action. |
| `dependencyFindings` | Inspect endpoint IDs/refs, status, evidence paths, mapping bases and `declarationState` (`missing`, `empty` or `present`) where applicable. |
| `coverage` | Check `coveredFields`, mapped/unpaired and fact/edge counts, plus `exclusions` and their paths, keys, reasons and actions. |
| `ownerActions` | Follow the collected record-specific repair or clarification actions. |

The result schemas reject additional properties in the report and its nested records. Only explicitly nullable fields, such as an unavailable ID, declared value or action, may be null. A refusal is a separate result with `status`, `useful=false`, `reason`, `path` and nullable `limit`/`observed`; it has no comparison groups.

| Status | Meaning |
| --- | --- |
| `covered_preserved` | Matched covered declarations agree, with no unresolved covered items or unpaired records. Exclusions remain visible. |
| `differences` | At least one covered declaration changed, was removed or was added. Uncertainty may also exist. |
| `inconclusive` | No definite difference, but correspondence or covered facts/edges remain unresolved, or no records matched. |
| `refused` | Invalid input, an invalid mapping or a resource limit prevents a complete receipt. The reason/path/limit result omits offending values. |

Always inspect `hasUncertainty` independently of `status`, then read coverage and owner actions. Exclusions alone do not set `hasUncertainty`. Actions identify evidence paths and specific repairs or confirmations, such as restoring a declared target version or confirming an intentional replacement. Paths are JSON Pointers into the supplied artifacts.

#### Selected output example

This selected excerpt comes from the retained synthetic owner QA changed-envelope case, not the input example above or a customer run. It shows one actual changed finding and its associated action; it is not the complete response. The full receipt also includes other findings, correspondence, coverage and exclusions.

```json
{
  "status": "differences",
  "useful": true,
  "hasUncertainty": false,
  "fieldFindings": [
    {
      "basis": "exact_purl",
      "field": "name",
      "ownerAction": "Restore target name for SPDXRef-alpha / alpha from 'alpha-renamed' to 'alpha', or confirm intentional replacement; check /original/packages/0 and /converted/metadata/component (covered declared fact differs).",
      "reason": "covered declared fact differs",
      "sourceId": "SPDXRef-alpha",
      "sourcePaths": [
        "/original/packages/0/name"
      ],
      "sourceValue": "alpha",
      "status": "changed",
      "targetPaths": [
        "/converted/metadata/component/name"
      ],
      "targetRef": "alpha",
      "targetValue": "alpha-renamed"
    }
  ],
  "ownerActions": [
    "Restore target name for SPDXRef-alpha / alpha from 'alpha-renamed' to 'alpha', or confirm intentional replacement; check /original/packages/0 and /converted/metadata/component (covered declared fact differs)."
  ]
}
```

Retrieve the complete `REPORT` from the run's default key-value store, or its identical default-dataset item, to inspect every finding and exclusion. Reuse that stored result instead of starting another run.

Document hashes bind the **full parsed document**, including excluded content. SHA256 uses UTF-8 JSON with sorted object keys, compact separators, direct Unicode and no nonfinite numbers. Object-key order and whitespace do not change these digests; array order does. These are not raw-file hashes or a claim of RFC canonicalization. Raw duplicate JSON keys and original formatting cannot be recovered after platform parsing.

The full receipt is written to the default key-value store at `REPORT`, followed by one identical item in the default dataset. The output tab links to both. Retrieve either stored result instead of starting another run. A receipt establishes neither full schema conformance, inventory truth, vulnerability/license meaning, compliance, lossless conversion nor recipient acceptance.

### Recover and retain this run's evidence

1. Use your authorized Apify caller to [start asynchronously](https://docs.apify.com/api/v2/actors-runs-post). Save the returned run `id` and a caller-generated request key in your own records before proceeding; retain `defaultKeyValueStoreId` and `defaultDatasetId` from the run response.
2. Check `GET /v2/actor-runs/RUN_ID` with that saved ID ([run API](https://docs.apify.com/api/v2/actor-run-get)). If a wait or retrieval times out, check the same run again. A failed or timed-out run can still have stored output; inspect it before deciding what to do.
3. Retrieve the authoritative full receipt with `GET /v2/key-value-stores/STORE_ID/records/REPORT`, using that run's `defaultKeyValueStoreId` as `STORE_ID` ([record API](https://docs.apify.com/api/v2/key-value-store-record-get)). Its equal dataset item is available at `GET /v2/datasets/DATASET_ID/items?format=json`, using `defaultDatasetId` ([dataset items API](https://docs.apify.com/api/v2/dataset-items-get)). Check `REPORT` during failure recovery even if dataset delivery failed.
4. Export the full typed JSON, parse it and verify the input binding and decision-required evidence before acting or cleaning up. Keep `coverageVersion`, `sourceDigest` and `targetDigest` with the receipt; these digests bind parsed documents, not raw files. Read `useful`, `hasUncertainty`, findings, `coverage` and exclusions separately before deciding which declarations need repair.

Do not silently POST again after a failed retrieval. If the initial start response was lost and no run ID is known, reconcile your authorized run history against your recorded request and input before considering a paid restart. A missing response does not prove no run started; this workflow promises no universal start idempotency.

A table or optional dataset field projection can support a limited notification, but it is not the full report. Retain the run/storage IDs, the identity and version/profile fields above, and relevant coverage; retrieve omitted detail from this same run before making a detailed decision.

#### Selective cleanup

After the run is terminal, no restart is planned and your exported evidence has been parsed and verified, you may remove authorized copies you no longer need. Use your saved exact storage IDs, never a latest-run shortcut or an assumed shared store. See Apify's [record deletion](https://docs.apify.com/api/v2/key-value-store-record-delete) and [dataset deletion](https://docs.apify.com/api/v2/dataset-delete) references for platform operations, which were not exercised in this Actor's QA: deleting `INPUT` leaves `REPORT`; deleting `REPORT` does not delete the dataset. Input records, REPORT records, dataset items and external copies are separate. Removing one does not erase logs, exports, saved task input, source files or customer copies, or guarantee provider-wide erasure.

### Limits

All boundaries are inclusive. Invalid or oversized input is refused rather than truncated; a complete receipt that cannot fit is refused without charging a partial result.

| Resource | Maximum |
| --- | --- |
| Source packages | 500 |
| Target records, including `metadata.component` | 500 |
| Explicit mappings | 500 |
| Raw source relationships | 2,000 |
| Raw target `dependsOn` entries | 2,000 |
| Each serialized document | 1,048,576 UTF-8 bytes (1 MiB) |
| Complete serialized request | 2,200,000 UTF-8 bytes |
| Request JSON value/container nodes | 50,000 |
| Request depth, root at depth 1 | 32 |
| Any string, including keys | 4,096 Unicode codepoints |
| Complete serialized receipt | 4,194,304 compact UTF-8 bytes (4 MiB) |

Byte limits use the parsed-value serialization recipe described above. Repeated raw edges still count toward raw limits. Excluded content also counts toward byte, node, string and depth limits. The initial platform envelope is 256 MiB memory and a 60-second timeout. It is not a guarantee that every allowed input completes.

### Price and failure behavior

The current price is **$0.05 per complete useful receipt**, using one `report-produced` event after both output writes complete. The pricing contract excludes automatic Actor-start and dataset-item fees; platform operating cost is separate and is not passed through as an additional agentic usage charge.

`useful=true` requires at least one actually compared declared covered fact or edge. Preserved, changed, removed or added findings can qualify. Unresolved-only and all-`not_declared` receipts, demo and refusals have no customer event charge. **A partially compared `inconclusive` receipt can be useful and chargeable.** An unpriced run may store a useful receipt without a fee. Missing paid-event capacity refuses a priced request before potentially billable computation; demo bypasses this capacity check.

Storage failure stops before charging; a failed dataset write can still leave `REPORT` available. Only an integer `charged_count` of 1 confirms SDK event acceptance, which does not establish customer payment or settlement. A charge error after storage can leave a receipt available while billing remains unresolved. Inspect the existing report and provider record before rerunning. There is no automatic retry or exactly-once guarantee across crashes or independent reruns; a rerun is an independent request. Report content alone proves no payment.

### Data handling and alternatives

Send only artifacts you are permitted to transmit to Apify. Normal platform input/output retention applies: uploaded originals may persist. Covered IDs, names, versions, PURLs and digest values intentionally appear in the receipt. Excluded values are omitted from the receipt but may still be present in retained Actor input. No ephemeral processing, automatic deletion or redaction of the uploaded originals is promised.

Original-format generation where possible, independent SBOM validation, native sbomlyze comparison and authorized recipient import form a strong complete alternative. A restricted HTTP/MCP caller may find one hosted receipt convenient, but comparative savings, quality superiority, willingness to pay and external demand have not been measured.

# Actor input Schema

## `demo` (type: `boolean`):

Demo supplies a fixed synthetic pair. It forbids original, converted and mapping keys, even empty or null, and is always uncharged.

## `original` (type: `object`):

Required when demo is false. Inline parsed JSON only; no URL or filesystem input.

## `converted` (type: `object`):

Required when demo is false. Already-converted inline JSON; this Actor does not convert artifacts.

## `mapping` (type: `array`):

Optional caller-asserted mapping of eligible unique IDs; otherwise only unique exact declared PURLs can match.

## Actor input object example

```json
{
  "demo": true
}
```

# Actor output Schema

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

No description

## `report` (type: `string`):

No description

# 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 = {
    "demo": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("l3digital/sbom-preservation-receipt").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 = { "demo": True }

# Run the Actor and wait for it to finish
run = client.actor("l3digital/sbom-preservation-receipt").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 '{
  "demo": true
}' |
apify call l3digital/sbom-preservation-receipt --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,l3digital/sbom-preservation-receipt"
        }
    }
}
```

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/SmXUJKMc6pJKjwZKP/builds/B2Vm4v5FAp106qygm/openapi.json
