Saltar al contenido

Open source / Sentinel / sri-sentinel

sri-sentinel

Subresource Integrity (SRI) generator and checker: hashes <script> and <link> resources in built HTML (sha256/384/512, local and CDN), writes integrity + crossorigin, fails CI on missing, wrong or stale hashes. JSON manifest, Vite and webpack plugins, text/JSON/SARIF/Markdown output, GitHub Action. Zero dependencies.

En npm
$ npm install sri-sentinel

README

sri-sentinel

CI npm version License: MIT Node.js

Subresource Integrity (SRI) hashes for your built site: generate them, write them, and fail CI when they're missing, wrong or stale.

sri-sentinel scans the HTML in a built site directory for <script src>, <link rel="stylesheet">, rel="modulepreload", rel="preload" as="script|style", and integrity entries inside <script type="importmap">. It hashes the local files (and, with --remote, CDN files), then either writes integrity attributes (plus crossorigin="anonymous" for cross-origin resources) or checks that the existing ones are correct. Every finding includes a file, line number, severity and a concrete fix.

CLI, library, Vite / webpack / Rollup / esbuild / Astro integrations, and GitHub Action. Node.js 18+, zero runtime dependencies.

Install and run

npx sri-sentinel dist --write              # add integrity attributes to dist/**/*.html
npx sri-sentinel dist --check              # CI: fail on missing, wrong or stale integrity
npx sri-sentinel dist --write --remote     # also fetch and hash CDN resources
npx sri-sentinel dist --manifest sri.json  # JSON manifest of resource -> integrity
dist/index.html
     4: high     <link> "/assets/app.css?v=42" has no integrity attribute [missing-integrity]
        ↳ Add integrity="sha384-IBXuE+SKr6nOANxq1LIznMbjQUHDJ3PY6mUpF3ZQCTz1+rK8Fip9GAGqRq59yH3C"
     5: high     <script> "https://cdn.jsdelivr.net/npm/htmx.org@2.0.3/dist/htmx.min.js" has no integrity attribute [missing-integrity]
        ↳ Run with --remote to fetch and hash it, then --write
     6: critical Integrity for "./assets/app.js" does not match its content (wrong or stale) [integrity-mismatch]
        ↳ Replace with integrity="sha384-Tc1KWaETWL9ZNn+TiVtHwsBd+yyFv2LCslp7PjwHT5aJCyBwpxnW7xtPA1RdtKbo"
     7: medium   Could not resolve "/assets/vendor.js" (file not found: dist/assets/vendor.js) [unresolved-resource]
        ↳ Check the path and --base, or allowlist it

1 HTML file(s), 4 resource(s): 1 critical, 2 high, 1 medium

After --write --remote:

<link rel="stylesheet" href="/assets/app.css?v=42" integrity="sha384-IBXuE+SKr6nOANxq1LIznMbjQUHDJ3PY6mUpF3ZQCTz1+rK8Fip9GAGqRq59yH3C">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.3/dist/htmx.min.js" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>

Lazy / runtime chunks (webpack)

The webpack plugin patches the webpack 5 runtime after emit so code-split chunks loaded via import() get an integrity attribute on the injected <script> tag (similar to webpack-subresource-integrity). Top-level scripts in HTML are still updated by the HTML scan.

Set output.crossOriginLoading: 'anonymous' (required for cross-origin SRI). Disable runtime patching with { runtime: false }.

Import maps

For <script type="importmap">, sri-sentinel --write fills the top-level "integrity" object with digests for URLs referenced in imports / scopes:

<script type="importmap">
{
  "imports": { "app": "/assets/app.js" },
  "integrity": {
    "/assets/app.js": "sha384-…"
  }
}
</script>

Rules

RuleSeverityWhat it catches
integrity-mismatchcriticalThe integrity value doesn't match the file: wrong, or stale after a rebuild
missing-integrityhighA script, stylesheet, modulepreload, or import map entry with no integrity
invalid-integrityhighMalformed value, wrong digest length or unsupported algorithm (sha1-, md5-)
missing-crossoriginhighCross-origin resource with integrity but no crossorigin. The browser will block it
unresolved-resourcemediumLocal file not found or outside the site root, so it can't be hashed
fetch-failedmedium--remote couldn't download the resource (HTTP error, timeout)
weak-algorithmlowValid hash, but weaker than the configured algorithm (e.g. sha256 when sha384 is set)
remote-unverifiedinfoRemote resource has integrity, but you didn't pass --remote, so it wasn't verified
dynamic-resourceinfoTemplated URL ({{ }}, ${}, <% %>, {% %}) skipped

Verification works the way browsers do it: only the strongest algorithm in the attribute counts, any hash of that algorithm may match, and base64url and ?options are accepted.

How URLs are resolved

ReferenceResolved to
app.js, ./app.js, ../assets/app.jsRelative to the HTML file
/assets/app.jsSite root (the directory you scan). With --base /app/, /app/assets/app.js maps to assets/app.js
https://example.com/assets/app.jsLocal if --origin https://example.com is set, otherwise remote (cross-origin)
//cdn.example.com/x.jsRemote, over https
app.js?v=3#xThe query string and fragment are ignored for file lookup and kept in the HTML
data:, blob:, inline <script>Ignored
Paths escaping the site rootunresolved-resource, never read

Add data-sri-ignore to a tag to skip it.

Options

--check                Report missing, wrong or stale integrity (default)
--write                Write integrity (and crossorigin) attributes into the HTML
-a, --algorithm <alg>  sha256 | sha384 | sha512 (default: sha384)
--remote               Fetch and hash remote (CDN) resources
--base <path>          Public base path, e.g. /app/ (default: /)
--origin <url>         Site origin; absolute URLs on it are treated as local files
--crossorigin <v>      anonymous | use-credentials (default: anonymous)
--manifest <file>      Write a JSON manifest of resource -> integrity
--config <file>        Config file (default: .sri-sentinel.json or package.json "sri-sentinel")
--fail-on <sev>        critical | high | medium | low | info | none (default: high)
--format <fmt>         text | json | sarif | markdown (default: text)
--json/--sarif/--markdown <file>   Also write that report to a file
--timeout <ms>         Remote fetch timeout (default: 10000)
-q, --quiet            Only print the summary

Exit codes: 0 pass, 1 problems at or above --fail-on, 2 usage or runtime error. With --write, problems it fixed don't count.

Config

.sri-sentinel.json (or a "sri-sentinel" key in package.json):

{
  "algorithm": "sha384",
  "base": "/",
  "origin": "https://example.com",
  "remote": true,
  "failOn": "high",
  "allowlist": [
    "https://www.googletagmanager.com/**",
    { "rule": "unresolved-resource", "url": "/injected-at-runtime.js", "reason": "added by the CDN" },
    { "url": "https://cdn.example.com/beta/*", "reason": "rolling build", "expires": "2027-01-01" }
  ],
  "ignore": ["legacy/**"],
  "rules": { "dynamic-resource": "off", "weak-algorithm": "medium" }
}

An allowlist entry without a rule exempts the resource completely, and it's never rewritten. With a rule, only that finding is suppressed. Strings take the form "url-glob" or "rule:url-glob". Entries past their expires date stop applying.

Manifest

{
  "algorithm": "sha384",
  "resources": {
    "/assets/app.css": { "integrity": "sha384-IBXu…", "file": "dist/assets/app.css", "crossOrigin": false },
    "https://cdn.jsdelivr.net/npm/htmx.org@2.0.3/dist/htmx.min.js": { "integrity": "sha384-0895…", "crossOrigin": true }
  }
}

Vite

// vite.config.js
import sri from 'sri-sentinel/vite';
export default { plugins: [sri({ remote: true, manifest: 'sri-manifest.json' })] };

Runs after the bundle is written (closeBundle), rewrites every HTML file in build.outDir and uses Vite's base. The build fails if problems at failOn (default high) remain. The dev server isn't affected.

webpack

// webpack.config.js
import { SriSentinelPlugin } from 'sri-sentinel/webpack';
export default {
  output: { crossOriginLoading: 'anonymous' },
  plugins: [new HtmlWebpackPlugin(), new SriSentinelPlugin({ algorithm: 'sha384' })],
};

After emit: patches the runtime for lazy-chunk SRI, then rewrites HTML in output.path. Uses output.publicPath when it's an absolute path. Set { runtime: false } to skip runtime patching. Remaining problems are added to compilation.errors.

Rollup

import sri from 'sri-sentinel/rollup';
export default { plugins: [sri({ manifest: 'sri.json' })] };

Runs on writeBundle over the output directory.

esbuild

import { sriSentinelEsbuild } from 'sri-sentinel/esbuild';
export default {
  plugins: [sriSentinelEsbuild({ failOn: 'high' })],
};

Runs on successful onEnd when outdir is set.

Astro

// astro.config.mjs
import sri from 'sri-sentinel/astro';
export default { integrations: [sri({ remote: true })] };

Runs on astro:build:done over the build output directory.

GitHub Action

- uses: MrLouisWright/sri-sentinel@v1
  with:
    paths: dist
    mode: check          # or write
    remote: 'true'
    fail-on: high
    sarif: sri.sarif

Inputs: paths, mode, algorithm, remote, base, fail-on, config, manifest, sarif, json, job-summary (Markdown report written to the job summary) and args.

Library

ESM

import { scan, computeIntegrity, verifyIntegrity, formatSarif, exitCode } from 'sri-sentinel';

CommonJS

const { scan, computeIntegrity } = require('sri-sentinel');
const result = await scan({ paths: ['dist'], write: true, remote: true, config: { algorithm: 'sha512' } });
console.log(result.manifest, result.findings);
computeIntegrity('console.log(1)');             // 'sha384-…'
verifyIntegrity(buffer, 'sha384-… sha512-…');   // { valid, ok, algorithm }

TypeScript declarations are generated from source (npm run build). This package does not generate CSP headers or inline script hashes (use a dedicated CSP tool for that).

Support

If this project is useful to you, you can support its development with a donation: Buy me a coffee. It helps keep these tools maintained, documented and free for everyone.

Credit (optional)

Free to use under MIT, no attribution required. If sri-sentinel helps you, a link back is appreciated:

<a href="https://louiswright.co.uk">Built with sri-sentinel by Louis Wright</a>
[![Built with sri-sentinel by Louis Wright](https://img.shields.io/badge/built%20with-sri--sentinel-blue)](https://louiswright.co.uk)

License

MIT © Louis Wright (Louis Wright)