@s4e/jsentinel
Version:
Script integrity and clipboard tamper detection for browsers.
387 lines (336 loc) • 11.9 kB
Markdown
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
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.
```bash
npm i @s4e/jsentinel
```
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>;`
```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" }
}
```
```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" }
]
}
```
Commercial license required. Contact sales@s4e.io for licensing information and pricing.