@sentinel-atl/hardening 0.3.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 +102 -0
- package/dist/audit-rotation.d.ts +33 -0
- package/dist/audit-rotation.d.ts.map +1 -0
- package/dist/audit-rotation.js +120 -0
- package/dist/audit-rotation.js.map +1 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +117 -0
- package/dist/auth.js.map +1 -0
- package/dist/cors.d.ts +34 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +86 -0
- package/dist/cors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/nonce-store.d.ts +50 -0
- package/dist/nonce-store.d.ts.map +1 -0
- package/dist/nonce-store.js +88 -0
- package/dist/nonce-store.js.map +1 -0
- package/dist/rate-limit.d.ts +55 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +116 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/security-headers.d.ts +36 -0
- package/dist/security-headers.d.ts.map +1 -0
- package/dist/security-headers.js +48 -0
- package/dist/security-headers.js.map +1 -0
- package/dist/tls.d.ts +33 -0
- package/dist/tls.d.ts.map +1 -0
- package/dist/tls.js +41 -0
- package/dist/tls.js.map +1 -0
- package/package.json +43 -0
- package/src/__tests__/hardening.test.ts +472 -0
- package/src/audit-rotation.ts +149 -0
- package/src/auth.ts +162 -0
- package/src/cors.ts +118 -0
- package/src/index.ts +62 -0
- package/src/nonce-store.ts +111 -0
- package/src/rate-limit.ts +141 -0
- package/src/security-headers.ts +79 -0
- package/src/tls.ts +66 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent nonce store — survives server restarts.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the SentinelStore interface to provide a Set-like API for nonce tracking.
|
|
5
|
+
* Nonces auto-expire based on intent expiry to prevent unbounded growth.
|
|
6
|
+
*/
|
|
7
|
+
import type { SentinelStore } from '@sentinel-atl/store';
|
|
8
|
+
export interface NonceStoreConfig {
|
|
9
|
+
/** Underlying persistent store */
|
|
10
|
+
store: SentinelStore;
|
|
11
|
+
/** Key prefix (default: 'nonce:') */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
/** Default TTL in seconds (default: 300 = 5 minutes) */
|
|
14
|
+
defaultTtl?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A Set<string>-compatible nonce tracker backed by persistent storage.
|
|
18
|
+
*
|
|
19
|
+
* Drop-in replacement for the in-memory Set<string> used in validateIntent().
|
|
20
|
+
* Nonces are stored with a TTL so they auto-expire and don't grow forever.
|
|
21
|
+
*/
|
|
22
|
+
export declare class NonceStore {
|
|
23
|
+
private store;
|
|
24
|
+
private prefix;
|
|
25
|
+
private defaultTtl;
|
|
26
|
+
constructor(config: NonceStoreConfig);
|
|
27
|
+
/** Check if a nonce has been seen. */
|
|
28
|
+
has(nonce: string): Promise<boolean>;
|
|
29
|
+
/** Mark a nonce as seen with optional TTL in seconds. */
|
|
30
|
+
add(nonce: string, ttlSeconds?: number): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Create a Set<string>-compatible adapter for use with validateIntent().
|
|
33
|
+
*
|
|
34
|
+
* Returns an object that looks like Set<string> with .has() and .add()
|
|
35
|
+
* but uses async persistent storage under the hood.
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT: The returned adapter makes .has() and .add() synchronous-looking
|
|
38
|
+
* by maintaining a local cache that's flushed asynchronously. For strict
|
|
39
|
+
* distributed replay protection, use the async methods directly.
|
|
40
|
+
*/
|
|
41
|
+
toSyncAdapter(): Set<string> & {
|
|
42
|
+
flush(): Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Pre-load nonces from the store into a local Set for synchronous checking.
|
|
46
|
+
* Useful at startup to restore state from a previous session.
|
|
47
|
+
*/
|
|
48
|
+
preload(): Promise<Set<string>>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=nonce-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce-store.d.ts","sourceRoot":"","sources":["../src/nonce-store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;;;;GAKG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;gBAEf,MAAM,EAAE,gBAAgB;IAMpC,sCAAsC;IAChC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI1C,yDAAyD;IACnD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5D;;;;;;;;;OASG;IACH,aAAa,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG;QAAE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE;IAuCzD;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;CAQtC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent nonce store — survives server restarts.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the SentinelStore interface to provide a Set-like API for nonce tracking.
|
|
5
|
+
* Nonces auto-expire based on intent expiry to prevent unbounded growth.
|
|
6
|
+
*/
|
|
7
|
+
// ─── Persistent Nonce Set ────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* A Set<string>-compatible nonce tracker backed by persistent storage.
|
|
10
|
+
*
|
|
11
|
+
* Drop-in replacement for the in-memory Set<string> used in validateIntent().
|
|
12
|
+
* Nonces are stored with a TTL so they auto-expire and don't grow forever.
|
|
13
|
+
*/
|
|
14
|
+
export class NonceStore {
|
|
15
|
+
store;
|
|
16
|
+
prefix;
|
|
17
|
+
defaultTtl;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.store = config.store;
|
|
20
|
+
this.prefix = config.prefix ?? 'nonce:';
|
|
21
|
+
this.defaultTtl = config.defaultTtl ?? 300;
|
|
22
|
+
}
|
|
23
|
+
/** Check if a nonce has been seen. */
|
|
24
|
+
async has(nonce) {
|
|
25
|
+
return this.store.has(this.prefix + nonce);
|
|
26
|
+
}
|
|
27
|
+
/** Mark a nonce as seen with optional TTL in seconds. */
|
|
28
|
+
async add(nonce, ttlSeconds) {
|
|
29
|
+
await this.store.set(this.prefix + nonce, '1', ttlSeconds ?? this.defaultTtl);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a Set<string>-compatible adapter for use with validateIntent().
|
|
33
|
+
*
|
|
34
|
+
* Returns an object that looks like Set<string> with .has() and .add()
|
|
35
|
+
* but uses async persistent storage under the hood.
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT: The returned adapter makes .has() and .add() synchronous-looking
|
|
38
|
+
* by maintaining a local cache that's flushed asynchronously. For strict
|
|
39
|
+
* distributed replay protection, use the async methods directly.
|
|
40
|
+
*/
|
|
41
|
+
toSyncAdapter() {
|
|
42
|
+
const self = this;
|
|
43
|
+
const pendingAdds = [];
|
|
44
|
+
const localCache = new Set();
|
|
45
|
+
const adapter = {
|
|
46
|
+
has(nonce) {
|
|
47
|
+
return localCache.has(nonce);
|
|
48
|
+
},
|
|
49
|
+
add(nonce) {
|
|
50
|
+
localCache.add(nonce);
|
|
51
|
+
pendingAdds.push({ nonce });
|
|
52
|
+
return adapter;
|
|
53
|
+
},
|
|
54
|
+
get size() {
|
|
55
|
+
return localCache.size;
|
|
56
|
+
},
|
|
57
|
+
async flush() {
|
|
58
|
+
for (const { nonce } of pendingAdds) {
|
|
59
|
+
await self.add(nonce);
|
|
60
|
+
}
|
|
61
|
+
pendingAdds.length = 0;
|
|
62
|
+
},
|
|
63
|
+
// Satisfy Set interface minimally
|
|
64
|
+
delete: (nonce) => localCache.delete(nonce),
|
|
65
|
+
clear: () => localCache.clear(),
|
|
66
|
+
forEach: (cb) => localCache.forEach(cb),
|
|
67
|
+
entries: () => localCache.entries(),
|
|
68
|
+
keys: () => localCache.keys(),
|
|
69
|
+
values: () => localCache.values(),
|
|
70
|
+
[Symbol.iterator]: () => localCache[Symbol.iterator](),
|
|
71
|
+
[Symbol.toStringTag]: 'NonceStore',
|
|
72
|
+
};
|
|
73
|
+
return adapter;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Pre-load nonces from the store into a local Set for synchronous checking.
|
|
77
|
+
* Useful at startup to restore state from a previous session.
|
|
78
|
+
*/
|
|
79
|
+
async preload() {
|
|
80
|
+
const keys = await this.store.keys(this.prefix);
|
|
81
|
+
const set = new Set();
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
set.add(key.slice(this.prefix.length));
|
|
84
|
+
}
|
|
85
|
+
return set;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=nonce-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce-store.js","sourceRoot":"","sources":["../src/nonce-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limit response headers per RFC 6585 / draft-ietf-httpapi-ratelimit-headers.
|
|
3
|
+
*
|
|
4
|
+
* Adds standard headers so clients know their quota status:
|
|
5
|
+
* RateLimit-Limit: total requests allowed per window
|
|
6
|
+
* RateLimit-Remaining: requests remaining in current window
|
|
7
|
+
* RateLimit-Reset: seconds until window resets
|
|
8
|
+
* Retry-After: seconds to wait (only on 429)
|
|
9
|
+
*/
|
|
10
|
+
import type { ServerResponse } from 'node:http';
|
|
11
|
+
export interface RateLimitInfo {
|
|
12
|
+
/** Maximum requests per window */
|
|
13
|
+
limit: number;
|
|
14
|
+
/** Remaining requests in current window */
|
|
15
|
+
remaining: number;
|
|
16
|
+
/** When the window resets (Unix timestamp in seconds) */
|
|
17
|
+
resetAt: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Set rate limit headers on a response.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setRateLimitHeaders(res: ServerResponse, info: RateLimitInfo): void;
|
|
23
|
+
/**
|
|
24
|
+
* Send a 429 Too Many Requests response with Retry-After header.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sendRateLimited(res: ServerResponse, info: RateLimitInfo): void;
|
|
27
|
+
/**
|
|
28
|
+
* Production-grade rate limiter with header support.
|
|
29
|
+
*/
|
|
30
|
+
export declare class RateLimiter {
|
|
31
|
+
private maxRequests;
|
|
32
|
+
private windowMs;
|
|
33
|
+
private windows;
|
|
34
|
+
constructor(maxRequests: number, windowMs: number);
|
|
35
|
+
/**
|
|
36
|
+
* Check if a request is allowed and return rate limit info.
|
|
37
|
+
*/
|
|
38
|
+
check(key: string): {
|
|
39
|
+
allowed: boolean;
|
|
40
|
+
info: RateLimitInfo;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Clean up expired windows to prevent memory leaks.
|
|
44
|
+
* Call periodically (e.g., every 5 minutes).
|
|
45
|
+
*/
|
|
46
|
+
cleanup(): number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse a rate limit spec like "100/min" into limiter parameters.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseRateLimit(spec: string): {
|
|
52
|
+
max: number;
|
|
53
|
+
windowMs: number;
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAIhD,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAMlF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAc9E;AAID;;GAEG;AACH,qBAAa,WAAW;IAIpB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,QAAQ;IAJlB,OAAO,CAAC,OAAO,CAAyD;gBAG9D,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM;IAG1B;;OAEG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAE;IAuC7D;;;OAGG;IACH,OAAO,IAAI,MAAM;CAWlB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAU9E"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limit response headers per RFC 6585 / draft-ietf-httpapi-ratelimit-headers.
|
|
3
|
+
*
|
|
4
|
+
* Adds standard headers so clients know their quota status:
|
|
5
|
+
* RateLimit-Limit: total requests allowed per window
|
|
6
|
+
* RateLimit-Remaining: requests remaining in current window
|
|
7
|
+
* RateLimit-Reset: seconds until window resets
|
|
8
|
+
* Retry-After: seconds to wait (only on 429)
|
|
9
|
+
*/
|
|
10
|
+
// ─── Header Application ──────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Set rate limit headers on a response.
|
|
13
|
+
*/
|
|
14
|
+
export function setRateLimitHeaders(res, info) {
|
|
15
|
+
const retryAfter = Math.max(0, Math.ceil(info.resetAt - Date.now() / 1000));
|
|
16
|
+
res.setHeader('RateLimit-Limit', String(info.limit));
|
|
17
|
+
res.setHeader('RateLimit-Remaining', String(Math.max(0, info.remaining)));
|
|
18
|
+
res.setHeader('RateLimit-Reset', String(retryAfter));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Send a 429 Too Many Requests response with Retry-After header.
|
|
22
|
+
*/
|
|
23
|
+
export function sendRateLimited(res, info) {
|
|
24
|
+
const retryAfter = Math.max(1, Math.ceil(info.resetAt - Date.now() / 1000));
|
|
25
|
+
res.writeHead(429, {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'Retry-After': String(retryAfter),
|
|
28
|
+
'RateLimit-Limit': String(info.limit),
|
|
29
|
+
'RateLimit-Remaining': '0',
|
|
30
|
+
'RateLimit-Reset': String(retryAfter),
|
|
31
|
+
});
|
|
32
|
+
res.end(JSON.stringify({
|
|
33
|
+
error: 'Too Many Requests',
|
|
34
|
+
retryAfter,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
// ─── Enhanced Rate Limiter ──────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Production-grade rate limiter with header support.
|
|
40
|
+
*/
|
|
41
|
+
export class RateLimiter {
|
|
42
|
+
maxRequests;
|
|
43
|
+
windowMs;
|
|
44
|
+
windows = new Map();
|
|
45
|
+
constructor(maxRequests, windowMs) {
|
|
46
|
+
this.maxRequests = maxRequests;
|
|
47
|
+
this.windowMs = windowMs;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if a request is allowed and return rate limit info.
|
|
51
|
+
*/
|
|
52
|
+
check(key) {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const entry = this.windows.get(key);
|
|
55
|
+
if (!entry || now >= entry.resetAt) {
|
|
56
|
+
const resetAt = now + this.windowMs;
|
|
57
|
+
this.windows.set(key, { count: 1, resetAt });
|
|
58
|
+
return {
|
|
59
|
+
allowed: true,
|
|
60
|
+
info: {
|
|
61
|
+
limit: this.maxRequests,
|
|
62
|
+
remaining: this.maxRequests - 1,
|
|
63
|
+
resetAt: Math.ceil(resetAt / 1000),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (entry.count >= this.maxRequests) {
|
|
68
|
+
return {
|
|
69
|
+
allowed: false,
|
|
70
|
+
info: {
|
|
71
|
+
limit: this.maxRequests,
|
|
72
|
+
remaining: 0,
|
|
73
|
+
resetAt: Math.ceil(entry.resetAt / 1000),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
entry.count++;
|
|
78
|
+
return {
|
|
79
|
+
allowed: true,
|
|
80
|
+
info: {
|
|
81
|
+
limit: this.maxRequests,
|
|
82
|
+
remaining: this.maxRequests - entry.count,
|
|
83
|
+
resetAt: Math.ceil(entry.resetAt / 1000),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clean up expired windows to prevent memory leaks.
|
|
89
|
+
* Call periodically (e.g., every 5 minutes).
|
|
90
|
+
*/
|
|
91
|
+
cleanup() {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
let removed = 0;
|
|
94
|
+
for (const [key, entry] of this.windows) {
|
|
95
|
+
if (now >= entry.resetAt) {
|
|
96
|
+
this.windows.delete(key);
|
|
97
|
+
removed++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return removed;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse a rate limit spec like "100/min" into limiter parameters.
|
|
105
|
+
*/
|
|
106
|
+
export function parseRateLimit(spec) {
|
|
107
|
+
const match = spec.match(/^(\d+)\/(min|hour|day)$/);
|
|
108
|
+
if (!match)
|
|
109
|
+
return { max: 100, windowMs: 60_000 };
|
|
110
|
+
const max = parseInt(match[1]);
|
|
111
|
+
const windowMs = match[2] === 'min' ? 60_000
|
|
112
|
+
: match[2] === 'hour' ? 3_600_000
|
|
113
|
+
: 86_400_000;
|
|
114
|
+
return { max, windowMs };
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Response Headers — standard HTTP security headers for all Sentinel servers.
|
|
3
|
+
*
|
|
4
|
+
* Sets headers recommended by OWASP:
|
|
5
|
+
* X-Content-Type-Options: nosniff
|
|
6
|
+
* X-Frame-Options: DENY
|
|
7
|
+
* X-XSS-Protection: 0 (modern browsers use CSP instead)
|
|
8
|
+
* Content-Security-Policy: default-src 'none'
|
|
9
|
+
* Strict-Transport-Security: max-age=31536000; includeSubDomains (when TLS)
|
|
10
|
+
* Referrer-Policy: strict-origin-when-cross-origin
|
|
11
|
+
* Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
12
|
+
*/
|
|
13
|
+
import type { ServerResponse } from 'node:http';
|
|
14
|
+
export interface SecurityHeadersConfig {
|
|
15
|
+
/** Whether to add HSTS header (only makes sense over TLS) */
|
|
16
|
+
hsts?: boolean;
|
|
17
|
+
/** HSTS max-age in seconds (default: 31536000 = 1 year) */
|
|
18
|
+
hstsMaxAge?: number;
|
|
19
|
+
/** Custom Content-Security-Policy (default: "default-src 'none'") */
|
|
20
|
+
contentSecurityPolicy?: string;
|
|
21
|
+
/** Custom Permissions-Policy */
|
|
22
|
+
permissionsPolicy?: string;
|
|
23
|
+
/** Whether to add X-Frame-Options (default: true) */
|
|
24
|
+
frameOptions?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Apply standard security headers to a response.
|
|
28
|
+
*/
|
|
29
|
+
export declare function applySecurityHeaders(res: ServerResponse, config?: SecurityHeadersConfig): void;
|
|
30
|
+
/**
|
|
31
|
+
* Create a security headers config from environment variables.
|
|
32
|
+
*
|
|
33
|
+
* SENTINEL_HSTS=true (enabled when TLS is enabled)
|
|
34
|
+
*/
|
|
35
|
+
export declare function securityHeadersConfigFromEnv(): SecurityHeadersConfig;
|
|
36
|
+
//# sourceMappingURL=security-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gCAAgC;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,MAAM,CAAC,EAAE,qBAAqB,GAC7B,IAAI,CAgCN;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,IAAI,qBAAqB,CAKpE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Response Headers — standard HTTP security headers for all Sentinel servers.
|
|
3
|
+
*
|
|
4
|
+
* Sets headers recommended by OWASP:
|
|
5
|
+
* X-Content-Type-Options: nosniff
|
|
6
|
+
* X-Frame-Options: DENY
|
|
7
|
+
* X-XSS-Protection: 0 (modern browsers use CSP instead)
|
|
8
|
+
* Content-Security-Policy: default-src 'none'
|
|
9
|
+
* Strict-Transport-Security: max-age=31536000; includeSubDomains (when TLS)
|
|
10
|
+
* Referrer-Policy: strict-origin-when-cross-origin
|
|
11
|
+
* Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Apply standard security headers to a response.
|
|
15
|
+
*/
|
|
16
|
+
export function applySecurityHeaders(res, config) {
|
|
17
|
+
// Prevent MIME type sniffing
|
|
18
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
19
|
+
// Disable legacy XSS filter (CSP is the modern approach)
|
|
20
|
+
res.setHeader('X-XSS-Protection', '0');
|
|
21
|
+
// Content Security Policy
|
|
22
|
+
res.setHeader('Content-Security-Policy', config?.contentSecurityPolicy ?? "default-src 'none'");
|
|
23
|
+
// Clickjacking protection
|
|
24
|
+
if (config?.frameOptions !== false) {
|
|
25
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
26
|
+
}
|
|
27
|
+
// Referrer policy
|
|
28
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
29
|
+
// Permissions policy
|
|
30
|
+
res.setHeader('Permissions-Policy', config?.permissionsPolicy ?? 'camera=(), microphone=(), geolocation=()');
|
|
31
|
+
// HSTS (only over TLS)
|
|
32
|
+
if (config?.hsts) {
|
|
33
|
+
const maxAge = config.hstsMaxAge ?? 31_536_000;
|
|
34
|
+
res.setHeader('Strict-Transport-Security', `max-age=${maxAge}; includeSubDomains`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a security headers config from environment variables.
|
|
39
|
+
*
|
|
40
|
+
* SENTINEL_HSTS=true (enabled when TLS is enabled)
|
|
41
|
+
*/
|
|
42
|
+
export function securityHeadersConfigFromEnv() {
|
|
43
|
+
return {
|
|
44
|
+
hsts: process.env['SENTINEL_HSTS'] === 'true' ||
|
|
45
|
+
!!process.env['SENTINEL_TLS_CERT'],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=security-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiBH;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAmB,EACnB,MAA8B;IAE9B,6BAA6B;IAC7B,GAAG,CAAC,SAAS,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;IAEnD,yDAAyD;IACzD,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IAEvC,0BAA0B;IAC1B,GAAG,CAAC,SAAS,CACX,yBAAyB,EACzB,MAAM,EAAE,qBAAqB,IAAI,oBAAoB,CACtD,CAAC;IAEF,0BAA0B;IAC1B,IAAI,MAAM,EAAE,YAAY,KAAK,KAAK,EAAE,CAAC;QACnC,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;IAED,kBAAkB;IAClB,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,iCAAiC,CAAC,CAAC;IAEpE,qBAAqB;IACrB,GAAG,CAAC,SAAS,CACX,oBAAoB,EACpB,MAAM,EAAE,iBAAiB,IAAI,0CAA0C,CACxE,CAAC;IAEF,uBAAuB;IACvB,IAAI,MAAM,EAAE,IAAI,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,IAAI,UAAU,CAAC;QAC/C,GAAG,CAAC,SAAS,CAAC,2BAA2B,EAAE,WAAW,MAAM,qBAAqB,CAAC,CAAC;IACrF,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B;IAC1C,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,MAAM;YACvC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;KACzC,CAAC;AACJ,CAAC"}
|
package/dist/tls.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS support — wraps HTTP servers with HTTPS using Node.js native TLS.
|
|
3
|
+
*
|
|
4
|
+
* Reads cert/key from files or environment variables:
|
|
5
|
+
* SENTINEL_TLS_CERT — path to PEM certificate
|
|
6
|
+
* SENTINEL_TLS_KEY — path to PEM private key
|
|
7
|
+
*/
|
|
8
|
+
import { type Server as HttpsServer } from 'node:https';
|
|
9
|
+
import { type Server as HttpServer, type RequestListener } from 'node:http';
|
|
10
|
+
export interface TlsConfig {
|
|
11
|
+
/** Whether TLS is enabled */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Path to PEM certificate file */
|
|
14
|
+
certPath?: string;
|
|
15
|
+
/** Path to PEM private key file */
|
|
16
|
+
keyPath?: string;
|
|
17
|
+
/** PEM certificate string (alternative to certPath) */
|
|
18
|
+
cert?: string;
|
|
19
|
+
/** PEM private key string (alternative to keyPath) */
|
|
20
|
+
key?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create an HTTP or HTTPS server based on TLS configuration.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createSecureServer(handler: RequestListener, tls?: TlsConfig): HttpServer | HttpsServer;
|
|
26
|
+
/**
|
|
27
|
+
* Create TLS config from environment variables.
|
|
28
|
+
*
|
|
29
|
+
* SENTINEL_TLS_CERT=./certs/server.crt
|
|
30
|
+
* SENTINEL_TLS_KEY=./certs/server.key
|
|
31
|
+
*/
|
|
32
|
+
export declare function tlsConfigFromEnv(): TlsConfig;
|
|
33
|
+
//# sourceMappingURL=tls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tls.d.ts","sourceRoot":"","sources":["../src/tls.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAqC,KAAK,MAAM,IAAI,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3F,OAAO,EAAoC,KAAK,MAAM,IAAI,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAK9G,MAAM,WAAW,SAAS;IACxB,6BAA6B;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAID;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,eAAe,EACxB,GAAG,CAAC,EAAE,SAAS,GACd,UAAU,GAAG,WAAW,CAa1B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,SAAS,CAS5C"}
|
package/dist/tls.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS support — wraps HTTP servers with HTTPS using Node.js native TLS.
|
|
3
|
+
*
|
|
4
|
+
* Reads cert/key from files or environment variables:
|
|
5
|
+
* SENTINEL_TLS_CERT — path to PEM certificate
|
|
6
|
+
* SENTINEL_TLS_KEY — path to PEM private key
|
|
7
|
+
*/
|
|
8
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
9
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
// ─── Factory ──────────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Create an HTTP or HTTPS server based on TLS configuration.
|
|
14
|
+
*/
|
|
15
|
+
export function createSecureServer(handler, tls) {
|
|
16
|
+
if (!tls?.enabled) {
|
|
17
|
+
return createHttpServer(handler);
|
|
18
|
+
}
|
|
19
|
+
const cert = tls.cert ?? (tls.certPath ? readFileSync(tls.certPath, 'utf-8') : undefined);
|
|
20
|
+
const key = tls.key ?? (tls.keyPath ? readFileSync(tls.keyPath, 'utf-8') : undefined);
|
|
21
|
+
if (!cert || !key) {
|
|
22
|
+
throw new Error('TLS enabled but no certificate/key provided. Set certPath/keyPath or cert/key.');
|
|
23
|
+
}
|
|
24
|
+
return createHttpsServer({ cert, key }, handler);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create TLS config from environment variables.
|
|
28
|
+
*
|
|
29
|
+
* SENTINEL_TLS_CERT=./certs/server.crt
|
|
30
|
+
* SENTINEL_TLS_KEY=./certs/server.key
|
|
31
|
+
*/
|
|
32
|
+
export function tlsConfigFromEnv() {
|
|
33
|
+
const certPath = process.env['SENTINEL_TLS_CERT'];
|
|
34
|
+
const keyPath = process.env['SENTINEL_TLS_KEY'];
|
|
35
|
+
return {
|
|
36
|
+
enabled: !!(certPath && keyPath),
|
|
37
|
+
certPath,
|
|
38
|
+
keyPath,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=tls.js.map
|
package/dist/tls.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tls.js","sourceRoot":"","sources":["../src/tls.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,IAAI,iBAAiB,EAA8B,MAAM,YAAY,CAAC;AAC3F,OAAO,EAAE,YAAY,IAAI,gBAAgB,EAAmD,MAAM,WAAW,CAAC;AAC9G,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAiBvC,yEAAyE;AAEzE;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAwB,EACxB,GAAe;IAEf,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC;QAClB,OAAO,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC1F,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAEtF,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,gFAAgF,CAAC,CAAC;IACpG,CAAC;IAED,OAAO,iBAAiB,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAEhD,OAAO;QACL,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,OAAO,CAAC;QAChC,QAAQ;QACR,OAAO;KACR,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentinel-atl/hardening",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Production hardening middleware — API key auth, CORS, TLS, rate-limit headers, nonce persistence, audit rotation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"lint": "tsc --noEmit",
|
|
18
|
+
"clean": "rm -rf dist"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@sentinel-atl/store": "*"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"vitest": "^3.2.4",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"@types/node": "^20.0.0"
|
|
27
|
+
},
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/sentinel-atl/project-sentinel.git",
|
|
32
|
+
"directory": "packages/hardening"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"sentinel",
|
|
36
|
+
"security",
|
|
37
|
+
"hardening",
|
|
38
|
+
"auth",
|
|
39
|
+
"cors",
|
|
40
|
+
"tls",
|
|
41
|
+
"production"
|
|
42
|
+
]
|
|
43
|
+
}
|