@secfn/core 0.0.1 → 0.0.2
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 +30 -322
- package/dist/encryption.d.ts +8 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/encryption.js +76 -0
- package/dist/encryption.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +47 -0
- package/dist/errors.js.map +1 -0
- package/dist/id.d.ts +3 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +11 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +9 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -11
- package/dist/index.js.map +1 -1
- package/dist/scanner/reporters.d.ts +14 -0
- package/dist/scanner/reporters.d.ts.map +1 -0
- package/dist/scanner/reporters.js +85 -0
- package/dist/scanner/reporters.js.map +1 -0
- package/dist/scanner/rules.d.ts +24 -0
- package/dist/scanner/rules.d.ts.map +1 -0
- package/dist/scanner/rules.js +141 -0
- package/dist/scanner/rules.js.map +1 -0
- package/dist/scanner/scanner.d.ts +25 -0
- package/dist/scanner/scanner.d.ts.map +1 -0
- package/dist/scanner/scanner.js +73 -0
- package/dist/scanner/scanner.js.map +1 -0
- package/dist/scanner/utils.d.ts +15 -0
- package/dist/scanner/utils.d.ts.map +1 -0
- package/dist/scanner/utils.js +40 -0
- package/dist/scanner/utils.js.map +1 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +246 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +29 -26
- package/dist/access-control/rbac.d.ts +0 -48
- package/dist/access-control/rbac.d.ts.map +0 -1
- package/dist/access-control/rbac.js +0 -203
- package/dist/access-control/rbac.js.map +0 -1
- package/dist/core/secfn.d.ts +0 -28
- package/dist/core/secfn.d.ts.map +0 -1
- package/dist/core/secfn.js +0 -73
- package/dist/core/secfn.js.map +0 -1
- package/dist/monitoring/security-monitor.d.ts +0 -46
- package/dist/monitoring/security-monitor.d.ts.map +0 -1
- package/dist/monitoring/security-monitor.js +0 -172
- package/dist/monitoring/security-monitor.js.map +0 -1
- package/dist/rate-limiting/rate-limiter.d.ts +0 -31
- package/dist/rate-limiting/rate-limiter.d.ts.map +0 -1
- package/dist/rate-limiting/rate-limiter.js +0 -140
- package/dist/rate-limiting/rate-limiter.js.map +0 -1
- package/dist/scanning/patterns.d.ts +0 -3
- package/dist/scanning/patterns.d.ts.map +0 -1
- package/dist/scanning/patterns.js +0 -139
- package/dist/scanning/patterns.js.map +0 -1
- package/dist/scanning/scanner.d.ts +0 -36
- package/dist/scanning/scanner.d.ts.map +0 -1
- package/dist/scanning/scanner.js +0 -152
- package/dist/scanning/scanner.js.map +0 -1
- package/dist/secrets/encryption.d.ts +0 -17
- package/dist/secrets/encryption.d.ts.map +0 -1
- package/dist/secrets/encryption.js +0 -47
- package/dist/secrets/encryption.js.map +0 -1
- package/dist/secrets/vault.d.ts +0 -43
- package/dist/secrets/vault.d.ts.map +0 -1
- package/dist/secrets/vault.js +0 -126
- package/dist/secrets/vault.js.map +0 -1
- package/dist/storage/indexeddb-storage.d.ts +0 -37
- package/dist/storage/indexeddb-storage.d.ts.map +0 -1
- package/dist/storage/indexeddb-storage.js +0 -284
- package/dist/storage/indexeddb-storage.js.map +0 -1
- package/dist/types/index.d.ts +0 -189
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -2
- package/dist/types/index.js.map +0 -1
- package/dist/utils/helpers.d.ts +0 -8
- package/dist/utils/helpers.d.ts.map +0 -1
- package/dist/utils/helpers.js +0 -73
- package/dist/utils/helpers.js.map +0 -1
- package/examples/basic-usage.ts +0 -143
- package/src/access-control/rbac.ts +0 -271
- package/src/core/secfn.ts +0 -90
- package/src/index.ts +0 -27
- package/src/monitoring/security-monitor.ts +0 -245
- package/src/rate-limiting/rate-limiter.ts +0 -181
- package/src/scanning/patterns.ts +0 -140
- package/src/scanning/scanner.ts +0 -199
- package/src/secrets/encryption.ts +0 -74
- package/src/secrets/vault.ts +0 -187
- package/src/storage/indexeddb-storage.ts +0 -355
- package/src/types/index.ts +0 -213
- package/src/utils/helpers.ts +0 -84
- package/tsconfig.json +0 -26
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import type { RateLimitRule, RateLimitEntry, RateLimitResult, RateLimitViolation } from '../types/index.js';
|
|
2
|
-
import type { IndexedDBStorage } from '../storage/indexeddb-storage.js';
|
|
3
|
-
import { generateId } from '../utils/helpers.js';
|
|
4
|
-
|
|
5
|
-
export interface RateLimiterConfig {
|
|
6
|
-
storage: IndexedDBStorage;
|
|
7
|
-
rules: {
|
|
8
|
-
global?: RateLimitRule;
|
|
9
|
-
perUser?: RateLimitRule;
|
|
10
|
-
perIP?: RateLimitRule;
|
|
11
|
-
perEndpoint?: Record<string, RateLimitRule>;
|
|
12
|
-
};
|
|
13
|
-
onLimitExceeded?: (violation: RateLimitViolation) => void | Promise<void>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface RateLimitCheckRequest {
|
|
17
|
-
userId?: string;
|
|
18
|
-
ip: string;
|
|
19
|
-
endpoint?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class RateLimiter {
|
|
23
|
-
private storage: IndexedDBStorage;
|
|
24
|
-
private rules: RateLimiterConfig['rules'];
|
|
25
|
-
private onLimitExceeded?: (violation: RateLimitViolation) => void | Promise<void>;
|
|
26
|
-
|
|
27
|
-
constructor(config: RateLimiterConfig) {
|
|
28
|
-
this.storage = config.storage;
|
|
29
|
-
this.rules = config.rules;
|
|
30
|
-
this.onLimitExceeded = config.onLimitExceeded;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async check(request: RateLimitCheckRequest): Promise<RateLimitResult> {
|
|
34
|
-
const checks: Array<{ key: string; rule: RateLimitRule }> = [];
|
|
35
|
-
|
|
36
|
-
if (this.rules.global) {
|
|
37
|
-
checks.push({
|
|
38
|
-
key: 'global',
|
|
39
|
-
rule: this.rules.global,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (this.rules.perUser && request.userId) {
|
|
44
|
-
checks.push({
|
|
45
|
-
key: `user:${request.userId}`,
|
|
46
|
-
rule: this.rules.perUser,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (this.rules.perIP) {
|
|
51
|
-
checks.push({
|
|
52
|
-
key: `ip:${request.ip}`,
|
|
53
|
-
rule: this.rules.perIP,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (this.rules.perEndpoint && request.endpoint) {
|
|
58
|
-
const endpointRule = this.rules.perEndpoint[request.endpoint];
|
|
59
|
-
if (endpointRule) {
|
|
60
|
-
checks.push({
|
|
61
|
-
key: `endpoint:${request.endpoint}:${request.ip}`,
|
|
62
|
-
rule: endpointRule,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
for (const check of checks) {
|
|
68
|
-
const result = await this.checkLimit(check.key, check.rule);
|
|
69
|
-
|
|
70
|
-
if (!result.allowed) {
|
|
71
|
-
await this.handleViolation({
|
|
72
|
-
id: generateId('violation'),
|
|
73
|
-
timestamp: Date.now(),
|
|
74
|
-
key: check.key,
|
|
75
|
-
ip: request.ip,
|
|
76
|
-
userId: request.userId,
|
|
77
|
-
endpoint: request.endpoint || 'unknown',
|
|
78
|
-
requestCount: result.limit || 0,
|
|
79
|
-
limit: check.rule.requests,
|
|
80
|
-
window: check.rule.window,
|
|
81
|
-
blocked: true,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
return result;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
allowed: true,
|
|
90
|
-
remaining: checks.length > 0 ? checks[0].rule.requests : 0,
|
|
91
|
-
resetAt: Date.now() + (checks.length > 0 ? checks[0].rule.window : 0),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private async checkLimit(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
const entry = await this.storage.getRateLimitEntry(key);
|
|
98
|
-
|
|
99
|
-
if (!entry || now >= entry.resetAt) {
|
|
100
|
-
const newEntry: RateLimitEntry = {
|
|
101
|
-
key,
|
|
102
|
-
count: 1,
|
|
103
|
-
resetAt: now + rule.window,
|
|
104
|
-
blocked: false,
|
|
105
|
-
};
|
|
106
|
-
await this.storage.setRateLimitEntry(newEntry);
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
allowed: true,
|
|
110
|
-
remaining: rule.requests - 1,
|
|
111
|
-
resetAt: newEntry.resetAt,
|
|
112
|
-
limit: rule.requests,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (entry.blocked && entry.blockedUntil && now < entry.blockedUntil) {
|
|
117
|
-
return {
|
|
118
|
-
allowed: false,
|
|
119
|
-
remaining: 0,
|
|
120
|
-
resetAt: entry.resetAt,
|
|
121
|
-
retryAfter: entry.blockedUntil - now,
|
|
122
|
-
limit: rule.requests,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (entry.count >= rule.requests) {
|
|
127
|
-
entry.blocked = true;
|
|
128
|
-
entry.blockedUntil = now + (rule.blockDuration || rule.window);
|
|
129
|
-
await this.storage.setRateLimitEntry(entry);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
allowed: false,
|
|
133
|
-
remaining: 0,
|
|
134
|
-
resetAt: entry.resetAt,
|
|
135
|
-
retryAfter: entry.blockedUntil - now,
|
|
136
|
-
limit: rule.requests,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
entry.count++;
|
|
141
|
-
await this.storage.setRateLimitEntry(entry);
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
allowed: true,
|
|
145
|
-
remaining: rule.requests - entry.count,
|
|
146
|
-
resetAt: entry.resetAt,
|
|
147
|
-
limit: rule.requests,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private async handleViolation(violation: RateLimitViolation): Promise<void> {
|
|
152
|
-
if (this.onLimitExceeded) {
|
|
153
|
-
try {
|
|
154
|
-
await this.onLimitExceeded(violation);
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error('[SecFn] Error in rate limit violation handler:', error);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async reset(key: string): Promise<void> {
|
|
162
|
-
await this.storage.deleteRateLimitEntry(key);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async resetUser(userId: string): Promise<void> {
|
|
166
|
-
await this.reset(`user:${userId}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async resetIP(ip: string): Promise<void> {
|
|
170
|
-
await this.reset(`ip:${ip}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async unblock(key: string): Promise<void> {
|
|
174
|
-
const entry = await this.storage.getRateLimitEntry(key);
|
|
175
|
-
if (entry) {
|
|
176
|
-
entry.blocked = false;
|
|
177
|
-
entry.blockedUntil = undefined;
|
|
178
|
-
await this.storage.setRateLimitEntry(entry);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
package/src/scanning/patterns.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { SecretPattern } from '../types/index.js';
|
|
2
|
-
|
|
3
|
-
export const DEFAULT_PATTERNS: SecretPattern[] = [
|
|
4
|
-
{
|
|
5
|
-
name: 'AWS Access Key',
|
|
6
|
-
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
7
|
-
entropy: 4.5,
|
|
8
|
-
description: 'AWS Access Key ID',
|
|
9
|
-
severity: 'critical',
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
name: 'AWS Secret Key',
|
|
13
|
-
pattern: /aws[_-]?secret[_-]?access[_-]?key[=:\s]+['"]?([a-zA-Z0-9/+=]{40})['"]?/gi,
|
|
14
|
-
entropy: 5.0,
|
|
15
|
-
description: 'AWS Secret Access Key',
|
|
16
|
-
severity: 'critical',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: 'Generic API Key',
|
|
20
|
-
pattern: /api[_-]?key[_-]?[=:]\s*['"]?([a-zA-Z0-9]{32,})['"]?/gi,
|
|
21
|
-
entropy: 4.0,
|
|
22
|
-
description: 'Generic API key pattern',
|
|
23
|
-
severity: 'high',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: 'Private Key',
|
|
27
|
-
pattern: /-----BEGIN (RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY-----/g,
|
|
28
|
-
description: 'Private cryptographic key',
|
|
29
|
-
severity: 'critical',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'JWT Token',
|
|
33
|
-
pattern: /eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g,
|
|
34
|
-
description: 'JSON Web Token',
|
|
35
|
-
severity: 'high',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: 'GitHub Token',
|
|
39
|
-
pattern: /ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g,
|
|
40
|
-
entropy: 4.5,
|
|
41
|
-
description: 'GitHub Personal Access Token',
|
|
42
|
-
severity: 'critical',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'Stripe API Key',
|
|
46
|
-
pattern: /sk_live_[a-zA-Z0-9]{24,}|rk_live_[a-zA-Z0-9]{24,}/g,
|
|
47
|
-
entropy: 4.5,
|
|
48
|
-
description: 'Stripe API Key',
|
|
49
|
-
severity: 'critical',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: 'Google API Key',
|
|
53
|
-
pattern: /AIza[0-9A-Za-z_-]{35}/g,
|
|
54
|
-
entropy: 4.5,
|
|
55
|
-
description: 'Google API Key',
|
|
56
|
-
severity: 'high',
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'Slack Token',
|
|
60
|
-
pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/g,
|
|
61
|
-
entropy: 4.5,
|
|
62
|
-
description: 'Slack API Token',
|
|
63
|
-
severity: 'high',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: 'OpenAI API Key',
|
|
67
|
-
pattern: /sk-[a-zA-Z0-9]{48}/g,
|
|
68
|
-
entropy: 4.5,
|
|
69
|
-
description: 'OpenAI API Key',
|
|
70
|
-
severity: 'critical',
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: 'Anthropic API Key',
|
|
74
|
-
pattern: /sk-ant-[a-zA-Z0-9_-]{95,}/g,
|
|
75
|
-
entropy: 4.5,
|
|
76
|
-
description: 'Anthropic API Key',
|
|
77
|
-
severity: 'critical',
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'Database Connection String',
|
|
81
|
-
pattern: /(postgres|mysql|mongodb):\/\/[^\s'"<>]+:[^\s'"<>]+@[^\s'"<>]+/gi,
|
|
82
|
-
description: 'Database connection string with credentials',
|
|
83
|
-
severity: 'critical',
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: 'Password in Code',
|
|
87
|
-
pattern: /(password|passwd|pwd)[=:\s]+['"]([^'"]{8,})['"]/gi,
|
|
88
|
-
entropy: 3.5,
|
|
89
|
-
description: 'Hardcoded password',
|
|
90
|
-
severity: 'high',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: 'Bearer Token',
|
|
94
|
-
pattern: /Bearer\s+[a-zA-Z0-9_\-\.]{20,}/gi,
|
|
95
|
-
entropy: 4.0,
|
|
96
|
-
description: 'Bearer authentication token',
|
|
97
|
-
severity: 'high',
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'NPM Token',
|
|
101
|
-
pattern: /npm_[a-zA-Z0-9]{36}/g,
|
|
102
|
-
entropy: 4.5,
|
|
103
|
-
description: 'NPM Access Token',
|
|
104
|
-
severity: 'high',
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: 'Twilio API Key',
|
|
108
|
-
pattern: /SK[a-f0-9]{32}/g,
|
|
109
|
-
entropy: 4.5,
|
|
110
|
-
description: 'Twilio API Key',
|
|
111
|
-
severity: 'high',
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
name: 'SendGrid API Key',
|
|
115
|
-
pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g,
|
|
116
|
-
entropy: 4.5,
|
|
117
|
-
description: 'SendGrid API Key',
|
|
118
|
-
severity: 'high',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'Mailgun API Key',
|
|
122
|
-
pattern: /key-[a-f0-9]{32}/g,
|
|
123
|
-
entropy: 4.0,
|
|
124
|
-
description: 'Mailgun API Key',
|
|
125
|
-
severity: 'high',
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: 'Azure Storage Key',
|
|
129
|
-
pattern: /DefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[^;]+/gi,
|
|
130
|
-
description: 'Azure Storage Connection String',
|
|
131
|
-
severity: 'critical',
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: 'Generic Secret',
|
|
135
|
-
pattern: /(secret|token|api[_-]?key|auth[_-]?token)[=:\s]+['"]([a-zA-Z0-9_\-\/.+=]{20,})['"]/gi,
|
|
136
|
-
entropy: 3.5,
|
|
137
|
-
description: 'Generic secret pattern',
|
|
138
|
-
severity: 'medium',
|
|
139
|
-
},
|
|
140
|
-
];
|
package/src/scanning/scanner.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|