Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 40 additions & 114 deletions examples/express-basic/README.md
Original file line number Diff line number Diff line change
@@ -1,142 +1,68 @@
# Express.js Basic Example
# See a WebDecoy tripwire work in Express

Basic example of Web Decoy integration with Express.js.
![Controlled local test: ordinary traffic returns200; the decoy records DENY in monitor mode and403 only when enforcement is enabled.](docs/tripwire-demo.png)

## Setup
Run a local server, compare an ordinary request with a controlled decoy request,
and inspect the verdict before choosing to block. Local rules need no account
or API key. This demo starts in monitor mode and binds to `127.0.0.1`.

1. Install dependencies:
## Run from this repository

```bash
npm install
```

2. Copy `.env.example` to `.env` and add your API key:

```bash
cp .env.example .env
```

3. Edit `.env` and set your Web Decoy API key:

```env
WEBDECOY_API_KEY=sk_live_your_key_here
```

## Run

Development mode:

```bash
npm run dev
```

Production build:

```bash
npm run build
npm start
```

## Test

The server will start on `http://localhost:3000`.

### Endpoints

- `GET /health` - Health check (unprotected)
- `GET /` - Protected home endpoint
- `GET /api/data` - Protected API endpoint
- `POST /api/submit` - Protected POST endpoint

### Testing Bot Detection

Use these curl commands to test different bot detection scenarios:

#### 1. Chrome Browser (should ALLOW)

Real browser with full security headers - passes local analysis with minimal threat score:

```bash
curl -s http://localhost:3000/ \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" \
-H "Accept-Language: en-US,en;q=0.5" \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Sec-Ch-Ua: \"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"" \
-H "Sec-Ch-Ua-Mobile: ?0" \
-H "Sec-Ch-Ua-Platform: \"macOS\"" \
-H "Sec-Fetch-Dest: document" \
-H "Sec-Fetch-Mode: navigate" \
-H "Sec-Fetch-Site: none" \
-H "Sec-Fetch-User: ?1"
```

#### 2. Python Requests (should BLOCK)

Common scraping library - easily detected and blocked:

```bash
curl -s http://localhost:3000/ \
-H "User-Agent: python-requests/2.28.0"
```

#### 3. Curl User-Agent (should BLOCK)

Default curl user-agent - flagged as automated tool:
Requires Node.js 20+ for the verification tests. From the repository root:

```bash
curl -s http://localhost:3000/ \
-H "User-Agent: curl/8.1.2"
npm ci
npm run build -w @webdecoy/node -w @webdecoy/express
npm run dev -w webdecoy-express-example
```

#### 4. Empty User-Agent (should BLOCK)
## Compare two requests

Missing user-agent is highly suspicious:
In another terminal:

```bash
curl -s http://localhost:3000/ \
-H "User-Agent: "
curl -i http://127.0.0.1:3000/
curl -i http://127.0.0.1:3000/demo-decoy
```

#### 5. Scrapy Bot (should BLOCK)
| Request | Monitor-mode HTTP response | Local decision |
| --- | --- | --- |
| `/` | `200` | `ALLOW`, `tripwire: false` |
| `/demo-decoy` | `404`, from the demo route | `DENY`, `tripwire: true` |

Known web scraping framework:
The second request is deliberately made by you. It demonstrates that a selected
path matched a rule, not that a malicious AI bot was identified. The `404` alone
is not a detection receipt: inspect the `decision` JSON, which comes from
`req.webdecoyDecision`.

```bash
curl -s http://localhost:3000/ \
-H "User-Agent: Scrapy/2.8.0 (+https://scrapy.org)"
```
## Opt into enforcement on this local demo

#### 6. Go HTTP Client (should BLOCK)

Default Go HTTP client user-agent:
Stop the server with Ctrl+C, then restart it:

```bash
curl -s http://localhost:3000/ \
-H "User-Agent: Go-http-client/1.1"
WEBDECOY_MODE=enforce npm run dev -w webdecoy-express-example
```

### Simulating External IP Addresses
Repeat the two requests. `/` remains `200`; `/demo-decoy` returns `403` before
the demo route runs. Restart without that variable to return to monitor mode.

When testing locally, connections come from `::1` (IPv6 localhost). To simulate external IPs for realistic testing, use the `X-Forwarded-For` header:
Run the HTTP checks without a browser or API key:

```bash
curl -s http://localhost:3000/ \
-H "User-Agent: python-requests/2.28.0" \
-H "X-Forwarded-For: 203.0.113.50"
npm test -w webdecoy-express-example
```

This detection will show the IP `203.0.113.50` in the Web Decoy dashboard instead of `::1`.

## What's Happening
## Use it on your own app

1. All requests (except `/health`) go through Web Decoy middleware
2. The middleware extracts request metadata (IP, headers, etc.)
3. Local analysis checks for suspicious patterns
4. If needed, server-side verification with TLS fingerprinting
5. Request is allowed or blocked based on threat score
6. Detection info is available via `req.webdecoy`
The example keeps one controlled decoy path and exposes summaries in local
responses for verification. In production, keep verdicts in server-side logs,
choose decoy paths outside legitimate user flows, and review scanners,
prefetchers and accessibility flows before enforcing. Changing a User-Agent
does not prove that a client is malicious or human.

## Learn More
Behind a proxy, configure the documented `trustProxy` setting for your actual
topology; never trust arbitrary forwarded headers. This example does not
collect JA4, identify every AI agent, or report to the cloud dashboard.

See the [main README](../../README.md) for more documentation.
[Express setup documentation](https://docs.webdecoy.com/sdk-plugins/express/?utm_source=github&utm_medium=example&utm_campaign=express_tripwire_proof)
· [Optional WebDecoy dashboard](https://app.webdecoy.com/?utm_source=github&utm_medium=example&utm_campaign=express_tripwire_proof)
Binary file added examples/express-basic/docs/tripwire-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions examples/express-basic/docs/tripwire-demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions examples/express-basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
"name": "webdecoy-express-example",
"version": "0.1.0",
"private": true,
"description": "Basic Express.js example with Web Decoy",
"description": "Verify local WebDecoy tripwires in Express, then opt into enforcement",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
"start": "node dist/index.js",
"test": "tsx --test test/*.test.ts"
},
"dependencies": {
"@webdecoy/express": "*",
Expand Down
36 changes: 36 additions & 0 deletions examples/express-basic/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import express from 'express';
import { webdecoy } from '@webdecoy/express';
import { tripwire } from '@webdecoy/node';

// Local demonstration: expose summaries here so the verdict is visible.
// Keep this information server-side in a production application.
export function createApp(mode: 'monitor' | 'enforce' = 'monitor') {
const app = express();
app.disable('x-powered-by');
app.use(express.json());
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
app.use(webdecoy({
mode,
rules: [tripwire({ paths: ['/demo-decoy'], includeDefaults: false })],
skipPaths: ['/health'],
}));
function decision(req: express.Request) {
return {
conclusion: req.webdecoyDecision?.conclusion,
tripwire: req.webdecoyDecision?.deniedBy('tripwire') ?? false,
};
}
app.get('/', (req, res) => {
res.json({ message: 'Ordinary route', decision: decision(req) });
});
app.get('/demo-decoy', (req, res) => {
res.status(404).json({ message: 'Controlled decoy route', decision: decision(req) });
});
app.get('/api/data', (req, res) => {
res.json({ data: { message: 'Example data' }, decision: decision(req) });
});
app.post('/api/submit', (req, res) => {
res.json({ success: true, received: req.body.data, decision: decision(req) });
});
return app;
}
93 changes: 6 additions & 87 deletions examples/express-basic/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,8 @@
/**
* Basic Express.js example with Web Decoy protection
*
* This example demonstrates:
* - Basic middleware setup
* - Protecting all routes
* - Skipping health check endpoints
* - Accessing detection info in routes
*/
import { createApp } from './app';

import express from 'express';
import { webdecoy } from '@webdecoy/express';
import dotenv from 'dotenv';

// Load environment variables
dotenv.config();

const app = express();
const port = process.env.PORT || 3000;

// Parse JSON bodies
app.use(express.json());

// Health check endpoint (unprotected)
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});

// Add Web Decoy protection
app.use(
webdecoy({
apiKey: process.env.WEBDECOY_API_KEY!,
apiUrl: process.env.WEBDECOY_API_URL,
threatScoreThreshold: 70, // Block requests with score >= 70
skipPaths: ['/health'], // Don't protect health checks
debug: process.env.NODE_ENV === 'development',
tlsRejectUnauthorized: process.env.NODE_ENV !== 'development', // Allow self-signed certs in dev
})
);

// Protected routes
app.get('/', (req, res) => {
res.json({
message: 'Hello from Web Decoy protected API!',
detection: req.webdecoy,
});
});

app.get('/api/data', (req, res) => {
// Access detection info
const isBot = req.webdecoy?.bot_detected;
const threatLevel = req.webdecoy?.threat_level;

res.json({
data: {
message: 'This is protected data',
timestamp: new Date().toISOString(),
},
security: {
bot_detected: isBot,
threat_level: threatLevel,
detection_id: req.webdecoy?.detection_id,
},
});
});

app.post('/api/submit', (req, res) => {
// Even POST requests are protected
const { data } = req.body;

res.json({
success: true,
received: data,
detection_id: req.webdecoy?.detection_id,
});
});

// Error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err);
res.status(500).json({ error: 'Internal server error' });
});

// Start server
app.listen(port, () => {
console.log(`🚀 Server running on http://localhost:${port}`);
console.log(`🛡️ Web Decoy protection enabled`);
console.log(`📊 Health check available at http://localhost:${port}/health`);
const mode = process.env.WEBDECOY_MODE === 'enforce' ? 'enforce' : 'monitor';
const port = Number(process.env.PORT || 3000);
createApp(mode).listen(port, '127.0.0.1', () => {
console.log(`WebDecoy local demo: http://127.0.0.1:${port} (${mode} mode)`);
console.log('Compare / with /demo-decoy. No API key or cloud reporting.');
});
Loading
Loading