@secfn/core 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.
Files changed (64) hide show
  1. package/README.md +334 -0
  2. package/dist/access-control/rbac.d.ts +48 -0
  3. package/dist/access-control/rbac.d.ts.map +1 -0
  4. package/dist/access-control/rbac.js +203 -0
  5. package/dist/access-control/rbac.js.map +1 -0
  6. package/dist/core/secfn.d.ts +28 -0
  7. package/dist/core/secfn.d.ts.map +1 -0
  8. package/dist/core/secfn.js +73 -0
  9. package/dist/core/secfn.js.map +1 -0
  10. package/dist/index.d.ts +12 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +12 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/monitoring/security-monitor.d.ts +46 -0
  15. package/dist/monitoring/security-monitor.d.ts.map +1 -0
  16. package/dist/monitoring/security-monitor.js +172 -0
  17. package/dist/monitoring/security-monitor.js.map +1 -0
  18. package/dist/rate-limiting/rate-limiter.d.ts +31 -0
  19. package/dist/rate-limiting/rate-limiter.d.ts.map +1 -0
  20. package/dist/rate-limiting/rate-limiter.js +140 -0
  21. package/dist/rate-limiting/rate-limiter.js.map +1 -0
  22. package/dist/scanning/patterns.d.ts +3 -0
  23. package/dist/scanning/patterns.d.ts.map +1 -0
  24. package/dist/scanning/patterns.js +139 -0
  25. package/dist/scanning/patterns.js.map +1 -0
  26. package/dist/scanning/scanner.d.ts +36 -0
  27. package/dist/scanning/scanner.d.ts.map +1 -0
  28. package/dist/scanning/scanner.js +152 -0
  29. package/dist/scanning/scanner.js.map +1 -0
  30. package/dist/secrets/encryption.d.ts +17 -0
  31. package/dist/secrets/encryption.d.ts.map +1 -0
  32. package/dist/secrets/encryption.js +47 -0
  33. package/dist/secrets/encryption.js.map +1 -0
  34. package/dist/secrets/vault.d.ts +43 -0
  35. package/dist/secrets/vault.d.ts.map +1 -0
  36. package/dist/secrets/vault.js +126 -0
  37. package/dist/secrets/vault.js.map +1 -0
  38. package/dist/storage/indexeddb-storage.d.ts +37 -0
  39. package/dist/storage/indexeddb-storage.d.ts.map +1 -0
  40. package/dist/storage/indexeddb-storage.js +284 -0
  41. package/dist/storage/indexeddb-storage.js.map +1 -0
  42. package/dist/types/index.d.ts +189 -0
  43. package/dist/types/index.d.ts.map +1 -0
  44. package/dist/types/index.js +2 -0
  45. package/dist/types/index.js.map +1 -0
  46. package/dist/utils/helpers.d.ts +8 -0
  47. package/dist/utils/helpers.d.ts.map +1 -0
  48. package/dist/utils/helpers.js +73 -0
  49. package/dist/utils/helpers.js.map +1 -0
  50. package/examples/basic-usage.ts +143 -0
  51. package/package.json +68 -0
  52. package/src/access-control/rbac.ts +271 -0
  53. package/src/core/secfn.ts +90 -0
  54. package/src/index.ts +27 -0
  55. package/src/monitoring/security-monitor.ts +245 -0
  56. package/src/rate-limiting/rate-limiter.ts +181 -0
  57. package/src/scanning/patterns.ts +140 -0
  58. package/src/scanning/scanner.ts +199 -0
  59. package/src/secrets/encryption.ts +74 -0
  60. package/src/secrets/vault.ts +187 -0
  61. package/src/storage/indexeddb-storage.ts +355 -0
  62. package/src/types/index.ts +213 -0
  63. package/src/utils/helpers.ts +84 -0
  64. package/tsconfig.json +26 -0
@@ -0,0 +1,199 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { glob } from 'glob';
3
+ import simpleGit, { type SimpleGit } from 'simple-git';
4
+ import type { SecretPattern, SecretScanResult } from '../types/index.js';
5
+ import type { IndexedDBStorage } from '../storage/indexeddb-storage.js';
6
+ import { DEFAULT_PATTERNS } from './patterns.js';
7
+ import { generateId, calculateEntropy, redactSecret } from '../utils/helpers.js';
8
+
9
+ export interface SecretScannerConfig {
10
+ patterns?: SecretPattern[];
11
+ excludePaths?: string[];
12
+ minEntropy?: number;
13
+ maxFileSize?: number;
14
+ storage?: IndexedDBStorage;
15
+ }
16
+
17
+ export interface ScanOptions {
18
+ saveResults?: boolean;
19
+ }
20
+
21
+ export interface GitScanOptions {
22
+ branch?: string;
23
+ commits?: number;
24
+ }
25
+
26
+ export class SecretScanner {
27
+ private patterns: SecretPattern[];
28
+ private excludePaths: string[];
29
+ private minEntropy: number;
30
+ private maxFileSize: number;
31
+ private storage?: IndexedDBStorage;
32
+ private git: SimpleGit;
33
+
34
+ constructor(config: SecretScannerConfig = {}) {
35
+ this.patterns = config.patterns || DEFAULT_PATTERNS;
36
+ this.excludePaths = config.excludePaths || ['node_modules/**', 'dist/**', 'build/**', '.git/**'];
37
+ this.minEntropy = config.minEntropy || 3.0;
38
+ this.maxFileSize = config.maxFileSize || 1024 * 1024;
39
+ this.storage = config.storage;
40
+ this.git = simpleGit();
41
+ }
42
+
43
+ async scanDirectory(dir: string, options: ScanOptions = {}): Promise<SecretScanResult[]> {
44
+ const files = await glob('**/*', {
45
+ cwd: dir,
46
+ absolute: true,
47
+ nodir: true,
48
+ ignore: this.excludePaths,
49
+ });
50
+
51
+ const results: SecretScanResult[] = [];
52
+
53
+ for (const file of files) {
54
+ try {
55
+ const fileResults = await this.scanFile(file);
56
+ results.push(...fileResults);
57
+ } catch (error) {
58
+ console.error(`[SecFn] Error scanning file ${file}:`, error);
59
+ }
60
+ }
61
+
62
+ if (options.saveResults && this.storage) {
63
+ for (const result of results) {
64
+ await this.storage.saveScanResult(result);
65
+ }
66
+ }
67
+
68
+ return results;
69
+ }
70
+
71
+ async scanFile(filePath: string): Promise<SecretScanResult[]> {
72
+ try {
73
+ const content = await readFile(filePath, 'utf-8');
74
+
75
+ if (content.length > this.maxFileSize) {
76
+ console.warn(`[SecFn] Skipping large file: ${filePath}`);
77
+ return [];
78
+ }
79
+
80
+ return this.scanContent(content, filePath);
81
+ } catch (error) {
82
+ console.error(`[SecFn] Error reading file ${filePath}:`, error);
83
+ return [];
84
+ }
85
+ }
86
+
87
+ scanContent(content: string, filePath: string): SecretScanResult[] {
88
+ const results: SecretScanResult[] = [];
89
+ const lines = content.split('\n');
90
+
91
+ for (const pattern of this.patterns) {
92
+ const matches = content.matchAll(pattern.pattern);
93
+
94
+ for (const match of matches) {
95
+ if (!match[0] || !match.index) continue;
96
+
97
+ const matchedSecret = match[1] || match[0];
98
+ const entropy = calculateEntropy(matchedSecret);
99
+
100
+ if (pattern.entropy && entropy < Math.min(pattern.entropy, this.minEntropy)) {
101
+ continue;
102
+ }
103
+
104
+ const { line, column } = this.getLineAndColumn(content, match.index);
105
+ const context = this.getContext(lines, line);
106
+
107
+ const result: SecretScanResult = {
108
+ id: generateId('scan'),
109
+ timestamp: Date.now(),
110
+ file: filePath,
111
+ line,
112
+ column,
113
+ pattern: pattern.name,
114
+ match: matchedSecret,
115
+ redactedMatch: redactSecret(matchedSecret),
116
+ severity: pattern.severity,
117
+ entropy,
118
+ context,
119
+ resolved: false,
120
+ falsePositive: false,
121
+ };
122
+
123
+ results.push(result);
124
+ }
125
+ }
126
+
127
+ return results;
128
+ }
129
+
130
+ async scanGitHistory(options: GitScanOptions = {}): Promise<SecretScanResult[]> {
131
+ const branch = options.branch || 'HEAD';
132
+ const commitCount = options.commits || 100;
133
+
134
+ const log = await this.git.log([branch, `-n`, `${commitCount}`]);
135
+ const results: SecretScanResult[] = [];
136
+
137
+ for (const commit of log.all) {
138
+ try {
139
+ const diff = await this.git.show([commit.hash]);
140
+ const commitResults = this.scanContent(diff, `commit:${commit.hash.substring(0, 7)}`);
141
+
142
+ for (const result of commitResults) {
143
+ result.metadata = {
144
+ commit: commit.hash,
145
+ author: commit.author_name,
146
+ date: commit.date,
147
+ message: commit.message,
148
+ };
149
+ }
150
+
151
+ results.push(...commitResults);
152
+ } catch (error) {
153
+ console.error(`[SecFn] Error scanning commit ${commit.hash}:`, error);
154
+ }
155
+ }
156
+
157
+ if (this.storage) {
158
+ for (const result of results) {
159
+ await this.storage.saveScanResult(result);
160
+ }
161
+ }
162
+
163
+ return results;
164
+ }
165
+
166
+ addPattern(pattern: SecretPattern): void {
167
+ this.patterns.push(pattern);
168
+ }
169
+
170
+ removePattern(name: string): void {
171
+ this.patterns = this.patterns.filter(p => p.name !== name);
172
+ }
173
+
174
+ getPatterns(): SecretPattern[] {
175
+ return [...this.patterns];
176
+ }
177
+
178
+ private getLineAndColumn(content: string, index: number): { line: number; column: number } {
179
+ const beforeMatch = content.substring(0, index);
180
+ const lines = beforeMatch.split('\n');
181
+ const line = lines.length;
182
+ const column = lines[lines.length - 1].length + 1;
183
+
184
+ return { line, column };
185
+ }
186
+
187
+ private getContext(lines: string[], lineNumber: number, contextLines = 2): string {
188
+ const start = Math.max(0, lineNumber - contextLines - 1);
189
+ const end = Math.min(lines.length, lineNumber + contextLines);
190
+ return lines.slice(start, end).join('\n');
191
+ }
192
+
193
+ async getScanHistory(limit = 100): Promise<SecretScanResult[]> {
194
+ if (!this.storage) {
195
+ throw new Error('Storage not configured');
196
+ }
197
+ return await this.storage.getScanHistory(limit);
198
+ }
199
+ }
@@ -0,0 +1,74 @@
1
+ import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from 'crypto';
2
+
3
+ export interface EncryptedData {
4
+ iv: string;
5
+ encrypted: string;
6
+ authTag: string;
7
+ }
8
+
9
+ export class SecretEncryption {
10
+ private algorithm = 'aes-256-gcm';
11
+ private keyLength = 32;
12
+ private ivLength = 16;
13
+ private salt = 'secfn-salt-v1';
14
+ private iterations = 100000;
15
+
16
+ async encrypt(plaintext: string, masterKey: string): Promise<string> {
17
+ const key = this.deriveKey(masterKey);
18
+ const iv = randomBytes(this.ivLength);
19
+ const cipher = createCipheriv(this.algorithm, key, iv) as any;
20
+
21
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex');
22
+ encrypted += cipher.final('hex');
23
+ const authTag = cipher.getAuthTag();
24
+
25
+ const data: EncryptedData = {
26
+ iv: iv.toString('hex'),
27
+ encrypted,
28
+ authTag: authTag.toString('hex'),
29
+ };
30
+
31
+ return JSON.stringify(data);
32
+ }
33
+
34
+ async decrypt(ciphertext: string, masterKey: string): Promise<string> {
35
+ const key = this.deriveKey(masterKey);
36
+ const data: EncryptedData = JSON.parse(ciphertext);
37
+
38
+ const decipher = createDecipheriv(
39
+ this.algorithm,
40
+ key,
41
+ Buffer.from(data.iv, 'hex')
42
+ ) as any;
43
+ decipher.setAuthTag(Buffer.from(data.authTag, 'hex'));
44
+
45
+ let decrypted = decipher.update(data.encrypted, 'hex', 'utf8');
46
+ decrypted += decipher.final('utf8');
47
+
48
+ return decrypted;
49
+ }
50
+
51
+ private deriveKey(masterKey: string): Buffer {
52
+ return pbkdf2Sync(
53
+ masterKey,
54
+ this.salt,
55
+ this.iterations,
56
+ this.keyLength,
57
+ 'sha256'
58
+ );
59
+ }
60
+
61
+ isEncrypted(value: string): boolean {
62
+ try {
63
+ const parsed = JSON.parse(value);
64
+ return (
65
+ typeof parsed === 'object' &&
66
+ 'iv' in parsed &&
67
+ 'encrypted' in parsed &&
68
+ 'authTag' in parsed
69
+ );
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,187 @@
1
+ import type { Secret, SecretAccess, Environment } from '../types/index.js';
2
+ import type { IndexedDBStorage } from '../storage/indexeddb-storage.js';
3
+ import { SecretEncryption } from './encryption.js';
4
+ import { generateId, isExpired } from '../utils/helpers.js';
5
+
6
+ export interface SecretsVaultConfig {
7
+ storage: IndexedDBStorage;
8
+ encryption: {
9
+ algorithm: 'AES-256-GCM';
10
+ masterKey: string;
11
+ keyDerivation?: 'PBKDF2' | 'scrypt';
12
+ };
13
+ rotation?: {
14
+ checkInterval: number;
15
+ notifyBefore: number;
16
+ };
17
+ }
18
+
19
+ export interface SetSecretOptions {
20
+ tags?: string[];
21
+ environment?: Environment;
22
+ expiresAt?: number;
23
+ rotateEvery?: number;
24
+ createdBy?: string;
25
+ }
26
+
27
+ export class SecretsVault {
28
+ private storage: IndexedDBStorage;
29
+ private encryption: SecretEncryption;
30
+ private masterKey: string;
31
+ private rotationInterval?: NodeJS.Timeout;
32
+
33
+ constructor(config: SecretsVaultConfig) {
34
+ this.storage = config.storage;
35
+ this.encryption = new SecretEncryption();
36
+ this.masterKey = config.encryption.masterKey;
37
+
38
+ if (config.rotation) {
39
+ this.startRotationCheck(config.rotation.checkInterval, config.rotation.notifyBefore);
40
+ }
41
+ }
42
+
43
+ async set(key: string, value: string, options: SetSecretOptions = {}): Promise<string> {
44
+ const existing = await this.storage.getSecretByKey(key);
45
+ const now = Date.now();
46
+
47
+ const encryptedValue = await this.encryption.encrypt(value, this.masterKey);
48
+
49
+ const secret: Secret = {
50
+ id: existing?.id || generateId('secret'),
51
+ key,
52
+ value: encryptedValue,
53
+ encrypted: true,
54
+ version: existing ? existing.version + 1 : 1,
55
+ tags: options.tags || [],
56
+ environment: options.environment,
57
+ expiresAt: options.expiresAt,
58
+ rotateEvery: options.rotateEvery,
59
+ lastRotated: now,
60
+ lastAccessed: undefined,
61
+ accessCount: 0,
62
+ createdBy: options.createdBy || 'system',
63
+ createdAt: existing?.createdAt || now,
64
+ updatedAt: now,
65
+ };
66
+
67
+ await this.storage.saveSecret(secret);
68
+
69
+ await this.logAccess(secret.id, options.createdBy || 'system', 'write');
70
+
71
+ return secret.id;
72
+ }
73
+
74
+ async get(key: string, userId = 'system'): Promise<string | null> {
75
+ const secret = await this.storage.getSecretByKey(key);
76
+
77
+ if (!secret) return null;
78
+
79
+ if (isExpired(secret.expiresAt)) {
80
+ throw new Error(`Secret '${key}' has expired`);
81
+ }
82
+
83
+ const decryptedValue = await this.encryption.decrypt(secret.value, this.masterKey);
84
+
85
+ secret.lastAccessed = Date.now();
86
+ secret.accessCount += 1;
87
+ await this.storage.saveSecret(secret);
88
+
89
+ await this.logAccess(secret.id, userId, 'read');
90
+
91
+ return decryptedValue;
92
+ }
93
+
94
+ async list(filter?: { environment?: Environment; tags?: string[] }): Promise<Array<Omit<Secret, 'value'>>> {
95
+ const secrets = await this.storage.listSecrets(filter);
96
+
97
+ return secrets.map(({ value, ...rest }) => rest);
98
+ }
99
+
100
+ async rotate(key: string, newValue: string, userId = 'system'): Promise<void> {
101
+ const secret = await this.storage.getSecretByKey(key);
102
+
103
+ if (!secret) {
104
+ throw new Error(`Secret '${key}' not found`);
105
+ }
106
+
107
+ const encryptedValue = await this.encryption.encrypt(newValue, this.masterKey);
108
+
109
+ secret.value = encryptedValue;
110
+ secret.version += 1;
111
+ secret.lastRotated = Date.now();
112
+ secret.updatedAt = Date.now();
113
+
114
+ await this.storage.saveSecret(secret);
115
+
116
+ await this.logAccess(secret.id, userId, 'rotate');
117
+ }
118
+
119
+ async delete(key: string, userId = 'system'): Promise<void> {
120
+ const secret = await this.storage.getSecretByKey(key);
121
+
122
+ if (!secret) {
123
+ throw new Error(`Secret '${key}' not found`);
124
+ }
125
+
126
+ await this.storage.deleteSecret(secret.id);
127
+
128
+ await this.logAccess(secret.id, userId, 'delete');
129
+ }
130
+
131
+ async getAccessLog(key: string, limit = 100): Promise<SecretAccess[]> {
132
+ const secret = await this.storage.getSecretByKey(key);
133
+
134
+ if (!secret) {
135
+ throw new Error(`Secret '${key}' not found`);
136
+ }
137
+
138
+ return await this.storage.getSecretAccessLog(secret.id, limit);
139
+ }
140
+
141
+ async getSecretsNeedingRotation(beforeMs: number = 7 * 24 * 60 * 60 * 1000): Promise<Secret[]> {
142
+ const allSecrets = await this.storage.listSecrets();
143
+ const now = Date.now();
144
+ const threshold = now + beforeMs;
145
+
146
+ return allSecrets.filter(secret => {
147
+ if (!secret.rotateEvery || !secret.lastRotated) return false;
148
+
149
+ const nextRotation = secret.lastRotated + secret.rotateEvery;
150
+ return nextRotation <= threshold;
151
+ });
152
+ }
153
+
154
+ async getExpiredSecrets(): Promise<Secret[]> {
155
+ const allSecrets = await this.storage.listSecrets();
156
+ return allSecrets.filter(secret => isExpired(secret.expiresAt));
157
+ }
158
+
159
+ private async logAccess(secretId: string, userId: string, action: 'read' | 'write' | 'delete' | 'rotate'): Promise<void> {
160
+ const accessLog: SecretAccess = {
161
+ id: generateId('access'),
162
+ secretId,
163
+ userId,
164
+ action,
165
+ timestamp: Date.now(),
166
+ };
167
+
168
+ await this.storage.logSecretAccess(accessLog);
169
+ }
170
+
171
+ private startRotationCheck(interval: number, notifyBefore: number): void {
172
+ this.rotationInterval = setInterval(async () => {
173
+ const needsRotation = await this.getSecretsNeedingRotation(notifyBefore);
174
+
175
+ if (needsRotation.length > 0) {
176
+ console.warn(`[SecFn] ${needsRotation.length} secret(s) need rotation:`,
177
+ needsRotation.map(s => s.key));
178
+ }
179
+ }, interval);
180
+ }
181
+
182
+ stop(): void {
183
+ if (this.rotationInterval) {
184
+ clearInterval(this.rotationInterval);
185
+ }
186
+ }
187
+ }