# Third-Party Script Integrity Pin and Content Drift Gate (`kingii98/third-party-script-integrity-pin-and-content-drift-gate`) Actor

Reads the served HTML of each page, lists every external script and stylesheet, and judges each one against your origin allowlist, your integrity policy and a rule that rejects mutable CDN paths such as 'latest', 'main' or a bare major version. It then fe

- **URL**: https://apify.com/kingii98/third-party-script-integrity-pin-and-content-drift-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Third-Party Script Integrity Pin and Content Drift Gate

Show that no third-party script on a production page can change without notice.

The Actor reads the **served HTML** of each page that you give it. For every
external script and stylesheet it judges the origin, the Subresource Integrity
attribute and the shape of the URL, and it then fetches the asset and stores the
SHA-256 hash of the bytes. On the next run it compares that hash again, so a CDN
file that changed under a fixed URL becomes a **FAIL**, even when you deployed
nothing.

HTTP only. No browser, no proxy, no external database, no paid API.

### What an inventory cannot see

Two things break a third-party script policy:

1. The markup is wrong. A script comes from a host that nobody approved, it has
   no integrity hash, or its URL points at `latest`, at a branch name, or at a
   bare major version. Any of these lets the file change.
2. The bytes changed. The URL is fixed, your code did not change, and the file
   behind the URL is a different file today.

An inventory Actor reports the first kind. Only a stored hash reports the second
kind, and that is the point of this Actor.

### Limit that you must know

The Actor reads the raw HTML that the server sends. **It cannot see a script
that another script injects at run time**, because it does not run JavaScript
and it does not open a browser. A tag manager that writes more tags is
invisible here.

The page record holds the field `belowExternalScriptFloor`. It is `true` when
the served HTML holds fewer than two external scripts. If that field is `true`
on most of your pages, the raw-HTML method misses the scripts that matter on
your site, and this Actor is the wrong tool for it.

### Input

Every field has a default, so a run with empty input `{}` works.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `urls` | array | two public demo pages | 1 to 200 public page URLs. The Actor does not crawl. |
| `allowedScriptOrigins` | array | four public CDNs | Hosts that may serve code. A host, an origin or a `*.example.com` wildcard. |
| `integrityPolicy` | string | `required` | `required`, `warn` or `off` for a missing integrity attribute. |
| `allowedHashAlgorithms` | array | `sha384`, `sha512` | SRI algorithms that your policy accepts. |
| `requireCrossorigin` | boolean | `true` | Warn when a cross-origin tag has integrity but no `crossorigin`. |
| `mutablePathTokens` | array | `latest`, `main`, `master`, … | Words that name a moving target. |
| `rejectBareMajorVersion` | boolean | `true` | Treat `/v3/` and `pkg@3` as mutable. |
| `checks` | array | all seven | The checks that decide the gate. |
| `includeSameOrigin` | boolean | `false` | Also judge and hash your own files. |
| `hashAssets` | boolean | `true` | Fetch and hash each asset. Off gives a markup-only audit. |
| `maxAssetsPerPage` | integer | 40 | Cap on the references judged on one page. |
| `maxAssets` | integer | 200 | Cap on the assets hashed in one run. |
| `maxAssetBytes` | integer | 2097152 | Cap on the bytes read from one asset. |
| `maxHtmlBytes` | integer | 2097152 | Cap on the bytes read from one page. |
| `maxRedirects` | integer | 5 | Hop limit for each fetch. |
| `timeoutSeconds` | integer | 20 | Timeout for one request. |
| `concurrency` | integer | 4 | Requests at the same time. |
| `stateStoreName` | string | `script-asset-hashes` | Named key-value store that holds the last accepted hash. |
| `updateState` | boolean | `true` | Write this run's hashes into the store. |
| `userAgent` | string | `ScriptIntegrityPinGate/0.1 …` | ASCII only. |

### Checks

| Check | Status when it is broken | What it means |
| --- | --- | --- |
| `https_scheme` | FAIL | The asset is loaded over plain HTTP, so any network hop can replace it. |
| `origin_allowlist` | FAIL | The host is not on your allowlist. With an empty allowlist this becomes a WARN. |
| `integrity_present` | FAIL (or WARN) | A cross-origin tag has no SRI hash, so the browser accepts any bytes. |
| `integrity_algorithm` | WARN | The SRI hash uses an algorithm outside your allowed list. |
| `crossorigin_attribute` | WARN | A cross-origin tag has integrity but no `crossorigin`, so the browser cannot check the hash. |
| `immutable_path` | FAIL | The URL names `latest`, a branch, a bare major version or a minor-only version. WARN when the URL holds no version at all. |
| `content_drift` | FAIL | The bytes behind this fixed URL are not the bytes of the last run. |

A page verdict is the worst verdict of its references: **PASS**, **WARN** or
**FAIL**. The run gate passes when no page fails and no page was blocked.

#### How the URL pin is read

The Actor reads the path, the `package@version` part and the version query
value of each URL, in this order:

1. A moving word (`latest`, `main`, …) in a path segment or a version query
   value. The words `latest`, `current`, `stable`, `edge`, `canary` and
   `nightly` also count inside a file name, such as `jquery-latest.min.js`.
2. An exact release (`5.3.2`, `jquery-4.0.0.min.js`) or a commit or content
   hash. This is an immutable pin.
3. A bare major version (`/v3/`, `pkg@3`) or a minor-only version (`pkg@1.2`).
   This is mutable, unless `rejectBareMajorVersion` is off.
4. A build hash inside the file name.
5. Nothing of the above: the URL is `unversioned`, which is a WARN.

A file name such as `main.js` is **not** read as the branch `main`, because that
would give a false alarm on every ordinary site.

### Output

Three record types go into the dataset.

**Page record** (`recordType: "page"`): the page URL, the final URL, the HTTP
status, the counts of external and same-origin scripts and stylesheets, the
count of inline scripts, and an `assets` array. Each entry of that array holds
the source URL, the reference type with its `kind`, the origin classification,
the integrity attribute and its algorithms, the `crossorigin` attribute, the pin
style with the mutable-path finding, the SHA-256 hash and the drift state, one
status for every check, and a verdict of PASS, WARN or FAIL.

A `<link rel="modulepreload">` element, and a `<link rel="preload" as="script">`
element, both pull JavaScript that runs with the same trust as a plain `<script>`
tag. Both therefore count as **scripts**, in the `kind` field and in
`externalScripts`. A stylesheet, and a `<link rel="preload" as="style">`
element, count as style.

**Asset record** (`recordType: "asset"`): one record for each external asset
URL. It holds the SHA-256 hash of the fetched bytes, the byte size, the hash of
the last run, and a drift state of `NEW`, `UNCHANGED` or
`CHANGED_UNDER_SAME_URL`. An asset that could not be fetched, or one that is
larger than `maxAssetBytes`, gets the state `NOT_HASHED` and a WARN.

**Summary record** (`recordType: "summary"`): the machine-readable verdict for
your CI job. Read `gatePass` (boolean), `status` (`OK` or `GATE_FAIL`),
`assetsChanged` (the count of assets that changed under a fixed URL) and
`changedAssetUrls`.

A failed gate is **not** a failed run. The run ends SUCCEEDED, and your CI job
reads `gatePass` from the summary record.

### State

A named key-value store, `stateStoreName`, holds one record for each asset URL
with the last accepted hash. This is what makes the drift state possible. No
external database is needed.

Set `updateState` to `false` for a dry run that must not accept a change.

### Use in CI

Run the Actor in your pre-deploy job and once a day against production. The
daily run is the one that matters: a CDN file can change under a fixed URL at
any time, and no deploy of your own code takes place.

### Pricing (pay per event)

| Event | Unit | Counted |
| --- | --- | --- |
| `run-started` | one run | Once, after the input is read and found valid. |
| `page-scanned` | one page scanned | Once for each page whose HTML was fetched and parsed. A refused or unreachable page is not charged. |
| `asset-hashed` | one external asset fetched and hashed | Once for each asset that was fetched in full and hashed. One asset URL that many pages share is fetched once and charged once. An asset that failed to load, or one above `maxAssetBytes`, is not charged. |

Set `hashAssets` to `false` for a markup-only audit that charges no
`asset-hashed` event.

### Safety

Every page URL, every asset URL and every redirect hop is checked before a
request goes out. Loopback, private and reserved addresses are refused, and so
are URLs that carry credentials. A refused target is a dataset record, not a
failed run.

### Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `urls` (type: `array`):

1 to 200 public page URLs. The Actor reads the served HTML of each page. It does not crawl the site, and it does not run JavaScript. Private, loopback and reserved addresses are refused, and so are URLs that hold credentials.

## `allowedScriptOrigins` (type: `array`):

Hosts that may serve a script or a stylesheet to your pages. Write a host (cdn.example.com), an origin (https://cdn.example.com) or a wildcard (\*.example.com). The page origin is always allowed. An empty list turns the origin check into a warning, because there is then nothing to judge against.

## `integrityPolicy` (type: `string`):

How hard the Actor is on a cross-origin tag that carries no integrity attribute.

## `allowedHashAlgorithms` (type: `array`):

The SRI hash algorithms that your policy accepts. A hash outside this list is a warning, because it is weaker than the policy asks for.

## `requireCrossorigin` (type: `boolean`):

A cross-origin tag that carries integrity also needs crossorigin, or the browser cannot check the hash. Turn this off to stop the warning.

## `mutablePathTokens` (type: `array`):

Words that name a moving target instead of one release. A path segment or a version query value that holds one of these words fails the immutable\_path check. The words 'latest', 'current', 'stable', 'edge', 'canary' and 'nightly' are also matched inside a file name, such as jquery-latest.min.js.

## `rejectBareMajorVersion` (type: `boolean`):

Treat a path such as /v3/ or /npm/package@3/ as mutable, because the file behind it changes with every patch release. A minor-only path such as @1.2 is treated the same way.

## `checks` (type: `array`):

The checks that decide the gate. An empty list runs every check.

## `includeSameOrigin` (type: `boolean`):

Also list and hash the scripts and stylesheets that your own origin serves. Your own files change with every deploy, so this is off by default.

## `hashAssets` (type: `boolean`):

Fetch every listed asset in full, hash the bytes with SHA-256 and compare the hash with the last run. Turn this off for a markup-only audit that charges no asset event.

## `maxAssetsPerPage` (type: `integer`):

Hard cap on the references judged on one page. References above the cap are counted in the page record and are not judged.

## `maxAssets` (type: `integer`):

Hard cap on the assets fetched and hashed in one run. One asset URL that many pages share is fetched once and charged once.

## `maxAssetBytes` (type: `integer`):

Hard cap on the bytes read from one asset. A larger asset is reported without a hash, because a hash of a part of the file cannot prove that the whole file is unchanged.

## `maxHtmlBytes` (type: `integer`):

Hard cap on the bytes read from one page. A larger document is cut, and the references in the part that was read are still judged.

## `maxRedirects` (type: `integer`):

Hop limit for one page fetch and for one asset fetch. Every hop passes the private-address guard.

## `timeoutSeconds` (type: `integer`):

Timeout for one HTTP request, both for a page fetch and for an asset fetch.

## `concurrency` (type: `integer`):

How many page or asset requests run at the same time. Keep this low, so your site and your CDN are not put under load.

## `stateStoreName` (type: `string`):

Name of the named key-value store that keeps the last accepted SHA-256 hash of each asset URL. Use one name for each environment, so the drift state compares the correct history.

## `updateState` (type: `boolean`):

Write the hash of this run into the store, so that it becomes the accepted hash for the next run. Turn it off for a dry run that must not accept a change.

## `userAgent` (type: `string`):

User-Agent header sent with every request, so your site and your CDN can identify this Actor in their logs. ASCII characters only.

## Actor input object example

```json
{
  "urls": [
    "https://getbootstrap.com/",
    "https://jquery.com/"
  ],
  "allowedScriptOrigins": [
    "code.jquery.com",
    "cdn.jsdelivr.net",
    "cdnjs.cloudflare.com",
    "unpkg.com"
  ],
  "integrityPolicy": "required",
  "allowedHashAlgorithms": [
    "sha384",
    "sha512"
  ],
  "requireCrossorigin": true,
  "mutablePathTokens": [
    "latest",
    "main",
    "master",
    "next",
    "canary",
    "beta",
    "edge",
    "dev",
    "current",
    "stable",
    "nightly",
    "head"
  ],
  "rejectBareMajorVersion": true,
  "checks": [
    "https_scheme",
    "origin_allowlist",
    "integrity_present",
    "integrity_algorithm",
    "crossorigin_attribute",
    "immutable_path",
    "content_drift"
  ],
  "includeSameOrigin": false,
  "hashAssets": true,
  "maxAssetsPerPage": 40,
  "maxAssets": 200,
  "maxAssetBytes": 2097152,
  "maxHtmlBytes": 2097152,
  "maxRedirects": 5,
  "timeoutSeconds": 20,
  "concurrency": 4,
  "stateStoreName": "script-asset-hashes",
  "updateState": true,
  "userAgent": "ScriptIntegrityPinGate/0.1 (+https://apify.com)"
}
```

# Actor output Schema

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

No description

## `assetHashes` (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 = {
    "urls": [
        "https://getbootstrap.com/",
        "https://jquery.com/"
    ],
    "allowedScriptOrigins": [
        "code.jquery.com",
        "cdn.jsdelivr.net",
        "cdnjs.cloudflare.com",
        "unpkg.com"
    ],
    "integrityPolicy": "required",
    "allowedHashAlgorithms": [
        "sha384",
        "sha512"
    ],
    "requireCrossorigin": true,
    "mutablePathTokens": [
        "latest",
        "main",
        "master",
        "next",
        "canary",
        "beta",
        "edge",
        "dev",
        "current",
        "stable",
        "nightly",
        "head"
    ],
    "rejectBareMajorVersion": true,
    "checks": [
        "https_scheme",
        "origin_allowlist",
        "integrity_present",
        "integrity_algorithm",
        "crossorigin_attribute",
        "immutable_path",
        "content_drift"
    ],
    "includeSameOrigin": false,
    "hashAssets": true,
    "maxAssetsPerPage": 40,
    "maxAssets": 200,
    "maxAssetBytes": 2097152,
    "maxHtmlBytes": 2097152,
    "maxRedirects": 5,
    "timeoutSeconds": 20,
    "concurrency": 4,
    "stateStoreName": "script-asset-hashes",
    "updateState": true,
    "userAgent": "ScriptIntegrityPinGate/0.1 (+https://apify.com)"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/third-party-script-integrity-pin-and-content-drift-gate").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 = {
    "urls": [
        "https://getbootstrap.com/",
        "https://jquery.com/",
    ],
    "allowedScriptOrigins": [
        "code.jquery.com",
        "cdn.jsdelivr.net",
        "cdnjs.cloudflare.com",
        "unpkg.com",
    ],
    "integrityPolicy": "required",
    "allowedHashAlgorithms": [
        "sha384",
        "sha512",
    ],
    "requireCrossorigin": True,
    "mutablePathTokens": [
        "latest",
        "main",
        "master",
        "next",
        "canary",
        "beta",
        "edge",
        "dev",
        "current",
        "stable",
        "nightly",
        "head",
    ],
    "rejectBareMajorVersion": True,
    "checks": [
        "https_scheme",
        "origin_allowlist",
        "integrity_present",
        "integrity_algorithm",
        "crossorigin_attribute",
        "immutable_path",
        "content_drift",
    ],
    "includeSameOrigin": False,
    "hashAssets": True,
    "maxAssetsPerPage": 40,
    "maxAssets": 200,
    "maxAssetBytes": 2097152,
    "maxHtmlBytes": 2097152,
    "maxRedirects": 5,
    "timeoutSeconds": 20,
    "concurrency": 4,
    "stateStoreName": "script-asset-hashes",
    "updateState": True,
    "userAgent": "ScriptIntegrityPinGate/0.1 (+https://apify.com)",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/third-party-script-integrity-pin-and-content-drift-gate").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 '{
  "urls": [
    "https://getbootstrap.com/",
    "https://jquery.com/"
  ],
  "allowedScriptOrigins": [
    "code.jquery.com",
    "cdn.jsdelivr.net",
    "cdnjs.cloudflare.com",
    "unpkg.com"
  ],
  "integrityPolicy": "required",
  "allowedHashAlgorithms": [
    "sha384",
    "sha512"
  ],
  "requireCrossorigin": true,
  "mutablePathTokens": [
    "latest",
    "main",
    "master",
    "next",
    "canary",
    "beta",
    "edge",
    "dev",
    "current",
    "stable",
    "nightly",
    "head"
  ],
  "rejectBareMajorVersion": true,
  "checks": [
    "https_scheme",
    "origin_allowlist",
    "integrity_present",
    "integrity_algorithm",
    "crossorigin_attribute",
    "immutable_path",
    "content_drift"
  ],
  "includeSameOrigin": false,
  "hashAssets": true,
  "maxAssetsPerPage": 40,
  "maxAssets": 200,
  "maxAssetBytes": 2097152,
  "maxHtmlBytes": 2097152,
  "maxRedirects": 5,
  "timeoutSeconds": 20,
  "concurrency": 4,
  "stateStoreName": "script-asset-hashes",
  "updateState": true,
  "userAgent": "ScriptIntegrityPinGate/0.1 (+https://apify.com)"
}' |
apify call kingii98/third-party-script-integrity-pin-and-content-drift-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/third-party-script-integrity-pin-and-content-drift-gate"
        }
    }
}

```

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/IqBlAAWV87K2g3Wal/builds/MYeNr0987ZCXAdlLC/openapi.json
