UNPKG

@s4e/jsentinel

Version:

Script integrity and clipboard tamper detection for browsers.

387 lines (336 loc) 11.9 kB
## @s4e/jsentinel Lightweight browser sentinel for script integrity, clipboard tamper detection, extension activity monitoring, and security telemetry. Framework‑agnostic, event‑driven. - **Script integrity (same‑origin)**: hashes JS/CSS at runtime and compares to your manifest (SRI‑style) - **Inline scripts**: distinguishes stable vs volatile inline blocks; hashes stable in manifest, warns on volatile - **External scripts (CDN/remote)**: accepts pinned versioned CDNs; supports dynamic‑loader pinning/allow rules - **Dynamic script monitoring**: MutationObserver detects newly added scripts and src attribute changes - **Clipboard tamper**: detects when pasted text differs from what the user copied - **Extension activity monitoring**: detects suspicious browser extension behaviors and form data access patterns - **Anti‑tamper**: runtime guard against critical API patching - **Network telemetry**: fetch/XHR/beacon start/end with timing, status, headers - **Headers visibility**: minimal security headers snapshot - **CSP violation monitoring**: captures and reports Content Security Policy violations for security analysis ### Step 1 — Generate the manifest (CLI) Use the companion CLI to scan your build output and write `manifest.json`. CLI on npm: [@s4e/jsentinel-cli](https://www.npmjs.com/package/@s4e/jsentinel-cli) ```bash npm i -D @s4e/jsentinel-cli ``` Create `jsentinel.config.json` for clean CI usage (with schema IntelliSense): Obtain your dashboard API token (`s4eToken`) via sales@s4e.io. ```json { "$schema": "./node_modules/@s4e/jsentinel-cli/schema/jsentinel.config.schema.json", "baseUrl": "/", "inputs": [".next/static", "public"], "output": "public/manifest.json", "s4eToken": "<YOUR_S4E_TOKEN>" } ``` Run after your build: ```bash npx @s4e/jsentinel-cli jsentinel manifest -c jsentinel.config.json ``` This writes `public/manifest.json`, which the runtime reads by default. ### Step 2 — Install the runtime ```bash npm i @s4e/jsentinel # yarn add @s4e/jsentinel # pnpm add @s4e/jsentinel # bun add @s4e/jsentinel ``` ### Step 3 — Integrate (React, Vue, Svelte, Angular) Place initialization in any client‑only part of your app and listen to a single DOM event with a typed payload. Use your license token (request via sales@s4e.io). You can initialize in any client‑only file, page, or component across frameworks. React example: ```tsx import { useEffect } from "react"; import { initJSentinel } from "@s4e/jsentinel"; export function Security() { useEffect(() => { initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" }); const onMsg = (e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => { const msg = e.detail; if (msg.type === "ready") console.log("JSentinel ready", msg.data); if (msg.type === "alert") console.warn("Security alert", msg.data); }; window.addEventListener("jsentinel", onMsg as EventListener); return () => window.removeEventListener("jsentinel", onMsg as EventListener); }, []); return null; } ``` Vue 3 example (`src/main.ts`): ```ts import { createApp } from "vue"; import App from "./App.vue"; import { initJSentinel } from "@s4e/jsentinel"; initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" }); window.addEventListener( "jsentinel", (e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => { const msg = e.detail; if (msg.type === "alert") console.warn(msg); }, ); createApp(App).mount("#app"); ``` Svelte example: ```svelte <script lang="ts"> import { onMount } from "svelte"; import { initJSentinel } from "@s4e/jsentinel"; onMount(() => { initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" }); const onMsg = (e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => { const msg = e.detail; if (msg.type === "alert") console.warn(msg); }; window.addEventListener("jsentinel", onMsg as EventListener); return () => window.removeEventListener("jsentinel", onMsg as EventListener); }); </script> ``` Angular example: ```ts import { Component, OnInit } from "@angular/core"; import { initJSentinel } from "@s4e/jsentinel"; @Component({ selector: "app-root", templateUrl: "./app.component.html" }) export class AppComponent implements OnInit { ngOnInit(): void { initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" }); window.addEventListener( "jsentinel", (e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => { const msg = e.detail; if (msg.type === "alert") console.warn(msg); }, ); } } ``` > The important part is the typed event listener: `e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>;` ### Step 4 — Options ```ts type JSentinelOptions = { license: string; // REQUIRED manifestPath?: string; // default: "/manifest.json" trustedHosts?: string[]; // Allow specific external hosts (e.g., ["beacon-v2.helpscout.net", "https://cdn.example.com"]) features?: { clipboard?: boolean; // default: true headers?: boolean; // default: false csp?: boolean; // default: true network?: boolean; // default: false extension?: boolean; // default: true (extension activity monitoring) }; skipHostCheck?: boolean; // default: false debug?: boolean; // default: false }; ``` **Configuration options:** - `license` (required): Your JSentinel license token from sales@s4e.io - `manifestPath` (optional): Path to your manifest.json (default: `/manifest.json`) - `trustedHosts` (optional): Whitelist specific external hosts whose scripts should not trigger `unpinned-url` alerts. You can provide either just the hostname (e.g., `"beacon-v2.helpscout.net"`) or a full URL (e.g., `"https://scripts.simpleanalyticscdn.com"`). This is useful for CDNs and third-party services you trust. - `features` (optional): Enable/disable specific features ### Step 5 — Events (what you’ll receive) Listen to a single DOM `CustomEvent("jsentinel")` with typed payloads: ```ts window.addEventListener( "jsentinel", (e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => { const msg = e.detail; // switch on msg.type }, ); ``` - **ready**: emitted after initialization and the first scan - Payload: `{ version, scan: { total, alerts, verified } }` - Example: ```json { "type": "ready", "data": { "version": "0.2.0", "scan": { "total": 12, "alerts": 1, "verified": 11 } } } ``` - **alert**: unified security alerts for all violations and suspicious behaviors (includes `userAgent` in data) - Common reasons: - Script integrity: `hash-mismatch`, `not-in-manifest`, `fetch-failed`, `volatile-content`, `unpinned-url`, `insecure-protocol`, `unable-to-hash` - Extension monitoring: `suspicious-script-detected`, `excessive-form-access` - Anti-tamper: `watchdog-timeout`, `wasm-verification-failed`, `native-api-modified` - Clipboard: `clipboard-tamper`, `clipboard-permission-denied` - CSP: `csp-violation` - Example (includes user-friendly description): ```json { "type": "alert", "data": { "key": "/assets/app.js", "src": "/assets/app.js", "inline": false, "expected": "sha256-...", "got": "sha256-...", "severity": "error", "reason": "hash-mismatch", "description": "Script hash doesn't match manifest", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } ``` - Example (same‑origin hash mismatch): ```json { "type": "alert", "data": { "key": "/assets/app.js", "src": "/assets/app.js", "inline": false, "expected": "sha256-...", "got": "sha256-...", "severity": "error", "reason": "hash-mismatch", "description": "Script hash doesn't match manifest", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } ``` - Example (inline not listed in manifest): ```json { "type": "alert", "data": { "key": "inline", "src": null, "inline": true, "got": "sha256-...", "severity": "error", "reason": "not-in-manifest", "description": "Script not found in integrity manifest", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } ``` - Example (native API modification detected): ```json { "type": "alert", "data": { "key": "native-api-tamper", "src": null, "inline": false, "severity": "error", "reason": "native-api-modified", "description": "Native API has been modified", "meta": { "modifiedAPIs": [ { "api": "EventTarget.prototype.addEventListener", "newSource": "function addEventListener() { /* extension modified */ }" }, { "api": "window.fetch", "newSource": "function fetch() { /* intercepted */ }" } ], "count": 2, "ts": 1710000000000 }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } ``` - **network.request** / **network.response**: fetch/XHR/beacon telemetry - Example request: ```json { "type": "network.request", "data": { "id": "k9x", "kind": "fetch", "url": "https://api.example.com/?***", "method": "GET", "ts": 1710000000000 } } ``` - Example response: ```json { "type": "network.response", "data": { "id": "k9x", "kind": "fetch", "url": "https://api.example.com/?***", "status": 200, "ok": true, "duration_ms": 132, "headers": { "content-type": "application/json" }, "ts": 1710000000123 } } ``` - **headers.checked** / **headers.error**: minimal security headers snapshot or an error - Example: ```json { "type": "headers.checked", "data": { "url": "https://example.com/", "status": 200, "headers": { "content-security-policy": "default-src 'self'" }, "ts": 1710000000000 } } ``` ```json { "type": "alert", "data": { "key": "csp-script-src", "src": "https://example.com/loader.js", "inline": false, "expected": undefined, "got": "https://example.com/loader.js", "severity": "error", "reason": "csp-violation", "description": "Content Security Policy violation detected", "meta": { "effectiveDirective": "script-src", "blockedURI": "https://example.com/loader.js", "ts": 1710000000000 }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } ``` - **headers.error**: minimal security headers error - Example: ```json { "type": "headers.error", "data": { "message": "Failed to fetch headers", "ts": 1710000000000 } } ``` - **error**: library‑level error (e.g., license or init step) - Example: ```json { "type": "error", "data": { "stage": "license", "message": "LICENSE_DENIED" } } ``` ### Manifest format (reference) ```json { "version": "2024-07-01T12:34:56.000Z", "scripts": [ { "src": "/assets/app.js", "type": "same-origin", "hash": "sha256-..." }, { "inline": "console.log('hi')", "type": "inline-stable", "hash": "sha256-..." }, { "src": "https://cdn.example.com/pkg@1.2.3/index.js", "type": "versioned-cdn" } ] } ``` ### License Commercial license required. Contact sales@s4e.io for licensing information and pricing.