@donotdev/security 0.0.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 +201 -0
- package/dist/client/HealthMonitor.d.ts +85 -0
- package/dist/client/HealthMonitor.d.ts.map +1 -0
- package/dist/client/HealthMonitor.js +1 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +1 -0
- package/dist/common/AuthHardening.d.ts +3 -0
- package/dist/common/AuthHardening.d.ts.map +1 -0
- package/dist/common/AuthHardening.js +1 -0
- package/dist/common/SecurityConfig.d.ts +11 -0
- package/dist/common/SecurityConfig.d.ts.map +1 -0
- package/dist/common/SecurityConfig.js +0 -0
- package/dist/common/index.d.ts +2 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +0 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/server/AnomalyDetector.d.ts +68 -0
- package/dist/server/AnomalyDetector.d.ts.map +1 -0
- package/dist/server/AnomalyDetector.js +2 -0
- package/dist/server/AuditLogger.d.ts +40 -0
- package/dist/server/AuditLogger.d.ts.map +1 -0
- package/dist/server/AuditLogger.js +2 -0
- package/dist/server/AuthHardening.d.ts +3 -0
- package/dist/server/AuthHardening.d.ts.map +1 -0
- package/dist/server/AuthHardening.js +1 -0
- package/dist/server/DndevSecurity.d.ts +147 -0
- package/dist/server/DndevSecurity.d.ts.map +1 -0
- package/dist/server/DndevSecurity.js +1 -0
- package/dist/server/PiiEncryptor.d.ts +70 -0
- package/dist/server/PiiEncryptor.d.ts.map +1 -0
- package/dist/server/PiiEncryptor.js +1 -0
- package/dist/server/PrivacyManager.d.ts +89 -0
- package/dist/server/PrivacyManager.d.ts.map +1 -0
- package/dist/server/PrivacyManager.js +1 -0
- package/dist/server/RateLimiter.d.ts +80 -0
- package/dist/server/RateLimiter.d.ts.map +1 -0
- package/dist/server/RateLimiter.js +1 -0
- package/dist/server/SecretValidator.d.ts +26 -0
- package/dist/server/SecretValidator.d.ts.map +1 -0
- package/dist/server/SecretValidator.js +1 -0
- package/dist/server/index.d.ts +16 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# @donotdev/security
|
|
2
|
+
|
|
3
|
+
Opt-in SOC2-grade security controls for DoNotDev apps — audit logging, rate limiting, PII encryption, anomaly detection, auth hardening, and GDPR privacy management.
|
|
4
|
+
|
|
5
|
+
> **Opt-in by design.** Baseline safety (brute-force lockout, session timeout) lives in `@donotdev/core` and is always active. This package adds the compliance and observability layer for teams that need SOC2, GDPR, or enterprise-grade audit trails.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @donotdev/security
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Import Paths
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// Client-safe (HealthMonitor, AuthHardening, SecurityContext type)
|
|
17
|
+
import { HealthMonitor, AuthHardening } from '@donotdev/security';
|
|
18
|
+
|
|
19
|
+
// Server-only (DndevSecurity, AuditLogger, PiiEncryptor, etc.)
|
|
20
|
+
import { DndevSecurity } from '@donotdev/security/server';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Zero-config (all baseline controls active)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// functions/src/security.ts
|
|
29
|
+
import { DndevSecurity } from '@donotdev/security/server';
|
|
30
|
+
|
|
31
|
+
export const security = new DndevSecurity();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pass to `CrudService` and base functions:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { createBaseFunction } from '@donotdev/functions/firebase';
|
|
38
|
+
import { security } from './security';
|
|
39
|
+
|
|
40
|
+
export const myFunction = createBaseFunction(
|
|
41
|
+
'my-operation',
|
|
42
|
+
schema,
|
|
43
|
+
handler,
|
|
44
|
+
'user',
|
|
45
|
+
undefined, // requiredRole config
|
|
46
|
+
security // ← opt-in SOC2 audit trail
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Full SOC2 config
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export const security = new DndevSecurity({
|
|
54
|
+
// PII field encryption (AES-256-GCM)
|
|
55
|
+
piiSecret: process.env.PII_SECRET,
|
|
56
|
+
|
|
57
|
+
// Auth hardening overrides (default: 5 attempts → 15min lockout, 8h session)
|
|
58
|
+
auth: {
|
|
59
|
+
maxAttempts: 3,
|
|
60
|
+
lockoutMs: 30 * 60 * 1000,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Data retention (SOC2 P6)
|
|
64
|
+
retention: [
|
|
65
|
+
{ collection: 'audit_logs', days: 365 },
|
|
66
|
+
{ collection: 'sessions', days: 90 },
|
|
67
|
+
],
|
|
68
|
+
|
|
69
|
+
// Anomaly detection with custom handler
|
|
70
|
+
anomaly: {
|
|
71
|
+
authFailures: 5,
|
|
72
|
+
onAnomaly: (type, count, userId) =>
|
|
73
|
+
notifySlack(`[ANOMALY] ${type} x${count} by ${userId}`),
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Pluggable rate limit backend (Firestore, Postgres, Redis)
|
|
77
|
+
// Default: in-memory (fine for single-instance, not for serverless)
|
|
78
|
+
rateLimitBackend: {
|
|
79
|
+
check: (key, cfg) => checkRateLimitWithFirestore(key, cfg),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Controls
|
|
85
|
+
|
|
86
|
+
| Control | Default | Config |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Structured audit log (stdout JSON) | ✅ on | `logger` |
|
|
89
|
+
| Rate limiting (100 writes / 500 reads per min) | ✅ on | `rateLimit` |
|
|
90
|
+
| In-memory rate limit backend | ✅ default | `rateLimitBackend` to swap |
|
|
91
|
+
| Brute-force lockout (5 attempts → 15min) | ✅ on | `auth` |
|
|
92
|
+
| Session timeout tracking (8h idle) | ✅ on | `auth.sessionTimeoutMs` |
|
|
93
|
+
| Anomaly detection (stderr warn) | ✅ on | `anomaly` + `onAnomaly` |
|
|
94
|
+
| Secret scrubbing on all log output | ✅ on | — |
|
|
95
|
+
| PII field encryption (AES-256-GCM) | ❌ opt-in | `piiSecret` |
|
|
96
|
+
| Data retention / right-to-erasure | ❌ opt-in | `retention` |
|
|
97
|
+
|
|
98
|
+
## Package Structure
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
security/src/
|
|
102
|
+
├── client/
|
|
103
|
+
│ └── HealthMonitor.ts # Circuit breaker + uptime tracking (browser-safe)
|
|
104
|
+
├── common/
|
|
105
|
+
│ ├── AuthHardening.ts # Re-export stub → canonical impl in @donotdev/core
|
|
106
|
+
│ └── SecurityConfig.ts # Re-exports SecurityContext types from @donotdev/core
|
|
107
|
+
└── server/
|
|
108
|
+
├── DndevSecurity.ts # Main SOC2 orchestrator (implements SecurityContext)
|
|
109
|
+
├── AuditLogger.ts # Structured JSON audit log (stdout / custom transport)
|
|
110
|
+
├── RateLimiter.ts # In-memory fixed-window rate limiter
|
|
111
|
+
├── PiiEncryptor.ts # AES-256-GCM field-level encryption
|
|
112
|
+
├── AuthHardening.ts # Re-export → common/AuthHardening
|
|
113
|
+
├── AnomalyDetector.ts # Threshold-based behavioral alerts
|
|
114
|
+
├── PrivacyManager.ts # Retention policies + right-to-erasure
|
|
115
|
+
└── SecretValidator.ts # Secret scrubbing for log output
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> **`AuthHardening` canonical implementation** lives in `@donotdev/core` (always installed).
|
|
119
|
+
> `@donotdev/security` re-exports it for backwards compatibility.
|
|
120
|
+
> Firebase and Supabase providers import directly from `@donotdev/core` — no forced security dep.
|
|
121
|
+
|
|
122
|
+
## SecurityContext Interface
|
|
123
|
+
|
|
124
|
+
`DndevSecurity` implements `SecurityContext` from `@donotdev/core`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
interface SecurityContext {
|
|
128
|
+
audit(event: Omit<AuditEvent, 'timestamp'>): void | Promise<void>;
|
|
129
|
+
checkRateLimit(key: string, operation: 'read' | 'write'): Promise<void>;
|
|
130
|
+
encryptPii<T>(data: T, piiFields: string[]): T;
|
|
131
|
+
decryptPii<T>(data: T, piiFields: string[]): T;
|
|
132
|
+
authHardening?: AuthHardeningContext;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Any object implementing this interface can be passed as `security` — no hard dep on this package.
|
|
137
|
+
|
|
138
|
+
## Audit Event Types
|
|
139
|
+
|
|
140
|
+
Covers SOC2 CC6, CC7, C1, P1–P8:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
type AuditEventType =
|
|
144
|
+
| 'auth.login.success' | 'auth.login.failure' | 'auth.logout'
|
|
145
|
+
| 'auth.locked' | 'auth.unlocked' | 'auth.session.expired'
|
|
146
|
+
| 'auth.mfa.enrolled' | 'auth.mfa.challenged'
|
|
147
|
+
| 'auth.password.reset' | 'auth.role.changed'
|
|
148
|
+
| 'crud.create' | 'crud.read' | 'crud.update' | 'crud.delete'
|
|
149
|
+
| 'pii.access' | 'pii.export' | 'pii.erase'
|
|
150
|
+
| 'rate_limit.exceeded' | 'anomaly.detected' | 'config.changed';
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## PII Encryption
|
|
154
|
+
|
|
155
|
+
Mark fields for encryption in your entity definition, then pass `security` to `CrudService`:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Entity definition
|
|
159
|
+
const UserEntity = defineEntity('users', {
|
|
160
|
+
fields: { email: field.email(), ssn: field.string() },
|
|
161
|
+
security: { piiFields: ['email', 'ssn'] },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Service setup
|
|
165
|
+
const security = new DndevSecurity({ piiSecret: process.env.PII_SECRET });
|
|
166
|
+
crudService.setSecurity(security);
|
|
167
|
+
// email + ssn are now transparently encrypted at rest
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Pluggable Rate Limit Backend
|
|
171
|
+
|
|
172
|
+
For serverless/distributed deployments where in-memory state is lost between invocations:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { checkRateLimitWithFirestore } from '@donotdev/functions/shared';
|
|
176
|
+
|
|
177
|
+
const security = new DndevSecurity({
|
|
178
|
+
rateLimitBackend: {
|
|
179
|
+
check: (key, cfg) => checkRateLimitWithFirestore(key, cfg),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Implement `RateLimitBackend` from `@donotdev/core` for custom backends (Redis, Postgres, etc.):
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import type { RateLimitBackend } from '@donotdev/core';
|
|
188
|
+
|
|
189
|
+
const redisBackend: RateLimitBackend = {
|
|
190
|
+
async check(key, { maxAttempts, windowMs, blockDurationMs }) {
|
|
191
|
+
// your Redis logic
|
|
192
|
+
return { allowed: true, remaining: 99, resetAt: null, blockRemainingSeconds: null };
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
All rights reserved. The DoNotDev framework and its premium features are the exclusive property of **Ambroise Park Consulting**.
|
|
200
|
+
|
|
201
|
+
© Ambroise Park Consulting – 2025
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HealthMonitor
|
|
3
|
+
* @description Circuit breaker pattern + health check probes for availability.
|
|
4
|
+
* Covers SOC2 Availability Principle (A1): health monitoring, graceful degradation.
|
|
5
|
+
*
|
|
6
|
+
* Zero deps — uses fetch + AbortSignal for liveness probes.
|
|
7
|
+
*
|
|
8
|
+
* @version 0.0.1
|
|
9
|
+
* @since 0.0.1
|
|
10
|
+
* @author AMBROISE PARK Consulting
|
|
11
|
+
*/
|
|
12
|
+
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
|
|
13
|
+
export interface CircuitBreakerConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Failure count threshold before circuit opens (default: 5).
|
|
16
|
+
*/
|
|
17
|
+
failureThreshold?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Cooldown before attempting half-open (ms, default: 10_000).
|
|
20
|
+
*/
|
|
21
|
+
resetTimeoutMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Request timeout for health checks (ms, default: 3_000).
|
|
24
|
+
*/
|
|
25
|
+
probeTimeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface HealthMonitorConfig extends CircuitBreakerConfig {
|
|
28
|
+
/**
|
|
29
|
+
* URL to probe for liveness (e.g., '/api/health').
|
|
30
|
+
* If not set, liveness checks always return true.
|
|
31
|
+
*/
|
|
32
|
+
livenessUrl?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Availability monitor: circuit breaker + liveness probe (SOC2 A1).
|
|
36
|
+
*
|
|
37
|
+
* Wrap downstream calls with `protect()` to prevent cascading failures.
|
|
38
|
+
* Use `checkLiveness()` in health check endpoints.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const monitor = new HealthMonitor({ livenessUrl: '/api/health', failureThreshold: 3 });
|
|
43
|
+
*
|
|
44
|
+
* // Protect a downstream call:
|
|
45
|
+
* const safeCall = monitor.protect(async () => fetchUserData(id));
|
|
46
|
+
* try {
|
|
47
|
+
* const data = await safeCall();
|
|
48
|
+
* } catch {
|
|
49
|
+
* // Circuit open — show cached/degraded UI
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* // Liveness probe in API route:
|
|
53
|
+
* const alive = await monitor.checkLiveness();
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @version 0.0.1
|
|
57
|
+
* @since 0.0.1
|
|
58
|
+
* @author AMBROISE PARK Consulting
|
|
59
|
+
*/
|
|
60
|
+
export declare class HealthMonitor {
|
|
61
|
+
private readonly breaker;
|
|
62
|
+
private readonly config;
|
|
63
|
+
private _status;
|
|
64
|
+
/**
|
|
65
|
+
* Guards the half-open state: only one probe executes at a time.
|
|
66
|
+
* Concurrent calls in half-open state are rejected rather than all executing,
|
|
67
|
+
* which would re-open the circuit on a mass failure storm.
|
|
68
|
+
*/
|
|
69
|
+
private _probing;
|
|
70
|
+
constructor(config?: HealthMonitorConfig);
|
|
71
|
+
/**
|
|
72
|
+
* Wrap an async function with circuit-breaker protection.
|
|
73
|
+
* The returned function throws when the circuit is open.
|
|
74
|
+
* In half-open state only one probe executes at a time — concurrent calls are rejected.
|
|
75
|
+
*/
|
|
76
|
+
protect<T>(fn: () => Promise<T>): () => Promise<T>;
|
|
77
|
+
/** Current circuit health status. */
|
|
78
|
+
get status(): HealthStatus;
|
|
79
|
+
/**
|
|
80
|
+
* HTTP liveness probe — returns true if the endpoint responds 2xx within timeout.
|
|
81
|
+
* Always returns true if no `livenessUrl` configured.
|
|
82
|
+
*/
|
|
83
|
+
checkLiveness(): Promise<boolean>;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=HealthMonitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HealthMonitor.d.ts","sourceRoot":"","sources":["../../src/client/HealthMonitor.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAwDD,MAAM,WAAW,mBAAoB,SAAQ,oBAAoB;IAC/D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,OAAO,CAA2B;IAC1C;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,GAAE,mBAAwB;IAK5C;;;;OAIG;IACH,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC;IAuClD,qCAAqC;IACrC,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC;CAWxC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class i{state="closed";failures=0;lastOpenedAt=0;failureThreshold;resetTimeoutMs;constructor(e={}){this.failureThreshold=e.failureThreshold??5,this.resetTimeoutMs=e.resetTimeoutMs??1e4}_evaluateState(){return this.state==="open"&&Date.now()-this.lastOpenedAt>this.resetTimeoutMs&&(this.state="half-open"),this.state}get currentState(){return this.state}isOpen(){return this._evaluateState()==="open"}recordSuccess(){this.failures=0,this.state="closed"}recordFailure(){this.failures+=1,this.failures>=this.failureThreshold&&(this.state="open",this.lastOpenedAt=Date.now())}}class a{breaker;config;_status="healthy";_probing=!1;constructor(e={}){this.config=e,this.breaker=new i(e)}protect(e){return async()=>{if(this.breaker.isOpen())throw this._status="unhealthy",new Error("[dndev/security] Circuit breaker open \u2014 downstream service unavailable");const s=this.breaker.currentState==="half-open";if(s){if(this._probing)throw this._status="unhealthy",new Error("[dndev/security] Circuit breaker half-open \u2014 probe in progress, retry shortly");this._probing=!0}try{const t=await e();return this.breaker.recordSuccess(),this._status="healthy",t}catch(t){throw this.breaker.recordFailure(),this._status=this.breaker.isOpen()?"unhealthy":"degraded",t}finally{s&&(this._probing=!1)}}}get status(){return this._status}async checkLiveness(){if(!this.config.livenessUrl)return!0;try{return(await fetch(this.config.livenessUrl,{signal:AbortSignal.timeout(this.config.probeTimeoutMs??3e3)})).ok}catch{return!1}}}export{a as HealthMonitor};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { HealthMonitor } from './HealthMonitor';
|
|
2
|
+
export type { HealthMonitorConfig, HealthStatus, CircuitBreakerConfig } from './HealthMonitor';
|
|
3
|
+
export type { SecurityContext, AuditEvent, AuditEventType } from '../common/SecurityConfig';
|
|
4
|
+
export { AuthHardening } from '../common/AuthHardening';
|
|
5
|
+
export type { AuthHardeningConfig, LockoutResult } from '../common/AuthHardening';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAG/F,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG5F,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{HealthMonitor as t}from"./HealthMonitor";import{AuthHardening as n}from"../common/AuthHardening";export{n as AuthHardening,t as HealthMonitor};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthHardening.d.ts","sourceRoot":"","sources":["../../src/common/AuthHardening.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AuthHardening as n}from"@donotdev/core";export{n as AuthHardening};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Security Config Types
|
|
3
|
+
* @description Re-exports SecurityContext types from @donotdev/core.
|
|
4
|
+
* Keeping the canonical definition in core avoids circular deps.
|
|
5
|
+
*
|
|
6
|
+
* @version 0.0.1
|
|
7
|
+
* @since 0.0.1
|
|
8
|
+
* @author AMBROISE PARK Consulting
|
|
9
|
+
*/
|
|
10
|
+
export type { AuditEventType, AuditEvent, SecurityContext, AuthHardeningContext, ServerRateLimitConfig, ServerRateLimitResult, RateLimitBackend } from '@donotdev/core';
|
|
11
|
+
//# sourceMappingURL=SecurityConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityConfig.d.ts","sourceRoot":"","sources":["../../src/common/SecurityConfig.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/common/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
|
|
File without changes
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { HealthMonitor } from './client/HealthMonitor';
|
|
2
|
+
export type { HealthMonitorConfig, HealthStatus, CircuitBreakerConfig } from './client/HealthMonitor';
|
|
3
|
+
export type { SecurityContext, AuditEvent, AuditEventType } from './common/SecurityConfig';
|
|
4
|
+
export { AuthHardening } from './common/AuthHardening';
|
|
5
|
+
export type { AuthHardeningConfig, LockoutResult } from './common/AuthHardening';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEtG,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAG3F,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{HealthMonitor as t}from"./client/HealthMonitor";import{AuthHardening as n}from"./common/AuthHardening";export{n as AuthHardening,t as HealthMonitor};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AnomalyDetector
|
|
3
|
+
* @description Threshold-based behavioral anomaly detection with configurable alerting.
|
|
4
|
+
* Covers SOC2 CC7.2 (anomaly detection), CC7.3 (incident response triggers).
|
|
5
|
+
*
|
|
6
|
+
* @version 0.0.1
|
|
7
|
+
* @since 0.0.1
|
|
8
|
+
* @author AMBROISE PARK Consulting
|
|
9
|
+
*/
|
|
10
|
+
export type AnomalyType = 'auth.failures' | 'bulk.deletes' | 'bulk.reads' | 'bulk.exports' | 'rate_limit.exceeded';
|
|
11
|
+
export interface AnomalyThresholds {
|
|
12
|
+
/**
|
|
13
|
+
* Max auth failures per window before alert.
|
|
14
|
+
* @default 10
|
|
15
|
+
*/
|
|
16
|
+
authFailures?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Max deletes per window before alert.
|
|
19
|
+
* @default 50
|
|
20
|
+
*/
|
|
21
|
+
bulkDeletes?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Max reads per window before alert.
|
|
24
|
+
* @default 1000
|
|
25
|
+
*/
|
|
26
|
+
bulkReads?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Max bulk exports per window before alert.
|
|
29
|
+
* @default 5
|
|
30
|
+
*/
|
|
31
|
+
bulkExports?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Max rate-limit-exceeded events per window before alert.
|
|
34
|
+
* Defaults to 10 rather than 1 to prevent flooding alert handlers (Slack, PagerDuty)
|
|
35
|
+
* during sustained attacks. Every individual breach is still recorded in the audit log.
|
|
36
|
+
* @default 10
|
|
37
|
+
*/
|
|
38
|
+
rateLimitExceeded?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Window size in milliseconds.
|
|
41
|
+
* @default 60_000 (1 minute)
|
|
42
|
+
*/
|
|
43
|
+
windowMs?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Called when an anomaly is detected.
|
|
47
|
+
* @param type - anomaly type
|
|
48
|
+
* @param count - current count in window
|
|
49
|
+
* @param userId - user who triggered it (undefined for system)
|
|
50
|
+
*/
|
|
51
|
+
export type AnomalyHandler = (type: AnomalyType, count: number, userId?: string) => void;
|
|
52
|
+
export declare class AnomalyDetector {
|
|
53
|
+
private readonly counters;
|
|
54
|
+
private readonly thresholds;
|
|
55
|
+
private readonly onAnomaly;
|
|
56
|
+
constructor(thresholds?: AnomalyThresholds, onAnomaly?: AnomalyHandler);
|
|
57
|
+
/**
|
|
58
|
+
* Record an event and fire `onAnomaly` if threshold is breached.
|
|
59
|
+
* For `rate_limit.exceeded`, the handler fires once per window threshold
|
|
60
|
+
* (not on every single breach) to prevent flooding alert sinks during attacks.
|
|
61
|
+
*/
|
|
62
|
+
record(type: AnomalyType, userId?: string): void;
|
|
63
|
+
private getThreshold;
|
|
64
|
+
private _evictExpired;
|
|
65
|
+
/** Current count for a type + userId in the active window. */
|
|
66
|
+
getCount(type: AnomalyType, userId?: string): number;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=AnomalyDetector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnomalyDetector.d.ts","sourceRoot":"","sources":["../../src/server/AnomalyDetector.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,MAAM,MAAM,WAAW,GACnB,eAAe,GACf,cAAc,GACd,YAAY,GACZ,cAAc,GACd,qBAAqB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;AA8BzF,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;gBAE/B,UAAU,GAAE,iBAAsB,EAAE,SAAS,CAAC,EAAE,cAAc;IA0B1E;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IA4BhD,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,aAAa;IAQrB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM;CAOrD"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const c=1e4;class l{counters=new Map;thresholds;onAnomaly;constructor(t={},o){this.thresholds={authFailures:t.authFailures??10,bulkDeletes:t.bulkDeletes??50,bulkReads:t.bulkReads??1e3,bulkExports:t.bulkExports??5,rateLimitExceeded:t.rateLimitExceeded??10,windowMs:t.windowMs??6e4},this.onAnomaly=o??((s,e,n)=>{process.stderr.write(JSON.stringify({level:"warn",service:"dndev-anomaly",type:"anomaly.detected",anomalyType:s,count:e,userId:n,timestamp:new Date().toISOString()})+`
|
|
2
|
+
`)})}record(t,o){const s=`${t}:${o??"__global__"}`,e=Date.now();!this.counters.has(s)&&this.counters.size>=1e4&&this._evictExpired(e);const r=this.counters.get(s)??{count:0,windowStart:e};e-r.windowStart>this.thresholds.windowMs&&(r.count=0,r.windowStart=e),r.count+=1,this.counters.set(s,r);const i=this.getThreshold(t);r.count===i&&this.onAnomaly(t,r.count,o)}getThreshold(t){switch(t){case"auth.failures":return this.thresholds.authFailures;case"bulk.deletes":return this.thresholds.bulkDeletes;case"bulk.reads":return this.thresholds.bulkReads;case"bulk.exports":return this.thresholds.bulkExports;case"rate_limit.exceeded":return this.thresholds.rateLimitExceeded}}_evictExpired(t){for(const[o,s]of this.counters)t-s.windowStart>this.thresholds.windowMs&&this.counters.delete(o)}getCount(t,o){const s=`${t}:${o??"__global__"}`,e=this.counters.get(s);return!e||Date.now()-e.windowStart>this.thresholds.windowMs?0:e.count}}export{l as AnomalyDetector};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AuditEvent } from '../common/SecurityConfig';
|
|
2
|
+
export interface AuditLoggerOptions {
|
|
3
|
+
/** Log level (default: 'info') */
|
|
4
|
+
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
5
|
+
/** Service name tag added to every entry (default: 'dndev') */
|
|
6
|
+
service?: string;
|
|
7
|
+
/** Custom writer — defaults to process.stdout (JSON per line) */
|
|
8
|
+
write?: (entry: Record<string, unknown>) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Structured JSON audit logger (SOC2 CC7.1).
|
|
12
|
+
*
|
|
13
|
+
* Produces one JSON object per line on stdout. Pipe to cloud logging
|
|
14
|
+
* (Cloud Logging, Datadog, Splunk, etc.) for SIEM ingestion and immutable storage.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const logger = new AuditLogger({ service: 'my-app' });
|
|
19
|
+
* logger.log({ type: 'auth.login.success', userId: 'u123' });
|
|
20
|
+
* // → {"level":"info","service":"my-app","type":"auth.login.success","userId":"u123","timestamp":"..."}
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @version 0.0.1
|
|
24
|
+
* @since 0.0.1
|
|
25
|
+
* @author AMBROISE PARK Consulting
|
|
26
|
+
*/
|
|
27
|
+
export declare class AuditLogger {
|
|
28
|
+
private readonly service;
|
|
29
|
+
private readonly level;
|
|
30
|
+
private readonly write;
|
|
31
|
+
constructor(opts?: AuditLoggerOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Log an audit event with automatic timestamp.
|
|
34
|
+
* Secrets in all event fields are automatically scrubbed before writing.
|
|
35
|
+
*/
|
|
36
|
+
log(event: Omit<AuditEvent, 'timestamp'> & {
|
|
37
|
+
timestamp?: string;
|
|
38
|
+
}): void;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=AuditLogger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuditLogger.d.ts","sourceRoot":"","sources":["../../src/server/AuditLogger.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,MAAM,WAAW,kBAAkB;IACjC,kCAAkC;IAClC,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAC5C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAClD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2C;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2C;gBAErD,IAAI,GAAE,kBAAuB;IAqBzC;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAUzE"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{scrubSecrets as s}from"./SecretValidator";class c{service;level;write;constructor(t={}){this.service=t.service??"dndev",this.level=t.level??"info",this.write=t.write??(e=>{let i;try{i=JSON.stringify(e)}catch{i=JSON.stringify({level:e.level,service:e.service,type:e.type,timestamp:e.timestamp,_serializeError:"Audit entry contained non-serializable values"})}process.stdout.write(i+`
|
|
2
|
+
`)})}log(t){const e=s(t),i={level:this.level,service:this.service,...e,timestamp:e.timestamp??new Date().toISOString()};this.write(i)}}export{c as AuditLogger};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthHardening.d.ts","sourceRoot":"","sources":["../../src/server/AuthHardening.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AuthHardening as n}from"../common/AuthHardening";export{n as AuthHardening};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DndevSecurity
|
|
3
|
+
* @description Main SOC2 security context. Wires all server-side security controls
|
|
4
|
+
* into a single object that CrudService and auth adapters consume.
|
|
5
|
+
*
|
|
6
|
+
* Zero-config path: all baseline controls active with sensible defaults.
|
|
7
|
+
* Advanced controls (PII encryption, MFA, retention) require explicit config.
|
|
8
|
+
*
|
|
9
|
+
* @version 0.0.1
|
|
10
|
+
* @since 0.0.1
|
|
11
|
+
* @author AMBROISE PARK Consulting
|
|
12
|
+
*/
|
|
13
|
+
import { AuditLogger } from './AuditLogger';
|
|
14
|
+
import { DndevRateLimiter } from './RateLimiter';
|
|
15
|
+
import { PiiEncryptor } from './PiiEncryptor';
|
|
16
|
+
import { AuthHardening } from './AuthHardening';
|
|
17
|
+
import { AnomalyDetector } from './AnomalyDetector';
|
|
18
|
+
import { PrivacyManager } from './PrivacyManager';
|
|
19
|
+
import type { AuditLoggerOptions } from './AuditLogger';
|
|
20
|
+
import type { RateLimiterOptions } from './RateLimiter';
|
|
21
|
+
import type { AuthHardeningConfig } from './AuthHardening';
|
|
22
|
+
import type { AnomalyThresholds, AnomalyHandler } from './AnomalyDetector';
|
|
23
|
+
import type { RetentionPolicy } from './PrivacyManager';
|
|
24
|
+
import type { SecurityContext, AuditEvent, AuthHardeningContext, RateLimitBackend } from '../common/SecurityConfig';
|
|
25
|
+
export interface DndevSecurityConfig {
|
|
26
|
+
/**
|
|
27
|
+
* Master secret for PII field encryption (AES-256-GCM).
|
|
28
|
+
* Required if any entity defines `security.piiFields`.
|
|
29
|
+
* Store in your secret manager — never in code.
|
|
30
|
+
*/
|
|
31
|
+
piiSecret?: string;
|
|
32
|
+
/** PII encryption salt override (default: 'dndev-pii-v1') */
|
|
33
|
+
piiSalt?: string;
|
|
34
|
+
/** Rate limiter options (default: 100 writes/min, 500 reads/min) */
|
|
35
|
+
rateLimit?: RateLimiterOptions;
|
|
36
|
+
/** Auth hardening options (default: 5 attempts → 15min lockout, 8h session) */
|
|
37
|
+
auth?: AuthHardeningConfig;
|
|
38
|
+
/** Anomaly detection thresholds + handler */
|
|
39
|
+
anomaly?: AnomalyThresholds & {
|
|
40
|
+
onAnomaly?: AnomalyHandler;
|
|
41
|
+
};
|
|
42
|
+
/** Data retention policies (required for SOC2 P6) */
|
|
43
|
+
retention?: RetentionPolicy[];
|
|
44
|
+
/** Audit logger options (default: JSON to stdout) */
|
|
45
|
+
logger?: AuditLoggerOptions;
|
|
46
|
+
/**
|
|
47
|
+
* Pluggable rate limit backend (default: in-memory).
|
|
48
|
+
* Provide a Firestore, Postgres, or Redis-backed implementation for
|
|
49
|
+
* distributed/serverless deployments where in-memory state is lost between invocations.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { checkRateLimitWithFirestore } from '@donotdev/functions/shared';
|
|
54
|
+
* const security = new DndevSecurity({
|
|
55
|
+
* rateLimitBackend: { check: (key, cfg) => checkRateLimitWithFirestore(key, cfg) },
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
rateLimitBackend?: RateLimitBackend;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Main SOC2 security context for DoNotDev applications.
|
|
63
|
+
*
|
|
64
|
+
* Implements `SecurityContext` — pass an instance to `CrudService` and auth adapters.
|
|
65
|
+
* All baseline controls are ON by default at zero config:
|
|
66
|
+
* - Structured audit logging (stdout JSON)
|
|
67
|
+
* - Rate limiting (100 writes/min, 500 reads/min per key)
|
|
68
|
+
* - Brute-force lockout (5 attempts → 15min lockout)
|
|
69
|
+
* - Session timeout tracking (8h idle)
|
|
70
|
+
* - Anomaly detection (stderr warn at thresholds)
|
|
71
|
+
* - Secret scrubbing on all log output
|
|
72
|
+
*
|
|
73
|
+
* Advanced controls require explicit config:
|
|
74
|
+
* - PII encryption → `piiSecret`
|
|
75
|
+
* - Data retention/erasure → `retention`
|
|
76
|
+
* - Custom anomaly handler → `anomaly.onAnomaly`
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* // Zero-config (baseline SOC2 controls):
|
|
81
|
+
* export const security = new DndevSecurity();
|
|
82
|
+
*
|
|
83
|
+
* // Full SOC2 config:
|
|
84
|
+
* export const security = new DndevSecurity({
|
|
85
|
+
* piiSecret: process.env.PII_SECRET,
|
|
86
|
+
* auth: { maxAttempts: 3, lockoutMs: 30 * 60 * 1000 },
|
|
87
|
+
* retention: [
|
|
88
|
+
* { collection: 'audit_logs', days: 365 },
|
|
89
|
+
* { collection: 'sessions', days: 90 },
|
|
90
|
+
* ],
|
|
91
|
+
* anomaly: {
|
|
92
|
+
* authFailures: 5,
|
|
93
|
+
* onAnomaly: (type, count, userId) => notifySlack(`${type} x${count} by ${userId}`),
|
|
94
|
+
* },
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @version 0.0.1
|
|
99
|
+
* @since 0.0.1
|
|
100
|
+
* @author AMBROISE PARK Consulting
|
|
101
|
+
*/
|
|
102
|
+
export declare class DndevSecurity implements SecurityContext {
|
|
103
|
+
/** Structured audit logger (stdout JSON) */
|
|
104
|
+
readonly auditLogger: AuditLogger;
|
|
105
|
+
/** Rate limiter (in-memory fixed-window, used when no rateLimitBackend provided) */
|
|
106
|
+
readonly rateLimiter: DndevRateLimiter;
|
|
107
|
+
/** PII field encryptor (null if piiSecret not provided) */
|
|
108
|
+
readonly piiEncryptor: PiiEncryptor | null;
|
|
109
|
+
/**
|
|
110
|
+
* Auth hardening (brute-force lockout, session timeout).
|
|
111
|
+
* Also satisfies `SecurityContext.authHardening` — auth adapters delegate
|
|
112
|
+
* lockout here to avoid running a second, hardcoded lockout Map in parallel.
|
|
113
|
+
*/
|
|
114
|
+
readonly authHardening: AuthHardening & AuthHardeningContext;
|
|
115
|
+
/** Anomaly detector (threshold-based behavioral alerts) */
|
|
116
|
+
readonly anomalyDetector: AnomalyDetector;
|
|
117
|
+
/** Privacy manager (retention policies, right-to-erasure) */
|
|
118
|
+
readonly privacyManager: PrivacyManager;
|
|
119
|
+
/** Optional pluggable backend (Firestore, Postgres, Redis) — replaces in-memory limiter */
|
|
120
|
+
private readonly _rateLimitBackend?;
|
|
121
|
+
/** RateLimitConfig derived from rateLimit options, used when delegating to backend */
|
|
122
|
+
private readonly _backendWriteConfig;
|
|
123
|
+
private readonly _backendReadConfig;
|
|
124
|
+
constructor(config?: DndevSecurityConfig);
|
|
125
|
+
/**
|
|
126
|
+
* Emit a structured audit event. Secrets in metadata are automatically scrubbed.
|
|
127
|
+
*/
|
|
128
|
+
audit(event: Omit<AuditEvent, 'timestamp'>): void;
|
|
129
|
+
/**
|
|
130
|
+
* Check rate limit for key + operation.
|
|
131
|
+
* Delegates to injected `rateLimitBackend` when provided (Firestore, Postgres, Redis),
|
|
132
|
+
* otherwise falls back to the in-memory limiter.
|
|
133
|
+
* @throws {Error} 'Rate limit exceeded' when threshold is breached.
|
|
134
|
+
*/
|
|
135
|
+
checkRateLimit(key: string, operation: 'read' | 'write'): Promise<void>;
|
|
136
|
+
/** Encrypt PII fields using AES-256-GCM (no-op if piiSecret not configured). */
|
|
137
|
+
encryptPii<T extends Record<string, unknown>>(data: T, piiFields: string[]): T;
|
|
138
|
+
/** Decrypt PII fields (no-op if piiSecret not configured). */
|
|
139
|
+
decryptPii<T extends Record<string, unknown>>(data: T, piiFields: string[]): T;
|
|
140
|
+
/**
|
|
141
|
+
* Record a behavioral anomaly event. Called by CrudService after mutations so
|
|
142
|
+
* the anomaly detector can fire alerts when thresholds are breached.
|
|
143
|
+
*/
|
|
144
|
+
private static readonly VALID_ANOMALY_TYPES;
|
|
145
|
+
recordAnomaly(type: string, userId?: string): void;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=DndevSecurity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DndevSecurity.d.ts","sourceRoot":"","sources":["../../src/server/DndevSecurity.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAe,MAAM,mBAAmB,CAAC;AACxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,oBAAoB,EAAE,gBAAgB,EAAyB,MAAM,0BAA0B,CAAC;AAE3I,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,6CAA6C;IAC7C,OAAO,CAAC,EAAE,iBAAiB,GAAG;QAAE,SAAS,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IAC7D,qDAAqD;IACrD,SAAS,CAAC,EAAE,eAAe,EAAE,CAAC;IAC9B,qDAAqD;IACrD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,aAAc,YAAW,eAAe;IACnD,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,2DAA2D;IAC3D,QAAQ,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,aAAa,GAAG,oBAAoB,CAAC;IAC7D,2DAA2D;IAC3D,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,2FAA2F;IAC3F,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAmB;IACtD,sFAAsF;IACtF,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAwB;IAC5D,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAwB;gBAE/C,MAAM,GAAE,mBAAwB;IAkC5C;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,IAAI;IAOjD;;;;;OAKG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB7E,gFAAgF;IAChF,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC;IAK9E,8DAA8D;IAC9D,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC;IAK9E;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAMxC;IAEH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CASnD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AuditLogger as n}from"./AuditLogger";import{DndevRateLimiter as o}from"./RateLimiter";import{PiiEncryptor as d}from"./PiiEncryptor";import{AuthHardening as s}from"./AuthHardening";import{AnomalyDetector as c}from"./AnomalyDetector";import{PrivacyManager as m}from"./PrivacyManager";import{scrubSecrets as l}from"./SecretValidator";class i{auditLogger;rateLimiter;piiEncryptor;authHardening;anomalyDetector;privacyManager;_rateLimitBackend;_backendWriteConfig;_backendReadConfig;constructor(t={}){if(this.auditLogger=new n(t.logger),this.rateLimiter=new o(t.rateLimit),t.piiSecret&&!t.piiSalt)throw new Error("[dndev/security] DndevSecurity: PII encryption requires both piiSecret and piiSalt configuration. Provide a per-deployment unique salt stored in your secret manager.");this.piiEncryptor=t.piiSecret&&t.piiSalt?new d(t.piiSecret,t.piiSalt):null,this.authHardening=new s(t.auth),this.anomalyDetector=new c(t.anomaly,t.anomaly?.onAnomaly),this.privacyManager=new m(t.retention),this._rateLimitBackend=t.rateLimitBackend;const e=(t.rateLimit?.writes?.durationSeconds??60)*1e3,r=(t.rateLimit?.reads?.durationSeconds??60)*1e3;this._backendWriteConfig={maxAttempts:t.rateLimit?.writes?.points??100,windowMs:e,blockDurationMs:e},this._backendReadConfig={maxAttempts:t.rateLimit?.reads?.points??500,windowMs:r,blockDurationMs:r}}audit(t){const e=t.metadata?l(t.metadata):void 0;this.auditLogger.log({...t,metadata:e})}async checkRateLimit(t,e){if(this._rateLimitBackend){const r=e==="write"?this._backendWriteConfig:this._backendReadConfig,a=await this._rateLimitBackend.check(t,r);if(!a.allowed)throw this.anomalyDetector.record("rate_limit.exceeded",t),new Error(`Rate limit exceeded. Try again in ${a.blockRemainingSeconds} seconds.`);return}try{await this.rateLimiter.check(t,e)}catch(r){throw this.anomalyDetector.record("rate_limit.exceeded",t),r}}encryptPii(t,e){return!this.piiEncryptor||e.length===0?t:this.piiEncryptor.encryptFields(t,e)}decryptPii(t,e){return!this.piiEncryptor||e.length===0?t:this.piiEncryptor.decryptFields(t,e)}static VALID_ANOMALY_TYPES=new Set(["auth.failures","bulk.deletes","bulk.reads","bulk.exports","rate_limit.exceeded"]);recordAnomaly(t,e){if(!i.VALID_ANOMALY_TYPES.has(t))throw new Error(`[dndev/security] DndevSecurity: unknown anomaly type "${t}". Valid types: ${[...i.VALID_ANOMALY_TYPES].join(", ")}`);this.anomalyDetector.record(t,e)}}export{i as DndevSecurity};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM PII encryptor with per-field IV (SOC2 C1, CC6.1).
|
|
3
|
+
*
|
|
4
|
+
* **IMPORTANT:** Instantiate ONCE at app startup (e.g. inside `DndevSecurity`).
|
|
5
|
+
* The constructor calls `scryptSync` which is CPU-intensive by design — repeated
|
|
6
|
+
* construction per-request is a DoS vector.
|
|
7
|
+
*
|
|
8
|
+
* Each field gets a fresh random IV — identical plaintext produces different ciphertext.
|
|
9
|
+
* Output format: `dnpii1:<ivHex>:<authTagHex>:<ciphertextHex>`
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Salt MUST be a per-deployment secret stored alongside piiSecret in your secret manager.
|
|
14
|
+
* // NEVER use a hard-coded or framework-level salt.
|
|
15
|
+
* const enc = new PiiEncryptor(process.env.PII_SECRET!, process.env.PII_SALT!);
|
|
16
|
+
* const stored = enc.encryptFields({ email: 'alice@example.com', name: 'Alice' }, ['email']);
|
|
17
|
+
* // stored.email → 'dnpii1:a1b2...:c3d4...:e5f6...' (encrypted)
|
|
18
|
+
* // stored.name → 'Alice' (unchanged)
|
|
19
|
+
*
|
|
20
|
+
* const plain = enc.decryptFields(stored, ['email']);
|
|
21
|
+
* // plain.email → 'alice@example.com'
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @version 0.0.2
|
|
25
|
+
* @since 0.0.1
|
|
26
|
+
* @author AMBROISE PARK Consulting
|
|
27
|
+
*/
|
|
28
|
+
export declare class PiiEncryptor {
|
|
29
|
+
private readonly key;
|
|
30
|
+
/**
|
|
31
|
+
* @param secret - Master secret (min 32 chars). Store in secret manager, never in code.
|
|
32
|
+
* @param salt - Per-deployment salt. Must be unique per deployment and stored as a secret.
|
|
33
|
+
* Do NOT use a shared or hard-coded value — a universal salt eliminates
|
|
34
|
+
* rainbow-table resistance for the derived key.
|
|
35
|
+
*/
|
|
36
|
+
constructor(secret: string, salt: string);
|
|
37
|
+
/**
|
|
38
|
+
* Encrypt a single string value.
|
|
39
|
+
* @returns `dnpii1:<iv>:<tag>:<ciphertext>` (all hex, prefixed for unambiguous detection)
|
|
40
|
+
*/
|
|
41
|
+
encrypt(plaintext: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Decrypt a value produced by `encrypt()`.
|
|
44
|
+
* Supports the legacy format (no `dnpii1:` prefix) for backward compatibility.
|
|
45
|
+
* @throws if ciphertext is malformed or authentication tag is invalid.
|
|
46
|
+
*/
|
|
47
|
+
decrypt(ciphertext: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Encrypt specified fields in a data object (returns new object, input unchanged).
|
|
50
|
+
* Non-string field values and missing fields are silently skipped.
|
|
51
|
+
*/
|
|
52
|
+
encryptFields<T extends Record<string, unknown>>(data: T, piiFields: string[]): T;
|
|
53
|
+
/**
|
|
54
|
+
* Detect whether a string was produced by `encrypt()`.
|
|
55
|
+
* Checks for the `dnpii1:` version prefix — unambiguous, no false positives.
|
|
56
|
+
* Also accepts the legacy format (24-char IV hex + 32-char tag hex) for backward compat.
|
|
57
|
+
*/
|
|
58
|
+
private isEncrypted;
|
|
59
|
+
/** Zero the derived key buffer. Call when the encryptor is no longer needed. */
|
|
60
|
+
dispose(): void;
|
|
61
|
+
/** TC39 explicit resource management support. */
|
|
62
|
+
[Symbol.dispose](): void;
|
|
63
|
+
/**
|
|
64
|
+
* Decrypt specified fields in a data object (returns new object, input unchanged).
|
|
65
|
+
* Fields that do not pass the encrypted-format check are left as-is.
|
|
66
|
+
* Auth tag failures (wrong key / tampered data) are re-thrown — they must not be silenced.
|
|
67
|
+
*/
|
|
68
|
+
decryptFields<T extends Record<string, unknown>>(data: T, piiFields: string[]): T;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=PiiEncryptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PiiEncryptor.d.ts","sourceRoot":"","sources":["../../src/server/PiiEncryptor.ts"],"names":[],"mappings":"AAmCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B;;;;;OAKG;gBACS,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAexC;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAWlC;;;;OAIG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IA8BnC;;;OAGG;IACH,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC;IAYjF;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAgBnB,gFAAgF;IAChF,OAAO,IAAI,IAAI;IAIf,iDAAiD;IACjD,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;IAIxB;;;;OAIG;IACH,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC;CAYlF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createCipheriv as y,createDecipheriv as u,randomBytes as g,scryptSync as E}from"node:crypto";const d="aes-256-gcm",v=32,i=12,a=16,o="dnpii1:";class w{key;constructor(t,r){if(!t||t.length<32)throw new Error("[dndev/security] PiiEncryptor: secret must be at least 32 characters");if(!r||r.length<8)throw new Error("[dndev/security] PiiEncryptor: salt is required and must be at least 8 characters. Use a per-deployment secret stored in your secret manager \u2014 never a hard-coded value.");this.key=E(t,r,v,{N:65536,r:8,p:1})}encrypt(t){const r=g(i),e=y(d,this.key,r),s=Buffer.concat([e.update(t,"utf8"),e.final()]),n=e.getAuthTag();return`${o}${r.toString("hex")}:${n.toString("hex")}:${s.toString("hex")}`}decrypt(t){const e=(t.startsWith(o)?t.slice(o.length):t).split(":");if(e.length!==3)throw new Error("[dndev/security] PiiEncryptor: invalid ciphertext format");const[s,n,l]=e,c=Buffer.from(s,"hex"),h=Buffer.from(n,"hex"),p=Buffer.from(l,"hex");if(c.length!==i)throw new Error(`[dndev/security] PiiEncryptor: invalid IV length ${c.length}, expected ${i}`);if(h.length!==a)throw new Error(`[dndev/security] PiiEncryptor: invalid auth tag length ${h.length}, expected ${a}`);const f=u(d,this.key,c);return f.setAuthTag(h),f.update(p).toString("utf8")+f.final("utf8")}encryptFields(t,r){if(r.length===0)return t;const e={...t};for(const s of r){const n=e[s];typeof n=="string"&&(e[s]=this.encrypt(n))}return e}isEncrypted(t){if(t.startsWith(o))return!0;const r=t.split(":");if(r.length!==3)return!1;const[e,s]=r,n=/^[0-9a-f]+$/i;return e.length===i*2&&s.length===a*2&&n.test(e)&&n.test(s)}dispose(){this.key.fill(0)}[Symbol.dispose](){this.dispose()}decryptFields(t,r){if(r.length===0)return t;const e={...t};for(const s of r){const n=e[s];typeof n=="string"&&this.isEncrypted(n)&&(e[s]=this.decrypt(n))}return e}}export{w as PiiEncryptor};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview PrivacyManager
|
|
3
|
+
* @description Right-to-erasure workflows and data retention policies.
|
|
4
|
+
* Covers SOC2 Privacy Principle (P1-P8) and GDPR Article 17.
|
|
5
|
+
*
|
|
6
|
+
* @version 0.0.1
|
|
7
|
+
* @since 0.0.1
|
|
8
|
+
* @author AMBROISE PARK Consulting
|
|
9
|
+
*/
|
|
10
|
+
export interface RetentionPolicy {
|
|
11
|
+
/** Collection/table name */
|
|
12
|
+
collection: string;
|
|
13
|
+
/**
|
|
14
|
+
* Days to retain data. 0 = no automatic purge.
|
|
15
|
+
* After `days` days, `shouldPurge()` returns true.
|
|
16
|
+
*/
|
|
17
|
+
days: number;
|
|
18
|
+
/**
|
|
19
|
+
* Field to read for the document's creation timestamp (ISO 8601 string).
|
|
20
|
+
* @default 'createdAt'
|
|
21
|
+
*/
|
|
22
|
+
dateField?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ErasureRequest {
|
|
25
|
+
/** User whose data should be erased */
|
|
26
|
+
userId: string;
|
|
27
|
+
/** Collections to erase data from */
|
|
28
|
+
collections: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Callback that performs the actual deletion for one collection.
|
|
31
|
+
* Called once per collection in `collections`.
|
|
32
|
+
*/
|
|
33
|
+
deleteUserData: (collection: string, userId: string) => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export interface ErasureResult {
|
|
36
|
+
/** Collections successfully erased */
|
|
37
|
+
erased: string[];
|
|
38
|
+
/** Collections that failed with error messages */
|
|
39
|
+
errors: Array<{
|
|
40
|
+
collection: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Privacy Manager — right-to-erasure (GDPR Art. 17) and retention policies (SOC2 P6).
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const privacy = new PrivacyManager([
|
|
50
|
+
* { collection: 'audit_logs', days: 365 },
|
|
51
|
+
* { collection: 'sessions', days: 90 },
|
|
52
|
+
* ]);
|
|
53
|
+
*
|
|
54
|
+
* // Erase all user data across collections:
|
|
55
|
+
* await privacy.eraseUser({
|
|
56
|
+
* userId: 'u123',
|
|
57
|
+
* collections: ['users', 'orders', 'sessions'],
|
|
58
|
+
* deleteUserData: async (col, uid) => db.from(col).delete().eq('user_id', uid),
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Check if a document should be purged:
|
|
62
|
+
* privacy.shouldPurge('audit_logs', doc.createdAt); // true if > 365 days old
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @version 0.0.1
|
|
66
|
+
* @since 0.0.1
|
|
67
|
+
* @author AMBROISE PARK Consulting
|
|
68
|
+
*/
|
|
69
|
+
export declare class PrivacyManager {
|
|
70
|
+
private readonly policies;
|
|
71
|
+
constructor(policies?: RetentionPolicy[]);
|
|
72
|
+
/**
|
|
73
|
+
* Execute right-to-erasure for a user across multiple collections.
|
|
74
|
+
* Calls `deleteUserData` for each collection; collects partial errors without aborting.
|
|
75
|
+
* @throws {Error} if `collections` is empty — a silent no-op would be a GDPR violation.
|
|
76
|
+
*/
|
|
77
|
+
eraseUser(request: ErasureRequest): Promise<ErasureResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Check if a document should be purged based on its age and the configured retention policy.
|
|
80
|
+
* Returns `false` if no policy is configured for the collection.
|
|
81
|
+
*
|
|
82
|
+
* @param collection - Collection name
|
|
83
|
+
* @param dateIso - ISO 8601 creation timestamp of the document
|
|
84
|
+
*/
|
|
85
|
+
shouldPurge(collection: string, dateIso: string): boolean;
|
|
86
|
+
/** Return all configured retention policies (used by `dn soc2` checker). */
|
|
87
|
+
getPolicies(): RetentionPolicy[];
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=PrivacyManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PrivacyManager.d.ts","sourceRoot":"","sources":["../../src/server/PrivacyManager.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe;IAC9B,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kDAAkD;IAClD,MAAM,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;gBAEjC,QAAQ,GAAE,eAAe,EAAO;IAI5C;;;;OAIG;IACG,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IA2BhE;;;;;;OAMG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IAwBzD,4EAA4E;IAC5E,WAAW,IAAI,eAAe,EAAE;CAGjC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class a{policies;constructor(e=[]){this.policies=e}async eraseUser(e){if(e.collections.length===0)throw new Error("[dndev/security] eraseUser: collections array is empty. Provide at least one collection to erase user data from. A no-op erasure silently violates GDPR Art. 17.");const r=[],s=[];for(const o of e.collections)try{await e.deleteUserData(o,e.userId),r.push(o)}catch(t){s.push({collection:o,message:t instanceof Error?t.message:String(t)})}return{erased:r,errors:s}}shouldPurge(e,r){const s=this.policies.find(n=>n.collection===e);if(!s||s.days===0)return!1;if(!r)throw new Error(`[dndev/security] shouldPurge: missing dateIso for collection "${e}". Expected ISO 8601 string. Cannot determine if document should be purged.`);const o=new Date(r).getTime();if(isNaN(o))throw new Error(`[dndev/security] shouldPurge: invalid dateIso "${r}" for collection "${e}". Expected ISO 8601 string. Cannot determine if document should be purged.`);const t=Date.now()-o,i=s.days*24*60*60*1e3;return t>i}getPolicies(){return this.policies}}export{a as PrivacyManager};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview RateLimiter
|
|
3
|
+
* @description In-memory fixed-window rate limiter. Zero deps.
|
|
4
|
+
* Covers SOC2 CC6.6 / OWASP API4: unrestricted resource consumption.
|
|
5
|
+
*
|
|
6
|
+
* Note: This is a fixed-window (not sliding-window) implementation. A burst of
|
|
7
|
+
* `points` requests at the end of window N and `points` at the start of N+1 can
|
|
8
|
+
* briefly double the effective rate. Use a Redis-backed backend with a true
|
|
9
|
+
* sliding-window for strict rate control in production.
|
|
10
|
+
*
|
|
11
|
+
* For distributed (multi-replica) deployments, implement RateLimiterBackend
|
|
12
|
+
* and provide a Redis-backed implementation.
|
|
13
|
+
*
|
|
14
|
+
* @version 0.0.1
|
|
15
|
+
* @since 0.0.1
|
|
16
|
+
* @author AMBROISE PARK Consulting
|
|
17
|
+
*/
|
|
18
|
+
export interface RateLimitWindow {
|
|
19
|
+
/** Max requests in window (default: 100 for writes, 500 for reads) */
|
|
20
|
+
points: number;
|
|
21
|
+
/** Window duration in seconds (default: 60) */
|
|
22
|
+
durationSeconds: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Backend interface for rate limiting — implement with Redis in production.
|
|
26
|
+
*
|
|
27
|
+
* @version 0.0.2
|
|
28
|
+
* @since 0.0.1
|
|
29
|
+
* @author AMBROISE PARK Consulting
|
|
30
|
+
*/
|
|
31
|
+
export interface RateLimiterBackend {
|
|
32
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
33
|
+
reset(key: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* In-memory rate limiter (single instance / single replica).
|
|
37
|
+
* Performs lazy eviction of expired entries when the store exceeds
|
|
38
|
+
* {@link EVICTION_THRESHOLD} keys to prevent unbounded memory growth.
|
|
39
|
+
*
|
|
40
|
+
* @version 0.0.1
|
|
41
|
+
* @since 0.0.1
|
|
42
|
+
* @author AMBROISE PARK Consulting
|
|
43
|
+
*/
|
|
44
|
+
export declare class MemoryRateLimiterBackend implements RateLimiterBackend {
|
|
45
|
+
private readonly store;
|
|
46
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
47
|
+
reset(key: string): Promise<void>;
|
|
48
|
+
private _evictExpired;
|
|
49
|
+
}
|
|
50
|
+
export interface RateLimiterOptions {
|
|
51
|
+
writes?: Partial<RateLimitWindow>;
|
|
52
|
+
reads?: Partial<RateLimitWindow>;
|
|
53
|
+
/** Custom backend (default: MemoryRateLimiterBackend) */
|
|
54
|
+
backend?: RateLimiterBackend;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Rate limiter with separate write/read limits (SOC2 CC6.6).
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const limiter = new DndevRateLimiter({ writes: { points: 50 } });
|
|
62
|
+
* await limiter.check('user:abc', 'write'); // throws if > 50 writes/min
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @version 0.0.1
|
|
66
|
+
* @since 0.0.1
|
|
67
|
+
* @author AMBROISE PARK Consulting
|
|
68
|
+
*/
|
|
69
|
+
export declare class DndevRateLimiter {
|
|
70
|
+
private readonly backend;
|
|
71
|
+
private readonly writes;
|
|
72
|
+
private readonly reads;
|
|
73
|
+
constructor(opts?: RateLimiterOptions);
|
|
74
|
+
/**
|
|
75
|
+
* Consume one point for key + operation.
|
|
76
|
+
* @throws {Error} with message 'Rate limit exceeded' when threshold breached.
|
|
77
|
+
*/
|
|
78
|
+
check(key: string, operation: 'read' | 'write'): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=RateLimiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../../src/server/RateLimiter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,eAAe,EAAE,MAAM,CAAC;CACzB;AASD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAKD;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,YAAW,kBAAkB;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IAElD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBzD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,OAAO,CAAC,aAAa;CAStB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACjC,yDAAyD;IACzD,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;gBAE5B,IAAI,GAAE,kBAAuB;IAYzC;;;OAGG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAWrE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const c=1e4;class o{store=new Map;async increment(t,s){const e=Date.now(),n=this.store.get(t);return!n||e-n.windowStart>s?(!n&&this.store.size>=1e4&&this._evictExpired(e),this.store.set(t,{count:1,windowStart:e,windowMs:s}),1):(n.count+=1,n.count)}async reset(t){this.store.delete(t)}_evictExpired(t){for(const[s,e]of this.store)t-e.windowStart>e.windowMs&&this.store.delete(s)}}class d{backend;writes;reads;constructor(t={}){this.backend=t.backend??new o,this.writes={points:t.writes?.points??100,durationSeconds:t.writes?.durationSeconds??60},this.reads={points:t.reads?.points??500,durationSeconds:t.reads?.durationSeconds??60}}async check(t,s){const e=s==="write"?this.writes:this.reads,n=e.durationSeconds*1e3,i=await this.backend.increment(`${s}:${t}`,n);if(i>e.points)throw new Error(`Rate limit exceeded: ${i}/${e.points} ${s} requests in ${e.durationSeconds}s`)}}export{d as DndevRateLimiter,o as MemoryRateLimiterBackend};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively scrub secret values from a log payload.
|
|
3
|
+
* Replaces matching string values with `[REDACTED]`.
|
|
4
|
+
* Safe to use as a Pino serializer or before any `console.log` call.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* const payload = { user: 'alice', password: 'hunter2', meta: { apiKey: 'sk_live_abc' } };
|
|
9
|
+
* console.log(JSON.stringify(scrubSecrets(payload)));
|
|
10
|
+
* // → {"user":"alice","password":"[REDACTED]","meta":{"apiKey":"[REDACTED]"}}
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function scrubSecrets(value: unknown): unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Assert that an object does NOT contain any detectable secrets.
|
|
16
|
+
* Throws before the value would reach a log sink.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* assertNoSecrets(responsePayload, 'API response');
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @throws {Error} if secrets are detected or if the value is non-serializable
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertNoSecrets(obj: unknown, context: string): void;
|
|
26
|
+
//# sourceMappingURL=SecretValidator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecretValidator.d.ts","sourceRoot":"","sources":["../../src/server/SecretValidator.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAsBpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAmBnE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=[/password\s*[:=]\s*\S+/gi,/secret\s*[:=]\s*\S+/gi,/api[_-]?key\s*[:=]\s*\S+/gi,/token\s*[:=]\s*\S+/gi,/bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,/-----BEGIN .+?-----/g,/sk_live_[A-Za-z0-9]+/g,/sk_test_[A-Za-z0-9]+/g,/ghp_[A-Za-z0-9]{36,}/g,/gho_[A-Za-z0-9]{36,}/g,/AKIA[A-Z0-9]{16}/g,/xox[bpsa]-[A-Za-z0-9\-]+/g,/glpat-[A-Za-z0-9\-_]{20,}/g],a=/password|passwd|secret|token|apikey|api_key|credential|private_key|access_key|\bauth\b/i;function i(e){if(typeof e=="string"){let t=e;for(const r of n)t=t.replace(r,"[REDACTED]");return t}if(Array.isArray(e))return e.map(i);if(e!==null&&typeof e=="object"){const t={};for(const[r,s]of Object.entries(e))t[r]=a.test(r)?"[REDACTED]":i(s);return t}return e}function o(e,t){let r,s;try{s=JSON.stringify(e),r=JSON.stringify(i(e))}catch{throw new Error(`[dndev/security] assertNoSecrets: cannot serialize value in "${t}". Non-serializable values cannot be verified for secrets. Audit the object manually.`)}if(r!==s)throw new Error(`[dndev/security] Secret detected in ${t}. Aborting to prevent credential leak.`)}export{o as assertNoSecrets,i as scrubSecrets};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { AuditLogger } from './AuditLogger';
|
|
2
|
+
export type { AuditLoggerOptions } from './AuditLogger';
|
|
3
|
+
export { DndevRateLimiter, MemoryRateLimiterBackend } from './RateLimiter';
|
|
4
|
+
export type { RateLimiterBackend, RateLimiterOptions, RateLimitWindow } from './RateLimiter';
|
|
5
|
+
export { PiiEncryptor } from './PiiEncryptor';
|
|
6
|
+
export { AuthHardening } from './AuthHardening';
|
|
7
|
+
export type { AuthHardeningConfig, LockoutResult } from './AuthHardening';
|
|
8
|
+
export { AnomalyDetector } from './AnomalyDetector';
|
|
9
|
+
export type { AnomalyThresholds, AnomalyHandler, AnomalyType } from './AnomalyDetector';
|
|
10
|
+
export { PrivacyManager } from './PrivacyManager';
|
|
11
|
+
export type { RetentionPolicy, ErasureRequest, ErasureResult } from './PrivacyManager';
|
|
12
|
+
export { scrubSecrets, assertNoSecrets } from './SecretValidator';
|
|
13
|
+
export { DndevSecurity } from './DndevSecurity';
|
|
14
|
+
export type { DndevSecurityConfig } from './DndevSecurity';
|
|
15
|
+
export type { SecurityContext, AuditEvent, AuditEventType } from '../common/SecurityConfig';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAC3E,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE7F,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAExF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAElE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAG3D,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AuditLogger as o}from"./AuditLogger";import{DndevRateLimiter as m,MemoryRateLimiterBackend as i}from"./RateLimiter";import{PiiEncryptor as p}from"./PiiEncryptor";import{AuthHardening as f}from"./AuthHardening";import{AnomalyDetector as x}from"./AnomalyDetector";import{PrivacyManager as s}from"./PrivacyManager";import{scrubSecrets as g,assertNoSecrets as u}from"./SecretValidator";import{DndevSecurity as A}from"./DndevSecurity";export{x as AnomalyDetector,o as AuditLogger,f as AuthHardening,m as DndevRateLimiter,A as DndevSecurity,i as MemoryRateLimiterBackend,p as PiiEncryptor,s as PrivacyManager,u as assertNoSecrets,g as scrubSecrets};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@donotdev/security",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
7
|
+
"description": "SOC2-grade security controls for DoNotDev — audit logging, rate limiting, PII encryption, auth hardening, anomaly detection, privacy management",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./server": {
|
|
17
|
+
"types": "./dist/server/index.d.ts",
|
|
18
|
+
"import": "./dist/server/index.js",
|
|
19
|
+
"default": "./dist/server/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsc --noEmit --watch --listFiles false --listEmittedFiles false",
|
|
24
|
+
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
25
|
+
"type-check": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@donotdev/core": "^0.0.24"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"package.json",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE.md"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/donotdev/dndev.git"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"donotdev",
|
|
45
|
+
"dndev",
|
|
46
|
+
"security",
|
|
47
|
+
"soc2",
|
|
48
|
+
"audit",
|
|
49
|
+
"rate-limit",
|
|
50
|
+
"encryption",
|
|
51
|
+
"privacy",
|
|
52
|
+
"typescript"
|
|
53
|
+
],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"registry": "https://registry.npmjs.org",
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|