Skip to content

Commit

Permalink
feat: add detect sensitive info rule (#1300)
Browse files Browse the repository at this point in the history
Adds a new rule that can be used to detect sensitive information being sent in a request when it isn't expected.
  • Loading branch information
e-moran authored Aug 26, 2024
1 parent 6f8a20c commit 006e344
Show file tree
Hide file tree
Showing 66 changed files with 9,011 additions and 55 deletions.
1 change: 1 addition & 0 deletions .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"arcjet-next": "1.0.0-alpha.21",
"arcjet-node": "1.0.0-alpha.21",
"arcjet-sveltekit": "1.0.0-alpha.21",
"body": "1.0.0-alpha.21",
"decorate": "1.0.0-alpha.21",
"duration": "1.0.0-alpha.21",
"env": "1.0.0-alpha.21",
Expand Down
42 changes: 42 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ updates:
- dependency-name: eslint
versions: [">=9"]

- package-ecosystem: npm
directory: /examples/nextjs-14-sensitive-info
schedule:
# Our dependencies should be checked daily
interval: daily
assignees:
- blaine-arcjet
reviewers:
- blaine-arcjet
commit-message:
prefix: deps(example)
prefix-development: deps(example)
groups:
dependencies:
patterns:
- "*"
ignore:
# Ignore updates to the @types/node package due to conflict between
# Headers in DOM.
- dependency-name: "@types/node"
versions: [">18.18"]
# TODO(#539): Upgrade to eslint 9
- dependency-name: eslint
versions: [">=9"]

- package-ecosystem: npm
directory: /examples/nextjs-14-app-dir-validate-email
schedule:
Expand Down Expand Up @@ -405,6 +430,23 @@ updates:
patterns:
- "*"

- package-ecosystem: npm
directory: /examples/express-sensitive-info
schedule:
# Our dependencies should be checked daily
interval: daily
assignees:
- blaine-arcjet
reviewers:
- blaine-arcjet
commit-message:
prefix: deps(example)
prefix-development: deps(example)
groups:
dependencies:
patterns:
- "*"

- package-ecosystem: npm
directory: /examples/nodejs-express-launchdarkly
schedule:
Expand Down
5 changes: 5 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
"component": "@arcjet/sveltekit",
"skip-github-release": true
},
"body": {
"component": "@arcjet/body",
"skip-github-release": true
},
"decorate": {
"component": "@arcjet/decorate",
"skip-github-release": true
Expand Down Expand Up @@ -127,6 +131,7 @@
"@arcjet/eslint-config",
"@arcjet/headers",
"@arcjet/ip",
"@arcjet/body",
"@arcjet/logger",
"@arcjet/protocol",
"@arcjet/rollup-config",
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/reusable-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,48 @@ jobs:
working-directory: examples/nextjs-14-permit
run: npm run build

nextjs-14-sensitive-info:
name: Next.js 14 + Sensitive Info
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# Environment security
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
disable-sudo: true
egress-policy: block
allowed-endpoints: >
fonts.googleapis.com:443
fonts.gstatic.com:443
github.com:443
registry.npmjs.org:443
# Checkout
# Most toolchains require checkout first
- name: Checkout
uses: actions/checkout@v4

# Language toolchains
- name: Install Node
uses: actions/[email protected]
with:
node-version: 20

# Workflow

- name: Install dependencies
run: npm ci

- name: Install example dependencies
working-directory: examples/nextjs-14-sensitive-info
run: npm ci

- name: Build
working-directory: examples/nextjs-14-sensitive-info
run: npm run build

nodejs-hono-rl:
name: Node.js + Hono + Rate Limit
runs-on: ubuntu-latest
Expand Down
53 changes: 50 additions & 3 deletions analyze/edge-light.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { ArcjetLogger, ArcjetRequestDetails } from "@arcjet/protocol";

import * as core from "./wasm/arcjet_analyze_js_req.component.js";
import { instantiate } from "./wasm/arcjet_analyze_js_req.component.js";
import type {
ImportObject,
EmailValidationConfig,
BotDetectionResult,
BotType,
EmailValidationResult,
DetectedSensitiveInfoEntity,
SensitiveInfoEntities,
SensitiveInfoEntity,
SensitiveInfoResult,
} from "./wasm/arcjet_analyze_js_req.component.js";
import type { ArcjetJsReqSensitiveInformationIdentifier } from "./wasm/interfaces/arcjet-js-req-sensitive-information-identifier.js";

import componentCoreWasm from "./wasm/arcjet_analyze_js_req.component.core.wasm?module";
import componentCore2Wasm from "./wasm/arcjet_analyze_js_req.component.core2.wasm?module";
Expand All @@ -26,6 +31,9 @@ interface AnalyzeContext {
characteristics: string[];
}

type DetectSensitiveInfoFunction =
typeof ArcjetJsReqSensitiveInformationIdentifier.detect;

async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
if (path === "arcjet_analyze_js_req.component.core.wasm") {
return componentCoreWasm;
Expand All @@ -40,9 +48,20 @@ async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
throw new Error(`Unknown path: ${path}`);
}

async function init(context: AnalyzeContext) {
function noOpDetect(): SensitiveInfoEntity[] {
return [];
}

async function init(
context: AnalyzeContext,
detectSensitiveInfo?: DetectSensitiveInfoFunction,
) {
const { log } = context;

if (typeof detectSensitiveInfo !== "function") {
detectSensitiveInfo = noOpDetect;
}

const coreImports: ImportObject = {
"arcjet:js-req/logger": {
debug(msg) {
Expand All @@ -69,10 +88,13 @@ async function init(context: AnalyzeContext) {
return "unknown";
},
},
"arcjet:js-req/sensitive-information-identifier": {
detect: detectSensitiveInfo,
},
};

try {
return core.instantiate(moduleFromPath, coreImports);
return instantiate(moduleFromPath, coreImports);
} catch {
log.debug("WebAssembly is not supported in this runtime");
}
Expand All @@ -94,6 +116,9 @@ export {
* almost certain this request was not a bot.
*/
type BotDetectionResult,
type DetectedSensitiveInfoEntity,
type SensitiveInfoEntity,
type DetectSensitiveInfoFunction,
};

/**
Expand Down Expand Up @@ -161,3 +186,25 @@ export async function detectBot(
};
}
}
export async function detectSensitiveInfo(
context: AnalyzeContext,
candidate: string,
entities: SensitiveInfoEntities,
contextWindowSize: number,
detect?: DetectSensitiveInfoFunction,
): Promise<SensitiveInfoResult> {
const analyze = await init(context, detect);

if (typeof analyze !== "undefined") {
const skipCustomDetect = typeof detect !== "function";
return analyze.detectSensitiveInfo(candidate, {
entities,
contextWindowSize,
skipCustomDetect,
});
} else {
throw new Error(
"SENSITIVE_INFO rule failed to run because Wasm is not supported in this environment.",
);
}
}
54 changes: 51 additions & 3 deletions analyze/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { ArcjetLogger, ArcjetRequestDetails } from "@arcjet/protocol";

import * as core from "./wasm/arcjet_analyze_js_req.component.js";
import { instantiate } from "./wasm/arcjet_analyze_js_req.component.js";
import type {
ImportObject,
EmailValidationConfig,
BotDetectionResult,
BotType,
EmailValidationResult,
DetectedSensitiveInfoEntity,
SensitiveInfoEntities,
SensitiveInfoEntity,
SensitiveInfoResult,
} from "./wasm/arcjet_analyze_js_req.component.js";
import type { ArcjetJsReqSensitiveInformationIdentifier } from "./wasm/interfaces/arcjet-js-req-sensitive-information-identifier.js";

import { wasm as componentCoreWasm } from "./wasm/arcjet_analyze_js_req.component.core.wasm?js";
import { wasm as componentCore2Wasm } from "./wasm/arcjet_analyze_js_req.component.core2.wasm?js";
Expand All @@ -26,6 +31,9 @@ interface AnalyzeContext {
characteristics: string[];
}

type DetectSensitiveInfoFunction =
typeof ArcjetJsReqSensitiveInformationIdentifier.detect;

// TODO: Do we actually need this wasmCache or does `import` cache correctly?
const wasmCache = new Map<string, WebAssembly.Module>();

Expand Down Expand Up @@ -54,9 +62,20 @@ async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
throw new Error(`Unknown path: ${path}`);
}

async function init(context: AnalyzeContext) {
function noOpDetect(): SensitiveInfoEntity[] {
return [];
}

async function init(
context: AnalyzeContext,
detectSensitiveInfo?: DetectSensitiveInfoFunction,
) {
const { log } = context;

if (typeof detectSensitiveInfo !== "function") {
detectSensitiveInfo = noOpDetect;
}

const coreImports: ImportObject = {
"arcjet:js-req/logger": {
debug(msg) {
Expand All @@ -83,10 +102,13 @@ async function init(context: AnalyzeContext) {
return "unknown";
},
},
"arcjet:js-req/sensitive-information-identifier": {
detect: detectSensitiveInfo,
},
};

try {
return core.instantiate(moduleFromPath, coreImports);
return instantiate(moduleFromPath, coreImports);
} catch {
log.debug("WebAssembly is not supported in this runtime");
}
Expand All @@ -108,6 +130,9 @@ export {
* almost certain this request was not a bot.
*/
type BotDetectionResult,
type DetectedSensitiveInfoEntity,
type SensitiveInfoEntity,
type DetectSensitiveInfoFunction,
};

/**
Expand Down Expand Up @@ -175,3 +200,26 @@ export async function detectBot(
};
}
}

export async function detectSensitiveInfo(
context: AnalyzeContext,
candidate: string,
entities: SensitiveInfoEntities,
contextWindowSize: number,
detect?: DetectSensitiveInfoFunction,
): Promise<SensitiveInfoResult> {
const analyze = await init(context, detect);

if (typeof analyze !== "undefined") {
const skipCustomDetect = typeof detect !== "function";
return analyze.detectSensitiveInfo(candidate, {
entities,
contextWindowSize,
skipCustomDetect,
});
} else {
throw new Error(
"SENSITIVE_INFO rule failed to run because Wasm is not supported in this environment.",
);
}
}
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core2.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core3.wasm
Binary file not shown.
28 changes: 28 additions & 0 deletions analyze/wasm/arcjet_analyze_js_req.component.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { SensitiveInfoEntity } from './interfaces/arcjet-js-req-sensitive-information-identifier.js';
export { SensitiveInfoEntity };
/**
* # Variants
*
Expand Down Expand Up @@ -35,16 +37,42 @@ export interface EmailValidationConfig {
allowDomainLiteral: boolean,
blockedEmails: Array<string>,
}
export type SensitiveInfoEntities = SensitiveInfoEntitiesAllow | SensitiveInfoEntitiesDeny;
export interface SensitiveInfoEntitiesAllow {
tag: 'allow',
val: Array<SensitiveInfoEntity>,
}
export interface SensitiveInfoEntitiesDeny {
tag: 'deny',
val: Array<SensitiveInfoEntity>,
}
export interface SensitiveInfoConfig {
entities: SensitiveInfoEntities,
contextWindowSize?: number,
skipCustomDetect: boolean,
}
export interface DetectedSensitiveInfoEntity {
start: number,
end: number,
identifiedType: SensitiveInfoEntity,
}
export interface SensitiveInfoResult {
allowed: Array<DetectedSensitiveInfoEntity>,
denied: Array<DetectedSensitiveInfoEntity>,
}
import { ArcjetJsReqEmailValidatorOverrides } from './interfaces/arcjet-js-req-email-validator-overrides.js';
import { ArcjetJsReqLogger } from './interfaces/arcjet-js-req-logger.js';
import { ArcjetJsReqSensitiveInformationIdentifier } from './interfaces/arcjet-js-req-sensitive-information-identifier.js';
export interface ImportObject {
'arcjet:js-req/email-validator-overrides': typeof ArcjetJsReqEmailValidatorOverrides,
'arcjet:js-req/logger': typeof ArcjetJsReqLogger,
'arcjet:js-req/sensitive-information-identifier': typeof ArcjetJsReqSensitiveInformationIdentifier,
}
export interface Root {
detectBot(headers: string, patternsAdd: string, patternsRemove: string): BotDetectionResult,
generateFingerprint(request: string, characteristics: Array<string>): string,
isValidEmail(candidate: string, options: EmailValidationConfig): EmailValidationResult,
detectSensitiveInfo(content: string, options: SensitiveInfoConfig): SensitiveInfoResult,
}

/**
Expand Down
Loading

0 comments on commit 006e344

Please sign in to comment.