guardian-risk-express 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -18
- package/dist/index.cjs +129 -5
- package/dist/index.d.cts +75 -12
- package/dist/index.d.ts +75 -12
- package/dist/index.js +126 -6
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -6,34 +6,57 @@
|
|
|
6
6
|
npm install guardian-risk guardian-risk-express
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Express integration for [guardian-risk](https://www.npmjs.com/package/guardian-risk). Validates client IP, reads HTTP metadata, and provides production middleware.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Planned signals
|
|
11
|
+
## Signals
|
|
14
12
|
|
|
15
13
|
| Signal | Source |
|
|
16
14
|
|--------|--------|
|
|
17
|
-
| `clientIp` | `req.ip` /
|
|
18
|
-
| `userAgent` | `User-Agent` header |
|
|
15
|
+
| `clientIp` | Validated `req.ip` / `req.ips` (trust proxy aware) |
|
|
16
|
+
| `userAgent` | `User-Agent` header (truncated) |
|
|
19
17
|
| `requestMethod` | `req.method` |
|
|
20
|
-
| `
|
|
18
|
+
| `requestPath` | `req.path` |
|
|
21
19
|
|
|
22
|
-
##
|
|
20
|
+
## Production usage
|
|
23
21
|
|
|
24
22
|
```typescript
|
|
23
|
+
import express from 'express';
|
|
25
24
|
import { Guardian } from 'guardian-risk';
|
|
26
|
-
import { expressPlugin,
|
|
25
|
+
import { expressPlugin, guardianMiddleware } from 'guardian-risk-express';
|
|
26
|
+
import { redisPlugin } from 'guardian-risk-redis';
|
|
27
|
+
|
|
28
|
+
const app = express();
|
|
29
|
+
app.set('trust proxy', 1); // required behind load balancers
|
|
30
|
+
|
|
31
|
+
const template = new Guardian()
|
|
32
|
+
.use(expressPlugin({ trustProxy: true }))
|
|
33
|
+
.use(redisPlugin({ url: process.env.REDIS_URL }))
|
|
34
|
+
.rule({ name: 'HighRate', when: (s) => (s.requestsPerMinute ?? 0) > 120, score: 40 });
|
|
35
|
+
|
|
36
|
+
app.get('/health', (_req, res) => res.json({ ok: true }));
|
|
37
|
+
|
|
38
|
+
app.use(
|
|
39
|
+
guardianMiddleware(template, {
|
|
40
|
+
blockAboveScore: 70,
|
|
41
|
+
onAnalyzeError: 'block', // fail closed when hooks fail
|
|
42
|
+
exposeBlockDetails: false, // never leak reasons to clients
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
```
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
## Security notes
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
- Set **`app.set('trust proxy', N)`** when behind a reverse proxy — otherwise `clientIp` reflects the proxy, not the user.
|
|
50
|
+
- **`clientIp` is validated** — malformed `X-Forwarded-For` values are rejected.
|
|
51
|
+
- Use **`onAnalyzeError: 'block'`** in production when `blockAboveScore` is set.
|
|
52
|
+
- Use **`template.fork()`** or `guardianMiddleware` — never share one `Guardian` across requests.
|
|
53
|
+
- Headers and user agents are **spoofable** — treat as hints, not proof.
|
|
54
|
+
|
|
55
|
+
## API
|
|
36
56
|
|
|
37
|
-
|
|
57
|
+
- `expressPlugin(options)` — registers `beforeAnalyze` hook
|
|
58
|
+
- `fromRequest(req, guardian, options)` — manual per-request signal injection
|
|
59
|
+
- `guardianMiddleware(template, options)` — Express middleware with optional blocking
|
|
60
|
+
- `analyzeRequest(template, req, options)` — programmatic analyze helper
|
|
38
61
|
|
|
39
|
-
|
|
62
|
+
See [SECURITY.md](../../SECURITY.md) and [MIGRATION.md](../../MIGRATION.md).
|
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,141 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var guardianRisk = require('guardian-risk');
|
|
4
|
+
|
|
5
|
+
// src/request.ts
|
|
6
|
+
var MAX_HEADER_LENGTH = 512;
|
|
7
|
+
function fromRequest(req, guardian, options = {}) {
|
|
8
|
+
const { trustProxy = false } = options;
|
|
9
|
+
const clientIp = resolveClientIp(req, trustProxy);
|
|
10
|
+
const userAgent = truncate(readHeader(req, "user-agent") ?? "unknown", MAX_HEADER_LENGTH);
|
|
11
|
+
const contentLength = Number(readHeader(req, "content-length") ?? 0);
|
|
12
|
+
const acceptLanguage = truncate(
|
|
13
|
+
readHeader(req, "accept-language") ?? "unknown",
|
|
14
|
+
MAX_HEADER_LENGTH
|
|
15
|
+
);
|
|
16
|
+
return guardian.signal("clientIp", clientIp).signal("userAgent", userAgent).signal("requestMethod", req.method ?? "UNKNOWN").signal("requestPath", truncate(req.path ?? req.originalUrl ?? "/", MAX_HEADER_LENGTH)).signal("contentLength", Number.isFinite(contentLength) ? contentLength : 0).signal("acceptLanguage", acceptLanguage).signal("requestSource", "express");
|
|
17
|
+
}
|
|
18
|
+
function expressBeforeAnalyze(options = {}) {
|
|
19
|
+
return ({ data: req, guardian }) => {
|
|
20
|
+
fromRequest(req, guardian, options);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function resolveClientIp(req, trustProxy) {
|
|
24
|
+
if (trustProxy) {
|
|
25
|
+
if (req.ips && req.ips.length > 0) {
|
|
26
|
+
for (const candidate of req.ips) {
|
|
27
|
+
const parsed = guardianRisk.parseIpAddress(candidate);
|
|
28
|
+
if (parsed) {
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const forwarded = readHeader(req, "x-forwarded-for");
|
|
34
|
+
if (forwarded) {
|
|
35
|
+
for (const part of forwarded.split(",")) {
|
|
36
|
+
const parsed = guardianRisk.parseIpAddress(part);
|
|
37
|
+
if (parsed) {
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const fromReqIp = req.ip ? guardianRisk.parseIpAddress(req.ip) : null;
|
|
44
|
+
if (fromReqIp) {
|
|
45
|
+
return fromReqIp;
|
|
46
|
+
}
|
|
47
|
+
const fromSocket = req.socket?.remoteAddress ? guardianRisk.parseIpAddress(req.socket.remoteAddress) : null;
|
|
48
|
+
if (fromSocket) {
|
|
49
|
+
return fromSocket;
|
|
50
|
+
}
|
|
51
|
+
return "unknown";
|
|
52
|
+
}
|
|
53
|
+
function readHeader(req, name) {
|
|
54
|
+
const fromGetter = req.get?.(name);
|
|
55
|
+
if (fromGetter) {
|
|
56
|
+
return fromGetter;
|
|
57
|
+
}
|
|
58
|
+
const value = req.headers[name.toLowerCase()] ?? req.headers[name];
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return value[0];
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function truncate(value, max) {
|
|
65
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/middleware.ts
|
|
69
|
+
async function analyzeRequest(template, req, options = {}) {
|
|
70
|
+
const guardian = template.fork();
|
|
71
|
+
if (!template.getInstalledPlugins().includes("guardian-risk-express")) {
|
|
72
|
+
fromRequest(req, guardian, options);
|
|
73
|
+
}
|
|
74
|
+
const report = await guardian.analyzeAsync(req);
|
|
75
|
+
return { guardian, report };
|
|
76
|
+
}
|
|
77
|
+
function guardianMiddleware(template, options = {}) {
|
|
78
|
+
const {
|
|
79
|
+
attachToRequest = true,
|
|
80
|
+
blockAboveScore,
|
|
81
|
+
trustProxy = false,
|
|
82
|
+
exposeBlockDetails = false
|
|
83
|
+
} = options;
|
|
84
|
+
const onAnalyzeError = options.onAnalyzeError ?? (blockAboveScore !== void 0 ? "block" : "pass");
|
|
85
|
+
return (req, res, next) => {
|
|
86
|
+
void (async () => {
|
|
87
|
+
try {
|
|
88
|
+
const guardian = template.fork();
|
|
89
|
+
if (!template.getInstalledPlugins().includes("guardian-risk-express")) {
|
|
90
|
+
fromRequest(req, guardian, { trustProxy });
|
|
91
|
+
}
|
|
92
|
+
const report = await guardian.analyzeAsync(req);
|
|
93
|
+
if (attachToRequest) {
|
|
94
|
+
req.guardian = guardian;
|
|
95
|
+
req.riskReport = report;
|
|
96
|
+
}
|
|
97
|
+
if (blockAboveScore !== void 0 && report.score >= blockAboveScore) {
|
|
98
|
+
sendBlocked(res, report, exposeBlockDetails);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
next();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (onAnalyzeError === "block") {
|
|
104
|
+
res.status(503).json({ error: "Risk analysis unavailable" });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
next(error);
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function sendBlocked(res, report, exposeDetails) {
|
|
113
|
+
if (exposeDetails) {
|
|
114
|
+
res.status(403).json({
|
|
115
|
+
error: "Request blocked",
|
|
116
|
+
score: report.score,
|
|
117
|
+
level: report.level,
|
|
118
|
+
reasons: report.reasons
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
res.status(403).json({ error: "Request blocked" });
|
|
123
|
+
}
|
|
124
|
+
|
|
3
125
|
// src/index.ts
|
|
4
126
|
function expressPlugin(options = {}) {
|
|
5
|
-
const
|
|
127
|
+
const hook = expressBeforeAnalyze(options);
|
|
6
128
|
return {
|
|
7
129
|
name: "guardian-risk-express",
|
|
8
|
-
install(
|
|
130
|
+
install(guardian) {
|
|
131
|
+
guardian.beforeAnalyze(hook);
|
|
9
132
|
}
|
|
10
133
|
};
|
|
11
134
|
}
|
|
12
|
-
function fromRequest(_req, guardian) {
|
|
13
|
-
return guardian.signal("requestSource", "express").signal("expressPlugin", "stub");
|
|
14
|
-
}
|
|
15
135
|
|
|
136
|
+
exports.analyzeRequest = analyzeRequest;
|
|
137
|
+
exports.expressBeforeAnalyze = expressBeforeAnalyze;
|
|
16
138
|
exports.expressPlugin = expressPlugin;
|
|
17
139
|
exports.fromRequest = fromRequest;
|
|
140
|
+
exports.guardianMiddleware = guardianMiddleware;
|
|
141
|
+
exports.resolveClientIp = resolveClientIp;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,22 +1,85 @@
|
|
|
1
1
|
import * as guardian_risk from 'guardian-risk';
|
|
2
|
-
import { Plugin } from 'guardian-risk';
|
|
2
|
+
import { RiskReport, Guardian, Plugin } from 'guardian-risk';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Minimal Express request shape — no express import required at runtime. */
|
|
5
|
+
interface ExpressRequestLike {
|
|
6
|
+
readonly ip?: string;
|
|
7
|
+
readonly ips?: readonly string[];
|
|
8
|
+
readonly method?: string;
|
|
9
|
+
readonly path?: string;
|
|
10
|
+
readonly originalUrl?: string;
|
|
11
|
+
readonly headers: Record<string, string | string[] | undefined>;
|
|
12
|
+
readonly socket?: {
|
|
13
|
+
readonly remoteAddress?: string;
|
|
14
|
+
};
|
|
15
|
+
get?(name: string): string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Options for reading request signals. */
|
|
5
18
|
interface ExpressPluginOptions {
|
|
6
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Trust proxy-forwarded IPs (requires `app.set('trust proxy', ...)` in Express).
|
|
21
|
+
*/
|
|
7
22
|
readonly trustProxy?: boolean;
|
|
8
23
|
}
|
|
9
24
|
/**
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
25
|
+
* Extract HTTP request signals and attach them to a Guardian instance.
|
|
26
|
+
*/
|
|
27
|
+
declare function fromRequest(req: ExpressRequestLike, guardian: guardian_risk.Guardian, options?: ExpressPluginOptions): guardian_risk.Guardian;
|
|
28
|
+
/**
|
|
29
|
+
* Returns a beforeAnalyze hook that enriches signals from an Express request.
|
|
15
30
|
*/
|
|
16
|
-
declare function
|
|
31
|
+
declare function expressBeforeAnalyze(options?: ExpressPluginOptions): guardian_risk.BeforeAnalyzeHook<ExpressRequestLike>;
|
|
17
32
|
/**
|
|
18
|
-
*
|
|
33
|
+
* Resolve client IP with validation. Spoofed or invalid values are rejected.
|
|
34
|
+
*/
|
|
35
|
+
declare function resolveClientIp(req: ExpressRequestLike, trustProxy: boolean): string;
|
|
36
|
+
|
|
37
|
+
/** Express-compatible middleware types (optional peer). */
|
|
38
|
+
interface ExpressRequest extends ExpressRequestLike {
|
|
39
|
+
riskReport?: RiskReport;
|
|
40
|
+
guardian?: Guardian;
|
|
41
|
+
}
|
|
42
|
+
type ExpressNextFunction = (error?: unknown) => void;
|
|
43
|
+
interface ExpressResponse {
|
|
44
|
+
status(code: number): ExpressResponse;
|
|
45
|
+
json(body: unknown): void;
|
|
46
|
+
}
|
|
47
|
+
type ExpressRequestHandler = (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => void;
|
|
48
|
+
type AnalyzeErrorPolicy = 'pass' | 'block';
|
|
49
|
+
interface GuardianMiddlewareOptions extends ExpressPluginOptions {
|
|
50
|
+
readonly attachToRequest?: boolean;
|
|
51
|
+
/** Block when score is greater than or equal to this threshold. */
|
|
52
|
+
readonly blockAboveScore?: number;
|
|
53
|
+
/**
|
|
54
|
+
* When analysis fails (hook timeout, VPN error), `block` returns 503.
|
|
55
|
+
* Defaults to `block` when `blockAboveScore` is set, otherwise `pass`.
|
|
56
|
+
*/
|
|
57
|
+
readonly onAnalyzeError?: AnalyzeErrorPolicy;
|
|
58
|
+
/** Include score/reasons in block response (default: false). */
|
|
59
|
+
readonly exposeBlockDetails?: boolean;
|
|
60
|
+
}
|
|
61
|
+
declare function analyzeRequest(template: Guardian, req: ExpressRequestLike, options?: ExpressPluginOptions): Promise<{
|
|
62
|
+
guardian: Guardian;
|
|
63
|
+
report: RiskReport;
|
|
64
|
+
}>;
|
|
65
|
+
declare function guardianMiddleware(template: Guardian, options?: GuardianMiddlewareOptions): ExpressRequestHandler;
|
|
66
|
+
|
|
67
|
+
/** Options for the Express plugin. */
|
|
68
|
+
type ExpressPluginConfig = ExpressPluginOptions;
|
|
69
|
+
/**
|
|
70
|
+
* Express plugin — registers a beforeAnalyze hook for request signal collection.
|
|
71
|
+
*
|
|
72
|
+
* Usage with middleware (recommended):
|
|
73
|
+
* ```typescript
|
|
74
|
+
* app.use(guardianMiddleware(template, { trustProxy: true }));
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* Or manual:
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const g = template.fork().use(expressPlugin({ trustProxy: true }));
|
|
80
|
+
* const report = await g.analyzeAsync(req);
|
|
81
|
+
* ```
|
|
19
82
|
*/
|
|
20
|
-
declare function
|
|
83
|
+
declare function expressPlugin(options?: ExpressPluginConfig): Plugin;
|
|
21
84
|
|
|
22
|
-
export { type ExpressPluginOptions, expressPlugin, fromRequest };
|
|
85
|
+
export { type AnalyzeErrorPolicy, type ExpressNextFunction, type ExpressPluginConfig, type ExpressPluginOptions, type ExpressRequest, type ExpressRequestHandler, type ExpressRequestLike, type ExpressResponse, type GuardianMiddlewareOptions, analyzeRequest, expressBeforeAnalyze, expressPlugin, fromRequest, guardianMiddleware, resolveClientIp };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,85 @@
|
|
|
1
1
|
import * as guardian_risk from 'guardian-risk';
|
|
2
|
-
import { Plugin } from 'guardian-risk';
|
|
2
|
+
import { RiskReport, Guardian, Plugin } from 'guardian-risk';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Minimal Express request shape — no express import required at runtime. */
|
|
5
|
+
interface ExpressRequestLike {
|
|
6
|
+
readonly ip?: string;
|
|
7
|
+
readonly ips?: readonly string[];
|
|
8
|
+
readonly method?: string;
|
|
9
|
+
readonly path?: string;
|
|
10
|
+
readonly originalUrl?: string;
|
|
11
|
+
readonly headers: Record<string, string | string[] | undefined>;
|
|
12
|
+
readonly socket?: {
|
|
13
|
+
readonly remoteAddress?: string;
|
|
14
|
+
};
|
|
15
|
+
get?(name: string): string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Options for reading request signals. */
|
|
5
18
|
interface ExpressPluginOptions {
|
|
6
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Trust proxy-forwarded IPs (requires `app.set('trust proxy', ...)` in Express).
|
|
21
|
+
*/
|
|
7
22
|
readonly trustProxy?: boolean;
|
|
8
23
|
}
|
|
9
24
|
/**
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
25
|
+
* Extract HTTP request signals and attach them to a Guardian instance.
|
|
26
|
+
*/
|
|
27
|
+
declare function fromRequest(req: ExpressRequestLike, guardian: guardian_risk.Guardian, options?: ExpressPluginOptions): guardian_risk.Guardian;
|
|
28
|
+
/**
|
|
29
|
+
* Returns a beforeAnalyze hook that enriches signals from an Express request.
|
|
15
30
|
*/
|
|
16
|
-
declare function
|
|
31
|
+
declare function expressBeforeAnalyze(options?: ExpressPluginOptions): guardian_risk.BeforeAnalyzeHook<ExpressRequestLike>;
|
|
17
32
|
/**
|
|
18
|
-
*
|
|
33
|
+
* Resolve client IP with validation. Spoofed or invalid values are rejected.
|
|
34
|
+
*/
|
|
35
|
+
declare function resolveClientIp(req: ExpressRequestLike, trustProxy: boolean): string;
|
|
36
|
+
|
|
37
|
+
/** Express-compatible middleware types (optional peer). */
|
|
38
|
+
interface ExpressRequest extends ExpressRequestLike {
|
|
39
|
+
riskReport?: RiskReport;
|
|
40
|
+
guardian?: Guardian;
|
|
41
|
+
}
|
|
42
|
+
type ExpressNextFunction = (error?: unknown) => void;
|
|
43
|
+
interface ExpressResponse {
|
|
44
|
+
status(code: number): ExpressResponse;
|
|
45
|
+
json(body: unknown): void;
|
|
46
|
+
}
|
|
47
|
+
type ExpressRequestHandler = (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => void;
|
|
48
|
+
type AnalyzeErrorPolicy = 'pass' | 'block';
|
|
49
|
+
interface GuardianMiddlewareOptions extends ExpressPluginOptions {
|
|
50
|
+
readonly attachToRequest?: boolean;
|
|
51
|
+
/** Block when score is greater than or equal to this threshold. */
|
|
52
|
+
readonly blockAboveScore?: number;
|
|
53
|
+
/**
|
|
54
|
+
* When analysis fails (hook timeout, VPN error), `block` returns 503.
|
|
55
|
+
* Defaults to `block` when `blockAboveScore` is set, otherwise `pass`.
|
|
56
|
+
*/
|
|
57
|
+
readonly onAnalyzeError?: AnalyzeErrorPolicy;
|
|
58
|
+
/** Include score/reasons in block response (default: false). */
|
|
59
|
+
readonly exposeBlockDetails?: boolean;
|
|
60
|
+
}
|
|
61
|
+
declare function analyzeRequest(template: Guardian, req: ExpressRequestLike, options?: ExpressPluginOptions): Promise<{
|
|
62
|
+
guardian: Guardian;
|
|
63
|
+
report: RiskReport;
|
|
64
|
+
}>;
|
|
65
|
+
declare function guardianMiddleware(template: Guardian, options?: GuardianMiddlewareOptions): ExpressRequestHandler;
|
|
66
|
+
|
|
67
|
+
/** Options for the Express plugin. */
|
|
68
|
+
type ExpressPluginConfig = ExpressPluginOptions;
|
|
69
|
+
/**
|
|
70
|
+
* Express plugin — registers a beforeAnalyze hook for request signal collection.
|
|
71
|
+
*
|
|
72
|
+
* Usage with middleware (recommended):
|
|
73
|
+
* ```typescript
|
|
74
|
+
* app.use(guardianMiddleware(template, { trustProxy: true }));
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* Or manual:
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const g = template.fork().use(expressPlugin({ trustProxy: true }));
|
|
80
|
+
* const report = await g.analyzeAsync(req);
|
|
81
|
+
* ```
|
|
19
82
|
*/
|
|
20
|
-
declare function
|
|
83
|
+
declare function expressPlugin(options?: ExpressPluginConfig): Plugin;
|
|
21
84
|
|
|
22
|
-
export { type ExpressPluginOptions, expressPlugin, fromRequest };
|
|
85
|
+
export { type AnalyzeErrorPolicy, type ExpressNextFunction, type ExpressPluginConfig, type ExpressPluginOptions, type ExpressRequest, type ExpressRequestHandler, type ExpressRequestLike, type ExpressResponse, type GuardianMiddlewareOptions, analyzeRequest, expressBeforeAnalyze, expressPlugin, fromRequest, guardianMiddleware, resolveClientIp };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,134 @@
|
|
|
1
|
+
import { parseIpAddress } from 'guardian-risk';
|
|
2
|
+
|
|
3
|
+
// src/request.ts
|
|
4
|
+
var MAX_HEADER_LENGTH = 512;
|
|
5
|
+
function fromRequest(req, guardian, options = {}) {
|
|
6
|
+
const { trustProxy = false } = options;
|
|
7
|
+
const clientIp = resolveClientIp(req, trustProxy);
|
|
8
|
+
const userAgent = truncate(readHeader(req, "user-agent") ?? "unknown", MAX_HEADER_LENGTH);
|
|
9
|
+
const contentLength = Number(readHeader(req, "content-length") ?? 0);
|
|
10
|
+
const acceptLanguage = truncate(
|
|
11
|
+
readHeader(req, "accept-language") ?? "unknown",
|
|
12
|
+
MAX_HEADER_LENGTH
|
|
13
|
+
);
|
|
14
|
+
return guardian.signal("clientIp", clientIp).signal("userAgent", userAgent).signal("requestMethod", req.method ?? "UNKNOWN").signal("requestPath", truncate(req.path ?? req.originalUrl ?? "/", MAX_HEADER_LENGTH)).signal("contentLength", Number.isFinite(contentLength) ? contentLength : 0).signal("acceptLanguage", acceptLanguage).signal("requestSource", "express");
|
|
15
|
+
}
|
|
16
|
+
function expressBeforeAnalyze(options = {}) {
|
|
17
|
+
return ({ data: req, guardian }) => {
|
|
18
|
+
fromRequest(req, guardian, options);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function resolveClientIp(req, trustProxy) {
|
|
22
|
+
if (trustProxy) {
|
|
23
|
+
if (req.ips && req.ips.length > 0) {
|
|
24
|
+
for (const candidate of req.ips) {
|
|
25
|
+
const parsed = parseIpAddress(candidate);
|
|
26
|
+
if (parsed) {
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const forwarded = readHeader(req, "x-forwarded-for");
|
|
32
|
+
if (forwarded) {
|
|
33
|
+
for (const part of forwarded.split(",")) {
|
|
34
|
+
const parsed = parseIpAddress(part);
|
|
35
|
+
if (parsed) {
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const fromReqIp = req.ip ? parseIpAddress(req.ip) : null;
|
|
42
|
+
if (fromReqIp) {
|
|
43
|
+
return fromReqIp;
|
|
44
|
+
}
|
|
45
|
+
const fromSocket = req.socket?.remoteAddress ? parseIpAddress(req.socket.remoteAddress) : null;
|
|
46
|
+
if (fromSocket) {
|
|
47
|
+
return fromSocket;
|
|
48
|
+
}
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
function readHeader(req, name) {
|
|
52
|
+
const fromGetter = req.get?.(name);
|
|
53
|
+
if (fromGetter) {
|
|
54
|
+
return fromGetter;
|
|
55
|
+
}
|
|
56
|
+
const value = req.headers[name.toLowerCase()] ?? req.headers[name];
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return value[0];
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function truncate(value, max) {
|
|
63
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/middleware.ts
|
|
67
|
+
async function analyzeRequest(template, req, options = {}) {
|
|
68
|
+
const guardian = template.fork();
|
|
69
|
+
if (!template.getInstalledPlugins().includes("guardian-risk-express")) {
|
|
70
|
+
fromRequest(req, guardian, options);
|
|
71
|
+
}
|
|
72
|
+
const report = await guardian.analyzeAsync(req);
|
|
73
|
+
return { guardian, report };
|
|
74
|
+
}
|
|
75
|
+
function guardianMiddleware(template, options = {}) {
|
|
76
|
+
const {
|
|
77
|
+
attachToRequest = true,
|
|
78
|
+
blockAboveScore,
|
|
79
|
+
trustProxy = false,
|
|
80
|
+
exposeBlockDetails = false
|
|
81
|
+
} = options;
|
|
82
|
+
const onAnalyzeError = options.onAnalyzeError ?? (blockAboveScore !== void 0 ? "block" : "pass");
|
|
83
|
+
return (req, res, next) => {
|
|
84
|
+
void (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const guardian = template.fork();
|
|
87
|
+
if (!template.getInstalledPlugins().includes("guardian-risk-express")) {
|
|
88
|
+
fromRequest(req, guardian, { trustProxy });
|
|
89
|
+
}
|
|
90
|
+
const report = await guardian.analyzeAsync(req);
|
|
91
|
+
if (attachToRequest) {
|
|
92
|
+
req.guardian = guardian;
|
|
93
|
+
req.riskReport = report;
|
|
94
|
+
}
|
|
95
|
+
if (blockAboveScore !== void 0 && report.score >= blockAboveScore) {
|
|
96
|
+
sendBlocked(res, report, exposeBlockDetails);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
next();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (onAnalyzeError === "block") {
|
|
102
|
+
res.status(503).json({ error: "Risk analysis unavailable" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
next(error);
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function sendBlocked(res, report, exposeDetails) {
|
|
111
|
+
if (exposeDetails) {
|
|
112
|
+
res.status(403).json({
|
|
113
|
+
error: "Request blocked",
|
|
114
|
+
score: report.score,
|
|
115
|
+
level: report.level,
|
|
116
|
+
reasons: report.reasons
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
res.status(403).json({ error: "Request blocked" });
|
|
121
|
+
}
|
|
122
|
+
|
|
1
123
|
// src/index.ts
|
|
2
124
|
function expressPlugin(options = {}) {
|
|
3
|
-
const
|
|
125
|
+
const hook = expressBeforeAnalyze(options);
|
|
4
126
|
return {
|
|
5
127
|
name: "guardian-risk-express",
|
|
6
|
-
install(
|
|
128
|
+
install(guardian) {
|
|
129
|
+
guardian.beforeAnalyze(hook);
|
|
7
130
|
}
|
|
8
131
|
};
|
|
9
132
|
}
|
|
10
|
-
function fromRequest(_req, guardian) {
|
|
11
|
-
return guardian.signal("requestSource", "express").signal("expressPlugin", "stub");
|
|
12
|
-
}
|
|
13
133
|
|
|
14
|
-
export { expressPlugin, fromRequest };
|
|
134
|
+
export { analyzeRequest, expressBeforeAnalyze, expressPlugin, fromRequest, guardianMiddleware, resolveClientIp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardian-risk-express",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Express middleware plugin for guardian-risk — adds request signals",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"guardian-risk": "^0.
|
|
57
|
+
"guardian-risk": "^0.3.0",
|
|
58
58
|
"express": ">=4"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
@@ -65,10 +65,12 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"tsup": "^8.3.5",
|
|
67
67
|
"typescript": "^5.7.2",
|
|
68
|
-
"
|
|
68
|
+
"vitest": "^4.1.9",
|
|
69
|
+
"guardian-risk": "0.3.0"
|
|
69
70
|
},
|
|
70
71
|
"scripts": {
|
|
71
72
|
"build": "tsup",
|
|
73
|
+
"test": "vitest run",
|
|
72
74
|
"typecheck": "tsc --noEmit"
|
|
73
75
|
}
|
|
74
76
|
}
|