@chdlabs/my-shield 0.1.0 â 0.1.1
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 +84 -6
- package/package.json +5 -4
- package/src/admin/adminRouter.js +13 -0
- package/src/admin/dashboard/dashboard.css +20 -0
- package/src/admin/dashboard/dashboard.js +54 -1
- package/src/index.d.ts +6 -0
- package/src/index.js +4 -0
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- ð **404 Anomaly & Intelligence**: Captures every 404 URL probe, bans scanners probing $\ge 5$ unique paths in 60s, and auto-promotes frequent attack targets to active honeypots.
|
|
17
17
|
- ðū **Single-File Deduplicated Persistence**: Preserves learned probe patterns and promoted honeypots across process restarts in an atomic JSON file (`404_intelligence.json`).
|
|
18
18
|
- ð **Self-Contained Security Dashboard**: Built-in dark-themed telemetry console with real-time Server-Sent Events (SSE), charts, and active block management.
|
|
19
|
+
- ð **Safe By Default**: Admin dashboard is disabled by default (`admin.enabled: false`) with dedicated RBAC integration to prevent unauthorized telemetry access.
|
|
19
20
|
- ðŠķ **Zero Production Dependencies**: Pure JavaScript built exclusively with Node.js built-ins (`crypto`, `fs`, `path`, `events`).
|
|
20
21
|
|
|
21
22
|
---
|
|
@@ -70,6 +71,10 @@ import { createShield } from '@chdlabs/my-shield';
|
|
|
70
71
|
const app = express();
|
|
71
72
|
|
|
72
73
|
const shield = createShield({
|
|
74
|
+
whitelist: ['127.0.0.1', '::1'],
|
|
75
|
+
admin: {
|
|
76
|
+
enabled: true // Set to true to activate dashboard & telemetry (Default: false)
|
|
77
|
+
},
|
|
73
78
|
bloom: {
|
|
74
79
|
expectedItems: 100_000,
|
|
75
80
|
falsePositiveRate: 0.01
|
|
@@ -113,8 +118,10 @@ app.get('/api/data', (req, res) => res.json({ status: 'ok' }));
|
|
|
113
118
|
// 4. Mount 404 intelligence collector (AFTER application routes)
|
|
114
119
|
app.use(shield.notFoundCollector());
|
|
115
120
|
|
|
116
|
-
// 5. Mount admin dashboard (protect with your auth middleware)
|
|
117
|
-
app.use('/admin/shield',
|
|
121
|
+
// 5. Mount admin dashboard (protect API endpoints with your app's auth & RBAC middleware)
|
|
122
|
+
app.use('/admin/shield', shield.adminRouter({
|
|
123
|
+
authMiddleware: [authenticateUser, requireAdminRole]
|
|
124
|
+
}));
|
|
118
125
|
|
|
119
126
|
// Graceful shutdown: flushes 404 intelligence to disk
|
|
120
127
|
process.on('SIGTERM', async () => {
|
|
@@ -125,11 +132,83 @@ process.on('SIGTERM', async () => {
|
|
|
125
132
|
|
|
126
133
|
---
|
|
127
134
|
|
|
135
|
+
## ð Admin Dashboard & Role-Based Access Control (RBAC)
|
|
136
|
+
|
|
137
|
+
The built-in admin dashboard provides live telemetry, attack velocity, probe intelligence, active threat management, and real-time Server-Sent Events (SSE).
|
|
138
|
+
|
|
139
|
+
### 1. Enabling the Dashboard
|
|
140
|
+
By default, the admin dashboard is **disabled** (`admin.enabled: false`). If mounted while disabled, `/admin/shield` safely returns `404 Not Found`. To enable it, explicitly configure:
|
|
141
|
+
```javascript
|
|
142
|
+
const shield = createShield({
|
|
143
|
+
admin: {
|
|
144
|
+
enabled: true // Required to activate dashboard routes
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 2. Restricting Access to Administrators Only
|
|
150
|
+
Do **not** wrap `shield.adminRouter()` at the top-level route (e.g., `app.use('/admin/shield', requireAdmin, ...)`). Doing so causes browser page visits to fail with an immediate `401 Unauthorized` JSON response because web browsers do not send `Authorization` headers on initial page navigations.
|
|
151
|
+
|
|
152
|
+
Instead, pass your authentication and authorization middleware into `shield.adminRouter({ authMiddleware })`:
|
|
153
|
+
```javascript
|
|
154
|
+
app.use('/admin/shield', shield.adminRouter({
|
|
155
|
+
authMiddleware: [authenticateUser, requireAdminRole]
|
|
156
|
+
}));
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Why this pattern is secure:
|
|
160
|
+
1. **Public Static Delivery**: The browser can freely download the dashboard SPA shell (`index.html`, `dashboard.css`, `dashboard.js`).
|
|
161
|
+
2. **Protected API & Telemetry**: All data-bearing endpoints (`/admin/shield/api/stats`, `/admin/shield/api/blocked`, `/admin/shield/api/stream`, etc.) are strictly guarded by your `authMiddleware`.
|
|
162
|
+
3. **Graceful UI Feedback**: If a non-admin or unauthenticated visitor accesses the page, the dashboard renders an informative warning banner rather than a raw, broken JSON error.
|
|
163
|
+
|
|
164
|
+
### 3. Complete Express RBAC Example
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
// Step A: Authentication middleware (verifies JWT / Bearer token)
|
|
168
|
+
function authenticateUser(req, res, next) {
|
|
169
|
+
const authHeader = req.headers.authorization || (req.query?.token ? `Bearer ${req.query.token}` : null);
|
|
170
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
171
|
+
return res.status(401).json({ error: 'Authentication required. Missing Authorization header.' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const token = authHeader.split(' ')[1];
|
|
175
|
+
try {
|
|
176
|
+
const claims = verifyJwt(token); // Verify your JWT / PQC token
|
|
177
|
+
req.user = claims;
|
|
178
|
+
next();
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return res.status(401).json({ error: 'Invalid or expired session token.' });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step B: Role-Based Access Control (RBAC) middleware
|
|
185
|
+
function requireAdminRole(req, res, next) {
|
|
186
|
+
// Check user role from token claims or database lookup
|
|
187
|
+
if (!req.user || (req.user.role !== 'admin' && req.user.role !== 'super_admin')) {
|
|
188
|
+
return res.status(403).json({ error: 'Access denied: Administrator privileges required.' });
|
|
189
|
+
}
|
|
190
|
+
next();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Step C: Mount protected admin router
|
|
194
|
+
app.use('/admin/shield', shield.adminRouter({
|
|
195
|
+
authMiddleware: [authenticateUser, requireAdminRole]
|
|
196
|
+
}));
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 4. How the Dashboard Authenticates
|
|
200
|
+
The dashboard client automatically looks for the session token in the following order:
|
|
201
|
+
1. **URL Query Parameter**: Navigating to `http://your-server.com/admin/shield?token=<jwt>` extracts the token into `sessionStorage` and automatically scrubs the parameter from the browser's address bar to prevent token leakage.
|
|
202
|
+
2. **Storage Fallback**: Checks `sessionStorage` and `localStorage` under standard keys: `shield_session_token`, `pqc_session_token`, `token`, `access_token`, `auth_token`, or `jwt`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
128
206
|
## Configuration Reference
|
|
129
207
|
|
|
130
208
|
| Option | Type | Default | Description |
|
|
131
209
|
|---|---|---|---|
|
|
132
210
|
| `whitelist` | `string[]` | `[]` | List of IP addresses that are never blocked |
|
|
211
|
+
| `admin.enabled` | `boolean` | `false` | Explicit switch to enable the admin dashboard & telemetry |
|
|
133
212
|
| `bloom.expectedItems` | `number` | `100000` | Estimated distinct IPs to hold in filter |
|
|
134
213
|
| `bloom.falsePositiveRate` | `number` | `0.01` | Target false positive probability (1%) |
|
|
135
214
|
| `bloom.sizeBytes` | `number` | `null` | Explicit bit array size in bytes |
|
|
@@ -183,12 +262,11 @@ To publish `@chdlabs/my-shield` to npm:
|
|
|
183
262
|
```bash
|
|
184
263
|
npm login
|
|
185
264
|
```
|
|
186
|
-
2. Navigate to the `sdk` directory and publish:
|
|
265
|
+
2. Navigate to the `sdk-shield` directory and publish:
|
|
187
266
|
```bash
|
|
188
|
-
cd sdk
|
|
189
|
-
npm publish
|
|
267
|
+
cd sdk-shield
|
|
268
|
+
npm publish --access public
|
|
190
269
|
```
|
|
191
|
-
*(Note: Since `@chdlabs` is a scoped package, `"publishConfig": { "access": "public" }` is pre-configured in `package.json`).*
|
|
192
270
|
|
|
193
271
|
---
|
|
194
272
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chdlabs/my-shield",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Zero-latency security shield middleware with Bloom filter, honeypots, and 404 intelligence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"express": ">=4.18.0 || >=5.0.0"
|
|
20
20
|
},
|
|
21
|
-
"
|
|
22
|
-
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"express": "^5.2.1"
|
|
23
|
+
},
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": ">=20.0.0"
|
|
25
26
|
},
|
|
@@ -42,4 +43,4 @@
|
|
|
42
43
|
"scripts": {
|
|
43
44
|
"test": "node test/integration.test.js"
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
|
+
}
|
package/src/admin/adminRouter.js
CHANGED
|
@@ -15,6 +15,19 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
16
16
|
|
|
17
17
|
export function createAdminRouter(shield, options = {}) {
|
|
18
|
+
const isEnabled = options.enabled !== undefined ? Boolean(options.enabled) : Boolean(shield?.adminEnabled);
|
|
19
|
+
if (!isEnabled) {
|
|
20
|
+
shield?.logger?.warn?.('[SHIELD] Admin dashboard router is mounted, but admin is disabled in configuration. Enable via createShield({ admin: { enabled: true } }) or shield.adminRouter({ enabled: true })');
|
|
21
|
+
const disabledRouter = express.Router();
|
|
22
|
+
disabledRouter.use((req, res) => {
|
|
23
|
+
res.status(404).json({
|
|
24
|
+
error: 'Shield admin dashboard is disabled in configuration.',
|
|
25
|
+
hint: 'Enable via createShield({ admin: { enabled: true } }) or shield.adminRouter({ enabled: true })'
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
return disabledRouter;
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
const router = express.Router();
|
|
19
32
|
const dashboardDir = path.resolve(__dirname, 'dashboard');
|
|
20
33
|
const authMiddleware = Array.isArray(options.authMiddleware) ? options.authMiddleware : (options.authMiddleware ? [options.authMiddleware] : []);
|
|
@@ -532,3 +532,23 @@ body.shield-body {
|
|
|
532
532
|
from { transform: translateX(20px); opacity: 0; }
|
|
533
533
|
to { transform: translateX(0); opacity: 1; }
|
|
534
534
|
}
|
|
535
|
+
|
|
536
|
+
/* Auth Challenge Banners */
|
|
537
|
+
.auth-banner {
|
|
538
|
+
padding: 0.85rem 1.25rem;
|
|
539
|
+
margin-bottom: 1.5rem;
|
|
540
|
+
border-radius: var(--radius-md);
|
|
541
|
+
font-size: 0.875rem;
|
|
542
|
+
line-height: 1.4;
|
|
543
|
+
font-weight: 500;
|
|
544
|
+
}
|
|
545
|
+
.auth-banner-unauth {
|
|
546
|
+
background-color: rgba(234, 179, 8, 0.15);
|
|
547
|
+
border: 1px solid rgba(234, 179, 8, 0.4);
|
|
548
|
+
color: #fde047;
|
|
549
|
+
}
|
|
550
|
+
.auth-banner-forbidden {
|
|
551
|
+
background-color: rgba(239, 68, 68, 0.15);
|
|
552
|
+
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
553
|
+
color: #fca5a5;
|
|
554
|
+
}
|
|
@@ -49,8 +49,57 @@
|
|
|
49
49
|
}, 4000);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function setAuthBanner(type, message) {
|
|
53
|
+
let banner = document.getElementById('shield-auth-banner');
|
|
54
|
+
if (!banner) {
|
|
55
|
+
banner = document.createElement('div');
|
|
56
|
+
banner.id = 'shield-auth-banner';
|
|
57
|
+
const main = document.querySelector('.main-content');
|
|
58
|
+
if (main) {
|
|
59
|
+
main.insertBefore(banner, main.firstChild);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (banner) {
|
|
63
|
+
banner.className = `auth-banner ${type === '403' ? 'auth-banner-forbidden' : 'auth-banner-unauth'}`;
|
|
64
|
+
banner.textContent = message;
|
|
65
|
+
banner.style.display = 'block';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clearAuthBanner() {
|
|
70
|
+
const banner = document.getElementById('shield-auth-banner');
|
|
71
|
+
if (banner) {
|
|
72
|
+
banner.style.display = 'none';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
function getStoredToken() {
|
|
53
|
-
|
|
77
|
+
if (typeof window === 'undefined') return null;
|
|
78
|
+
|
|
79
|
+
// 1. Check URL query params for ?token=... or ?auth=...
|
|
80
|
+
try {
|
|
81
|
+
const params = new URLSearchParams(window.location.search);
|
|
82
|
+
const queryToken = params.get('token') || params.get('auth');
|
|
83
|
+
if (queryToken) {
|
|
84
|
+
sessionStorage.setItem('shield_session_token', queryToken);
|
|
85
|
+
params.delete('token');
|
|
86
|
+
params.delete('auth');
|
|
87
|
+
const cleanQuery = params.toString() ? `?${params.toString()}` : '';
|
|
88
|
+
window.history.replaceState({}, '', window.location.pathname + cleanQuery + window.location.hash);
|
|
89
|
+
return queryToken;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore URL parsing errors
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Check session and local storage with multi-key fallback
|
|
96
|
+
return sessionStorage.getItem('shield_session_token')
|
|
97
|
+
|| localStorage.getItem('shield_session_token')
|
|
98
|
+
|| localStorage.getItem('pqc_session_token')
|
|
99
|
+
|| localStorage.getItem('token')
|
|
100
|
+
|| localStorage.getItem('access_token')
|
|
101
|
+
|| localStorage.getItem('auth_token')
|
|
102
|
+
|| localStorage.getItem('jwt');
|
|
54
103
|
}
|
|
55
104
|
|
|
56
105
|
async function apiFetch(url, options = {}) {
|
|
@@ -61,9 +110,13 @@
|
|
|
61
110
|
}
|
|
62
111
|
const res = await fetch(url, { ...options, headers });
|
|
63
112
|
if (res.status === 401) {
|
|
113
|
+
setAuthBanner('401', 'â ïļ Authentication required: Please log in to the application with an administrator account to access telemetry and management APIs.');
|
|
64
114
|
showToast('Authentication required. Please login with an admin account.', 'error');
|
|
65
115
|
} else if (res.status === 403) {
|
|
116
|
+
setAuthBanner('403', 'ðŦ Access denied: Administrator privileges required for this account.');
|
|
66
117
|
showToast('Access denied: Administrator privileges required.', 'error');
|
|
118
|
+
} else if (res.ok) {
|
|
119
|
+
clearAuthBanner();
|
|
67
120
|
}
|
|
68
121
|
return res;
|
|
69
122
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -43,12 +43,17 @@ export interface IntelligenceOptions {
|
|
|
43
43
|
autoPromote?: AutoPromoteOptions;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface AdminOptions {
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
export interface ShieldOptions {
|
|
47
51
|
whitelist?: string[];
|
|
48
52
|
bloom?: BloomOptions;
|
|
49
53
|
lru?: LRUOptions;
|
|
50
54
|
honeypot?: HoneypotOptions;
|
|
51
55
|
intelligence?: IntelligenceOptions;
|
|
56
|
+
admin?: AdminOptions;
|
|
52
57
|
blockStatusCode?: number;
|
|
53
58
|
logger?: {
|
|
54
59
|
info?: (...args: any[]) => void;
|
|
@@ -64,6 +69,7 @@ export interface CostThrottleOptions {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
export interface AdminRouterOptions {
|
|
72
|
+
enabled?: boolean;
|
|
67
73
|
authMiddleware?: RequestHandler | RequestHandler[];
|
|
68
74
|
}
|
|
69
75
|
|
package/src/index.js
CHANGED
|
@@ -121,6 +121,10 @@ export class Shield extends EventEmitter {
|
|
|
121
121
|
shield: this,
|
|
122
122
|
logger: this.logger
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
// 8. Admin Dashboard Configuration (Default: false)
|
|
126
|
+
const adminOpts = options.admin || {};
|
|
127
|
+
this.adminEnabled = Boolean(adminOpts.enabled);
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/**
|